feat: extract facts with main agent LLM, bypass mem0 GPT-nano #15
+83
-111
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user