302 lines
11 KiB
Python
302 lines
11 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
Auto-memory management with proactive context retrieval
|
|||
|
|
Usage: auto_memory.py store "text" [--importance medium] [--tags tag1,tag2]
|
|||
|
|
auto_memory.py search "query" [--limit 3]
|
|||
|
|
auto_memory.py should_store "conversation_snippet"
|
|||
|
|
auto_memory.py context "current_topic" [--min-score 0.6]
|
|||
|
|
auto_memory.py proactive "user_message" [--auto-include]
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import json
|
|||
|
|
import subprocess
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
WORKSPACE = "/root/.openclaw/workspace"
|
|||
|
|
QDRANT_SKILL = f"{WORKSPACE}/skills/qdrant-memory/scripts"
|
|||
|
|
|
|||
|
|
def store_memory(text, importance="medium", tags=None, confidence="high",
|
|||
|
|
source_type="user", verified=True, expires=None):
|
|||
|
|
"""Store a memory automatically with full metadata"""
|
|||
|
|
cmd = [
|
|||
|
|
"python3", f"{QDRANT_SKILL}/store_memory.py",
|
|||
|
|
text,
|
|||
|
|
"--importance", importance,
|
|||
|
|
"--confidence", confidence,
|
|||
|
|
"--source-type", source_type,
|
|||
|
|
]
|
|||
|
|
if verified:
|
|||
|
|
cmd.append("--verified")
|
|||
|
|
if tags:
|
|||
|
|
cmd.extend(["--tags", ",".join(tags)])
|
|||
|
|
if expires:
|
|||
|
|
cmd.extend(["--expires", expires])
|
|||
|
|
|
|||
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|||
|
|
return result.returncode == 0
|
|||
|
|
|
|||
|
|
def search_memories(query, limit=3, min_score=0.0):
|
|||
|
|
"""Search memories for relevant context"""
|
|||
|
|
cmd = [
|
|||
|
|
"python3", f"{QDRANT_SKILL}/search_memories.py",
|
|||
|
|
query,
|
|||
|
|
"--limit", str(limit),
|
|||
|
|
"--json"
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|||
|
|
if result.returncode == 0:
|
|||
|
|
try:
|
|||
|
|
memories = json.loads(result.stdout)
|
|||
|
|
# Filter by score if specified
|
|||
|
|
if min_score > 0:
|
|||
|
|
memories = [m for m in memories if m.get("score", 0) >= min_score]
|
|||
|
|
return memories
|
|||
|
|
except:
|
|||
|
|
return []
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def should_store_memory(text):
|
|||
|
|
"""Determine if a memory should be stored based on content"""
|
|||
|
|
text_lower = text.lower()
|
|||
|
|
|
|||
|
|
# Explicit store markers (highest priority)
|
|||
|
|
explicit_markers = ["remember this", "note this", "save this", "log this", "record this"]
|
|||
|
|
if any(marker in text_lower for marker in explicit_markers):
|
|||
|
|
return True, "explicit_store", "high"
|
|||
|
|
|
|||
|
|
# Permanent markers (never expire)
|
|||
|
|
permanent_markers = [
|
|||
|
|
"my name is", "i am ", "i'm ", "call me", "i live in", "my address",
|
|||
|
|
"my phone", "my email", "my birthday", "i work at", "my job"
|
|||
|
|
]
|
|||
|
|
if any(marker in text_lower for marker in permanent_markers):
|
|||
|
|
return True, "permanent_fact", "high"
|
|||
|
|
|
|||
|
|
# Preference/decision indicators
|
|||
|
|
pref_markers = ["i prefer", "i like", "i want", "my favorite", "i need", "i use", "i choose"]
|
|||
|
|
if any(marker in text_lower for marker in pref_markers):
|
|||
|
|
return True, "preference", "high"
|
|||
|
|
|
|||
|
|
# Setup/achievement markers
|
|||
|
|
setup_markers = ["setup", "installed", "configured", "working", "completed", "finished", "created"]
|
|||
|
|
if any(marker in text_lower for marker in setup_markers):
|
|||
|
|
return True, "setup_complete", "medium"
|
|||
|
|
|
|||
|
|
# Rule/policy markers
|
|||
|
|
rule_markers = ["rule", "policy", "always", "never", "every", "schedule", "deadline"]
|
|||
|
|
if any(marker in text_lower for marker in rule_markers):
|
|||
|
|
return True, "rule_policy", "high"
|
|||
|
|
|
|||
|
|
# Temporary markers (should expire)
|
|||
|
|
temp_markers = ["for today", "for now", "temporarily", "this time only", "just for"]
|
|||
|
|
if any(marker in text_lower for marker in temp_markers):
|
|||
|
|
return True, "temporary", "low", "7d" # 7 day expiration
|
|||
|
|
|
|||
|
|
# Important keywords (check density)
|
|||
|
|
important_keywords = [
|
|||
|
|
"important", "critical", "essential", "key", "main", "primary",
|
|||
|
|
"password", "api key", "token", "secret", "backup", "restore",
|
|||
|
|
"decision", "choice", "selected", "chose", "picked"
|
|||
|
|
]
|
|||
|
|
matches = sum(1 for kw in important_keywords if kw in text_lower)
|
|||
|
|
if matches >= 2:
|
|||
|
|
return True, "keyword_match", "medium"
|
|||
|
|
|
|||
|
|
# Error/lesson learned markers
|
|||
|
|
lesson_markers = ["error", "mistake", "fixed", "solved", "lesson", "learned", "solution"]
|
|||
|
|
if any(marker in text_lower for marker in lesson_markers):
|
|||
|
|
return True, "lesson", "high"
|
|||
|
|
|
|||
|
|
return False, "not_important", None
|
|||
|
|
|
|||
|
|
def get_relevant_context(query, min_score=0.6, limit=5):
|
|||
|
|
"""Get relevant memories for current context with smart filtering"""
|
|||
|
|
memories = search_memories(query, limit=limit, min_score=min_score)
|
|||
|
|
|
|||
|
|
# Sort by importance and score
|
|||
|
|
importance_order = {"high": 0, "medium": 1, "low": 2}
|
|||
|
|
memories.sort(key=lambda m: (
|
|||
|
|
importance_order.get(m.get("importance", "medium"), 1),
|
|||
|
|
-m.get("score", 0)
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
return memories
|
|||
|
|
|
|||
|
|
def proactive_retrieval(user_message, auto_include=False):
|
|||
|
|
"""
|
|||
|
|
Proactively retrieve relevant memories based on user message.
|
|||
|
|
Returns relevant memories that might be helpful context.
|
|||
|
|
"""
|
|||
|
|
# Extract key concepts from the message
|
|||
|
|
# Simple approach: use the whole message as query
|
|||
|
|
# Better approach: extract noun phrases (could be enhanced)
|
|||
|
|
|
|||
|
|
memories = get_relevant_context(user_message, min_score=0.5, limit=5)
|
|||
|
|
|
|||
|
|
if not memories:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
# Filter for highly relevant or important memories
|
|||
|
|
proactive_memories = []
|
|||
|
|
for m in memories:
|
|||
|
|
score = m.get("score", 0)
|
|||
|
|
importance = m.get("importance", "medium")
|
|||
|
|
|
|||
|
|
# Include if:
|
|||
|
|
# - High score (0.7+) regardless of importance
|
|||
|
|
# - Medium score (0.5+) AND high importance
|
|||
|
|
if score >= 0.7 or (score >= 0.5 and importance == "high"):
|
|||
|
|
proactive_memories.append(m)
|
|||
|
|
|
|||
|
|
return proactive_memories
|
|||
|
|
|
|||
|
|
def format_context_for_prompt(memories):
|
|||
|
|
"""Format memories as context for the LLM prompt"""
|
|||
|
|
if not memories:
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
context = "\n[Relevant context from previous conversations]:\n"
|
|||
|
|
for i, m in enumerate(memories, 1):
|
|||
|
|
text = m.get("text", "")
|
|||
|
|
date = m.get("date", "unknown")
|
|||
|
|
importance = m.get("importance", "medium")
|
|||
|
|
|
|||
|
|
prefix = "🔴" if importance == "high" else "🟡" if importance == "medium" else "🟢"
|
|||
|
|
context += f"{prefix} [{date}] {text}\n"
|
|||
|
|
|
|||
|
|
return context
|
|||
|
|
|
|||
|
|
def auto_tag(text, reason):
|
|||
|
|
"""Automatically generate tags based on content"""
|
|||
|
|
tags = []
|
|||
|
|
|
|||
|
|
# Add tag based on reason
|
|||
|
|
reason_tags = {
|
|||
|
|
"explicit_store": "recorded",
|
|||
|
|
"permanent_fact": "identity",
|
|||
|
|
"preference": "preference",
|
|||
|
|
"setup_complete": "setup",
|
|||
|
|
"rule_policy": "policy",
|
|||
|
|
"temporary": "temporary",
|
|||
|
|
"keyword_match": "important",
|
|||
|
|
"lesson": "lesson"
|
|||
|
|
}
|
|||
|
|
if reason in reason_tags:
|
|||
|
|
tags.append(reason_tags[reason])
|
|||
|
|
|
|||
|
|
# Content-based tags
|
|||
|
|
text_lower = text.lower()
|
|||
|
|
content_tags = {
|
|||
|
|
"voice": ["voice", "tts", "stt", "whisper", "audio", "speak"],
|
|||
|
|
"tools": ["tool", "script", "command", "cli", "error"],
|
|||
|
|
"config": ["config", "setting", "setup", "install"],
|
|||
|
|
"memory": ["memory", "remember", "recall", "search"],
|
|||
|
|
"web": ["search", "web", "online", "internet"],
|
|||
|
|
"security": ["password", "token", "secret", "key", "auth"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for tag, keywords in content_tags.items():
|
|||
|
|
if any(kw in text_lower for kw in keywords):
|
|||
|
|
tags.append(tag)
|
|||
|
|
|
|||
|
|
return list(set(tags)) # Remove duplicates
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
parser = argparse.ArgumentParser(description="Auto-memory management")
|
|||
|
|
parser.add_argument("action", choices=[
|
|||
|
|
"store", "search", "should_store", "context",
|
|||
|
|
"proactive", "auto_process"
|
|||
|
|
])
|
|||
|
|
parser.add_argument("text", help="Text to process")
|
|||
|
|
parser.add_argument("--importance", default="medium", choices=["low", "medium", "high"])
|
|||
|
|
parser.add_argument("--tags", help="Comma-separated tags")
|
|||
|
|
parser.add_argument("--limit", type=int, default=3)
|
|||
|
|
parser.add_argument("--min-score", type=float, default=0.6)
|
|||
|
|
parser.add_argument("--auto-include", action="store_true", help="Auto-include context in response")
|
|||
|
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|||
|
|
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
if args.action == "store":
|
|||
|
|
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []
|
|||
|
|
if store_memory(args.text, args.importance, tags):
|
|||
|
|
result = {"stored": True, "importance": args.importance, "tags": tags}
|
|||
|
|
print(json.dumps(result) if args.json else f"✅ Stored: {args.text[:50]}...")
|
|||
|
|
else:
|
|||
|
|
result = {"stored": False, "error": "Failed to store"}
|
|||
|
|
print(json.dumps(result) if args.json else "❌ Failed to store")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
elif args.action == "search":
|
|||
|
|
results = search_memories(args.text, args.limit, args.min_score)
|
|||
|
|
if args.json:
|
|||
|
|
print(json.dumps(results))
|
|||
|
|
else:
|
|||
|
|
print(f"Found {len(results)} memories:")
|
|||
|
|
for r in results:
|
|||
|
|
print(f" [{r.get('score', 0):.2f}] {r.get('text', '')[:60]}...")
|
|||
|
|
|
|||
|
|
elif args.action == "should_store":
|
|||
|
|
should_store, reason, importance = should_store_memory(args.text)
|
|||
|
|
result = {"should_store": should_store, "reason": reason, "importance": importance}
|
|||
|
|
print(json.dumps(result) if args.json else f"Store? {should_store} ({reason}, {importance})")
|
|||
|
|
|
|||
|
|
elif args.action == "context":
|
|||
|
|
context = get_relevant_context(args.text, args.min_score, args.limit)
|
|||
|
|
if args.json:
|
|||
|
|
print(json.dumps(context))
|
|||
|
|
else:
|
|||
|
|
print(format_context_for_prompt(context))
|
|||
|
|
|
|||
|
|
elif args.action == "proactive":
|
|||
|
|
memories = proactive_retrieval(args.text, args.auto_include)
|
|||
|
|
if args.json:
|
|||
|
|
print(json.dumps(memories))
|
|||
|
|
else:
|
|||
|
|
if memories:
|
|||
|
|
print(f"🔍 Found {len(memories)} relevant memories:")
|
|||
|
|
for m in memories:
|
|||
|
|
score = m.get("score", 0)
|
|||
|
|
text = m.get("text", "")[:60]
|
|||
|
|
print(f" [{score:.2f}] {text}...")
|
|||
|
|
else:
|
|||
|
|
print("ℹ️ No highly relevant memories found")
|
|||
|
|
|
|||
|
|
elif args.action == "auto_process":
|
|||
|
|
# Full pipeline: check if should store, auto-tag, store, and return context
|
|||
|
|
should_store, reason, importance = should_store_memory(args.text)
|
|||
|
|
|
|||
|
|
result = {
|
|||
|
|
"should_store": should_store,
|
|||
|
|
"reason": reason,
|
|||
|
|
"stored": False
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if should_store:
|
|||
|
|
# Auto-generate tags
|
|||
|
|
tags = auto_tag(args.text, reason)
|
|||
|
|
if args.tags:
|
|||
|
|
tags.extend([t.strip() for t in args.tags.split(",")])
|
|||
|
|
tags = list(set(tags))
|
|||
|
|
|
|||
|
|
# Determine expiration for temporary memories
|
|||
|
|
expires = None
|
|||
|
|
if reason == "temporary":
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
expires = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
|
|||
|
|
|
|||
|
|
# Store it
|
|||
|
|
stored = store_memory(args.text, importance or "medium", tags,
|
|||
|
|
expires=expires)
|
|||
|
|
result["stored"] = stored
|
|||
|
|
result["tags"] = tags
|
|||
|
|
result["importance"] = importance
|
|||
|
|
|
|||
|
|
# Also get relevant context
|
|||
|
|
context = get_relevant_context(args.text, args.min_score, args.limit)
|
|||
|
|
result["context"] = context
|
|||
|
|
|
|||
|
|
print(json.dumps(result) if args.json else result)
|