new RPG system
This commit is contained in:
+59
-7
@@ -20,22 +20,37 @@ Return ONLY valid JSON (no markdown), as an array of objects:
|
||||
Rules:
|
||||
- Return at most 5 NEW facts per turn. If nothing new, return [].
|
||||
- Do NOT repeat or rephrase facts already listed under "Already known".
|
||||
- Facts must be durable (names, relations, inventory, locations, lasting world state).
|
||||
- Facts must be DURABLE world/character state only:
|
||||
traits, relationships, inventory, locations, secrets revealed, lasting abilities/rules.
|
||||
- NEVER store plot events or scene narration (no "they went", "they decided", "they hugged",
|
||||
"they started a new arc", "they stepped through the portal").
|
||||
- Skip momentary emotions unless they permanently change a relationship.
|
||||
- text <= 120 chars each.
|
||||
- rp_day: in-world time label (день 1, второй день, та же ночь, через год). Use RP time hint when unclear."""
|
||||
|
||||
FACTS_COMPRESS_SYSTEM = """You consolidate RPG session memory for a long-running chat.
|
||||
Return ONLY valid JSON (no markdown): an array of {"text": "...", "rp_day": "..."}.
|
||||
Return ONLY valid JSON (no markdown): an array of {{"text": "...", "rp_day": "..."}}.
|
||||
|
||||
Goals:
|
||||
- Use ONLY information from the input facts. NEVER invent or infer new facts.
|
||||
- Aggressively MERGE near-duplicates (same topic in RU/EN, Rin/Рин, Grigo/Григорий).
|
||||
- Keep ONE best fact per topic; combine rp_day if needed (e.g. "день 1–2").
|
||||
- DROP redundant, trivial, or superseded facts.
|
||||
- Keep: names, relationships, key locations, lasting magic/rules, inventory, unresolved threads.
|
||||
- DROP redundant, trivial, superseded, and ALL one-off narrative/event facts.
|
||||
- DROP facts that describe a single scene action (went, decided, hugged, called for help, stepped into portal).
|
||||
- KEEP durable state only: names, nicknames, relationships, inventory, home items, locations,
|
||||
lasting abilities, secrets/identity, unresolved mysteries.
|
||||
- Target at most {target} facts (fewer is better). Each text <= 120 chars.
|
||||
- rp_day = in-world labels only."""
|
||||
|
||||
_NARRATIVE_EVENT_RE = re.compile(
|
||||
r"(?:"
|
||||
r"отправил(?:ись|а|и)?|решили|начали|обнялись|шагнули|вызвали|стали ближе|"
|
||||
r"передали|раскрыла|начали новую арку|вместе шагнули|тактическ(?:ое|и) отступлени|"
|
||||
r"went to|decided to|hugged|stepped through|called for help|started a new arc"
|
||||
r")",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
_NAME_ALIASES = (
|
||||
("grigoriy", "григорий"),
|
||||
("grigo", "григо"),
|
||||
@@ -135,6 +150,38 @@ def facts_are_similar(a: str, b: str) -> bool:
|
||||
return overlap >= 0.32
|
||||
|
||||
|
||||
def is_likely_narrative_event(text: str) -> bool:
|
||||
"""One-off scene actions — not durable memory."""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return True
|
||||
if _NARRATIVE_EVENT_RE.search(t):
|
||||
return True
|
||||
if "новую арку" in t.lower() or "new arc" in t.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def filter_durable_facts(facts: list[dict]) -> list[dict]:
|
||||
return [f for f in facts if not is_likely_narrative_event(f.get("text", ""))]
|
||||
|
||||
|
||||
def validate_compressed_against_source(
|
||||
original: list[dict], compressed: list[dict]
|
||||
) -> list[dict]:
|
||||
"""Reject LLM-hallucinated facts not grounded in the input list."""
|
||||
if not compressed:
|
||||
return []
|
||||
out: list[dict] = []
|
||||
for c in compressed:
|
||||
text = (c.get("text") or "").strip()
|
||||
if not text or is_likely_narrative_event(text):
|
||||
continue
|
||||
if any(facts_are_similar(text, o.get("text", "")) for o in original):
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
|
||||
def dedupe_facts_fuzzy(facts: list[dict]) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
for f in facts:
|
||||
@@ -243,8 +290,10 @@ async def compress_facts(
|
||||
if entry:
|
||||
out.append(entry)
|
||||
if out:
|
||||
logger.info("compress_facts: %d -> %d", len(facts), len(out))
|
||||
return dedupe_facts_fuzzy(out)[:FACTS_STORE_LIMIT]
|
||||
out = validate_compressed_against_source(facts, out)
|
||||
if out:
|
||||
logger.info("compress_facts: %d -> %d", len(facts), len(out))
|
||||
return dedupe_facts_fuzzy(out)[:FACTS_STORE_LIMIT]
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("compress_facts JSON parse failed. Raw=%.400s", raw)
|
||||
return dedupe_facts_fuzzy(facts)[-target:]
|
||||
@@ -263,6 +312,7 @@ async def merge_facts_persist(
|
||||
existing_json, new_facts, rp_day_default=rp_day_default
|
||||
)
|
||||
facts = dedupe_facts_fuzzy(parse_facts_list(merged_json))
|
||||
facts = filter_durable_facts(facts)
|
||||
if len(facts) > FACTS_DEDUP_THRESHOLD:
|
||||
facts = await compress_facts(
|
||||
facts,
|
||||
@@ -340,6 +390,8 @@ async def extract_facts(
|
||||
continue
|
||||
if any(facts_are_similar(entry["text"], k["text"]) for k in known):
|
||||
continue
|
||||
if is_likely_narrative_event(entry["text"]):
|
||||
continue
|
||||
if not entry["rp_day"] and hint:
|
||||
entry["rp_day"] = hint[:80]
|
||||
out.append(entry)
|
||||
@@ -350,7 +402,7 @@ async def extract_facts(
|
||||
|
||||
|
||||
def facts_to_prompt(facts_json: str, max_items: int = FACTS_PROMPT_MAX) -> str:
|
||||
facts = dedupe_facts_fuzzy(parse_facts_list(facts_json))
|
||||
facts = filter_durable_facts(dedupe_facts_fuzzy(parse_facts_list(facts_json)))
|
||||
if not facts:
|
||||
return ""
|
||||
recent = facts[-max_items:]
|
||||
|
||||
Reference in New Issue
Block a user