92 lines
2.7 KiB
Python
92 lines
2.7 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
|
|
from services.llm import LLMError, send_message_with_model, send_message
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
FACTS_MODEL = os.getenv("RPG_FACTS_MODEL", "").strip() or "deepseek/deepseek-chat-v3"
|
|
|
|
FACTS_SYSTEM = """Extract stable facts from the conversation.
|
|
Return ONLY valid JSON (no markdown), as an array of short strings.
|
|
Rules:
|
|
- Facts must be durable (names, relations, inventory, locations, world rules).
|
|
- Do not include ephemeral actions unless they change state.
|
|
- Avoid duplicates.
|
|
- Keep each fact <= 120 chars.
|
|
Example output:
|
|
["User name is Alex", "We are in a ruined castle", "NPC Mira distrusts the user"]"""
|
|
|
|
|
|
def merge_facts(existing_json: str, new_facts: list[str], limit: int = 80) -> str:
|
|
try:
|
|
existing = json.loads(existing_json or "[]")
|
|
if not isinstance(existing, list):
|
|
existing = []
|
|
except json.JSONDecodeError:
|
|
existing = []
|
|
|
|
seen = {str(x).strip() for x in existing if str(x).strip()}
|
|
merged = [str(x).strip() for x in existing if str(x).strip()]
|
|
for f in new_facts:
|
|
s = str(f).strip()
|
|
if not s or s in seen:
|
|
continue
|
|
seen.add(s)
|
|
merged.append(s)
|
|
|
|
if len(merged) > limit:
|
|
merged = merged[-limit:]
|
|
return json.dumps(merged, ensure_ascii=False)
|
|
|
|
|
|
async def extract_facts(context_messages: list[dict]) -> list[str]:
|
|
# Build a compact transcript
|
|
transcript = "\n".join(
|
|
f"{m.get('role')}: {m.get('content','')}".strip()
|
|
for m in context_messages
|
|
if m.get("role") in ("user", "assistant")
|
|
)[-6000:]
|
|
|
|
messages = [
|
|
{"role": "system", "content": FACTS_SYSTEM},
|
|
{"role": "user", "content": transcript},
|
|
]
|
|
|
|
try:
|
|
raw = await (
|
|
send_message_with_model(messages, FACTS_MODEL)
|
|
if FACTS_MODEL
|
|
else send_message(messages)
|
|
)
|
|
except LLMError as e:
|
|
logger.warning("extract_facts LLM failed (model=%s): %s", FACTS_MODEL or "SYSTEM", e)
|
|
return []
|
|
except Exception as e:
|
|
logger.warning("extract_facts unexpected error: %s", e)
|
|
return []
|
|
|
|
try:
|
|
data = json.loads(raw.strip())
|
|
if isinstance(data, list):
|
|
return [str(x) for x in data][:40]
|
|
except Exception:
|
|
return []
|
|
return []
|
|
|
|
|
|
def facts_to_prompt(facts_json: str, max_items: int = 20) -> str:
|
|
try:
|
|
facts = json.loads(facts_json or "[]")
|
|
if not isinstance(facts, list):
|
|
return ""
|
|
except json.JSONDecodeError:
|
|
return ""
|
|
facts = [str(x).strip() for x in facts if str(x).strip()]
|
|
if not facts:
|
|
return ""
|
|
block = "\n".join(f"- {x}" for x in facts[-max_items:])
|
|
return f"--- Facts (persistent memory) ---\n{block}\n---"
|
|
|