feat: add memory extraction LLM tool and prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 12:53:18 -05:00
parent 8686f4fdd6
commit 67011535cd
2 changed files with 183 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
You are a memory extraction system for a Discord bot. Given a conversation between a user and the bot, extract any noteworthy information worth remembering for future interactions.
RULES:
- Only extract genuinely NEW information not already in the user's profile.
- Be concise — each memory should be one sentence max.
- Assign appropriate expiration based on how long the information stays relevant:
- "permanent": Stable facts — name, job, hobbies, games they play, personality traits, pets, relationships
- "30d": Semi-stable preferences, ongoing situations — "trying to quit Warzone", "grinding for rank 500"
- "7d": Temporary situations — "excited about upcoming DLC", "on vacation this week"
- "3d": Short-term context — "had a bad day", "playing with friends tonight"
- "1d": Momentary state — "drunk right now", "tilted from losses", "in a good mood"
- Assign topic tags that would help retrieve this memory later (game names, "personal", "work", "mood", etc.)
- Assign importance: "high" for things they'd expect you to remember, "medium" for useful context, "low" for minor color
- If you learn a permanent fact about the user, provide a profile_update that incorporates the new fact into their existing profile. Rewrite the ENTIRE profile summary — don't just append. Keep it under 500 characters.
- If nothing worth remembering was said, return an empty memories array and null profile_update.
- Do NOT store things the bot said — only facts about or from the user.
Use the extract_memories tool to report your findings.

View File

@@ -203,6 +203,55 @@ CONVERSATION_TOOL = {
},
}
MEMORY_EXTRACTION_TOOL = {
"type": "function",
"function": {
"name": "extract_memories",
"description": "Extract noteworthy memories from a conversation for future reference.",
"parameters": {
"type": "object",
"properties": {
"memories": {
"type": "array",
"items": {
"type": "object",
"properties": {
"memory": {
"type": "string",
"description": "A concise fact or observation worth remembering.",
},
"topics": {
"type": "array",
"items": {"type": "string"},
"description": "Topic tags for retrieval (e.g., 'gta', 'personal', 'warzone').",
},
"expiration": {
"type": "string",
"enum": ["1d", "3d", "7d", "30d", "permanent"],
"description": "How long this memory stays relevant.",
},
"importance": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "How important this memory is for future interactions.",
},
},
"required": ["memory", "topics", "expiration", "importance"],
},
"description": "Memories to store. Only include genuinely new or noteworthy information.",
},
"profile_update": {
"type": ["string", "null"],
"description": "Full updated profile summary incorporating new permanent facts, or null if no profile changes.",
},
},
"required": ["memories"],
},
},
}
MEMORY_EXTRACTION_PROMPT = (_PROMPTS_DIR / "memory_extraction.txt").read_text(encoding="utf-8")
_NO_TEMPERATURE_MODELS = {"gpt-5-nano", "o1", "o1-mini", "o1-preview", "o3", "o3-mini", "o4-mini"}
@@ -626,6 +675,122 @@ class LLMClient:
self._log_llm("chat", elapsed, False, req_json, error=str(e))
return None
async def extract_memories(
self,
conversation: list[dict[str, str]],
username: str,
current_profile: str = "",
) -> dict | None:
"""Extract memories from a conversation for a specific user.
Returns dict with "memories" list and optional "profile_update", or None on failure.
"""
# Format conversation as readable lines
convo_lines = []
for msg in conversation:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "assistant":
convo_lines.append(f"Bot: {content}")
else:
convo_lines.append(f"{username}: {content}")
convo_text = "\n".join(convo_lines)
user_content = ""
if current_profile:
user_content += f"=== CURRENT PROFILE FOR {username} ===\n{current_profile}\n\n"
else:
user_content += f"=== CURRENT PROFILE FOR {username} ===\n(no profile yet)\n\n"
user_content += f"=== CONVERSATION ===\n{convo_text}\n\n"
user_content += f"Extract any noteworthy memories from this conversation with {username}."
user_content = self._append_no_think(user_content)
req_json = json.dumps([
{"role": "system", "content": MEMORY_EXTRACTION_PROMPT[:500]},
{"role": "user", "content": user_content[:500]},
], default=str)
t0 = time.monotonic()
async with self._semaphore:
try:
temp_kwargs = {"temperature": 0.3} if self._supports_temperature else {}
response = await self._client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": MEMORY_EXTRACTION_PROMPT},
{"role": "user", "content": user_content},
],
tools=[MEMORY_EXTRACTION_TOOL],
tool_choice={"type": "function", "function": {"name": "extract_memories"}},
**temp_kwargs,
max_completion_tokens=1024,
)
elapsed = int((time.monotonic() - t0) * 1000)
choice = response.choices[0]
usage = response.usage
if choice.message.tool_calls:
tool_call = choice.message.tool_calls[0]
resp_text = tool_call.function.arguments
args = json.loads(resp_text)
self._log_llm("memory_extraction", elapsed, True, req_json, resp_text,
input_tokens=usage.prompt_tokens if usage else None,
output_tokens=usage.completion_tokens if usage else None)
return self._validate_memory_result(args)
logger.warning("No tool call in memory extraction response.")
self._log_llm("memory_extraction", elapsed, False, req_json, error="No tool call")
return None
except Exception as e:
elapsed = int((time.monotonic() - t0) * 1000)
logger.error("LLM memory extraction error: %s", e)
self._log_llm("memory_extraction", elapsed, False, req_json, error=str(e))
return None
@staticmethod
def _validate_memory_result(result: dict) -> dict:
"""Validate and normalize memory extraction result."""
valid_expirations = {"1d", "3d", "7d", "30d", "permanent"}
valid_importances = {"low", "medium", "high"}
memories = []
for mem in result.get("memories", []):
if not isinstance(mem, dict):
continue
memory_text = str(mem.get("memory", ""))[:500]
if not memory_text:
continue
topics = mem.get("topics", [])
if not isinstance(topics, list):
topics = []
topics = [str(t).lower() for t in topics]
expiration = str(mem.get("expiration", "7d"))
if expiration not in valid_expirations:
expiration = "7d"
importance = str(mem.get("importance", "medium"))
if importance not in valid_importances:
importance = "medium"
memories.append({
"memory": memory_text,
"topics": topics,
"expiration": expiration,
"importance": importance,
})
profile_update = result.get("profile_update")
if profile_update is not None:
profile_update = str(profile_update)[:500]
return {
"memories": memories,
"profile_update": profile_update,
}
async def analyze_image(
self,
image_bytes: bytes,