ultra-memory 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ultra-memory HTTP REST Server
4
+ 将 9 个记忆工具暴露为标准 HTTP 端点,供任意 LLM 平台通过 function calling 调用。
5
+
6
+ 零外部依赖,纯 Python stdlib。
7
+
8
+ 启动:
9
+ python3 platform/server.py # 默认 localhost:3200
10
+ python3 platform/server.py --port 8080
11
+ python3 platform/server.py --host 0.0.0.0 # 局域网访问(谨慎使用)
12
+
13
+ 端点一览:
14
+ GET /health 服务健康检查
15
+ GET /tools 列出所有工具
16
+ POST /tools/{tool_name} 调用工具
17
+ GET /session/current 获取当前活跃会话
18
+
19
+ 支持的工具(POST /tools/xxx):
20
+ memory_init 初始化会话
21
+ memory_status 查询会话状态与 context 压力
22
+ memory_log 记录操作(自动提取实体)
23
+ memory_recall 四层统一检索
24
+ memory_summarize 触发摘要压缩(含元压缩)
25
+ memory_restore 恢复上次会话
26
+ memory_profile 读写用户画像
27
+ memory_entities 查询实体索引
28
+ memory_extract_entities 全量重提取实体
29
+
30
+ 请求格式(JSON body):
31
+ {"project": "my-project", "resume": false}
32
+
33
+ 响应格式:
34
+ {"success": true, "output": "...", "tool": "memory_init"}
35
+ {"success": false, "error": "...", "tool": "memory_init"}
36
+ """
37
+
38
+ import os
39
+ import sys
40
+ import json
41
+ import subprocess
42
+ import argparse
43
+ import logging
44
+ from http.server import HTTPServer, BaseHTTPRequestHandler
45
+ from pathlib import Path
46
+ from urllib.parse import urlparse
47
+
48
+ if sys.stdout.encoding != "utf-8":
49
+ sys.stdout.reconfigure(encoding="utf-8")
50
+ if sys.stderr.encoding != "utf-8":
51
+ sys.stderr.reconfigure(encoding="utf-8")
52
+
53
+ # ── 路径配置 ──────────────────────────────────────────────────────────────
54
+ PLATFORM_DIR = Path(__file__).parent
55
+ SCRIPTS_DIR = PLATFORM_DIR.parent / "scripts"
56
+ PYTHON = sys.executable
57
+ ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
58
+
59
+ VERSION = "3.0.0"
60
+
61
+ logging.basicConfig(
62
+ level=logging.INFO,
63
+ format="[ultra-memory] %(asctime)s %(levelname)s %(message)s",
64
+ datefmt="%H:%M:%S",
65
+ )
66
+ log = logging.getLogger("ultra-memory")
67
+
68
+
69
+ # ── 工具路由表 ────────────────────────────────────────────────────────────
70
+
71
+ def _run_script(script: str, args: list[str], timeout: int = 20) -> tuple[bool, str]:
72
+ """运行 Python 脚本,返回 (success, output)"""
73
+ cmd = [PYTHON, str(SCRIPTS_DIR / script)] + args
74
+ try:
75
+ result = subprocess.run(
76
+ cmd, capture_output=True, text=True,
77
+ encoding="utf-8", errors="replace",
78
+ env={**os.environ, "ULTRA_MEMORY_HOME": str(ULTRA_MEMORY_HOME)},
79
+ timeout=timeout,
80
+ )
81
+ output = (result.stdout + result.stderr).strip()
82
+ return result.returncode == 0, output
83
+ except subprocess.TimeoutExpired:
84
+ return False, f"脚本执行超时(>{timeout}s)"
85
+ except Exception as e:
86
+ return False, str(e)
87
+
88
+
89
+ def tool_memory_init(body: dict) -> tuple[bool, str]:
90
+ args = ["--project", body.get("project", "default")]
91
+ if body.get("resume"):
92
+ args.append("--resume")
93
+ return _run_script("init.py", args)
94
+
95
+
96
+ def tool_memory_status(body: dict) -> tuple[bool, str]:
97
+ session_id = body.get("session_id", "")
98
+ if not session_id:
99
+ return False, "缺少 session_id 参数"
100
+
101
+ meta_file = ULTRA_MEMORY_HOME / "sessions" / session_id / "meta.json"
102
+ if not meta_file.exists():
103
+ return False, f"会话不存在: {session_id}"
104
+
105
+ with open(meta_file, encoding="utf-8") as f:
106
+ meta = json.load(f)
107
+
108
+ ok, pressure_out = _run_script("init.py", ["--check-pressure", session_id])
109
+ lines = [
110
+ f"会话 ID: {session_id}",
111
+ f"项目: {meta.get('project', 'default')}",
112
+ f"操作数: {meta.get('op_count', 0)}",
113
+ f"最后里程碑: {meta.get('last_milestone', '(无)')}",
114
+ f"上次压缩: {meta.get('last_summary_at', '(未压缩)')}",
115
+ pressure_out,
116
+ ]
117
+ return True, "\n".join(lines)
118
+
119
+
120
+ def tool_memory_log(body: dict) -> tuple[bool, str]:
121
+ session_id = body.get("session_id", "")
122
+ op_type = body.get("op_type", "")
123
+ summary = body.get("summary", "")
124
+ if not all([session_id, op_type, summary]):
125
+ return False, "缺少必填参数: session_id / op_type / summary"
126
+
127
+ args = [
128
+ "--session", session_id,
129
+ "--type", op_type,
130
+ "--summary", summary,
131
+ "--detail", json.dumps(body.get("detail", {}), ensure_ascii=False),
132
+ "--tags", ",".join(body.get("tags", [])),
133
+ ]
134
+ return _run_script("log_op.py", args)
135
+
136
+
137
+ def tool_memory_recall(body: dict) -> tuple[bool, str]:
138
+ session_id = body.get("session_id", "")
139
+ query = body.get("query", "")
140
+ if not all([session_id, query]):
141
+ return False, "缺少必填参数: session_id / query"
142
+
143
+ args = [
144
+ "--session", session_id,
145
+ "--query", query,
146
+ "--top-k", str(body.get("top_k", 5)),
147
+ ]
148
+ return _run_script("recall.py", args)
149
+
150
+
151
+ def tool_memory_summarize(body: dict) -> tuple[bool, str]:
152
+ session_id = body.get("session_id", "")
153
+ if not session_id:
154
+ return False, "缺少 session_id 参数"
155
+
156
+ args = ["--session", session_id]
157
+ if body.get("force"):
158
+ args.append("--force")
159
+ return _run_script("summarize.py", args)
160
+
161
+
162
+ def tool_memory_restore(body: dict) -> tuple[bool, str]:
163
+ args = ["--project", body.get("project", "default")]
164
+ if body.get("verbose"):
165
+ args.append("--verbose")
166
+ return _run_script("restore.py", args)
167
+
168
+
169
+ def tool_memory_profile(body: dict) -> tuple[bool, str]:
170
+ action = body.get("action", "read")
171
+ profile_file = ULTRA_MEMORY_HOME / "semantic" / "user_profile.json"
172
+
173
+ if action == "read":
174
+ try:
175
+ content = profile_file.read_text(encoding="utf-8")
176
+ return True, content
177
+ except FileNotFoundError:
178
+ return True, "{}"
179
+
180
+ elif action == "update":
181
+ profile = {}
182
+ if profile_file.exists():
183
+ try:
184
+ profile = json.loads(profile_file.read_text(encoding="utf-8"))
185
+ except Exception:
186
+ pass
187
+ profile.update(body.get("updates", {}))
188
+ from datetime import date
189
+ profile["last_updated"] = str(date.today())
190
+ profile_file.parent.mkdir(parents=True, exist_ok=True)
191
+ profile_file.write_text(
192
+ json.dumps(profile, ensure_ascii=False, indent=2), encoding="utf-8"
193
+ )
194
+ return True, "用户画像已更新"
195
+
196
+ return False, f"未知 action: {action},支持 read / update"
197
+
198
+
199
+ def tool_memory_entities(body: dict) -> tuple[bool, str]:
200
+ entities_file = ULTRA_MEMORY_HOME / "semantic" / "entities.jsonl"
201
+ target_type = body.get("entity_type", "all").lower()
202
+ query = body.get("query", "").lower()
203
+ top_k = int(body.get("top_k", 10))
204
+
205
+ if not entities_file.exists():
206
+ return True, "实体索引尚未建立,请先记录操作(memory_log)"
207
+
208
+ all_entities = []
209
+ for line in entities_file.read_text(encoding="utf-8").splitlines():
210
+ line = line.strip()
211
+ if not line:
212
+ continue
213
+ try:
214
+ all_entities.append(json.loads(line))
215
+ except json.JSONDecodeError:
216
+ continue
217
+
218
+ # 类型过滤
219
+ filtered = [e for e in all_entities
220
+ if target_type == "all" or e.get("entity_type") == target_type]
221
+
222
+ # 关键词过滤
223
+ if query:
224
+ filtered = [e for e in filtered
225
+ if query in e.get("name", "").lower()
226
+ or query in e.get("context", "").lower()]
227
+
228
+ # 去重(同类型同名保留最新)
229
+ seen: set[str] = set()
230
+ deduped = []
231
+ for e in reversed(filtered): # 倒序 → 最新优先
232
+ key = f"{e.get('entity_type')}:{e.get('name')}"
233
+ if key not in seen:
234
+ seen.add(key)
235
+ deduped.append(e)
236
+ deduped = deduped[:top_k]
237
+
238
+ if not deduped:
239
+ return True, "未找到匹配实体"
240
+
241
+ lines = [f"找到 {len(deduped)} 个实体:\n"]
242
+ for e in deduped:
243
+ et = e.get("entity_type", "?")
244
+ name = e.get("name", "?")
245
+ ctx = e.get("context", "")
246
+ extra = ""
247
+ if et == "dependency" and e.get("manager"):
248
+ extra = f" [via {e['manager']}]"
249
+ elif et == "decision" and e.get("rationale"):
250
+ extra = f"\n 依据: {e['rationale']}"
251
+ elif et == "error" and e.get("message"):
252
+ extra = f" ← {e['message']}"
253
+ lines.append(f"[{et}] {name}{extra}")
254
+ if ctx:
255
+ lines.append(f" 来源: {ctx}")
256
+ return True, "\n".join(lines)
257
+
258
+
259
+ def tool_memory_extract_entities(body: dict) -> tuple[bool, str]:
260
+ session_id = body.get("session_id", "")
261
+ if not session_id:
262
+ return False, "缺少 session_id 参数"
263
+ return _run_script("extract_entities.py", ["--session", session_id, "--all"])
264
+
265
+
266
+ # ── 工具注册表 ────────────────────────────────────────────────────────────
267
+
268
+ TOOL_HANDLERS = {
269
+ "memory_init": tool_memory_init,
270
+ "memory_status": tool_memory_status,
271
+ "memory_log": tool_memory_log,
272
+ "memory_recall": tool_memory_recall,
273
+ "memory_summarize": tool_memory_summarize,
274
+ "memory_restore": tool_memory_restore,
275
+ "memory_profile": tool_memory_profile,
276
+ "memory_entities": tool_memory_entities,
277
+ "memory_extract_entities": tool_memory_extract_entities,
278
+ }
279
+
280
+ TOOL_DESCRIPTIONS = {
281
+ "memory_init": "初始化会话,返回 session_id",
282
+ "memory_status": "查询会话状态与 context 压力级别",
283
+ "memory_log": "记录一条操作到日志(自动提取实体)",
284
+ "memory_recall": "四层统一检索:ops / summary / semantic / entity",
285
+ "memory_summarize": "触发摘要压缩(含分层元压缩)",
286
+ "memory_restore": "恢复上次会话,输出自然语言总结",
287
+ "memory_profile": "读写用户画像(技术栈、偏好)",
288
+ "memory_entities": "查询结构化实体索引(函数/文件/依赖/决策/错误)",
289
+ "memory_extract_entities": "对整个 ops.jsonl 全量重提取实体",
290
+ }
291
+
292
+
293
+ # ── HTTP 请求处理 ─────────────────────────────────────────────────────────
294
+
295
+ class MemoryHandler(BaseHTTPRequestHandler):
296
+
297
+ def log_message(self, fmt, *args):
298
+ log.info(f"{self.address_string()} {fmt % args}")
299
+
300
+ def _send_json(self, status: int, data: dict):
301
+ body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
302
+ self.send_response(status)
303
+ self.send_header("Content-Type", "application/json; charset=utf-8")
304
+ self.send_header("Content-Length", str(len(body)))
305
+ # CORS:允许本地 web 客户端调用
306
+ self.send_header("Access-Control-Allow-Origin", "*")
307
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
308
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
309
+ self.end_headers()
310
+ self.wfile.write(body)
311
+
312
+ def _read_body(self) -> dict:
313
+ length = int(self.headers.get("Content-Length", 0))
314
+ if length == 0:
315
+ return {}
316
+ raw = self.rfile.read(length)
317
+ try:
318
+ return json.loads(raw.decode("utf-8"))
319
+ except Exception:
320
+ return {}
321
+
322
+ def do_OPTIONS(self):
323
+ """CORS 预检"""
324
+ self.send_response(204)
325
+ self.send_header("Access-Control-Allow-Origin", "*")
326
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
327
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
328
+ self.end_headers()
329
+
330
+ def do_GET(self):
331
+ parsed = urlparse(self.path)
332
+ path = parsed.path.rstrip("/")
333
+
334
+ if path == "/health":
335
+ self._send_json(200, {
336
+ "status": "ok",
337
+ "version": VERSION,
338
+ "scripts_dir": str(SCRIPTS_DIR),
339
+ "storage": str(ULTRA_MEMORY_HOME),
340
+ })
341
+
342
+ elif path == "/tools":
343
+ self._send_json(200, {
344
+ "tools": [
345
+ {"name": name, "description": desc,
346
+ "endpoint": f"POST /tools/{name}"}
347
+ for name, desc in TOOL_DESCRIPTIONS.items()
348
+ ]
349
+ })
350
+
351
+ elif path == "/session/current":
352
+ # 返回最近活跃会话(跨项目)
353
+ sessions_dir = ULTRA_MEMORY_HOME / "sessions"
354
+ if not sessions_dir.exists():
355
+ self._send_json(200, {"session": None, "message": "尚无会话记录"})
356
+ return
357
+ latest = None
358
+ latest_ts = ""
359
+ for d in sessions_dir.iterdir():
360
+ if not d.is_dir():
361
+ continue
362
+ meta_f = d / "meta.json"
363
+ if not meta_f.exists():
364
+ continue
365
+ try:
366
+ m = json.loads(meta_f.read_text(encoding="utf-8"))
367
+ ts = m.get("last_op_at") or m.get("started_at", "")
368
+ if ts > latest_ts:
369
+ latest_ts = ts
370
+ latest = m
371
+ except Exception:
372
+ continue
373
+ self._send_json(200, {"session": latest})
374
+
375
+ else:
376
+ self._send_json(404, {"error": f"路径不存在: {path}"})
377
+
378
+ def do_POST(self):
379
+ parsed = urlparse(self.path)
380
+ path = parsed.path.rstrip("/")
381
+
382
+ # POST /tools/{tool_name}
383
+ if path.startswith("/tools/"):
384
+ tool_name = path[len("/tools/"):]
385
+
386
+ if tool_name not in TOOL_HANDLERS:
387
+ self._send_json(404, {
388
+ "error": f"未知工具: {tool_name}",
389
+ "available": list(TOOL_HANDLERS.keys()),
390
+ })
391
+ return
392
+
393
+ body = self._read_body()
394
+ handler = TOOL_HANDLERS[tool_name]
395
+
396
+ try:
397
+ success, output = handler(body)
398
+ self._send_json(200 if success else 500, {
399
+ "success": success,
400
+ "tool": tool_name,
401
+ "output": output,
402
+ })
403
+ except Exception as e:
404
+ log.exception(f"工具 {tool_name} 执行异常")
405
+ self._send_json(500, {
406
+ "success": False,
407
+ "tool": tool_name,
408
+ "error": str(e),
409
+ })
410
+ else:
411
+ self._send_json(404, {"error": f"路径不存在: {path}"})
412
+
413
+
414
+ # ── 主入口 ────────────────────────────────────────────────────────────────
415
+
416
+ def main():
417
+ parser = argparse.ArgumentParser(description="ultra-memory HTTP REST Server")
418
+ parser.add_argument("--host", default="127.0.0.1",
419
+ help="监听地址(默认 127.0.0.1,仅本机访问)")
420
+ parser.add_argument("--port", type=int, default=3200,
421
+ help="监听端口(默认 3200)")
422
+ parser.add_argument("--storage", default=None,
423
+ help="覆盖 ULTRA_MEMORY_HOME 路径")
424
+ args = parser.parse_args()
425
+
426
+ global ULTRA_MEMORY_HOME
427
+ if args.storage:
428
+ ULTRA_MEMORY_HOME = Path(args.storage)
429
+ os.environ["ULTRA_MEMORY_HOME"] = str(ULTRA_MEMORY_HOME)
430
+
431
+ server = HTTPServer((args.host, args.port), MemoryHandler)
432
+
433
+ log.info(f"ultra-memory REST Server v{VERSION} 已启动")
434
+ log.info(f"地址: http://{args.host}:{args.port}")
435
+ log.info(f"存储: {ULTRA_MEMORY_HOME}")
436
+ log.info(f"脚本: {SCRIPTS_DIR}")
437
+ log.info(f"工具: {list(TOOL_HANDLERS.keys())}")
438
+ log.info("按 Ctrl+C 停止服务")
439
+ log.info("")
440
+ log.info("快速测试:")
441
+ log.info(f" curl http://{args.host}:{args.port}/health")
442
+ log.info(f" curl http://{args.host}:{args.port}/tools")
443
+ log.info(f' curl -X POST http://{args.host}:{args.port}/tools/memory_init \\')
444
+ log.info(f' -H "Content-Type: application/json" \\')
445
+ log.info(f' -d \'{{"project": "my-project"}}\'')
446
+
447
+ try:
448
+ server.serve_forever()
449
+ except KeyboardInterrupt:
450
+ log.info("服务已停止")
451
+
452
+
453
+ if __name__ == "__main__":
454
+ main()
@@ -0,0 +1,176 @@
1
+ {
2
+ "function_declarations": [
3
+ {
4
+ "name": "memory_init",
5
+ "description": "Initialize ultra-memory session. Call at conversation start to create three-layer memory structure and inject historical context.",
6
+ "parameters": {
7
+ "type": "OBJECT",
8
+ "properties": {
9
+ "project": {
10
+ "type": "STRING",
11
+ "description": "Project name (default: 'default')"
12
+ },
13
+ "resume": {
14
+ "type": "BOOLEAN",
15
+ "description": "Try to resume the most recent session for this project"
16
+ }
17
+ }
18
+ }
19
+ },
20
+ {
21
+ "name": "memory_status",
22
+ "description": "Query current session status: operation count, last milestone, context pressure level (low/medium/high/critical). Use to decide when to compress.",
23
+ "parameters": {
24
+ "type": "OBJECT",
25
+ "properties": {
26
+ "session_id": {
27
+ "type": "STRING",
28
+ "description": "Session ID returned by memory_init"
29
+ }
30
+ },
31
+ "required": ["session_id"]
32
+ }
33
+ },
34
+ {
35
+ "name": "memory_log",
36
+ "description": "Record an operation to the current session log (Layer 1). Call after every significant action: file write, bash command, tool call, decision, error, or milestone.",
37
+ "parameters": {
38
+ "type": "OBJECT",
39
+ "properties": {
40
+ "session_id": {
41
+ "type": "STRING",
42
+ "description": "Current session ID"
43
+ },
44
+ "op_type": {
45
+ "type": "STRING",
46
+ "description": "Operation type: one of tool_call, file_write, file_read, bash_exec, reasoning, user_instruction, decision, error, milestone"
47
+ },
48
+ "summary": {
49
+ "type": "STRING",
50
+ "description": "Operation summary (under 50 words)"
51
+ },
52
+ "detail": {
53
+ "type": "OBJECT",
54
+ "description": "Additional detail fields (optional)"
55
+ },
56
+ "tags": {
57
+ "type": "ARRAY",
58
+ "items": { "type": "STRING" },
59
+ "description": "Tag list (optional, auto-generated if omitted)"
60
+ }
61
+ },
62
+ "required": ["session_id", "op_type", "summary"]
63
+ }
64
+ },
65
+ {
66
+ "name": "memory_recall",
67
+ "description": "Retrieve relevant records from all memory layers (ops log, summary, semantic, entity index). Use when you need to recall past work, decisions, or errors.",
68
+ "parameters": {
69
+ "type": "OBJECT",
70
+ "properties": {
71
+ "session_id": {
72
+ "type": "STRING",
73
+ "description": "Current session ID"
74
+ },
75
+ "query": {
76
+ "type": "STRING",
77
+ "description": "Search keywords (Chinese or English)"
78
+ },
79
+ "top_k": {
80
+ "type": "NUMBER",
81
+ "description": "Number of results to return (default: 5)"
82
+ }
83
+ },
84
+ "required": ["session_id", "query"]
85
+ }
86
+ },
87
+ {
88
+ "name": "memory_summarize",
89
+ "description": "Trigger summary compression of the current session (compresses ops log into summary.md). Call when context pressure is high or critical, or every ~50 operations.",
90
+ "parameters": {
91
+ "type": "OBJECT",
92
+ "properties": {
93
+ "session_id": {
94
+ "type": "STRING",
95
+ "description": "Current session ID"
96
+ },
97
+ "force": {
98
+ "type": "BOOLEAN",
99
+ "description": "Force compression even if operation count is low"
100
+ }
101
+ },
102
+ "required": ["session_id"]
103
+ }
104
+ },
105
+ {
106
+ "name": "memory_restore",
107
+ "description": "Restore the last session context for a project. Use at conversation start to continue a previous task across days.",
108
+ "parameters": {
109
+ "type": "OBJECT",
110
+ "properties": {
111
+ "project": {
112
+ "type": "STRING",
113
+ "description": "Project name (default: 'default')"
114
+ },
115
+ "verbose": {
116
+ "type": "BOOLEAN",
117
+ "description": "Show detailed operation records"
118
+ }
119
+ }
120
+ }
121
+ },
122
+ {
123
+ "name": "memory_profile",
124
+ "description": "Read or update user profile (tech stack, preferences, project list). Persist user-level preferences across all sessions.",
125
+ "parameters": {
126
+ "type": "OBJECT",
127
+ "properties": {
128
+ "action": {
129
+ "type": "STRING",
130
+ "description": "Operation type: read or update"
131
+ },
132
+ "updates": {
133
+ "type": "OBJECT",
134
+ "description": "Fields to update (required when action=update)"
135
+ }
136
+ },
137
+ "required": ["action"]
138
+ }
139
+ },
140
+ {
141
+ "name": "memory_entities",
142
+ "description": "Query structured entity index (functions, files, dependencies, decisions, errors). Use for precise questions like 'which functions were used', 'which packages were installed', 'what decisions were made'.",
143
+ "parameters": {
144
+ "type": "OBJECT",
145
+ "properties": {
146
+ "entity_type": {
147
+ "type": "STRING",
148
+ "description": "Entity type filter: function, file, dependency, decision, error, class, or all (no filter)"
149
+ },
150
+ "query": {
151
+ "type": "STRING",
152
+ "description": "Search keyword (optional, empty returns all of that type)"
153
+ },
154
+ "top_k": {
155
+ "type": "NUMBER",
156
+ "description": "Number of results to return (default: 10)"
157
+ }
158
+ }
159
+ }
160
+ },
161
+ {
162
+ "name": "memory_extract_entities",
163
+ "description": "Re-extract all structured entities from a session's ops log. Use to repair or initialize the entity index.",
164
+ "parameters": {
165
+ "type": "OBJECT",
166
+ "properties": {
167
+ "session_id": {
168
+ "type": "STRING",
169
+ "description": "Session ID"
170
+ }
171
+ },
172
+ "required": ["session_id"]
173
+ }
174
+ }
175
+ ]
176
+ }