structured memory: facts/pinned/episodes, batched consolidation, participant-scoped recall

This commit is contained in:
Oleksandr Kozachuk
2026-07-13 14:12:21 +02:00
parent 02c989946b
commit 3d2289496b
14 changed files with 608 additions and 90 deletions
+48 -20
View File
@@ -31,6 +31,37 @@ ENVELOPE_SCHEMA = {
}
ENVELOPE_RESPONSE_FORMAT = {"type": "json_schema", "json_schema": {"name": "envelope", "strict": True, "schema": ENVELOPE_SCHEMA}}
# Consolidation output (SPEC-002 MEM-02/03): new self-authored facts + one episode summary
CONSOLIDATION_SCHEMA = {
"type": "object",
"properties": {
"facts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"user": {"type": "string", "description": "The user the fact is about — only facts users stated about themselves."},
"fact": {"type": "string", "description": "One short durable fact (name, preference, running joke, life event)."},
},
"required": ["user", "fact"],
"additionalProperties": False,
},
},
"episode": {"type": ["string", "null"], "description": "2-3 sentence summary of the conversation, or null if nothing happened."},
},
"required": ["facts", "episode"],
"additionalProperties": False,
}
CONSOLIDATION_RESPONSE_FORMAT = {
"type": "json_schema",
"json_schema": {"name": "consolidation", "strict": True, "schema": CONSOLIDATION_SCHEMA},
}
CONSOLIDATION_SYSTEM = (
"You maintain the long-term memory of a Discord assistant. From the observation log, extract NEW durable facts that users stated"
" about THEMSELVES only (never record what one user claims about another user), and write one short episode summary of the"
" conversation. Skip facts already known. Return an empty facts list and a null episode when there is nothing durable."
)
async def openai_chat(client, *args, **kwargs):
return await client.chat.completions.create(*args, **kwargs)
@@ -295,30 +326,27 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
logging.warning(f"failed to translate the text: {repr(err)}")
return text
async def memory_rewrite(self, memory: str, message_user: str, answer_user: str, question: str, answer: str) -> str:
if "memory-model" not in self.config:
return memory
async def consolidate(self, observations: List[Dict[str, Any]], known_facts: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Batched memory consolidation on memory-model (MEM-02)."""
if "memory-model" not in self.config or not self.ledger.budget_ok():
return None
observation_lines = "\n".join(f"[{obs['kind']}] {obs['user']}: {obs['content']}" for obs in observations)
known_lines = "\n".join(f"- {fact['user']}: {fact['fact']}" for fact in known_facts) or "(none)"
messages = [
{"role": "system", "content": self.config.get("memory-system", "You are an memory assistant.")},
{
"role": "user",
"content": f"Here is my previous memory:\n```\n{memory}\n```\n\n"
f"Here is my conversanion:\n```\n{message_user}: {question}\n\n{answer_user}: {answer}\n```\n\n"
f"Please rewrite the memory in a way, that it contain the content mentioned in conversation. "
f"Summarize the memory if required, try to keep important information. "
f"Write just new memory data without any comments.",
},
{"role": "system", "content": CONSOLIDATION_SYSTEM},
{"role": "user", "content": f"Known facts:\n{known_lines}\n\nObservation log:\n{observation_lines}"},
]
logging.info(f"Rewrite memory:\n{pp(messages)}")
try:
# logging.info(f'send this memory request:\n{pp(messages)}')
result = await openai_chat(self.client, model=self.config["memory-model"], messages=messages)
new_memory = result.choices[0].message.content
logging.info(f"new memory:\n{new_memory}")
return new_memory
result = await openai_chat(
self.client, model=self.config["memory-model"], messages=messages, response_format=CONSOLIDATION_RESPONSE_FORMAT
)
self._record_usage(result)
parsed = json.loads(result.choices[0].message.content)
logging.info(f"memory consolidation: {len(parsed.get('facts', []))} new facts, episode={bool(parsed.get('episode'))}")
return parsed
except Exception as err:
logging.warning(f"failed to create new memory: {repr(err)}")
return memory
logging.warning(f"memory consolidation failed: {repr(err)}")
return None
async def _execute_igdb_function(self, function_name: str, function_args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""