new RPG system

This commit is contained in:
2026-06-05 14:57:15 +03:00
parent 6189a5fb74
commit 01b16dbeaa
29 changed files with 2395 additions and 311 deletions
+59 -7
View File
@@ -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. "день 12").
- 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:]