Spaces:
Running
Running
| """ | |
| AGENT MEMORY β Multi-tier Memory MCP Server | |
| Docker SDK on HF Spaces β no Gradio, no CSP issues. | |
| Memory tiers: | |
| episodic β events, experiences, conversations | |
| semantic β facts, knowledge, concepts | |
| procedural β skills, how-tos, workflows | |
| working β short-term scratch pad (TTL default 1h) | |
| NOTE: 'trace' tier is REMOVED β all telemetry now goes to agent-trace. | |
| Writes to tier='trace' are forwarded to agent-trace automatically. | |
| MCP tools: | |
| memory_store β write a memory | |
| memory_search β full-text + tag search | |
| memory_recall β get by id | |
| memory_update β update content/tags | |
| memory_forget β delete | |
| memory_list β list by tier/tag | |
| memory_stats β counts per tier | |
| """ | |
| import os, uuid, json, asyncio, time, re, urllib.request | |
| from pathlib import Path | |
| from datetime import datetime, timezone, timedelta | |
| from typing import Optional | |
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse | |
| BASE = Path(__file__).parent | |
| MEM_DIR = BASE / "memories" | |
| MEM_DIR.mkdir(exist_ok=True) | |
| # ββ FORGE infrastructure ββββββββββββββββββββββββββββββββββββββββββ | |
| TRACE_URL = os.environ.get("TRACE_URL", "https://chris4k-agent-trace.hf.space") | |
| def emit_trace(agent: str, event_type: str, payload: dict, status: str = "ok"): | |
| """Fire-and-forget event to agent-trace. Replaces internal trace tier.""" | |
| try: | |
| body = json.dumps({"agent": agent or "memory", "event_type": event_type, | |
| "status": status, "payload": payload}).encode() | |
| req = urllib.request.Request( | |
| f"{TRACE_URL}/api/trace", data=body, | |
| headers={"Content-Type": "application/json"}, method="POST") | |
| urllib.request.urlopen(req, timeout=2) | |
| except Exception: | |
| pass # never block memory operations | |
| TIERS = ["episodic", "semantic", "procedural", "working"] | |
| # "trace" tier is intentionally NOT in TIERS β forwarded to agent-trace | |
| # ββ Memory utils ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def now_ts(): return int(time.time()) | |
| def now_iso(): return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M") | |
| def mem_path(mid): return MEM_DIR / f"{mid}.json" | |
| def read_mem(mid): | |
| p = mem_path(mid) | |
| return json.loads(p.read_text()) if p.exists() else None | |
| def write_mem(m): | |
| m["updated_at"] = now_ts() | |
| mem_path(m["id"]).write_text(json.dumps(m, indent=2, ensure_ascii=False)) | |
| def new_memory(data: dict) -> dict: | |
| mid = uuid.uuid4().hex[:10] | |
| tier = data.get("tier", "episodic") | |
| # ββ Intercept legacy "trace" tier βββββββββββββββββββββββββββββ | |
| if tier == "trace": | |
| agent = str(data.get("agent", "memory")).strip() | |
| emit_trace(agent, "custom", { | |
| "content": str(data.get("content", ""))[:500], | |
| "tags": data.get("tags", []), | |
| "importance": data.get("importance", 3), | |
| "source": "memory_legacy_trace", | |
| }) | |
| # Return a stub so callers don't break | |
| return {"id": f"trace_fwd_{mid}", "tier": "trace", | |
| "content": data.get("content",""), "forwarded_to": "agent-trace"} | |
| if tier not in TIERS: tier = "episodic" | |
| ttl = int(data.get("ttl", 3600)) if tier == "working" else None | |
| m = { | |
| "id": mid, | |
| "tier": tier, | |
| "content": (data.get("content") or "").strip(), | |
| "summary": (data.get("summary") or "").strip(), | |
| "tags": [t.strip().lower() for t in data.get("tags", []) if str(t).strip()], | |
| "agent": (data.get("agent") or "").strip(), | |
| "importance": max(0, min(10, int(data.get("importance", 5)))), | |
| "source": (data.get("source") or "").strip(), | |
| "expires_at": now_ts() + ttl if ttl else None, | |
| "created_at": now_ts(), | |
| "updated_at": now_ts(), | |
| "access_count": 0, | |
| "last_accessed": None, | |
| } | |
| write_mem(m) | |
| # Emit a trace event for every write (light telemetry) | |
| emit_trace(m["agent"] or "memory", "custom", { | |
| "op": "memory_store", "tier": tier, "id": mid, | |
| "importance": m["importance"], "tags": m["tags"] | |
| }) | |
| return m | |
| def all_memories(include_expired=False): | |
| now = now_ts() | |
| out = [] | |
| for p in sorted(MEM_DIR.glob("*.json"), reverse=True): | |
| try: | |
| m = json.loads(p.read_text()) | |
| if not include_expired and m.get("expires_at") and m["expires_at"] < now: | |
| p.unlink() # auto-expire | |
| continue | |
| out.append(m) | |
| except: | |
| pass | |
| return out | |
| def search_memories(query: str, tier: str = "all", tag: str = "", agent: str = "", limit: int = 20): | |
| mems = all_memories() | |
| q = query.lower().strip() | |
| results = [] | |
| for m in mems: | |
| if tier != "all" and m["tier"] != tier: continue | |
| if tag and tag.lower() not in m["tags"]: continue | |
| if agent and m["agent"].lower() != agent.lower(): continue | |
| # score: content match + tag match + importance | |
| score = 0 | |
| text = (m["content"] + " " + m["summary"] + " " + " ".join(m["tags"])).lower() | |
| if q: | |
| words = q.split() | |
| for w in words: | |
| if w in text: score += 2 | |
| if q in text: score += 5 | |
| score += m.get("importance", 5) * 0.2 | |
| if not q or score > 0: | |
| results.append((score, m)) | |
| results.sort(key=lambda x: (-x[0], -x[1].get("created_at", 0))) | |
| return [m for _, m in results[:limit]] | |
| def bump_access(mid): | |
| m = read_mem(mid) | |
| if m: | |
| m["access_count"] = m.get("access_count", 0) + 1 | |
| m["last_accessed"] = now_ts() | |
| write_mem(m) | |
| return m | |
| # ββ Seed ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def seed(): | |
| if list(MEM_DIR.glob("*.json")): return | |
| samples = [ | |
| {"tier":"semantic","content":"JARVIS TheCore uses a five-layer memory architecture: ShortTermMemory, MidTermMemory, LongTermMemory, ProceduralMemory, and EpisodicMemory. The MemoryQualityGate filters before persistence.","tags":["jarvis","architecture","memory"],"agent":"christof","importance":9}, | |
| {"tier":"episodic","content":"Resolved SSL certificate error on ki-fusion-labs.de by regenerating Let's Encrypt cert via certbot. Root cause: cert expired after 90 days without auto-renewal configured.","tags":["ssl","ki-fusion-labs","devops","resolved"],"agent":"christof","importance":7}, | |
| {"tier":"procedural","content":"To deploy a HF Space with Docker SDK: (1) Set sdk: docker in README.md, (2) Add Dockerfile with uvicorn CMD on port 7860, (3) Use non-root user for security, (4) No StaticFiles β embed HTML as string in FastAPI route.","tags":["hf-spaces","docker","deployment","howto"],"agent":"christof","importance":8}, | |
| {"tier":"semantic","content":"bofrost* GDPR deletion architecture uses decentralized pull-based design across 14+ systems and 6 countries. Proof of Deletion (PoD) certificates required. Architecture Board approval needed.","tags":["gdpr","bofrost","architecture","deletion"],"agent":"christof","importance":9}, | |
| {"tier":"episodic","content":"BitNet 1.58-bit trainer achieved stable training after fixing: NaN losses (gradient clipping + LR warmup), dead layers (initialization scale), FlipRate issues (STE tuning), dataset distribution mismatch (balanced sampling).","tags":["bitnet","training","rtx5090","resolved"],"agent":"christof","importance":8}, | |
| {"tier":"working","content":"Current focus: Fix agent-kanban-board HF Space. Issue was sdk:gradio CSP blocking all JS. Fix: switch to sdk:docker, pure FastAPI.","tags":["kanban","hf-spaces","active"],"agent":"christof","importance":6,"ttl":86400}, | |
| {"tier":"procedural","content":"ki-fusion-labs.de LLM API uses GPU worker polling architecture. Workers poll /api/queue every 2s, pick up jobs, POST results back. Firewall-friendly β no inbound connections to workers needed.","tags":["ki-fusion-labs","llm-api","architecture","gpu"],"agent":"christof","importance":8}, | |
| ] | |
| for s in samples: | |
| new_memory(s) | |
| seed() | |
| # ββ FastAPI βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI(title="Agent Memory MCP") | |
| def jresp(data, status=200): return JSONResponse(content=data, status_code=status) | |
| # REST API | |
| async def list_memories(tier: str = "all", tag: str = "", agent: str = "", limit: int = 50): | |
| mems = all_memories() | |
| if tier != "all": mems = [m for m in mems if m["tier"] == tier] | |
| if tag: mems = [m for m in mems if tag.lower() in m["tags"]] | |
| if agent: mems = [m for m in mems if m["agent"].lower() == agent.lower()] | |
| mems.sort(key=lambda m: (-m.get("importance",5), -m.get("created_at",0))) | |
| return jresp(mems[:limit]) | |
| async def search(q: str = "", tier: str = "all", tag: str = "", agent: str = "", limit: int = 20): | |
| return jresp(search_memories(q, tier, tag, agent, limit)) | |
| async def get_memory(mid: str): | |
| m = bump_access(mid) | |
| if not m: raise HTTPException(404, "not found") | |
| return jresp(m) | |
| async def store_memory(request: Request): | |
| data = await request.json() | |
| if not data.get("content","").strip(): | |
| raise HTTPException(400, "content required") | |
| m = new_memory(data) | |
| return jresp({"status":"stored","id":m["id"],"memory":m}, 201) | |
| async def update_memory(mid: str, request: Request): | |
| data = await request.json() | |
| m = read_mem(mid) | |
| if not m: raise HTTPException(404) | |
| for k in ("content","summary","tags","importance","tier","source","agent"): | |
| if k in data: m[k] = data[k] | |
| write_mem(m) | |
| return jresp({"status":"updated","memory":m}) | |
| async def delete_memory(mid: str): | |
| p = mem_path(mid) | |
| if not p.exists(): raise HTTPException(404) | |
| p.unlink() | |
| return jresp({"status":"forgotten"}) | |
| async def stats(): | |
| mems = all_memories() | |
| by_tier = {t: 0 for t in TIERS} | |
| by_agent: dict = {} | |
| total_importance = 0 | |
| for m in mems: | |
| by_tier[m["tier"]] = by_tier.get(m["tier"], 0) + 1 | |
| a = m.get("agent") or "unknown" | |
| by_agent[a] = by_agent.get(a, 0) + 1 | |
| total_importance += m.get("importance", 5) | |
| return jresp({ | |
| "total": len(mems), | |
| "by_tier": by_tier, | |
| "by_agent": by_agent, | |
| "avg_importance": round(total_importance / len(mems), 1) if mems else 0, | |
| }) | |
| # ββ MCP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MCP_TOOLS = [ | |
| {"name":"memory_store", | |
| "description":"Store a new memory. Tier: episodic|semantic|procedural|working. Importance 0-10.", | |
| "inputSchema":{"type":"object","required":["content"],"properties":{ | |
| "content": {"type":"string","description":"Memory content"}, | |
| "summary": {"type":"string","description":"Short summary"}, | |
| "tier": {"type":"string","enum":["episodic","semantic","procedural","working"]}, | |
| "tags": {"type":"array","items":{"type":"string"}}, | |
| "agent": {"type":"string","description":"Agent or user ID"}, | |
| "importance":{"type":"integer","minimum":0,"maximum":10}, | |
| "source": {"type":"string"}, | |
| "ttl": {"type":"integer","description":"TTL in seconds (working tier only)"}, | |
| }}}, | |
| {"name":"memory_search", | |
| "description":"Search memories by content, tier, or tag. Returns ranked results.", | |
| "inputSchema":{"type":"object","properties":{ | |
| "query": {"type":"string"}, | |
| "tier": {"type":"string","enum":["all","episodic","semantic","procedural","working"]}, | |
| "tag": {"type":"string"}, | |
| "agent": {"type":"string"}, | |
| "limit": {"type":"integer","default":10}, | |
| }}}, | |
| {"name":"memory_recall", | |
| "description":"Retrieve a specific memory by ID.", | |
| "inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}, | |
| {"name":"memory_update", | |
| "description":"Update an existing memory's content, tags, or importance.", | |
| "inputSchema":{"type":"object","required":["id"],"properties":{ | |
| "id": {"type":"string"}, | |
| "content": {"type":"string"}, | |
| "summary": {"type":"string"}, | |
| "tags": {"type":"array","items":{"type":"string"}}, | |
| "importance":{"type":"integer"}, | |
| }}}, | |
| {"name":"memory_forget", | |
| "description":"Delete a memory permanently.", | |
| "inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}}, | |
| {"name":"memory_list", | |
| "description":"List memories by tier or tag.", | |
| "inputSchema":{"type":"object","properties":{ | |
| "tier": {"type":"string"}, | |
| "tag": {"type":"string"}, | |
| "agent": {"type":"string"}, | |
| "limit": {"type":"integer","default":20}, | |
| }}}, | |
| {"name":"memory_stats", | |
| "description":"Get memory statistics (counts per tier, agents, avg importance).", | |
| "inputSchema":{"type":"object","properties":{}}}, | |
| ] | |
| async def mcp_call(name, args): | |
| if name == "memory_store": | |
| if not args.get("content","").strip(): | |
| return json.dumps({"error":"content required"}) | |
| m = new_memory(args) | |
| return json.dumps({"stored":m["id"],"tier":m["tier"],"memory":m}) | |
| if name == "memory_search": | |
| results = search_memories( | |
| args.get("query",""), args.get("tier","all"), | |
| args.get("tag",""), args.get("agent",""), args.get("limit",10)) | |
| return json.dumps({"count":len(results),"results":results}) | |
| if name == "memory_recall": | |
| m = bump_access(args["id"]) | |
| return json.dumps(m or {"error":"not found"}) | |
| if name == "memory_update": | |
| m = read_mem(args["id"]) | |
| if not m: return json.dumps({"error":"not found"}) | |
| for k in ("content","summary","tags","importance","tier"): | |
| if k in args: m[k] = args[k] | |
| write_mem(m); return json.dumps({"updated":m["id"],"memory":m}) | |
| if name == "memory_forget": | |
| p = mem_path(args["id"]) | |
| if not p.exists(): return json.dumps({"error":"not found"}) | |
| p.unlink(); return json.dumps({"forgotten":args["id"]}) | |
| if name == "memory_list": | |
| mems = all_memories() | |
| t = args.get("tier","all"); tg = args.get("tag",""); ag = args.get("agent","") | |
| if t != "all": mems = [m for m in mems if m["tier"]==t] | |
| if tg: mems = [m for m in mems if tg.lower() in m["tags"]] | |
| if ag: mems = [m for m in mems if m["agent"].lower()==ag.lower()] | |
| mems.sort(key=lambda m: (-m.get("importance",5), -m.get("created_at",0))) | |
| return json.dumps({"count":len(mems),"memories":mems[:args.get("limit",20)]}) | |
| if name == "memory_stats": | |
| mems = all_memories() | |
| by_tier = {t:0 for t in TIERS} | |
| for m in mems: by_tier[m["tier"]] = by_tier.get(m["tier"],0)+1 | |
| return json.dumps({"total":len(mems),"by_tier":by_tier}) | |
| return json.dumps({"error":f"unknown tool: {name}"}) | |
| async def mcp_sse(): | |
| async def stream(): | |
| init = {"jsonrpc":"2.0","method":"notifications/initialized", | |
| "params":{"serverInfo":{"name":"agent-memory","version":"1.0"},"capabilities":{"tools":{}}}} | |
| yield f"data: {json.dumps(init)}\n\n" | |
| await asyncio.sleep(0.1) | |
| yield f"data: {json.dumps({'jsonrpc':'2.0','method':'notifications/tools/list_changed','params':{}})}\n\n" | |
| while True: | |
| await asyncio.sleep(25) | |
| yield f"data: {json.dumps({'jsonrpc':'2.0','method':'ping'})}\n\n" | |
| return StreamingResponse(stream(), media_type="text/event-stream", | |
| headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"}) | |
| async def mcp_rpc(request: Request): | |
| body = await request.json() | |
| method = body.get("method",""); rid = body.get("id",1) | |
| if method == "initialize": | |
| return jresp({"jsonrpc":"2.0","id":rid,"result":{ | |
| "serverInfo":{"name":"agent-memory","version":"1.0"},"capabilities":{"tools":{}}}}) | |
| if method == "tools/list": | |
| return jresp({"jsonrpc":"2.0","id":rid,"result":{"tools":MCP_TOOLS}}) | |
| if method == "tools/call": | |
| p = body.get("params",{}) | |
| res = await mcp_call(p.get("name",""), p.get("arguments",{})) | |
| return jresp({"jsonrpc":"2.0","id":rid,"result":{"content":[{"type":"text","text":res}]}}) | |
| return jresp({"jsonrpc":"2.0","id":rid,"error":{"code":-32601,"message":"Method not found"}}) | |
| # ββ SPA βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def serve_ui(): | |
| return HTMLResponse(content=SPA, media_type='text/html; charset=utf-8') | |
| SPA = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>AGENT MEMORY</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --bg:#0a0a0f;--s1:#111118;--s2:#161625;--bd:#1e1e2e;--bd2:#252540; | |
| --acc:#ff6b00;--acc2:#ff9500;--txt:#d8d8f0;--sub:#5a5a80;--dim:#2a2a50; | |
| --ep:#0ea5e9;--se:#7c3aed;--pr:#2ed573;--wk:#f0c040; | |
| --cr:#ff2244;--font:'Space Mono',monospace; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0;} | |
| html,body{height:100%;overflow:hidden;} | |
| body{font-family:var(--font);background:var(--bg);color:var(--txt); | |
| display:flex;flex-direction:column;height:100vh;} | |
| body::after{content:'';position:fixed;inset:0;pointer-events:none; | |
| background-image:repeating-linear-gradient(0deg,transparent,transparent 3px, | |
| rgba(255,107,0,.007) 3px,rgba(255,107,0,.007) 4px);} | |
| /* HEADER */ | |
| #hdr{flex-shrink:0;display:flex;align-items:center;padding:.85rem 1.8rem;gap:1.2rem; | |
| border-bottom:1px solid var(--bd);background:linear-gradient(180deg,#0e0e1e,var(--bg));z-index:10;} | |
| #logo{font-size:1.3rem;font-weight:700;letter-spacing:2px; | |
| background:linear-gradient(90deg,var(--acc),var(--acc2)); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;} | |
| #logo-sub{font-size:.52rem;color:var(--sub);letter-spacing:.28em;text-transform:uppercase;margin-top:2px;} | |
| #stat-bar{display:flex;gap:.55rem;flex:1;flex-wrap:wrap;} | |
| .sp{display:flex;align-items:center;gap:.4rem;background:var(--s1);border:1px solid var(--bd); | |
| border-radius:5px;padding:.26rem .6rem;font-size:.58rem;color:var(--sub);} | |
| .sp-n{font-size:.95rem;font-weight:700;line-height:1;} | |
| .sp-n.ep{color:var(--ep);}.sp-n.se{color:var(--se);} | |
| .sp-n.pr{color:var(--pr);}.sp-n.wk{color:var(--wk);} | |
| #btn-new{background:var(--acc);color:#000;border:none;padding:.42rem 1rem; | |
| font-family:var(--font);font-size:.68rem;font-weight:700;letter-spacing:.1em; | |
| text-transform:uppercase;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0; | |
| transition:background .12s,transform .1s;} | |
| #btn-new:hover{background:var(--acc2);transform:translateY(-1px);} | |
| /* TOOLBAR */ | |
| #toolbar{flex-shrink:0;display:flex;gap:.5rem;align-items:center;padding:.5rem 1.8rem; | |
| border-bottom:1px solid var(--bd);background:var(--s1);z-index:10;flex-wrap:wrap;} | |
| #search-input{background:var(--s2);border:1px solid var(--bd2);border-radius:5px; | |
| padding:.38rem .7rem;font-family:var(--font);font-size:.7rem;color:var(--txt); | |
| outline:none;width:220px;transition:border-color .12s;} | |
| #search-input:focus{border-color:var(--acc);} | |
| #search-btn{background:var(--acc);color:#000;border:none;padding:.38rem .7rem; | |
| font-family:var(--font);font-size:.65rem;font-weight:700;border-radius:4px;cursor:pointer;} | |
| .fsep{color:var(--bd2);} | |
| .tc{background:var(--s2);border:1px solid var(--bd2);border-radius:20px;padding:2px 9px; | |
| cursor:pointer;color:var(--sub);font-family:var(--font);font-size:.55rem; | |
| transition:all .12s;user-select:none;} | |
| .tc.on{border-color:var(--acc);color:var(--acc);background:#160b00;} | |
| .tc.ep-on{border-color:var(--ep);color:var(--ep);background:#050c18;} | |
| .tc.se-on{border-color:var(--se);color:var(--se);background:#110820;} | |
| .tc.pr-on{border-color:var(--pr);color:var(--pr);background:#02130a;} | |
| .tc.wk-on{border-color:var(--wk);color:var(--wk);background:#181400;} | |
| /* MAIN LAYOUT */ | |
| #main{flex:1;display:flex;gap:0;min-height:0;overflow:hidden;} | |
| /* MEMORY LIST */ | |
| #list-panel{width:400px;flex-shrink:0;border-right:1px solid var(--bd); | |
| display:flex;flex-direction:column;overflow:hidden;} | |
| #list-scroll{flex:1;overflow-y:auto;padding:.6rem;} | |
| #list-scroll::-webkit-scrollbar{width:4px;} | |
| #list-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;} | |
| #list-empty{font-size:.62rem;color:var(--dim);text-align:center;padding:2rem; | |
| border:1px dashed var(--bd);border-radius:6px;margin:.5rem;} | |
| /* MEMORY CARD */ | |
| .mc{background:var(--s1);border:1px solid var(--bd);border-radius:8px; | |
| padding:.65rem .8rem;margin-bottom:.42rem;cursor:pointer;position:relative; | |
| transition:border-color .12s,transform .08s;animation:cin .15s ease; | |
| padding-left:1rem;} | |
| @keyframes cin{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}} | |
| .mc:hover{border-color:var(--bd2);transform:translateY(-1px);} | |
| .mc.active{border-color:var(--acc);background:var(--s2);} | |
| .mc::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px;} | |
| .mc.ep::before{background:var(--ep);}.mc.se::before{background:var(--se);} | |
| .mc.pr::before{background:var(--pr);}.mc.wk::before{background:var(--wk);} | |
| .mc-top{display:flex;align-items:flex-start;gap:.4rem;margin-bottom:.3rem;} | |
| .mc-tier{font-size:.48rem;padding:1px 5px;border-radius:3px;text-transform:uppercase; | |
| letter-spacing:.1em;font-weight:700;flex-shrink:0;margin-top:1px;} | |
| .ep .mc-tier{background:#050c18;color:var(--ep);border:1px solid rgba(14,165,233,.15);} | |
| .se .mc-tier{background:#110820;color:var(--se);border:1px solid rgba(124,58,237,.15);} | |
| .pr .mc-tier{background:#02130a;color:var(--pr);border:1px solid rgba(46,213,115,.15);} | |
| .wk .mc-tier{background:#181400;color:var(--wk);border:1px solid rgba(240,192,64,.15);} | |
| .mc-title{flex:1;font-size:.72rem;font-weight:700;color:var(--txt);line-height:1.3;word-break:break-word;} | |
| .mc-imp{font-size:.58rem;color:var(--sub);} | |
| .mc-body{font-size:.61rem;color:var(--sub);line-height:1.48; | |
| max-height:48px;overflow:hidden;position:relative;margin-bottom:.32rem;} | |
| .mc-body::after{content:'';position:absolute;bottom:0;left:0;right:0;height:14px; | |
| background:linear-gradient(transparent,var(--s1));} | |
| .mc.active .mc-body::after{background:linear-gradient(transparent,var(--s2));} | |
| .mc-foot{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;} | |
| .mc-tag{font-size:.5rem;background:var(--bd2);border:1px solid var(--bd); | |
| border-radius:3px;padding:0 4px;color:var(--sub);} | |
| .mc-agent{font-size:.52rem;color:var(--sub);margin-left:auto;} | |
| .mc-agent .at{color:var(--ai,#7c3aed);} | |
| .mc-date{font-size:.49rem;color:var(--dim);} | |
| /* DETAIL PANEL */ | |
| #detail-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;} | |
| #detail-scroll{flex:1;overflow-y:auto;padding:1.4rem 1.8rem;} | |
| #detail-scroll::-webkit-scrollbar{width:4px;} | |
| #detail-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;} | |
| #detail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center; | |
| height:100%;gap:.8rem;color:var(--sub);} | |
| #detail-empty .big{font-size:2.5rem;opacity:.2;} | |
| #detail-empty .msg{font-size:.65rem;opacity:.4;letter-spacing:.1em;text-transform:uppercase;} | |
| #detail-content{display:none;} | |
| .d-tier-badge{display:inline-flex;align-items:center;gap:.5rem; | |
| font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.15em; | |
| padding:.3rem .7rem;border-radius:4px;margin-bottom:.8rem;} | |
| .d-tier-badge.ep{background:#050c18;color:var(--ep);border:1px solid rgba(14,165,233,.2);} | |
| .d-tier-badge.se{background:#110820;color:var(--se);border:1px solid rgba(124,58,237,.2);} | |
| .d-tier-badge.pr{background:#02130a;color:var(--pr);border:1px solid rgba(46,213,115,.2);} | |
| .d-tier-badge.wk{background:#181400;color:var(--wk);border:1px solid rgba(240,192,64,.2);} | |
| #d-title{font-size:1.05rem;font-weight:700;color:var(--txt);line-height:1.4;margin-bottom:.6rem;word-break:break-word;} | |
| #d-content{font-size:.78rem;color:var(--txt);line-height:1.7;margin-bottom:1rem; | |
| background:var(--s1);border:1px solid var(--bd);border-radius:7px;padding:1rem;white-space:pre-wrap;} | |
| .d-meta{display:grid;grid-template-columns:1fr 1fr;gap:.5rem .8rem;margin-bottom:1rem;} | |
| .d-meta-item{font-size:.6rem;} | |
| .d-meta-label{color:var(--sub);text-transform:uppercase;letter-spacing:.1em;font-size:.5rem;margin-bottom:.18rem;} | |
| .d-meta-val{color:var(--txt);} | |
| .d-tags{display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:1rem;} | |
| .d-tag{background:var(--s2);border:1px solid var(--bd2);border-radius:4px; | |
| padding:2px 8px;font-size:.58rem;color:var(--sub);} | |
| .imp-bar{height:4px;background:var(--bd2);border-radius:2px;margin-top:.2rem;} | |
| .imp-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--lo,#2ed573),var(--acc));} | |
| .d-actions{display:flex;gap:.5rem;margin-top:.8rem;} | |
| .d-btn{background:var(--s2);border:1px solid var(--bd2);color:var(--sub); | |
| padding:.38rem .75rem;font-family:var(--font);font-size:.62rem;border-radius:4px; | |
| cursor:pointer;transition:all .1s;} | |
| .d-btn:hover{background:var(--bd2);color:var(--txt);} | |
| .d-btn.danger:hover{background:#1e0508;color:var(--cr);border-color:#5a1020;} | |
| /* MODAL */ | |
| #modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:100; | |
| backdrop-filter:blur(4px);align-items:center;justify-content:center;} | |
| #modal.open{display:flex;} | |
| .mdl{background:var(--s1);border:1px solid var(--bd2);border-top:2px solid var(--acc); | |
| border-radius:12px;padding:1.5rem;width:560px;max-width:97vw;max-height:92vh; | |
| overflow-y:auto;animation:mdin .17s ease;position:relative;} | |
| @keyframes mdin{from{opacity:0;transform:scale(.96) translateY(-8px)}to{opacity:1;transform:none}} | |
| #mdl-title{font-size:.88rem;font-weight:700;letter-spacing:3px;color:var(--acc);margin-bottom:1rem;} | |
| #mdl-close{position:absolute;top:.85rem;right:.85rem;background:none;border:none;color:var(--sub); | |
| width:26px;height:26px;border-radius:4px;cursor:pointer;font-size:.85rem; | |
| display:flex;align-items:center;justify-content:center;transition:all .1s;} | |
| #mdl-close:hover{background:var(--bd2);color:var(--txt);} | |
| .fg{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;} | |
| .fl{margin-bottom:.72rem;} | |
| .fl label{display:block;font-size:.52rem;color:var(--sub);text-transform:uppercase; | |
| letter-spacing:.14em;margin-bottom:.25rem;} | |
| .fl input,.fl textarea,.fl select{width:100%;background:var(--s2);border:1px solid var(--bd2); | |
| border-radius:5px;padding:.44rem .62rem;font-family:var(--font);font-size:.72rem;color:var(--txt); | |
| outline:none;transition:border-color .12s;} | |
| .fl input:focus,.fl textarea:focus,.fl select:focus{border-color:var(--acc);} | |
| .fl textarea{min-height:120px;line-height:1.6;resize:vertical;} | |
| .fl select option{background:var(--s2);} | |
| #mdl-actions{display:flex;gap:.48rem;margin-top:.95rem;} | |
| #btn-save{flex:1;background:var(--acc);color:#000;border:none;padding:.5rem 1rem; | |
| font-family:var(--font);font-size:.68rem;font-weight:700;letter-spacing:.1em; | |
| text-transform:uppercase;border-radius:5px;cursor:pointer;transition:background .1s;} | |
| #btn-save:hover{background:var(--acc2);} | |
| #btn-cancel{background:var(--s2);color:var(--sub);border:1px solid var(--bd2);padding:.5rem 1rem; | |
| font-family:var(--font);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase; | |
| border-radius:5px;cursor:pointer;transition:all .1s;} | |
| #btn-cancel:hover{background:var(--bd2);color:var(--txt);} | |
| /* TOASTS */ | |
| #toasts{position:fixed;bottom:1rem;right:1rem;z-index:200;display:flex;flex-direction:column;gap:.38rem;} | |
| .tst{background:var(--s1);border:1px solid var(--bd2);border-left:3px solid var(--acc); | |
| padding:.45rem .82rem;font-size:.63rem;border-radius:6px;animation:tin .16s ease; | |
| color:var(--txt);max-width:260px;} | |
| .tst.err{border-left-color:var(--cr);} | |
| @keyframes tin{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:none}} | |
| #mcp-hint{position:fixed;bottom:1rem;left:1rem;z-index:10;background:var(--s1); | |
| border:1px solid var(--bd2);border-left:3px solid var(--se);border-radius:6px; | |
| padding:.42rem .82rem;font-size:.55rem;color:var(--sub);} | |
| #mcp-hint code{color:var(--se);} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="hdr"> | |
| <div> | |
| <div id="logo">AGENT MEMORY</div> | |
| <div id="logo-sub">Multi-tier Memory MCP · ki-fusion-labs.de</div> | |
| </div> | |
| <div id="stat-bar"> | |
| <div class="sp"><span class="sp-n ep" id="s-ep">0</span>EPISODIC</div> | |
| <div class="sp"><span class="sp-n se" id="s-se">0</span>SEMANTIC</div> | |
| <div class="sp"><span class="sp-n pr" id="s-pr">0</span>PROCEDURAL</div> | |
| <div class="sp"><span class="sp-n wk" id="s-wk">0</span>WORKING</div> | |
| </div> | |
| <button id="btn-new">+ Store Memory</button> | |
| </div> | |
| <div id="toolbar"> | |
| <input type="text" id="search-input" placeholder="Search memories..."> | |
| <button id="search-btn">🔍</button> | |
| <span class="fsep">|</span> | |
| <span class="tc on" id="f-all">ALL</span> | |
| <span class="tc" id="f-ep">📄 Episodic</span> | |
| <span class="tc" id="f-se">🧠 Semantic</span> | |
| <span class="tc" id="f-pr">⚙ Procedural</span> | |
| <span class="tc" id="f-wk">⏳ Working</span> | |
| </div> | |
| <div id="main"> | |
| <div id="list-panel"> | |
| <div id="list-scroll"> | |
| <div id="list-empty">No memories yet</div> | |
| </div> | |
| </div> | |
| <div id="detail-panel"> | |
| <div id="detail-scroll"> | |
| <div id="detail-empty"> | |
| <div class="big">🧠</div> | |
| <div class="msg">Select a memory to view</div> | |
| </div> | |
| <div id="detail-content"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="modal"> | |
| <div class="mdl"> | |
| <button id="mdl-close">✕</button> | |
| <div id="mdl-title">STORE MEMORY</div> | |
| <input type="hidden" id="eid"> | |
| <div class="fl"><label>Content *</label> | |
| <textarea id="fc" placeholder="What should be remembered?"></textarea></div> | |
| <div class="fl"><label>Summary (optional short label)</label> | |
| <input type="text" id="fs" placeholder="One-line summary"></div> | |
| <div class="fg"> | |
| <div class="fl"><label>Tier</label> | |
| <select id="ftier"> | |
| <option value="episodic">📄 Episodic</option> | |
| <option value="semantic">🧠 Semantic</option> | |
| <option value="procedural">⚙ Procedural</option> | |
| <option value="working">⏳ Working</option> | |
| </select> | |
| </div> | |
| <div class="fl"><label>Importance (0-10)</label> | |
| <input type="number" id="fimp" value="5" min="0" max="10"></div> | |
| <div class="fl"><label>Agent / Source</label> | |
| <input type="text" id="fagent" placeholder="christof / jarvis" list="ag-list"> | |
| <datalist id="ag-list"> | |
| <option value="christof"><option value="jarvis"><option value="research-agent"> | |
| <option value="forge-agent"><option value="monitor-agent"> | |
| </datalist> | |
| </div> | |
| <div class="fl"><label>TTL seconds (working only)</label> | |
| <input type="number" id="fttl" value="3600" min="60"></div> | |
| </div> | |
| <div class="fl"><label>Tags (comma separated)</label> | |
| <input type="text" id="ftags" placeholder="jarvis, architecture, memory"></div> | |
| <div id="mdl-actions"> | |
| <button id="btn-save">⚡ Save Memory</button> | |
| <button id="btn-cancel">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toasts"></div> | |
| <div id="mcp-hint">MCP: <code>GET /mcp/sse</code> | <code>memory_store</code> <code>memory_search</code></div> | |
| <script> | |
| var MEMS=[], FILTER='all', ACTIVE_ID=null, SEARCH_Q=''; | |
| function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');} | |
| function tsToDate(ts){if(!ts)return '';return new Date(ts*1000).toISOString().substring(0,10);} | |
| function post(url,data){return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});} | |
| function toast(msg,err){var el=document.createElement('div');el.className='tst'+(err?' err':''); | |
| el.textContent=(err?'\u26a0 ':'\u26a1 ')+msg; | |
| document.getElementById('toasts').appendChild(el);setTimeout(function(){el.remove();},2600);} | |
| var TIER_ICONS={episodic:'📄',semantic:'🧠',procedural:'⚙',working:'⏳'}; | |
| function loadMems(){ | |
| var url = FILTER=='all' | |
| ? (SEARCH_Q ? '/api/memories/search?q='+encodeURIComponent(SEARCH_Q)+'&limit=100' : '/api/memories?limit=100') | |
| : (SEARCH_Q ? '/api/memories/search?q='+encodeURIComponent(SEARCH_Q)+'&tier='+FILTER+'&limit=100' | |
| : '/api/memories?tier='+FILTER+'&limit=100'); | |
| fetch(url).then(function(r){return r.json();}).then(function(d){ | |
| MEMS = Array.isArray(d) ? d : (d.results || d.memories || []); | |
| renderList();loadStats(); | |
| }).catch(function(){toast('Error loading',true);}); | |
| } | |
| function loadStats(){ | |
| fetch('/api/stats').then(function(r){return r.json();}).then(function(s){ | |
| document.getElementById('s-ep').textContent=s.by_tier.episodic||0; | |
| document.getElementById('s-se').textContent=s.by_tier.semantic||0; | |
| document.getElementById('s-pr').textContent=s.by_tier.procedural||0; | |
| document.getElementById('s-wk').textContent=s.by_tier.working||0; | |
| }).catch(function(){}); | |
| } | |
| function renderList(){ | |
| var scroll=document.getElementById('list-scroll'); | |
| scroll.innerHTML=''; | |
| if(!MEMS.length){ | |
| var e=document.createElement('div');e.id='list-empty';e.textContent='No memories found'; | |
| scroll.appendChild(e);return; | |
| } | |
| MEMS.forEach(function(m){scroll.appendChild(makeCard(m));}); | |
| } | |
| function makeCard(m){ | |
| var card=document.createElement('div'); | |
| card.className='mc '+m.tier+(ACTIVE_ID==m.id?' active':''); | |
| card.id='mc-'+m.id; | |
| var title=m.summary||m.content.substring(0,60)+(m.content.length>60?'...':''); | |
| var preview=m.content.substring(0,100); | |
| var tags=(m.tags||[]).slice(0,3).map(function(t){return '<span class="mc-tag">'+esc(t)+'</span>';}).join(''); | |
| var agH=m.agent?'<span class="mc-agent"><span class="at">@</span>'+esc(m.agent)+'</span>':''; | |
| card.innerHTML= | |
| '<div class="mc-top"><span class="mc-tier">'+esc(m.tier)+'</span>' | |
| +'<div class="mc-title">'+esc(title)+'</div>' | |
| +'<span class="mc-imp">⭐'+m.importance+'</span></div>' | |
| +(preview?'<div class="mc-body">'+esc(preview)+'</div>':'') | |
| +'<div class="mc-foot">'+tags+agH | |
| +'<span class="mc-date">'+tsToDate(m.created_at)+'</span></div>'; | |
| card.addEventListener('click',function(){selectMem(m.id);}); | |
| return card; | |
| } | |
| function selectMem(id){ | |
| ACTIVE_ID=id; | |
| document.querySelectorAll('.mc').forEach(function(c){ | |
| c.classList.toggle('active',c.id=='mc-'+id);}); | |
| fetch('/api/memories/'+id).then(function(r){return r.json();}).then(function(m){renderDetail(m);}) | |
| .catch(function(){toast('Error loading memory',true);}); | |
| } | |
| function renderDetail(m){ | |
| document.getElementById('detail-empty').style.display='none'; | |
| var dc=document.getElementById('detail-content'); | |
| dc.style.display='block'; | |
| var title=m.summary||m.content.substring(0,80); | |
| var tags=(m.tags||[]).map(function(t){return '<span class="d-tag">'+esc(t)+'</span>';}).join(''); | |
| var impW=Math.round(m.importance*10)+'%'; | |
| var expires=m.expires_at?tsToDate(m.expires_at):'never'; | |
| dc.innerHTML= | |
| '<div class="d-tier-badge '+m.tier+'">'+TIER_ICONS[m.tier]+' '+esc(m.tier)+'</div>' | |
| +'<div id="d-title">'+esc(title)+'</div>' | |
| +'<div id="d-content">'+esc(m.content)+'</div>' | |
| +'<div class="d-tags">'+tags+'</div>' | |
| +'<div class="d-meta">' | |
| +'<div class="d-meta-item"><div class="d-meta-label">Importance</div>' | |
| +'<div class="d-meta-val">'+m.importance+'/10<div class="imp-bar"><div class="imp-fill" style="width:'+impW+'"></div></div></div></div>' | |
| +'<div class="d-meta-item"><div class="d-meta-label">Agent</div><div class="d-meta-val">'+(m.agent||'β')+'</div></div>' | |
| +'<div class="d-meta-item"><div class="d-meta-label">Created</div><div class="d-meta-val">'+tsToDate(m.created_at)+'</div></div>' | |
| +'<div class="d-meta-item"><div class="d-meta-label">Accessed</div><div class="d-meta-val">'+(m.access_count||0)+' times</div></div>' | |
| +'<div class="d-meta-item"><div class="d-meta-label">Expires</div><div class="d-meta-val">'+expires+'</div></div>' | |
| +'<div class="d-meta-item"><div class="d-meta-label">ID</div><div class="d-meta-val" style="font-size:.48rem;opacity:.5">'+m.id+'</div></div>' | |
| +'</div>' | |
| +'<div class="d-actions">' | |
| +'<button class="d-btn" id="d-edit">✎ Edit</button>' | |
| +'<button class="d-btn danger" id="d-del">🗑 Forget</button>' | |
| +'</div>'; | |
| document.getElementById('d-edit').addEventListener('click',function(){openModal(m);}); | |
| document.getElementById('d-del').addEventListener('click',function(){forgetMem(m.id);}); | |
| } | |
| function forgetMem(id){ | |
| if(!confirm('Forget this memory permanently?'))return; | |
| fetch('/api/memories/'+id,{method:'DELETE'}).then(function(){ | |
| toast('Memory forgotten');ACTIVE_ID=null; | |
| document.getElementById('detail-empty').style.display='flex'; | |
| document.getElementById('detail-content').style.display='none'; | |
| loadMems(); | |
| }).catch(function(){toast('Error',true);}); | |
| } | |
| // Filters | |
| function setFilter(f){ | |
| FILTER=f; | |
| document.getElementById('f-all').className='tc'+(f=='all'?' on':''); | |
| document.getElementById('f-ep').className='tc'+(f=='episodic'?' ep-on':''); | |
| document.getElementById('f-se').className='tc'+(f=='semantic'?' se-on':''); | |
| document.getElementById('f-pr').className='tc'+(f=='procedural'?' pr-on':''); | |
| document.getElementById('f-wk').className='tc'+(f=='working'?' wk-on':''); | |
| loadMems(); | |
| } | |
| document.getElementById('f-all').addEventListener('click',function(){setFilter('all');}); | |
| document.getElementById('f-ep').addEventListener('click',function(){setFilter('episodic');}); | |
| document.getElementById('f-se').addEventListener('click',function(){setFilter('semantic');}); | |
| document.getElementById('f-pr').addEventListener('click',function(){setFilter('procedural');}); | |
| document.getElementById('f-wk').addEventListener('click',function(){setFilter('working');}); | |
| document.getElementById('search-btn').addEventListener('click',function(){ | |
| SEARCH_Q=document.getElementById('search-input').value.trim();loadMems();}); | |
| document.getElementById('search-input').addEventListener('keydown',function(e){ | |
| if(e.key=='Enter'){SEARCH_Q=this.value.trim();loadMems();} | |
| if(e.key=='Escape'){this.value='';SEARCH_Q='';loadMems();} | |
| }); | |
| // Modal | |
| function openModal(mem){ | |
| document.getElementById('mdl-title').textContent=mem?'EDIT MEMORY':'STORE MEMORY'; | |
| document.getElementById('eid').value=mem?mem.id:''; | |
| document.getElementById('fc').value=mem?mem.content:''; | |
| document.getElementById('fs').value=mem?mem.summary:''; | |
| document.getElementById('ftier').value=mem?mem.tier:'episodic'; | |
| document.getElementById('fimp').value=mem?mem.importance:5; | |
| document.getElementById('fagent').value=mem?mem.agent:''; | |
| document.getElementById('ftags').value=mem?(mem.tags||[]).join(', '):''; | |
| document.getElementById('fttl').value='3600'; | |
| document.getElementById('modal').classList.add('open'); | |
| setTimeout(function(){document.getElementById('fc').focus();},80); | |
| } | |
| function closeModal(){document.getElementById('modal').classList.remove('open');} | |
| document.getElementById('btn-new').addEventListener('click',function(){openModal();}); | |
| document.getElementById('mdl-close').addEventListener('click',closeModal); | |
| document.getElementById('btn-cancel').addEventListener('click',closeModal); | |
| document.getElementById('modal').addEventListener('click',function(e){if(e.target===this)closeModal();}); | |
| document.getElementById('btn-save').addEventListener('click',function(){ | |
| var content=document.getElementById('fc').value.trim(); | |
| if(!content){document.getElementById('fc').focus();toast('Content required',true);return;} | |
| var id=document.getElementById('eid').value; | |
| var tags=document.getElementById('ftags').value.split(',').map(function(t){return t.trim();}).filter(Boolean); | |
| var pay={content:content,summary:document.getElementById('fs').value.trim(), | |
| tier:document.getElementById('ftier').value, | |
| importance:parseInt(document.getElementById('fimp').value)||5, | |
| agent:document.getElementById('fagent').value.trim(), | |
| tags:tags,ttl:parseInt(document.getElementById('fttl').value)||3600}; | |
| fetch(id?'/api/memories/'+id:'/api/memories', | |
| {method:id?'PATCH':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(pay)}) | |
| .then(function(r){if(!r.ok)throw new Error(r.status);return r.json();}) | |
| .then(function(d){ | |
| toast(id?'Memory updated':'Memory stored');closeModal();loadMems(); | |
| if(d.memory||d.id) setTimeout(function(){selectMem(d.id||d.memory.id);},200); | |
| }).catch(function(e){toast('Error: '+e.message,true);}); | |
| }); | |
| document.addEventListener('keydown',function(e){ | |
| if(e.key=='Escape')closeModal(); | |
| var active=document.activeElement; | |
| var typing=active&&(active.tagName=='INPUT'||active.tagName=='TEXTAREA'||active.tagName=='SELECT'); | |
| if(e.key=='n'&&!typing&&!e.ctrlKey&&!e.metaKey)openModal(); | |
| if((e.ctrlKey||e.metaKey)&&e.key=='Enter'&&document.getElementById('modal').classList.contains('open')) | |
| document.getElementById('btn-save').click(); | |
| if(e.key=='/'&&!typing){e.preventDefault();document.getElementById('search-input').focus();} | |
| }); | |
| loadMems(); | |
| </script> | |
| </body> | |
| </html>""" |