forked from SpeedyFoxAi/jarvis-memory
274 lines
9.7 KiB
Python
Executable File
274 lines
9.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Shared Activity Log for Kimi and Max
|
|
Prevents duplicate work by logging actions to Qdrant
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from qdrant_client import QdrantClient
|
|
from qdrant_client.models import Distance, VectorParams, PointStruct
|
|
|
|
QDRANT_URL = "http://10.0.0.40:6333"
|
|
COLLECTION_NAME = "activity_log"
|
|
VECTOR_SIZE = 768 # nomic-embed-text
|
|
|
|
# Embedding function (simple keyword-based for now, or use nomic)
|
|
def simple_embed(text: str) -> list[float]:
|
|
"""Simple hash-based embedding for semantic similarity"""
|
|
# In production, use nomic-embed-text via API
|
|
# For now, use a simple approach that groups similar texts
|
|
words = text.lower().split()
|
|
vector = [0.0] * VECTOR_SIZE
|
|
for i, word in enumerate(words[:100]): # Limit to first 100 words
|
|
h = hash(word) % VECTOR_SIZE
|
|
vector[h] += 1.0
|
|
# Normalize
|
|
norm = sum(x*x for x in vector) ** 0.5
|
|
if norm > 0:
|
|
vector = [x/norm for x in vector]
|
|
return vector
|
|
|
|
def init_collection(client: QdrantClient):
|
|
"""Create activity_log collection if not exists"""
|
|
collections = [c.name for c in client.get_collections().collections]
|
|
if COLLECTION_NAME not in collections:
|
|
client.create_collection(
|
|
collection_name=COLLECTION_NAME,
|
|
vectors_config=VectorParams(size=VECTOR_SIZE, distance=Distance.COSINE)
|
|
)
|
|
print(f"Created collection: {COLLECTION_NAME}")
|
|
|
|
def log_activity(
|
|
agent: str,
|
|
action_type: str,
|
|
description: str,
|
|
affected_files: Optional[list] = None,
|
|
status: str = "completed",
|
|
metadata: Optional[dict] = None
|
|
) -> str:
|
|
"""
|
|
Log an activity to the shared activity log
|
|
|
|
Args:
|
|
agent: "Kimi" or "Max"
|
|
action_type: e.g., "cron_created", "file_edited", "config_changed", "task_completed"
|
|
description: Human-readable description of what was done
|
|
affected_files: List of file paths or systems affected
|
|
status: "completed", "in_progress", "blocked", "failed"
|
|
metadata: Additional key-value pairs
|
|
|
|
Returns:
|
|
activity_id (UUID)
|
|
"""
|
|
client = QdrantClient(url=QDRANT_URL)
|
|
init_collection(client)
|
|
|
|
activity_id = str(uuid.uuid4())
|
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
|
|
# Build searchable text
|
|
searchable_text = f"{agent} {action_type} {description} {' '.join(affected_files or [])}"
|
|
vector = simple_embed(searchable_text)
|
|
|
|
payload = {
|
|
"agent": agent,
|
|
"action_type": action_type,
|
|
"description": description,
|
|
"affected_files": affected_files or [],
|
|
"status": status,
|
|
"timestamp": timestamp,
|
|
"date": date_str,
|
|
"activity_id": activity_id,
|
|
"metadata": metadata or {}
|
|
}
|
|
|
|
client.upsert(
|
|
collection_name=COLLECTION_NAME,
|
|
points=[PointStruct(id=activity_id, vector=vector, payload=payload)]
|
|
)
|
|
|
|
return activity_id
|
|
|
|
def get_recent_activities(
|
|
agent: Optional[str] = None,
|
|
action_type: Optional[str] = None,
|
|
hours: int = 24,
|
|
limit: int = 50
|
|
) -> list[dict]:
|
|
"""
|
|
Query recent activities
|
|
|
|
Args:
|
|
agent: Filter by agent name ("Kimi" or "Max") or None for both
|
|
action_type: Filter by action type or None for all
|
|
hours: Look back this many hours
|
|
limit: Max results
|
|
"""
|
|
client = QdrantClient(url=QDRANT_URL)
|
|
|
|
# Get all points and filter client-side (Qdrant payload filtering can be tricky)
|
|
# For small collections, this is fine. For large ones, use scroll with filter
|
|
all_points = client.scroll(
|
|
collection_name=COLLECTION_NAME,
|
|
limit=1000 # Get recent batch
|
|
)[0]
|
|
|
|
results = []
|
|
cutoff = datetime.now(timezone.utc).timestamp() - (hours * 3600)
|
|
|
|
for point in all_points:
|
|
payload = point.payload
|
|
ts = payload.get("timestamp", "")
|
|
try:
|
|
point_time = datetime.fromisoformat(ts.replace("Z", "+00:00")).timestamp()
|
|
except:
|
|
continue
|
|
|
|
if point_time < cutoff:
|
|
continue
|
|
|
|
if agent and payload.get("agent") != agent:
|
|
continue
|
|
|
|
if action_type and payload.get("action_type") != action_type:
|
|
continue
|
|
|
|
results.append(payload)
|
|
|
|
# Sort by timestamp descending
|
|
results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
|
return results[:limit]
|
|
|
|
def search_activities(query: str, limit: int = 10) -> list[dict]:
|
|
"""Semantic search across activity descriptions"""
|
|
client = QdrantClient(url=QDRANT_URL)
|
|
vector = simple_embed(query)
|
|
|
|
results = client.search(
|
|
collection_name=COLLECTION_NAME,
|
|
query_vector=vector,
|
|
limit=limit
|
|
)
|
|
|
|
return [r.payload for r in results]
|
|
|
|
def check_for_duplicates(action_type: str, description_keywords: str, hours: int = 6) -> bool:
|
|
"""
|
|
Check if similar work was recently done
|
|
Returns True if duplicate detected, False otherwise
|
|
"""
|
|
recent = get_recent_activities(action_type=action_type, hours=hours)
|
|
|
|
keywords = description_keywords.lower().split()
|
|
for activity in recent:
|
|
desc = activity.get("description", "").lower()
|
|
if all(kw in desc for kw in keywords):
|
|
print(f"⚠️ Duplicate detected: {activity['agent']} did similar work {activity['timestamp']}")
|
|
print(f" Description: {activity['description']}")
|
|
return True
|
|
|
|
return False
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Shared Activity Log for Kimi/Max")
|
|
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
|
|
# Log command
|
|
log_parser = subparsers.add_parser("log", help="Log an activity")
|
|
log_parser.add_argument("--agent", required=True, choices=["Kimi", "Max"], help="Which agent performed the action")
|
|
log_parser.add_argument("--action", required=True, help="Action type (e.g., cron_created, file_edited)")
|
|
log_parser.add_argument("--description", required=True, help="What was done")
|
|
log_parser.add_argument("--files", nargs="*", help="Files/systems affected")
|
|
log_parser.add_argument("--status", default="completed", choices=["completed", "in_progress", "blocked", "failed"])
|
|
log_parser.add_argument("--check-duplicate", action="store_true", help="Check for duplicates before logging")
|
|
log_parser.add_argument("--duplicate-keywords", help="Keywords to check for duplicates (if different from description)")
|
|
|
|
# Recent command
|
|
recent_parser = subparsers.add_parser("recent", help="Show recent activities")
|
|
recent_parser.add_argument("--agent", choices=["Kimi", "Max"], help="Filter by agent")
|
|
recent_parser.add_argument("--action", help="Filter by action type")
|
|
recent_parser.add_argument("--hours", type=int, default=24, help="Hours to look back")
|
|
recent_parser.add_argument("--limit", type=int, default=20, help="Max results")
|
|
|
|
# Search command
|
|
search_parser = subparsers.add_parser("search", help="Search activities")
|
|
search_parser.add_argument("query", help="Search query")
|
|
search_parser.add_argument("--limit", type=int, default=10)
|
|
|
|
# Check command
|
|
check_parser = subparsers.add_parser("check", help="Check for duplicate work")
|
|
check_parser.add_argument("--action", required=True, help="Action type")
|
|
check_parser.add_argument("--keywords", required=True, help="Keywords to check")
|
|
check_parser.add_argument("--hours", type=int, default=6, help="Hours to look back")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "log":
|
|
if args.check_duplicate:
|
|
keywords = args.duplicate_keywords or args.description
|
|
if check_for_duplicates(args.action, keywords):
|
|
response = input("Proceed anyway? (y/n): ")
|
|
if response.lower() != "y":
|
|
print("Cancelled.")
|
|
sys.exit(0)
|
|
|
|
activity_id = log_activity(
|
|
agent=args.agent,
|
|
action_type=args.action,
|
|
description=args.description,
|
|
affected_files=args.files,
|
|
status=args.status
|
|
)
|
|
print(f"✓ Logged activity: {activity_id}")
|
|
|
|
elif args.command == "recent":
|
|
activities = get_recent_activities(
|
|
agent=args.agent,
|
|
action_type=args.action,
|
|
hours=args.hours,
|
|
limit=args.limit
|
|
)
|
|
|
|
print(f"\nRecent activities (last {args.hours}h):\n")
|
|
for a in activities:
|
|
agent_icon = "🤖" if a["agent"] == "Max" else "🎙️"
|
|
status_icon = {
|
|
"completed": "✓",
|
|
"in_progress": "◐",
|
|
"blocked": "✗",
|
|
"failed": "⚠"
|
|
}.get(a["status"], "?")
|
|
|
|
print(f"{agent_icon} [{a['timestamp'][:19]}] {status_icon} {a['action_type']}")
|
|
print(f" {a['description']}")
|
|
if a['affected_files']:
|
|
print(f" Files: {', '.join(a['affected_files'])}")
|
|
print()
|
|
|
|
elif args.command == "search":
|
|
results = search_activities(args.query, args.limit)
|
|
|
|
print(f"\nSearch results for '{args.query}':\n")
|
|
for r in results:
|
|
print(f"[{r['agent']}] {r['action_type']}: {r['description']}")
|
|
print(f" {r['timestamp'][:19]} | Status: {r['status']}")
|
|
print()
|
|
|
|
elif args.command == "check":
|
|
is_dup = check_for_duplicates(args.action, args.keywords, args.hours)
|
|
sys.exit(1 if is_dup else 0)
|
|
|
|
else:
|
|
parser.print_help()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|