Chris4K commited on
Commit
cdb66db
·
verified ·
1 Parent(s): 9a3378c

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +10 -0
  2. README.md +65 -0
  3. main.py +787 -0
  4. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ RUN useradd -m -u 1000 user
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+ COPY . .
7
+ RUN mkdir -p memories && chown -R user:user /app
8
+ USER user
9
+ EXPOSE 7860
10
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Agent Memory
3
+ emoji: 🧠
4
+ colorFrom: purple
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: Multi-tier Memory MCP Server for AI Agents
9
+ ---
10
+
11
+ # 🧠 AGENT MEMORY — Multi-tier Memory MCP
12
+
13
+ Memory tiers: **episodic** · **semantic** · **procedural** · **working** (TTL)
14
+
15
+ ## MCP — Claude Desktop
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "agent-memory": {
20
+ "command": "npx",
21
+ "args": ["-y", "mcp-remote", "https://chris4k-agent-memory.hf.space/mcp/sse"]
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## MCP Tools
28
+ | Tool | Description |
29
+ |---|---|
30
+ | `memory_store` | Store a memory with tier, tags, importance |
31
+ | `memory_search` | Full-text search across all memories |
32
+ | `memory_recall` | Get memory by ID |
33
+ | `memory_update` | Update content, tags, importance |
34
+ | `memory_forget` | Delete a memory |
35
+ | `memory_list` | List by tier/tag/agent |
36
+ | `memory_stats` | Counts per tier |
37
+
38
+ ## REST API
39
+ ```
40
+ GET /api/memories?tier=semantic&tag=jarvis
41
+ GET /api/memories/search?q=GDPR&tier=all
42
+ GET /api/memories/{id}
43
+ POST /api/memories {"content":"...","tier":"episodic","tags":[],"importance":7}
44
+ PATCH /api/memories/{id}
45
+ DELETE /api/memories/{id}
46
+ GET /api/stats
47
+ ```
48
+
49
+ ## Agent Usage
50
+ ```python
51
+ import requests
52
+ BASE = "https://chris4k-agent-memory.hf.space"
53
+
54
+ # Store a memory
55
+ requests.post(f"{BASE}/api/memories", json={
56
+ "content": "User prefers async Python with FastAPI for all backend services.",
57
+ "tier": "semantic", "tags": ["preference","python"], "agent": "jarvis", "importance": 8
58
+ })
59
+
60
+ # Search
61
+ r = requests.get(f"{BASE}/api/memories/search?q=FastAPI&limit=5")
62
+ print(r.json())
63
+ ```
64
+
65
+ *Chris4K · ki-fusion-labs.de*
main.py ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AGENT MEMORY — Multi-tier Memory MCP Server
3
+ Docker SDK on HF Spaces — no Gradio, no CSP issues.
4
+
5
+ Memory tiers:
6
+ episodic — events, experiences, conversations
7
+ semantic — facts, knowledge, concepts
8
+ procedural — skills, how-tos, workflows
9
+ working — short-term scratch pad (TTL default 1h)
10
+
11
+ MCP tools:
12
+ memory_store — write a memory
13
+ memory_search — full-text + tag search
14
+ memory_recall — get by id
15
+ memory_update — update content/tags
16
+ memory_forget — delete
17
+ memory_list — list by tier/tag
18
+ memory_stats — counts per tier
19
+ """
20
+ import os, uuid, json, asyncio, time, re
21
+ from pathlib import Path
22
+ from datetime import datetime, timezone, timedelta
23
+ from typing import Optional
24
+
25
+ from fastapi import FastAPI, HTTPException, Request
26
+ from fastapi.responses import JSONResponse, HTMLResponse, StreamingResponse
27
+
28
+ BASE = Path(__file__).parent
29
+ MEM_DIR = BASE / "memories"
30
+ MEM_DIR.mkdir(exist_ok=True)
31
+
32
+ TIERS = ["episodic", "semantic", "procedural", "working"]
33
+
34
+ # ── Memory utils ──────────────────────────────────────────────────
35
+
36
+ def now_ts(): return int(time.time())
37
+ def now_iso(): return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
38
+
39
+ def mem_path(mid): return MEM_DIR / f"{mid}.json"
40
+
41
+ def read_mem(mid):
42
+ p = mem_path(mid)
43
+ return json.loads(p.read_text()) if p.exists() else None
44
+
45
+ def write_mem(m):
46
+ m["updated_at"] = now_ts()
47
+ mem_path(m["id"]).write_text(json.dumps(m, indent=2, ensure_ascii=False))
48
+
49
+ def new_memory(data: dict) -> dict:
50
+ mid = uuid.uuid4().hex[:10]
51
+ tier = data.get("tier", "episodic")
52
+ if tier not in TIERS: tier = "episodic"
53
+ ttl = int(data.get("ttl", 3600)) if tier == "working" else None
54
+ m = {
55
+ "id": mid,
56
+ "tier": tier,
57
+ "content": (data.get("content") or "").strip(),
58
+ "summary": (data.get("summary") or "").strip(),
59
+ "tags": [t.strip().lower() for t in data.get("tags", []) if str(t).strip()],
60
+ "agent": (data.get("agent") or "").strip(),
61
+ "importance": max(0, min(10, int(data.get("importance", 5)))),
62
+ "source": (data.get("source") or "").strip(),
63
+ "expires_at": now_ts() + ttl if ttl else None,
64
+ "created_at": now_ts(),
65
+ "updated_at": now_ts(),
66
+ "access_count": 0,
67
+ "last_accessed": None,
68
+ }
69
+ write_mem(m)
70
+ return m
71
+
72
+ def all_memories(include_expired=False):
73
+ now = now_ts()
74
+ out = []
75
+ for p in sorted(MEM_DIR.glob("*.json"), reverse=True):
76
+ try:
77
+ m = json.loads(p.read_text())
78
+ if not include_expired and m.get("expires_at") and m["expires_at"] < now:
79
+ p.unlink() # auto-expire
80
+ continue
81
+ out.append(m)
82
+ except:
83
+ pass
84
+ return out
85
+
86
+ def search_memories(query: str, tier: str = "all", tag: str = "", agent: str = "", limit: int = 20):
87
+ mems = all_memories()
88
+ q = query.lower().strip()
89
+ results = []
90
+ for m in mems:
91
+ if tier != "all" and m["tier"] != tier: continue
92
+ if tag and tag.lower() not in m["tags"]: continue
93
+ if agent and m["agent"].lower() != agent.lower(): continue
94
+ # score: content match + tag match + importance
95
+ score = 0
96
+ text = (m["content"] + " " + m["summary"] + " " + " ".join(m["tags"])).lower()
97
+ if q:
98
+ words = q.split()
99
+ for w in words:
100
+ if w in text: score += 2
101
+ if q in text: score += 5
102
+ score += m.get("importance", 5) * 0.2
103
+ if not q or score > 0:
104
+ results.append((score, m))
105
+ results.sort(key=lambda x: (-x[0], -x[1].get("created_at", 0)))
106
+ return [m for _, m in results[:limit]]
107
+
108
+ def bump_access(mid):
109
+ m = read_mem(mid)
110
+ if m:
111
+ m["access_count"] = m.get("access_count", 0) + 1
112
+ m["last_accessed"] = now_ts()
113
+ write_mem(m)
114
+ return m
115
+
116
+ # ── Seed ──────────────────────────────────────────────────────────
117
+
118
+ def seed():
119
+ if list(MEM_DIR.glob("*.json")): return
120
+ samples = [
121
+ {"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},
122
+ {"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},
123
+ {"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},
124
+ {"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},
125
+ {"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},
126
+ {"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},
127
+ {"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},
128
+ ]
129
+ for s in samples:
130
+ new_memory(s)
131
+
132
+ seed()
133
+
134
+ # ── FastAPI ───────────────────────────────────────────────────────
135
+
136
+ app = FastAPI(title="Agent Memory MCP")
137
+
138
+ def jresp(data, status=200): return JSONResponse(content=data, status_code=status)
139
+
140
+ # REST API
141
+
142
+ @app.get("/api/memories")
143
+ async def list_memories(tier: str = "all", tag: str = "", agent: str = "", limit: int = 50):
144
+ mems = all_memories()
145
+ if tier != "all": mems = [m for m in mems if m["tier"] == tier]
146
+ if tag: mems = [m for m in mems if tag.lower() in m["tags"]]
147
+ if agent: mems = [m for m in mems if m["agent"].lower() == agent.lower()]
148
+ mems.sort(key=lambda m: (-m.get("importance",5), -m.get("created_at",0)))
149
+ return jresp(mems[:limit])
150
+
151
+ @app.get("/api/memories/search")
152
+ async def search(q: str = "", tier: str = "all", tag: str = "", agent: str = "", limit: int = 20):
153
+ return jresp(search_memories(q, tier, tag, agent, limit))
154
+
155
+ @app.get("/api/memories/{mid}")
156
+ async def get_memory(mid: str):
157
+ m = bump_access(mid)
158
+ if not m: raise HTTPException(404, "not found")
159
+ return jresp(m)
160
+
161
+ @app.post("/api/memories")
162
+ async def store_memory(request: Request):
163
+ data = await request.json()
164
+ if not data.get("content","").strip():
165
+ raise HTTPException(400, "content required")
166
+ m = new_memory(data)
167
+ return jresp({"status":"stored","id":m["id"],"memory":m}, 201)
168
+
169
+ @app.patch("/api/memories/{mid}")
170
+ async def update_memory(mid: str, request: Request):
171
+ data = await request.json()
172
+ m = read_mem(mid)
173
+ if not m: raise HTTPException(404)
174
+ for k in ("content","summary","tags","importance","tier","source","agent"):
175
+ if k in data: m[k] = data[k]
176
+ write_mem(m)
177
+ return jresp({"status":"updated","memory":m})
178
+
179
+ @app.delete("/api/memories/{mid}")
180
+ async def delete_memory(mid: str):
181
+ p = mem_path(mid)
182
+ if not p.exists(): raise HTTPException(404)
183
+ p.unlink()
184
+ return jresp({"status":"forgotten"})
185
+
186
+ @app.get("/api/stats")
187
+ async def stats():
188
+ mems = all_memories()
189
+ by_tier = {t: 0 for t in TIERS}
190
+ by_agent: dict = {}
191
+ total_importance = 0
192
+ for m in mems:
193
+ by_tier[m["tier"]] = by_tier.get(m["tier"], 0) + 1
194
+ a = m.get("agent") or "unknown"
195
+ by_agent[a] = by_agent.get(a, 0) + 1
196
+ total_importance += m.get("importance", 5)
197
+ return jresp({
198
+ "total": len(mems),
199
+ "by_tier": by_tier,
200
+ "by_agent": by_agent,
201
+ "avg_importance": round(total_importance / len(mems), 1) if mems else 0,
202
+ })
203
+
204
+ # ── MCP ───────────────────────────────────────────────────────────
205
+
206
+ MCP_TOOLS = [
207
+ {"name":"memory_store",
208
+ "description":"Store a new memory. Tier: episodic|semantic|procedural|working. Importance 0-10.",
209
+ "inputSchema":{"type":"object","required":["content"],"properties":{
210
+ "content": {"type":"string","description":"Memory content"},
211
+ "summary": {"type":"string","description":"Short summary"},
212
+ "tier": {"type":"string","enum":["episodic","semantic","procedural","working"]},
213
+ "tags": {"type":"array","items":{"type":"string"}},
214
+ "agent": {"type":"string","description":"Agent or user ID"},
215
+ "importance":{"type":"integer","minimum":0,"maximum":10},
216
+ "source": {"type":"string"},
217
+ "ttl": {"type":"integer","description":"TTL in seconds (working tier only)"},
218
+ }}},
219
+ {"name":"memory_search",
220
+ "description":"Search memories by content, tier, or tag. Returns ranked results.",
221
+ "inputSchema":{"type":"object","properties":{
222
+ "query": {"type":"string"},
223
+ "tier": {"type":"string","enum":["all","episodic","semantic","procedural","working"]},
224
+ "tag": {"type":"string"},
225
+ "agent": {"type":"string"},
226
+ "limit": {"type":"integer","default":10},
227
+ }}},
228
+ {"name":"memory_recall",
229
+ "description":"Retrieve a specific memory by ID.",
230
+ "inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}},
231
+ {"name":"memory_update",
232
+ "description":"Update an existing memory's content, tags, or importance.",
233
+ "inputSchema":{"type":"object","required":["id"],"properties":{
234
+ "id": {"type":"string"},
235
+ "content": {"type":"string"},
236
+ "summary": {"type":"string"},
237
+ "tags": {"type":"array","items":{"type":"string"}},
238
+ "importance":{"type":"integer"},
239
+ }}},
240
+ {"name":"memory_forget",
241
+ "description":"Delete a memory permanently.",
242
+ "inputSchema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}},
243
+ {"name":"memory_list",
244
+ "description":"List memories by tier or tag.",
245
+ "inputSchema":{"type":"object","properties":{
246
+ "tier": {"type":"string"},
247
+ "tag": {"type":"string"},
248
+ "agent": {"type":"string"},
249
+ "limit": {"type":"integer","default":20},
250
+ }}},
251
+ {"name":"memory_stats",
252
+ "description":"Get memory statistics (counts per tier, agents, avg importance).",
253
+ "inputSchema":{"type":"object","properties":{}}},
254
+ ]
255
+
256
+ async def mcp_call(name, args):
257
+ if name == "memory_store":
258
+ if not args.get("content","").strip():
259
+ return json.dumps({"error":"content required"})
260
+ m = new_memory(args)
261
+ return json.dumps({"stored":m["id"],"tier":m["tier"],"memory":m})
262
+ if name == "memory_search":
263
+ results = search_memories(
264
+ args.get("query",""), args.get("tier","all"),
265
+ args.get("tag",""), args.get("agent",""), args.get("limit",10))
266
+ return json.dumps({"count":len(results),"results":results})
267
+ if name == "memory_recall":
268
+ m = bump_access(args["id"])
269
+ return json.dumps(m or {"error":"not found"})
270
+ if name == "memory_update":
271
+ m = read_mem(args["id"])
272
+ if not m: return json.dumps({"error":"not found"})
273
+ for k in ("content","summary","tags","importance","tier"):
274
+ if k in args: m[k] = args[k]
275
+ write_mem(m); return json.dumps({"updated":m["id"],"memory":m})
276
+ if name == "memory_forget":
277
+ p = mem_path(args["id"])
278
+ if not p.exists(): return json.dumps({"error":"not found"})
279
+ p.unlink(); return json.dumps({"forgotten":args["id"]})
280
+ if name == "memory_list":
281
+ mems = all_memories()
282
+ t = args.get("tier","all"); tg = args.get("tag",""); ag = args.get("agent","")
283
+ if t != "all": mems = [m for m in mems if m["tier"]==t]
284
+ if tg: mems = [m for m in mems if tg.lower() in m["tags"]]
285
+ if ag: mems = [m for m in mems if m["agent"].lower()==ag.lower()]
286
+ mems.sort(key=lambda m: (-m.get("importance",5), -m.get("created_at",0)))
287
+ return json.dumps({"count":len(mems),"memories":mems[:args.get("limit",20)]})
288
+ if name == "memory_stats":
289
+ mems = all_memories()
290
+ by_tier = {t:0 for t in TIERS}
291
+ for m in mems: by_tier[m["tier"]] = by_tier.get(m["tier"],0)+1
292
+ return json.dumps({"total":len(mems),"by_tier":by_tier})
293
+ return json.dumps({"error":f"unknown tool: {name}"})
294
+
295
+ @app.get("/mcp/sse")
296
+ async def mcp_sse():
297
+ async def stream():
298
+ init = {"jsonrpc":"2.0","method":"notifications/initialized",
299
+ "params":{"serverInfo":{"name":"agent-memory","version":"1.0"},"capabilities":{"tools":{}}}}
300
+ yield f"data: {json.dumps(init)}\n\n"
301
+ await asyncio.sleep(0.1)
302
+ yield f"data: {json.dumps({'jsonrpc':'2.0','method':'notifications/tools/list_changed','params':{}})}\n\n"
303
+ while True:
304
+ await asyncio.sleep(25)
305
+ yield f"data: {json.dumps({'jsonrpc':'2.0','method':'ping'})}\n\n"
306
+ return StreamingResponse(stream(), media_type="text/event-stream",
307
+ headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
308
+
309
+ @app.post("/mcp")
310
+ async def mcp_rpc(request: Request):
311
+ body = await request.json()
312
+ method = body.get("method",""); rid = body.get("id",1)
313
+ if method == "initialize":
314
+ return jresp({"jsonrpc":"2.0","id":rid,"result":{
315
+ "serverInfo":{"name":"agent-memory","version":"1.0"},"capabilities":{"tools":{}}}})
316
+ if method == "tools/list":
317
+ return jresp({"jsonrpc":"2.0","id":rid,"result":{"tools":MCP_TOOLS}})
318
+ if method == "tools/call":
319
+ p = body.get("params",{})
320
+ res = await mcp_call(p.get("name",""), p.get("arguments",{}))
321
+ return jresp({"jsonrpc":"2.0","id":rid,"result":{"content":[{"type":"text","text":res}]}})
322
+ return jresp({"jsonrpc":"2.0","id":rid,"error":{"code":-32601,"message":"Method not found"}})
323
+
324
+ # ── SPA ───────────────────────────────────────────────────────────
325
+
326
+ @app.get("/", response_class=HTMLResponse)
327
+ async def serve_ui():
328
+ return HTMLResponse(SPA)
329
+
330
+ SPA = """<!DOCTYPE html>
331
+ <html lang="en">
332
+ <head>
333
+ <meta charset="UTF-8">
334
+ <meta name="viewport" content="width=device-width,initial-scale=1">
335
+ <title>AGENT MEMORY</title>
336
+ <link rel="preconnect" href="https://fonts.googleapis.com">
337
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
338
+ <style>
339
+ :root{
340
+ --bg:#0a0a0f;--s1:#111118;--s2:#161625;--bd:#1e1e2e;--bd2:#252540;
341
+ --acc:#ff6b00;--acc2:#ff9500;--txt:#d8d8f0;--sub:#5a5a80;--dim:#2a2a50;
342
+ --ep:#0ea5e9;--se:#7c3aed;--pr:#2ed573;--wk:#f0c040;
343
+ --cr:#ff2244;--font:'Space Mono',monospace;
344
+ }
345
+ *{box-sizing:border-box;margin:0;padding:0;}
346
+ html,body{height:100%;overflow:hidden;}
347
+ body{font-family:var(--font);background:var(--bg);color:var(--txt);
348
+ display:flex;flex-direction:column;height:100vh;}
349
+ body::after{content:'';position:fixed;inset:0;pointer-events:none;
350
+ background-image:repeating-linear-gradient(0deg,transparent,transparent 3px,
351
+ rgba(255,107,0,.007) 3px,rgba(255,107,0,.007) 4px);}
352
+
353
+ /* HEADER */
354
+ #hdr{flex-shrink:0;display:flex;align-items:center;padding:.85rem 1.8rem;gap:1.2rem;
355
+ border-bottom:1px solid var(--bd);background:linear-gradient(180deg,#0e0e1e,var(--bg));z-index:10;}
356
+ #logo{font-size:1.3rem;font-weight:700;letter-spacing:2px;
357
+ background:linear-gradient(90deg,var(--acc),var(--acc2));
358
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
359
+ #logo-sub{font-size:.52rem;color:var(--sub);letter-spacing:.28em;text-transform:uppercase;margin-top:2px;}
360
+ #stat-bar{display:flex;gap:.55rem;flex:1;flex-wrap:wrap;}
361
+ .sp{display:flex;align-items:center;gap:.4rem;background:var(--s1);border:1px solid var(--bd);
362
+ border-radius:5px;padding:.26rem .6rem;font-size:.58rem;color:var(--sub);}
363
+ .sp-n{font-size:.95rem;font-weight:700;line-height:1;}
364
+ .sp-n.ep{color:var(--ep);}.sp-n.se{color:var(--se);}
365
+ .sp-n.pr{color:var(--pr);}.sp-n.wk{color:var(--wk);}
366
+ #btn-new{background:var(--acc);color:#000;border:none;padding:.42rem 1rem;
367
+ font-family:var(--font);font-size:.68rem;font-weight:700;letter-spacing:.1em;
368
+ text-transform:uppercase;border-radius:4px;cursor:pointer;white-space:nowrap;flex-shrink:0;
369
+ transition:background .12s,transform .1s;}
370
+ #btn-new:hover{background:var(--acc2);transform:translateY(-1px);}
371
+
372
+ /* TOOLBAR */
373
+ #toolbar{flex-shrink:0;display:flex;gap:.5rem;align-items:center;padding:.5rem 1.8rem;
374
+ border-bottom:1px solid var(--bd);background:var(--s1);z-index:10;flex-wrap:wrap;}
375
+ #search-input{background:var(--s2);border:1px solid var(--bd2);border-radius:5px;
376
+ padding:.38rem .7rem;font-family:var(--font);font-size:.7rem;color:var(--txt);
377
+ outline:none;width:220px;transition:border-color .12s;}
378
+ #search-input:focus{border-color:var(--acc);}
379
+ #search-btn{background:var(--acc);color:#000;border:none;padding:.38rem .7rem;
380
+ font-family:var(--font);font-size:.65rem;font-weight:700;border-radius:4px;cursor:pointer;}
381
+ .fsep{color:var(--bd2);}
382
+ .tc{background:var(--s2);border:1px solid var(--bd2);border-radius:20px;padding:2px 9px;
383
+ cursor:pointer;color:var(--sub);font-family:var(--font);font-size:.55rem;
384
+ transition:all .12s;user-select:none;}
385
+ .tc.on{border-color:var(--acc);color:var(--acc);background:#160b00;}
386
+ .tc.ep-on{border-color:var(--ep);color:var(--ep);background:#050c18;}
387
+ .tc.se-on{border-color:var(--se);color:var(--se);background:#110820;}
388
+ .tc.pr-on{border-color:var(--pr);color:var(--pr);background:#02130a;}
389
+ .tc.wk-on{border-color:var(--wk);color:var(--wk);background:#181400;}
390
+
391
+ /* MAIN LAYOUT */
392
+ #main{flex:1;display:flex;gap:0;min-height:0;overflow:hidden;}
393
+
394
+ /* MEMORY LIST */
395
+ #list-panel{width:400px;flex-shrink:0;border-right:1px solid var(--bd);
396
+ display:flex;flex-direction:column;overflow:hidden;}
397
+ #list-scroll{flex:1;overflow-y:auto;padding:.6rem;}
398
+ #list-scroll::-webkit-scrollbar{width:4px;}
399
+ #list-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;}
400
+ #list-empty{font-size:.62rem;color:var(--dim);text-align:center;padding:2rem;
401
+ border:1px dashed var(--bd);border-radius:6px;margin:.5rem;}
402
+
403
+ /* MEMORY CARD */
404
+ .mc{background:var(--s1);border:1px solid var(--bd);border-radius:8px;
405
+ padding:.65rem .8rem;margin-bottom:.42rem;cursor:pointer;position:relative;
406
+ transition:border-color .12s,transform .08s;animation:cin .15s ease;
407
+ padding-left:1rem;}
408
+ @keyframes cin{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
409
+ .mc:hover{border-color:var(--bd2);transform:translateY(-1px);}
410
+ .mc.active{border-color:var(--acc);background:var(--s2);}
411
+ .mc::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px;}
412
+ .mc.ep::before{background:var(--ep);}.mc.se::before{background:var(--se);}
413
+ .mc.pr::before{background:var(--pr);}.mc.wk::before{background:var(--wk);}
414
+ .mc-top{display:flex;align-items:flex-start;gap:.4rem;margin-bottom:.3rem;}
415
+ .mc-tier{font-size:.48rem;padding:1px 5px;border-radius:3px;text-transform:uppercase;
416
+ letter-spacing:.1em;font-weight:700;flex-shrink:0;margin-top:1px;}
417
+ .ep .mc-tier{background:#050c18;color:var(--ep);border:1px solid rgba(14,165,233,.15);}
418
+ .se .mc-tier{background:#110820;color:var(--se);border:1px solid rgba(124,58,237,.15);}
419
+ .pr .mc-tier{background:#02130a;color:var(--pr);border:1px solid rgba(46,213,115,.15);}
420
+ .wk .mc-tier{background:#181400;color:var(--wk);border:1px solid rgba(240,192,64,.15);}
421
+ .mc-title{flex:1;font-size:.72rem;font-weight:700;color:var(--txt);line-height:1.3;word-break:break-word;}
422
+ .mc-imp{font-size:.58rem;color:var(--sub);}
423
+ .mc-body{font-size:.61rem;color:var(--sub);line-height:1.48;
424
+ max-height:48px;overflow:hidden;position:relative;margin-bottom:.32rem;}
425
+ .mc-body::after{content:'';position:absolute;bottom:0;left:0;right:0;height:14px;
426
+ background:linear-gradient(transparent,var(--s1));}
427
+ .mc.active .mc-body::after{background:linear-gradient(transparent,var(--s2));}
428
+ .mc-foot{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;}
429
+ .mc-tag{font-size:.5rem;background:var(--bd2);border:1px solid var(--bd);
430
+ border-radius:3px;padding:0 4px;color:var(--sub);}
431
+ .mc-agent{font-size:.52rem;color:var(--sub);margin-left:auto;}
432
+ .mc-agent .at{color:var(--ai,#7c3aed);}
433
+ .mc-date{font-size:.49rem;color:var(--dim);}
434
+
435
+ /* DETAIL PANEL */
436
+ #detail-panel{flex:1;display:flex;flex-direction:column;overflow:hidden;}
437
+ #detail-scroll{flex:1;overflow-y:auto;padding:1.4rem 1.8rem;}
438
+ #detail-scroll::-webkit-scrollbar{width:4px;}
439
+ #detail-scroll::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px;}
440
+ #detail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;
441
+ height:100%;gap:.8rem;color:var(--sub);}
442
+ #detail-empty .big{font-size:2.5rem;opacity:.2;}
443
+ #detail-empty .msg{font-size:.65rem;opacity:.4;letter-spacing:.1em;text-transform:uppercase;}
444
+ #detail-content{display:none;}
445
+ .d-tier-badge{display:inline-flex;align-items:center;gap:.5rem;
446
+ font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.15em;
447
+ padding:.3rem .7rem;border-radius:4px;margin-bottom:.8rem;}
448
+ .d-tier-badge.ep{background:#050c18;color:var(--ep);border:1px solid rgba(14,165,233,.2);}
449
+ .d-tier-badge.se{background:#110820;color:var(--se);border:1px solid rgba(124,58,237,.2);}
450
+ .d-tier-badge.pr{background:#02130a;color:var(--pr);border:1px solid rgba(46,213,115,.2);}
451
+ .d-tier-badge.wk{background:#181400;color:var(--wk);border:1px solid rgba(240,192,64,.2);}
452
+ #d-title{font-size:1.05rem;font-weight:700;color:var(--txt);line-height:1.4;margin-bottom:.6rem;word-break:break-word;}
453
+ #d-content{font-size:.78rem;color:var(--txt);line-height:1.7;margin-bottom:1rem;
454
+ background:var(--s1);border:1px solid var(--bd);border-radius:7px;padding:1rem;white-space:pre-wrap;}
455
+ .d-meta{display:grid;grid-template-columns:1fr 1fr;gap:.5rem .8rem;margin-bottom:1rem;}
456
+ .d-meta-item{font-size:.6rem;}
457
+ .d-meta-label{color:var(--sub);text-transform:uppercase;letter-spacing:.1em;font-size:.5rem;margin-bottom:.18rem;}
458
+ .d-meta-val{color:var(--txt);}
459
+ .d-tags{display:flex;flex-wrap:wrap;gap:.3rem;margin-bottom:1rem;}
460
+ .d-tag{background:var(--s2);border:1px solid var(--bd2);border-radius:4px;
461
+ padding:2px 8px;font-size:.58rem;color:var(--sub);}
462
+ .imp-bar{height:4px;background:var(--bd2);border-radius:2px;margin-top:.2rem;}
463
+ .imp-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--lo,#2ed573),var(--acc));}
464
+ .d-actions{display:flex;gap:.5rem;margin-top:.8rem;}
465
+ .d-btn{background:var(--s2);border:1px solid var(--bd2);color:var(--sub);
466
+ padding:.38rem .75rem;font-family:var(--font);font-size:.62rem;border-radius:4px;
467
+ cursor:pointer;transition:all .1s;}
468
+ .d-btn:hover{background:var(--bd2);color:var(--txt);}
469
+ .d-btn.danger:hover{background:#1e0508;color:var(--cr);border-color:#5a1020;}
470
+
471
+ /* MODAL */
472
+ #modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:100;
473
+ backdrop-filter:blur(4px);align-items:center;justify-content:center;}
474
+ #modal.open{display:flex;}
475
+ .mdl{background:var(--s1);border:1px solid var(--bd2);border-top:2px solid var(--acc);
476
+ border-radius:12px;padding:1.5rem;width:560px;max-width:97vw;max-height:92vh;
477
+ overflow-y:auto;animation:mdin .17s ease;position:relative;}
478
+ @keyframes mdin{from{opacity:0;transform:scale(.96) translateY(-8px)}to{opacity:1;transform:none}}
479
+ #mdl-title{font-size:.88rem;font-weight:700;letter-spacing:3px;color:var(--acc);margin-bottom:1rem;}
480
+ #mdl-close{position:absolute;top:.85rem;right:.85rem;background:none;border:none;color:var(--sub);
481
+ width:26px;height:26px;border-radius:4px;cursor:pointer;font-size:.85rem;
482
+ display:flex;align-items:center;justify-content:center;transition:all .1s;}
483
+ #mdl-close:hover{background:var(--bd2);color:var(--txt);}
484
+ .fg{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;}
485
+ .fl{margin-bottom:.72rem;}
486
+ .fl label{display:block;font-size:.52rem;color:var(--sub);text-transform:uppercase;
487
+ letter-spacing:.14em;margin-bottom:.25rem;}
488
+ .fl input,.fl textarea,.fl select{width:100%;background:var(--s2);border:1px solid var(--bd2);
489
+ border-radius:5px;padding:.44rem .62rem;font-family:var(--font);font-size:.72rem;color:var(--txt);
490
+ outline:none;transition:border-color .12s;}
491
+ .fl input:focus,.fl textarea:focus,.fl select:focus{border-color:var(--acc);}
492
+ .fl textarea{min-height:120px;line-height:1.6;resize:vertical;}
493
+ .fl select option{background:var(--s2);}
494
+ #mdl-actions{display:flex;gap:.48rem;margin-top:.95rem;}
495
+ #btn-save{flex:1;background:var(--acc);color:#000;border:none;padding:.5rem 1rem;
496
+ font-family:var(--font);font-size:.68rem;font-weight:700;letter-spacing:.1em;
497
+ text-transform:uppercase;border-radius:5px;cursor:pointer;transition:background .1s;}
498
+ #btn-save:hover{background:var(--acc2);}
499
+ #btn-cancel{background:var(--s2);color:var(--sub);border:1px solid var(--bd2);padding:.5rem 1rem;
500
+ font-family:var(--font);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;
501
+ border-radius:5px;cursor:pointer;transition:all .1s;}
502
+ #btn-cancel:hover{background:var(--bd2);color:var(--txt);}
503
+
504
+ /* TOASTS */
505
+ #toasts{position:fixed;bottom:1rem;right:1rem;z-index:200;display:flex;flex-direction:column;gap:.38rem;}
506
+ .tst{background:var(--s1);border:1px solid var(--bd2);border-left:3px solid var(--acc);
507
+ padding:.45rem .82rem;font-size:.63rem;border-radius:6px;animation:tin .16s ease;
508
+ color:var(--txt);max-width:260px;}
509
+ .tst.err{border-left-color:var(--cr);}
510
+ @keyframes tin{from{opacity:0;transform:translateX(12px)}to{opacity:1;transform:none}}
511
+ #mcp-hint{position:fixed;bottom:1rem;left:1rem;z-index:10;background:var(--s1);
512
+ border:1px solid var(--bd2);border-left:3px solid var(--se);border-radius:6px;
513
+ padding:.42rem .82rem;font-size:.55rem;color:var(--sub);}
514
+ #mcp-hint code{color:var(--se);}
515
+ </style>
516
+ </head>
517
+ <body>
518
+
519
+ <div id="hdr">
520
+ <div>
521
+ <div id="logo">AGENT MEMORY</div>
522
+ <div id="logo-sub">Multi-tier Memory MCP &middot; ki-fusion-labs.de</div>
523
+ </div>
524
+ <div id="stat-bar">
525
+ <div class="sp"><span class="sp-n ep" id="s-ep">0</span>EPISODIC</div>
526
+ <div class="sp"><span class="sp-n se" id="s-se">0</span>SEMANTIC</div>
527
+ <div class="sp"><span class="sp-n pr" id="s-pr">0</span>PROCEDURAL</div>
528
+ <div class="sp"><span class="sp-n wk" id="s-wk">0</span>WORKING</div>
529
+ </div>
530
+ <button id="btn-new">+ Store Memory</button>
531
+ </div>
532
+
533
+ <div id="toolbar">
534
+ <input type="text" id="search-input" placeholder="Search memories...">
535
+ <button id="search-btn">&#128269;</button>
536
+ <span class="fsep">|</span>
537
+ <span class="tc on" id="f-all">ALL</span>
538
+ <span class="tc" id="f-ep">&#128196; Episodic</span>
539
+ <span class="tc" id="f-se">&#129504; Semantic</span>
540
+ <span class="tc" id="f-pr">&#9881;&#65039; Procedural</span>
541
+ <span class="tc" id="f-wk">&#9203; Working</span>
542
+ </div>
543
+
544
+ <div id="main">
545
+ <div id="list-panel">
546
+ <div id="list-scroll">
547
+ <div id="list-empty">No memories yet</div>
548
+ </div>
549
+ </div>
550
+ <div id="detail-panel">
551
+ <div id="detail-scroll">
552
+ <div id="detail-empty">
553
+ <div class="big">&#129504;</div>
554
+ <div class="msg">Select a memory to view</div>
555
+ </div>
556
+ <div id="detail-content"></div>
557
+ </div>
558
+ </div>
559
+ </div>
560
+
561
+ <div id="modal">
562
+ <div class="mdl">
563
+ <button id="mdl-close">&#x2715;</button>
564
+ <div id="mdl-title">STORE MEMORY</div>
565
+ <input type="hidden" id="eid">
566
+ <div class="fl"><label>Content *</label>
567
+ <textarea id="fc" placeholder="What should be remembered?"></textarea></div>
568
+ <div class="fl"><label>Summary (optional short label)</label>
569
+ <input type="text" id="fs" placeholder="One-line summary"></div>
570
+ <div class="fg">
571
+ <div class="fl"><label>Tier</label>
572
+ <select id="ftier">
573
+ <option value="episodic">&#128196; Episodic</option>
574
+ <option value="semantic">&#129504; Semantic</option>
575
+ <option value="procedural">&#9881;&#65039; Procedural</option>
576
+ <option value="working">&#9203; Working</option>
577
+ </select>
578
+ </div>
579
+ <div class="fl"><label>Importance (0-10)</label>
580
+ <input type="number" id="fimp" value="5" min="0" max="10"></div>
581
+ <div class="fl"><label>Agent / Source</label>
582
+ <input type="text" id="fagent" placeholder="christof / jarvis" list="ag-list">
583
+ <datalist id="ag-list">
584
+ <option value="christof"><option value="jarvis"><option value="research-agent">
585
+ <option value="forge-agent"><option value="monitor-agent">
586
+ </datalist>
587
+ </div>
588
+ <div class="fl"><label>TTL seconds (working only)</label>
589
+ <input type="number" id="fttl" value="3600" min="60"></div>
590
+ </div>
591
+ <div class="fl"><label>Tags (comma separated)</label>
592
+ <input type="text" id="ftags" placeholder="jarvis, architecture, memory"></div>
593
+ <div id="mdl-actions">
594
+ <button id="btn-save">&#x26A1; Save Memory</button>
595
+ <button id="btn-cancel">Cancel</button>
596
+ </div>
597
+ </div>
598
+ </div>
599
+
600
+ <div id="toasts"></div>
601
+ <div id="mcp-hint">MCP: <code>GET /mcp/sse</code> &nbsp;|&nbsp; <code>memory_store</code> <code>memory_search</code></div>
602
+
603
+ <script>
604
+ var MEMS=[], FILTER='all', ACTIVE_ID=null, SEARCH_Q='';
605
+
606
+ function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
607
+ function tsToDate(ts){if(!ts)return '';return new Date(ts*1000).toISOString().substring(0,10);}
608
+ function post(url,data){return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});}
609
+ function toast(msg,err){var el=document.createElement('div');el.className='tst'+(err?' err':'');
610
+ el.textContent=(err?'\u26a0 ':'\u26a1 ')+msg;
611
+ document.getElementById('toasts').appendChild(el);setTimeout(function(){el.remove();},2600);}
612
+
613
+ var TIER_ICONS={episodic:'\uD83D\uDCC4',semantic:'\uD83E\uDDE0',procedural:'\u2699\uFE0F',working:'\u23F3'};
614
+
615
+ function loadMems(){
616
+ var url = FILTER=='all'
617
+ ? (SEARCH_Q ? '/api/memories/search?q='+encodeURIComponent(SEARCH_Q)+'&limit=100' : '/api/memories?limit=100')
618
+ : (SEARCH_Q ? '/api/memories/search?q='+encodeURIComponent(SEARCH_Q)+'&tier='+FILTER+'&limit=100'
619
+ : '/api/memories?tier='+FILTER+'&limit=100');
620
+ fetch(url).then(function(r){return r.json();}).then(function(d){
621
+ MEMS = Array.isArray(d) ? d : (d.results || d.memories || []);
622
+ renderList();loadStats();
623
+ }).catch(function(){toast('Error loading',true);});
624
+ }
625
+
626
+ function loadStats(){
627
+ fetch('/api/stats').then(function(r){return r.json();}).then(function(s){
628
+ document.getElementById('s-ep').textContent=s.by_tier.episodic||0;
629
+ document.getElementById('s-se').textContent=s.by_tier.semantic||0;
630
+ document.getElementById('s-pr').textContent=s.by_tier.procedural||0;
631
+ document.getElementById('s-wk').textContent=s.by_tier.working||0;
632
+ }).catch(function(){});
633
+ }
634
+
635
+ function renderList(){
636
+ var scroll=document.getElementById('list-scroll');
637
+ scroll.innerHTML='';
638
+ if(!MEMS.length){
639
+ var e=document.createElement('div');e.id='list-empty';e.textContent='No memories found';
640
+ scroll.appendChild(e);return;
641
+ }
642
+ MEMS.forEach(function(m){scroll.appendChild(makeCard(m));});
643
+ }
644
+
645
+ function makeCard(m){
646
+ var card=document.createElement('div');
647
+ card.className='mc '+m.tier+(ACTIVE_ID==m.id?' active':'');
648
+ card.id='mc-'+m.id;
649
+ var title=m.summary||m.content.substring(0,60)+(m.content.length>60?'...':'');
650
+ var preview=m.content.substring(0,100);
651
+ var tags=(m.tags||[]).slice(0,3).map(function(t){return '<span class="mc-tag">'+esc(t)+'</span>';}).join('');
652
+ var agH=m.agent?'<span class="mc-agent"><span class="at">@</span>'+esc(m.agent)+'</span>':'';
653
+ card.innerHTML=
654
+ '<div class="mc-top"><span class="mc-tier">'+esc(m.tier)+'</span>'
655
+ +'<div class="mc-title">'+esc(title)+'</div>'
656
+ +'<span class="mc-imp">&#11088;'+m.importance+'</span></div>'
657
+ +(preview?'<div class="mc-body">'+esc(preview)+'</div>':'')
658
+ +'<div class="mc-foot">'+tags+agH
659
+ +'<span class="mc-date">'+tsToDate(m.created_at)+'</span></div>';
660
+ card.addEventListener('click',function(){selectMem(m.id);});
661
+ return card;
662
+ }
663
+
664
+ function selectMem(id){
665
+ ACTIVE_ID=id;
666
+ document.querySelectorAll('.mc').forEach(function(c){
667
+ c.classList.toggle('active',c.id=='mc-'+id);});
668
+ fetch('/api/memories/'+id).then(function(r){return r.json();}).then(function(m){renderDetail(m);})
669
+ .catch(function(){toast('Error loading memory',true);});
670
+ }
671
+
672
+ function renderDetail(m){
673
+ document.getElementById('detail-empty').style.display='none';
674
+ var dc=document.getElementById('detail-content');
675
+ dc.style.display='block';
676
+ var title=m.summary||m.content.substring(0,80);
677
+ var tags=(m.tags||[]).map(function(t){return '<span class="d-tag">'+esc(t)+'</span>';}).join('');
678
+ var impW=Math.round(m.importance*10)+'%';
679
+ var expires=m.expires_at?tsToDate(m.expires_at):'never';
680
+ dc.innerHTML=
681
+ '<div class="d-tier-badge '+m.tier+'">'+TIER_ICONS[m.tier]+' '+esc(m.tier)+'</div>'
682
+ +'<div id="d-title">'+esc(title)+'</div>'
683
+ +'<div id="d-content">'+esc(m.content)+'</div>'
684
+ +'<div class="d-tags">'+tags+'</div>'
685
+ +'<div class="d-meta">'
686
+ +'<div class="d-meta-item"><div class="d-meta-label">Importance</div>'
687
+ +'<div class="d-meta-val">'+m.importance+'/10<div class="imp-bar"><div class="imp-fill" style="width:'+impW+'"></div></div></div></div>'
688
+ +'<div class="d-meta-item"><div class="d-meta-label">Agent</div><div class="d-meta-val">'+(m.agent||'—')+'</div></div>'
689
+ +'<div class="d-meta-item"><div class="d-meta-label">Created</div><div class="d-meta-val">'+tsToDate(m.created_at)+'</div></div>'
690
+ +'<div class="d-meta-item"><div class="d-meta-label">Accessed</div><div class="d-meta-val">'+(m.access_count||0)+' times</div></div>'
691
+ +'<div class="d-meta-item"><div class="d-meta-label">Expires</div><div class="d-meta-val">'+expires+'</div></div>'
692
+ +'<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>'
693
+ +'</div>'
694
+ +'<div class="d-actions">'
695
+ +'<button class="d-btn" id="d-edit">&#9998; Edit</button>'
696
+ +'<button class="d-btn danger" id="d-del">&#128465; Forget</button>'
697
+ +'</div>';
698
+ document.getElementById('d-edit').addEventListener('click',function(){openModal(m);});
699
+ document.getElementById('d-del').addEventListener('click',function(){forgetMem(m.id);});
700
+ }
701
+
702
+ function forgetMem(id){
703
+ if(!confirm('Forget this memory permanently?'))return;
704
+ fetch('/api/memories/'+id,{method:'DELETE'}).then(function(){
705
+ toast('Memory forgotten');ACTIVE_ID=null;
706
+ document.getElementById('detail-empty').style.display='flex';
707
+ document.getElementById('detail-content').style.display='none';
708
+ loadMems();
709
+ }).catch(function(){toast('Error',true);});
710
+ }
711
+
712
+ // Filters
713
+ function setFilter(f){
714
+ FILTER=f;
715
+ document.getElementById('f-all').className='tc'+(f=='all'?' on':'');
716
+ document.getElementById('f-ep').className='tc'+(f=='episodic'?' ep-on':'');
717
+ document.getElementById('f-se').className='tc'+(f=='semantic'?' se-on':'');
718
+ document.getElementById('f-pr').className='tc'+(f=='procedural'?' pr-on':'');
719
+ document.getElementById('f-wk').className='tc'+(f=='working'?' wk-on':'');
720
+ loadMems();
721
+ }
722
+ document.getElementById('f-all').addEventListener('click',function(){setFilter('all');});
723
+ document.getElementById('f-ep').addEventListener('click',function(){setFilter('episodic');});
724
+ document.getElementById('f-se').addEventListener('click',function(){setFilter('semantic');});
725
+ document.getElementById('f-pr').addEventListener('click',function(){setFilter('procedural');});
726
+ document.getElementById('f-wk').addEventListener('click',function(){setFilter('working');});
727
+
728
+ document.getElementById('search-btn').addEventListener('click',function(){
729
+ SEARCH_Q=document.getElementById('search-input').value.trim();loadMems();});
730
+ document.getElementById('search-input').addEventListener('keydown',function(e){
731
+ if(e.key=='Enter'){SEARCH_Q=this.value.trim();loadMems();}
732
+ if(e.key=='Escape'){this.value='';SEARCH_Q='';loadMems();}
733
+ });
734
+
735
+ // Modal
736
+ function openModal(mem){
737
+ document.getElementById('mdl-title').textContent=mem?'EDIT MEMORY':'STORE MEMORY';
738
+ document.getElementById('eid').value=mem?mem.id:'';
739
+ document.getElementById('fc').value=mem?mem.content:'';
740
+ document.getElementById('fs').value=mem?mem.summary:'';
741
+ document.getElementById('ftier').value=mem?mem.tier:'episodic';
742
+ document.getElementById('fimp').value=mem?mem.importance:5;
743
+ document.getElementById('fagent').value=mem?mem.agent:'';
744
+ document.getElementById('ftags').value=mem?(mem.tags||[]).join(', '):'';
745
+ document.getElementById('fttl').value='3600';
746
+ document.getElementById('modal').classList.add('open');
747
+ setTimeout(function(){document.getElementById('fc').focus();},80);
748
+ }
749
+ function closeModal(){document.getElementById('modal').classList.remove('open');}
750
+ document.getElementById('btn-new').addEventListener('click',function(){openModal();});
751
+ document.getElementById('mdl-close').addEventListener('click',closeModal);
752
+ document.getElementById('btn-cancel').addEventListener('click',closeModal);
753
+ document.getElementById('modal').addEventListener('click',function(e){if(e.target===this)closeModal();});
754
+
755
+ document.getElementById('btn-save').addEventListener('click',function(){
756
+ var content=document.getElementById('fc').value.trim();
757
+ if(!content){document.getElementById('fc').focus();toast('Content required',true);return;}
758
+ var id=document.getElementById('eid').value;
759
+ var tags=document.getElementById('ftags').value.split(',').map(function(t){return t.trim();}).filter(Boolean);
760
+ var pay={content:content,summary:document.getElementById('fs').value.trim(),
761
+ tier:document.getElementById('ftier').value,
762
+ importance:parseInt(document.getElementById('fimp').value)||5,
763
+ agent:document.getElementById('fagent').value.trim(),
764
+ tags:tags,ttl:parseInt(document.getElementById('fttl').value)||3600};
765
+ fetch(id?'/api/memories/'+id:'/api/memories',
766
+ {method:id?'PATCH':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(pay)})
767
+ .then(function(r){if(!r.ok)throw new Error(r.status);return r.json();})
768
+ .then(function(d){
769
+ toast(id?'Memory updated':'Memory stored');closeModal();loadMems();
770
+ if(d.memory||d.id) setTimeout(function(){selectMem(d.id||d.memory.id);},200);
771
+ }).catch(function(e){toast('Error: '+e.message,true);});
772
+ });
773
+
774
+ document.addEventListener('keydown',function(e){
775
+ if(e.key=='Escape')closeModal();
776
+ var active=document.activeElement;
777
+ var typing=active&&(active.tagName=='INPUT'||active.tagName=='TEXTAREA'||active.tagName=='SELECT');
778
+ if(e.key=='n'&&!typing&&!e.ctrlKey&&!e.metaKey)openModal();
779
+ if((e.ctrlKey||e.metaKey)&&e.key=='Enter'&&document.getElementById('modal').classList.contains('open'))
780
+ document.getElementById('btn-save').click();
781
+ if(e.key=='/'&&!typing){e.preventDefault();document.getElementById('search-input').focus();}
782
+ });
783
+
784
+ loadMems();
785
+ </script>
786
+ </body>
787
+ </html>"""
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi>=0.111.0
2
+ uvicorn>=0.30.0
3
+ python-multipart>=0.0.9