feat: extract facts with main agent LLM, bypass mem0 GPT-nano #15

Merged
code-server merged 3 commits from feat/mem0-extract-llm into main 2026-03-04 14:16:46 +01:00
+83 -111
View File
@@ -45,100 +45,9 @@ class Mem0MemoryStore:
# Build custom extraction prompt tuned for nanobot conversations
from datetime import datetime
custom_prompt = f"""# Nanobot Fact Extraction Prompt
# Version: 1.0
# Date: {datetime.now().strftime("%Y-%m-%d")}
You are an information organizer for a personal AI assistant. Extract memorable facts from conversations between a user and their AI assistant.
## Context
Unlike consumer chatbots where users share personal details, this assistant is used for research, debugging, and task execution. Extract facts from BOTH user messages (what they care about / asked for) AND assistant messages (what was found / accomplished).
## What to Extract
1. **User interests and focus areas**: Topics the user asked to research or investigate
2. **Research findings**: Key facts, comparisons, or conclusions the assistant surfaced
3. **Technical work**: Systems debugged, problems solved, tools built or configured
4. **User preferences revealed through requests**: (e.g., asking for smart ring research implies interest in wearables)
5. **Project context**: What the user is working on, building, or planning
6. **Tools and tech stack**: Technologies, services, or platforms mentioned or used
## What NOT to Extract
- Command invocations or shell output
- JSON/code blobs or raw data
- Routine assistant acknowledgments ("Let me search for that", "I'll help you with that")
- Timestamps, file paths, or technical identifiers unless specifically significant
- Messages that are just "/new" or other system commands
- Tool use metadata or Python list notation artifacts
## Examples
### Example 1: Research Query with Findings
Input:
user: research smart rings that can help with ADHD focus tracking
assistant: I'll search for that. The top options are Oura Ring (best sleep data for ADHD correlation), Ultrahuman Ring AIR (real-time metabolic tracking), and RingConn (continuous SpO2). For ADHD specifically, Oura's HRV and sleep stage data correlate with focus capacity.
Output: {{"facts": [
"User researching smart rings for ADHD focus tracking",
"Oura Ring: best sleep stage data for ADHD (HRV correlates with focus)",
"Ultrahuman Ring AIR: real-time metabolic tracking",
"RingConn: continuous SpO2 monitoring"
]}}
### Example 2: Debugging/Technical Work
Input:
user: debug why mem0 is not extracting facts
assistant: Found the issue: mem0's default prompt only extracts from user messages, but nanobot users send commands not personal info. Also assistant content with tool_use arrives as a Python list which gets corrupted.
Output: {{"facts": [
"Debugged mem0 fact extraction issue",
"Root cause: default prompt ignores assistant messages",
"Assistant content corruption: list format not handled by parse_messages()"
]}}
### Example 3: Geopolitical Research
Input:
user: look at ukraine geopolitics situation
assistant: Key developments: NATO eastern flank reinforcement accelerated, grain deal negotiations stalled, EU economic impact is asymmetric with Eastern European members bearing higher costs.
Output: {{"facts": [
"User researched Ukraine geopolitics",
"NATO eastern flank reinforcement accelerated (2026)",
"Ukraine grain deal negotiations stalled",
"EU economic impact from Ukraine conflict is asymmetric, Eastern Europe most affected"
]}}
### Example 4: Skip - Just Tool Output
Input:
assistant: [{{'type': 'tool_use', 'id': 'tu_1', 'name': 'bash', ...}}]
tool: $ ls -la\\ntotal 48\\ndrwxr-xr-x 12 user staff...
Output: {{"facts": []}}
### Example 5: Skip - System Commands
Input:
user: /new
Output: {{"facts": []}}
### Example 6: Skip - No Meaningful Content
Input:
assistant: Let me help you with that.
user: ok
Output: {{"facts": []}}
## Instructions
- Today's date is {datetime.now().strftime("%Y-%m-%d")}.
- Extract from BOTH user and assistant messages.
- Prefer specific, searchable facts over vague summaries.
- Combine related user question + assistant answer into unified facts when possible.
- For transient/time-sensitive facts (location, health data, weather, notifications), ALWAYS include the date or time. Write "On 2026-03-01, Makar was in Barcelona" NOT "Makar is in Barcelona".
- Never phrase facts as present-tense universal truths when they are time-bound observations.
- Return empty list if the conversation contains only commands, tool output, or no meaningful substance.
- Respond only with the JSON object: {{"facts": ["fact1", "fact2", ...]}}, no other text.
Here is the conversation to extract facts from:
"""
today = datetime.now().strftime("%Y-%m-%d")
custom_prompt = f"Extract dated facts from this conversation as JSON: {{\"facts\": [...]}}. Today is {today}.\n\n"
self.custom_prompt = custom_prompt
# Initialize mem0 with optional config + custom prompt
# Extract only MemoryConfig-relevant fields
@@ -149,13 +58,9 @@ Here is the conversation to extract facts from:
if key in raw_config:
mem0_cfg_dict[key] = raw_config[key]
logger.debug(f"Extracted for MemoryConfig: {list(mem0_cfg_dict.keys())}")
logger.debug(f"Custom prompt length: {len(custom_prompt)} chars")
mem0_cfg_dict["custom_fact_extraction_prompt"] = custom_prompt
mem0_config = MemoryConfig(**mem0_cfg_dict)
logger.debug(f"MemoryConfig created: vector_store={mem0_config.vector_store.provider if mem0_config.vector_store else None}")
logger.debug(f"MemoryConfig.custom_fact_extraction_prompt is None: {mem0_config.custom_fact_extraction_prompt is None}")
self.memory = Memory(config=mem0_config)
logger.debug(f"Memory.config.custom_fact_extraction_prompt is None: {self.memory.config.custom_fact_extraction_prompt is None}")
logger.info("Mem0 memory system initialized with custom nanobot prompt")
@@ -243,6 +148,82 @@ Here is the conversation to extract facts from:
except Exception as e:
logger.error(f"Mem0 add failed: {e}")
async def extract_facts(
self,
messages: list[dict[str, Any]],
provider: Any,
model: str,
) -> list[str]:
"""Extract facts from conversation using the main agent's LLM provider."""
import json as _json
conv_text = ""
for msg in messages:
role = msg.get("role", "unknown")
content_val = msg.get("content", "")
if isinstance(content_val, str) and content_val.strip():
conv_text += f"{role}: {content_val}\n\n"
if not conv_text.strip():
return []
extraction_messages = [
{"role": "user", "content": self.custom_prompt + conv_text}
]
try:
response = await provider.chat(
messages=extraction_messages,
model=model,
max_tokens=2000,
temperature=0.3,
)
text = (response.content or "").strip()
if text.startswith("```"):
text = text.split("```")[1]
if text.startswith("json"):
text = text[4:]
text = text.strip()
data = _json.loads(text)
facts = data.get("facts", [])
if not isinstance(facts, list):
logger.warning(f"LLM returned non-list facts: {type(facts)}")
return []
logger.debug(f"Extracted {len(facts)} facts using {model}")
return facts
except Exception as e:
logger.error(f"Fact extraction failed: {e}")
return []
def store_facts(
self,
facts: list[str],
user_id: str,
session_id: str | None = None,
) -> None:
"""Store pre-extracted facts in mem0 with infer=False."""
if not facts:
return
metadata = {}
if session_id:
metadata["session_id"] = session_id
stored = 0
for fact in facts:
try:
self.memory.add(
fact,
user_id=user_id,
infer=False,
metadata=metadata if metadata else None,
)
stored += 1
except Exception as e:
logger.error(f"Failed to store fact '{fact[:50]}...': {e}")
logger.info(f"Stored {stored}/{len(facts)} facts for user {user_id}")
def get_memory_context(
self,
query: str,
@@ -400,19 +381,10 @@ Here is the conversation to extract facts from:
})
if mem0_messages:
# Debug: log what we're sending to mem0
import json
logger.debug(f"Mem0 consolidation sending {len(mem0_messages)} messages:")
for i, msg in enumerate(mem0_messages[:5]): # Log first 5
preview = msg['content'][:200] if len(msg['content']) > 200 else msg['content']
logger.debug(f" [{i}] {msg['role']}: {preview}")
# Add to mem0 - it handles extraction automatically
self.add_conversation(
mem0_messages,
user_id=user_id,
session_id=session.key
)
# Extract facts using the main agent's LLM (already paid for),
# then store with infer=False to bypass mem0's GPT-nano
facts = await self.extract_facts(mem0_messages, provider, model)
self.store_facts(facts, user_id=user_id, session_id=session.key)
# Update consolidation marker
if archive_all: