NexusMemory / backend /memory /extractor.py
bharatverse11's picture
Memory overhaul: cross-session (client_id), LLM extraction+dedup, provenance, edit endpoint
5b49ec1
Raw
History Blame Contribute Delete
4.77 kB
from langchain_ollama import OllamaLLM
from langchain.prompts import PromptTemplate
from services.llm import get_llm
import json
import logging
import re
logger = logging.getLogger(__name__)
MEMORY_EXTRACTION_PROMPT = PromptTemplate(
input_variables=["conversation"],
template="""You are a memory extraction system. Analyze the conversation and extract durable, important facts ABOUT THE USER.
Extract ONLY facts that are:
- Personal preferences (favorite things, likes, dislikes)
- Professional information (job, skills, frameworks they use)
- Goals or intentions
- Personal details the user explicitly shares
- Strong opinions on specific topics
Do NOT extract: questions, transient context, or facts about the assistant.
For each fact, pick a category from EXACTLY this set:
- technical, professional, preference, goal, general
Conversation:
{conversation}
Return a JSON array of objects, each: {{"fact": "...", "category": "...", "confidence": 0.0-1.0}}
- fact: a clear, concise third-person statement (e.g. "User prefers Python over JavaScript")
- confidence: how certain you are this is a durable fact (0.5-1.0)
If no important facts are found, return an empty array [].
Only return the JSON array, nothing else.""",
)
def extract_memories_from_conversation(
messages: list[dict],
llm: OllamaLLM = None,
) -> list[dict]:
"""
Extract important facts from recent conversation using LLM.
Returns list of dicts with 'fact' and 'category' keys.
"""
if not messages:
return []
# Build conversation text from recent messages
recent = messages[-6:] # Last 3 exchanges
conversation_text = "\n".join(
f"{m['role'].upper()}: {m['content']}" for m in recent
)
llm = llm or get_llm()
valid_categories = {"technical", "professional", "preference", "goal", "general"}
try:
chain = MEMORY_EXTRACTION_PROMPT | llm
result = chain.invoke({"conversation": conversation_text})
result_str = str(result).strip()
# Find the JSON array in the response (greedy to capture objects).
json_match = re.search(r'\[.*\]', result_str, re.DOTALL)
if not json_match:
return []
facts_raw = json.loads(json_match.group())
if not isinstance(facts_raw, list):
return []
extracted = []
for item in facts_raw:
# Support both the new object form and a bare string form.
if isinstance(item, str):
fact = item.strip()
category = categorize_fact(fact)
confidence = 0.8
elif isinstance(item, dict) and item.get("fact"):
fact = str(item["fact"]).strip()
category = str(item.get("category", "")).lower().strip()
if category not in valid_categories:
category = categorize_fact(fact)
try:
confidence = float(item.get("confidence", 0.8))
except (TypeError, ValueError):
confidence = 0.8
confidence = max(0.0, min(1.0, confidence))
else:
continue
if len(fact) > 5:
extracted.append({
"fact": fact,
"category": category,
"confidence": confidence,
})
logger.info(f"Extracted {len(extracted)} memory facts")
return extracted
except json.JSONDecodeError as e:
logger.warning(f"JSON parse failed for memory extraction: {e}")
return []
except Exception as e:
logger.error(f"Memory extraction failed: {e}")
return []
def categorize_fact(fact: str) -> str:
"""Simple rule-based fact categorization."""
fact_lower = fact.lower()
tech_keywords = [
"framework", "language", "library", "tool", "stack",
"python", "javascript", "react", "fastapi", "code",
"developer", "engineer", "programming",
]
preference_keywords = [
"favorite", "prefer", "like", "love", "enjoy",
"hate", "dislike", "best", "worst",
]
professional_keywords = [
"work", "job", "company", "team", "project",
"experience", "skill", "years",
]
goal_keywords = [
"want", "goal", "plan", "trying", "learning",
"building", "working on",
]
if any(k in fact_lower for k in tech_keywords):
return "technical"
elif any(k in fact_lower for k in goal_keywords):
return "goal"
elif any(k in fact_lower for k in professional_keywords):
return "professional"
elif any(k in fact_lower for k in preference_keywords):
return "preference"
else:
return "general"