Files
jarvis-memory/skills/mem-redis/scripts/search_mem.py

243 lines
7.9 KiB
Python
Raw Normal View History

2026-02-23 12:13:04 -06:00
#!/usr/bin/env python3
"""
Search memory: First Redis (exact), then Qdrant (semantic).
Usage: python3 search_mem.py "your search query" [--limit 10] [--user-id rob]
Searches:
1. Redis (mem:{user_id}) - exact text match in recent buffer
2. Qdrant (kimi_memories) - semantic similarity search
"""
import os
import sys
import json
import redis
import argparse
from pathlib import Path
from datetime import datetime
# Config
REDIS_HOST = os.getenv("REDIS_HOST", "10.0.0.36")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
USER_ID = os.getenv("USER_ID", "yourname")
QDRANT_URL = os.getenv("QDRANT_URL", "http://10.0.0.40:6333")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://10.0.0.10:11434/v1")
def search_redis(query, user_id, limit=20):
"""Search Redis buffer for exact text matches."""
try:
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
key = f"mem:{user_id}"
# Get all items from list
items = r.lrange(key, 0, -1)
if not items:
return []
query_lower = query.lower()
matches = []
for item in items:
try:
turn = json.loads(item)
content = turn.get('content', '').lower()
if query_lower in content:
matches.append({
'source': 'redis',
'turn': turn.get('turn'),
'role': turn.get('role'),
'content': turn.get('content'),
'timestamp': turn.get('timestamp'),
'score': 'exact'
})
except json.JSONDecodeError:
continue
# Sort by turn number descending (newest first)
matches.sort(key=lambda x: x.get('turn', 0), reverse=True)
return matches[:limit]
except Exception as e:
print(f"Redis search error: {e}", file=sys.stderr)
return []
def get_embedding(text):
"""Get embedding from Ollama."""
import urllib.request
payload = json.dumps({
"model": "snowflake-arctic-embed2",
"input": text
}).encode()
req = urllib.request.Request(
f"{OLLAMA_URL}/embeddings",
data=payload,
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read().decode())
return result.get('data', [{}])[0].get('embedding')
except Exception as e:
print(f"Embedding error: {e}", file=sys.stderr)
return None
def search_qdrant(query, user_id, limit=10):
"""Search Qdrant for semantic similarity."""
import urllib.request
embedding = get_embedding(query)
if not embedding:
return []
payload = json.dumps({
"vector": embedding,
"limit": limit,
"with_payload": True,
"filter": {
"must": [
{"key": "user_id", "match": {"value": user_id}}
]
}
}).encode()
req = urllib.request.Request(
f"{QDRANT_URL}/collections/kimi_memories/points/search",
data=payload,
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read().decode())
points = result.get('result', [])
matches = []
for point in points:
payload = point.get('payload', {})
matches.append({
'source': 'qdrant',
'score': round(point.get('score', 0), 3),
'turn': payload.get('turn_number'),
'role': payload.get('role'),
'content': payload.get('user_message') or payload.get('content', ''),
'ai_response': payload.get('ai_response', ''),
'timestamp': payload.get('timestamp'),
'conversation_id': payload.get('conversation_id')
})
return matches
except Exception as e:
print(f"Qdrant search error: {e}", file=sys.stderr)
return []
def format_result(result, index):
"""Format a single search result."""
source = result.get('source', 'unknown')
role = result.get('role', 'unknown')
turn = result.get('turn', '?')
score = result.get('score', '?')
content = result.get('content', '')
if len(content) > 200:
content = content[:200] + "..."
# Role emoji
role_emoji = "👤" if role == "user" else "🤖"
# Source indicator
source_icon = "🔴" if source == "redis" else "🔵"
lines = [
f"{source_icon} [{index}] Turn {turn} ({role}):",
f" {role_emoji} {content}"
]
if source == "qdrant" and result.get('ai_response'):
ai_resp = result['ai_response'][:150]
if len(result['ai_response']) > 150:
ai_resp += "..."
lines.append(f" 💬 AI: {ai_resp}")
if score != 'exact':
lines.append(f" 📊 Score: {score}")
else:
lines.append(f" 📊 Match: exact (Redis)")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Search memory: Redis first, then Qdrant")
parser.add_argument("query", help="Search query")
parser.add_argument("--limit", type=int, default=10, help="Results per source (default: 10)")
parser.add_argument("--user-id", default=USER_ID, help="User ID")
parser.add_argument("--redis-only", action="store_true", help="Only search Redis")
parser.add_argument("--qdrant-only", action="store_true", help="Only search Qdrant")
args = parser.parse_args()
print(f"🔍 Searching for: \"{args.query}\"\n")
all_results = []
# Search Redis first (unless qdrant-only)
if not args.qdrant_only:
print("📍 Searching Redis (exact match)...")
redis_results = search_redis(args.query, args.user_id, limit=args.limit)
if redis_results:
print(f"✅ Found {len(redis_results)} matches in Redis\n")
all_results.extend(redis_results)
else:
print("❌ No exact matches in Redis\n")
# Search Qdrant (unless redis-only)
if not args.redis_only:
print("🧠 Searching Qdrant (semantic similarity)...")
qdrant_results = search_qdrant(args.query, args.user_id, limit=args.limit)
if qdrant_results:
print(f"✅ Found {len(qdrant_results)} matches in Qdrant\n")
all_results.extend(qdrant_results)
else:
print("❌ No semantic matches in Qdrant\n")
# Display results
if not all_results:
print("No results found in either Redis or Qdrant.")
sys.exit(0)
print(f"=== Search Results ({len(all_results)} total) ===\n")
# Sort: Redis first (chronological), then Qdrant (by score)
redis_sorted = [r for r in all_results if r['source'] == 'redis']
qdrant_sorted = sorted(
[r for r in all_results if r['source'] == 'qdrant'],
key=lambda x: x.get('score', 0),
reverse=True
)
# Display Redis results first
if redis_sorted:
print("🔴 FROM REDIS (Recent Buffer):\n")
for i, result in enumerate(redis_sorted, 1):
print(format_result(result, i))
print()
# Then Qdrant results
if qdrant_sorted:
print("🔵 FROM QDRANT (Long-term Memory):\n")
for i, result in enumerate(qdrant_sorted, len(redis_sorted) + 1):
print(format_result(result, i))
print()
print(f"=== {len(all_results)} results ===")
if redis_sorted:
print(f" 🔴 Redis: {len(redis_sorted)} (exact, recent)")
if qdrant_sorted:
print(f" 🔵 Qdrant: {len(qdrant_sorted)} (semantic, long-term)")
if __name__ == "__main__":
main()