adaptive-memory-engine 0.1.6__py3-none-any.whl

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.
Files changed (72) hide show
  1. adaptive_memory_engine-0.1.6.dist-info/METADATA +228 -0
  2. adaptive_memory_engine-0.1.6.dist-info/RECORD +72 -0
  3. adaptive_memory_engine-0.1.6.dist-info/WHEEL +4 -0
  4. adaptive_memory_engine-0.1.6.dist-info/entry_points.txt +3 -0
  5. adaptive_memory_engine-0.1.6.dist-info/licenses/LICENSE +21 -0
  6. ame/__init__.py +1 -0
  7. ame/agent/__init__.py +1 -0
  8. ame/agent/mcp.py +474 -0
  9. ame/agent/memory_api.py +141 -0
  10. ame/agent/results.py +30 -0
  11. ame/bronze/schema.py +17 -0
  12. ame/bronze/store.py +38 -0
  13. ame/cli/__init__.py +1 -0
  14. ame/cli/main.py +903 -0
  15. ame/connectors/base.py +30 -0
  16. ame/connectors/contract.py +199 -0
  17. ame/connectors/github.py +66 -0
  18. ame/connectors/google.py +464 -0
  19. ame/connectors/google_oauth.py +156 -0
  20. ame/connectors/jira.py +66 -0
  21. ame/connectors/json_helpers.py +43 -0
  22. ame/connectors/markdown.py +116 -0
  23. ame/connectors/notion.py +59 -0
  24. ame/connectors/oauth_callback.py +102 -0
  25. ame/connectors/oauth_provider.py +250 -0
  26. ame/connectors/obsidian.py +19 -0
  27. ame/connectors/router.py +155 -0
  28. ame/connectors/slack.py +66 -0
  29. ame/connectors/slack_oauth.py +417 -0
  30. ame/connectors/sync_history.py +73 -0
  31. ame/context_budget.py +106 -0
  32. ame/core/config.py +77 -0
  33. ame/core/corpus.py +17 -0
  34. ame/core/errors.py +18 -0
  35. ame/core/paths.py +111 -0
  36. ame/core/state.py +57 -0
  37. ame/export/obsidian.py +123 -0
  38. ame/gold/builder.py +300 -0
  39. ame/gold/ontology.py +80 -0
  40. ame/gold/resolver.py +91 -0
  41. ame/gold/schema.py +40 -0
  42. ame/gold/store.py +45 -0
  43. ame/hardware/profiler.py +85 -0
  44. ame/hardware/tier.py +27 -0
  45. ame/hermes/__init__.py +3 -0
  46. ame/hermes/memory.py +209 -0
  47. ame/models/download.py +243 -0
  48. ame/models/ollama.py +60 -0
  49. ame/models/registry.py +101 -0
  50. ame/models/router.py +22 -0
  51. ame/pipeline.py +155 -0
  52. ame/query/diff.py +40 -0
  53. ame/query/engine.py +919 -0
  54. ame/query/memory_os.py +313 -0
  55. ame/query/mql.py +84 -0
  56. ame/query/multihop.py +264 -0
  57. ame/query/result.py +20 -0
  58. ame/sdk.py +52 -0
  59. ame/security.py +145 -0
  60. ame/silver/extractor.py +414 -0
  61. ame/silver/llm_extractor.py +181 -0
  62. ame/silver/prompts.py +56 -0
  63. ame/silver/rationale.py +140 -0
  64. ame/silver/schema.py +51 -0
  65. ame/silver/store.py +59 -0
  66. ame/storage/custom_kg.py +33 -0
  67. ame/storage/lightrag_adapter.py +362 -0
  68. ame/validation/confidence.py +5 -0
  69. ame/validation/grounding.py +10 -0
  70. ame/validation/type_gate.py +22 -0
  71. ame/writeback.py +173 -0
  72. memory/__init__.py +3 -0
ame/agent/mcp.py ADDED
@@ -0,0 +1,474 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any, TextIO
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from ame.agent.memory_api import AgentMemoryAPI
12
+ from ame.core.config import load_config
13
+ from ame.core.corpus import create_corpus, require_corpus
14
+ from ame.core.paths import ame_home, ensure_runtime_layout
15
+ from ame.hardware.profiler import HardwareProfiler
16
+ from ame.models.download import OllamaModelInstaller
17
+ from ame.models.registry import load_default_registry
18
+ from ame.models.router import ModelRouter
19
+ from ame.pipeline import MemoryPipeline
20
+
21
+
22
+ SERVER_VERSION = "0.1.5"
23
+
24
+
25
+ class McpToolSpec(BaseModel):
26
+ name: str
27
+ description: str
28
+ input_schema: dict[str, Any] = Field(default_factory=dict)
29
+
30
+
31
+ READ_TOOLS = [
32
+ McpToolSpec(
33
+ name="memory_query",
34
+ description="Answer a grounded natural-language memory question.",
35
+ input_schema={"type": "object", "properties": {"question": {"type": "string"}}},
36
+ ),
37
+ McpToolSpec(
38
+ name="memory_search",
39
+ description="Search local memory and return answer, sources, and confidence.",
40
+ input_schema={"type": "object", "properties": {"query": {"type": "string"}}},
41
+ ),
42
+ McpToolSpec(
43
+ name="memory_retrieve",
44
+ description="Retrieve raw matching graph nodes, edges, and documents.",
45
+ input_schema={"type": "object", "properties": {"query": {"type": "string"}, "k": {"type": "integer"}}},
46
+ ),
47
+ McpToolSpec(
48
+ name="memory_graph",
49
+ description="Return graph neighborhood for an entity.",
50
+ input_schema={"type": "object", "properties": {"entity": {"type": "string"}}},
51
+ ),
52
+ McpToolSpec(
53
+ name="memory_decisions",
54
+ description="Return accepted current decisions.",
55
+ input_schema={
56
+ "type": "object",
57
+ "properties": {
58
+ "project": {"type": "string"},
59
+ "from": {"type": "string"},
60
+ "to": {"type": "string"},
61
+ "current_only": {"type": "boolean"},
62
+ },
63
+ },
64
+ ),
65
+ McpToolSpec(
66
+ name="memory_timeline",
67
+ description="Return the decision timeline for a corpus or project.",
68
+ input_schema={"type": "object", "properties": {"project": {"type": "string"}}},
69
+ ),
70
+ McpToolSpec(
71
+ name="memory_why",
72
+ description="Return rationale for a decision.",
73
+ input_schema={"type": "object", "properties": {"decision": {"type": "string"}}},
74
+ ),
75
+ McpToolSpec(
76
+ name="memory_diff",
77
+ description="Return recent memory changes.",
78
+ input_schema={"type": "object", "properties": {"days": {"type": "integer"}}},
79
+ ),
80
+ McpToolSpec(
81
+ name="memory_write_decision",
82
+ description="Write a grounded decision memory.",
83
+ input_schema={
84
+ "type": "object",
85
+ "properties": {
86
+ "title": {"type": "string"},
87
+ "rationale": {"type": "string"},
88
+ "project": {"type": "string"},
89
+ "source": {"type": "string"},
90
+ "participants": {"type": "array", "items": {"type": "string"}},
91
+ },
92
+ },
93
+ ),
94
+ McpToolSpec(
95
+ name="memory_write_note",
96
+ description="Write a note memory.",
97
+ input_schema={"type": "object", "properties": {"title": {"type": "string"}, "content": {"type": "string"}}},
98
+ ),
99
+ ]
100
+
101
+
102
+ BOOTSTRAP_TOOLS = [
103
+ McpToolSpec(
104
+ name="ame_doctor",
105
+ description="Diagnose local AME runtime, hardware tier, and recommended local models.",
106
+ input_schema={"type": "object", "properties": {}},
107
+ ),
108
+ McpToolSpec(
109
+ name="ame_setup",
110
+ description="Plan or execute local model installation through Ollama. Use execute=false before asking the user for approval.",
111
+ input_schema={
112
+ "type": "object",
113
+ "properties": {
114
+ "execute": {"type": "boolean", "description": "Pull missing models when true. Defaults to false."},
115
+ },
116
+ },
117
+ ),
118
+ McpToolSpec(
119
+ name="ame_load",
120
+ description="Build Bronze/Silver/Gold memory from a local document folder.",
121
+ input_schema={
122
+ "type": "object",
123
+ "properties": {
124
+ "corpus_id": {"type": "string"},
125
+ "source_path": {"type": "string"},
126
+ "mode": {"type": "string", "enum": ["llm", "deterministic"]},
127
+ "profile": {"type": "string"},
128
+ },
129
+ "required": ["corpus_id", "source_path"],
130
+ },
131
+ ),
132
+ McpToolSpec(
133
+ name="ame_connect",
134
+ description="Return MCP client configuration for a built corpus.",
135
+ input_schema={
136
+ "type": "object",
137
+ "properties": {
138
+ "corpus_id": {"type": "string"},
139
+ "client": {"type": "string", "enum": ["generic", "codex", "claude"]},
140
+ },
141
+ "required": ["corpus_id"],
142
+ },
143
+ ),
144
+ McpToolSpec(
145
+ name="ame_corpora",
146
+ description="List local AME corpora.",
147
+ input_schema={"type": "object", "properties": {}},
148
+ ),
149
+ ]
150
+
151
+
152
+ class LocalMcpToolbox:
153
+ def __init__(self, corpus_root: Path):
154
+ self.api = AgentMemoryAPI(corpus_root)
155
+
156
+ @staticmethod
157
+ def manifest(corpus_id: str) -> dict[str, Any]:
158
+ return {
159
+ "name": "adaptive-memory-engine",
160
+ "transport": "local-stdio-compatible",
161
+ "corpus_id": corpus_id,
162
+ "tools": [tool.model_dump() for tool in READ_TOOLS],
163
+ }
164
+
165
+ def call(self, tool_name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
166
+ arguments = arguments or {}
167
+ if tool_name == "memory_query":
168
+ return self._dump(self.api.search(str(arguments.get("question", ""))))
169
+ if tool_name == "memory_search":
170
+ return self._dump(self.api.search(str(arguments.get("query", ""))))
171
+ if tool_name == "memory_retrieve":
172
+ return self._dump(self.api.retrieve(str(arguments.get("query", "")), int(arguments.get("k", 8))))
173
+ if tool_name == "memory_graph":
174
+ return self._dump(self.api.graph(str(arguments.get("entity", ""))))
175
+ if tool_name == "memory_decisions":
176
+ if arguments:
177
+ return self._dump(
178
+ self.api.decisions(
179
+ arguments.get("project"),
180
+ bool(arguments.get("current_only", True)),
181
+ date_from=arguments.get("from") or arguments.get("date_from"),
182
+ date_to=arguments.get("to") or arguments.get("date_to"),
183
+ )
184
+ )
185
+ return self._dump(self.api.current_decision())
186
+ if tool_name == "memory_timeline":
187
+ return self._dump(self.api.timeline(arguments.get("project")))
188
+ if tool_name == "memory_why":
189
+ return self._dump(self.api.why(str(arguments.get("decision", ""))))
190
+ if tool_name == "memory_diff":
191
+ return self._dump(self.api.diff(int(arguments.get("days", 7))))
192
+ if tool_name == "memory_write_decision":
193
+ return self._dump(
194
+ self.api.write_decision(
195
+ title=str(arguments.get("title", "")),
196
+ rationale=str(arguments.get("rationale", "")),
197
+ project=arguments.get("project"),
198
+ participants=arguments.get("participants") or [],
199
+ source=str(arguments.get("source") or "writeback"),
200
+ )
201
+ )
202
+ if tool_name == "memory_write_note":
203
+ return self._dump(self.api.write_note(str(arguments.get("title", "")), str(arguments.get("content", ""))))
204
+ raise ValueError(f"Unknown MCP tool: {tool_name}")
205
+
206
+ def _dump(self, value: Any) -> dict[str, Any]:
207
+ if hasattr(value, "model_dump"):
208
+ return value.model_dump()
209
+ if isinstance(value, dict):
210
+ return value
211
+ return {"value": value}
212
+
213
+
214
+ class BootstrapMcpToolbox:
215
+ def __init__(self, corpus_root: Path | None = None):
216
+ self.corpus_root = corpus_root
217
+
218
+ @staticmethod
219
+ def manifest(corpus_id: str | None = None) -> dict[str, Any]:
220
+ tools = _tools_for_mode(corpus_bound=corpus_id is not None)
221
+ return {
222
+ "name": "adaptive-memory-engine",
223
+ "transport": "local-stdio-compatible",
224
+ "corpus_id": corpus_id,
225
+ "tools": [tool.model_dump() for tool in tools],
226
+ }
227
+
228
+ def call(self, tool_name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
229
+ arguments = arguments or {}
230
+ if tool_name == "ame_doctor":
231
+ return self._doctor()
232
+ if tool_name == "ame_setup":
233
+ return self._setup(execute=bool(arguments.get("execute", False)))
234
+ if tool_name == "ame_load":
235
+ corpus_id = str(arguments.get("corpus_id") or "").strip()
236
+ source_path = str(arguments.get("source_path") or "").strip()
237
+ if not corpus_id:
238
+ raise ValueError("ame_load requires corpus_id")
239
+ if not source_path:
240
+ raise ValueError("ame_load requires source_path")
241
+ mode = str(arguments.get("mode") or "llm")
242
+ if mode not in {"llm", "deterministic"}:
243
+ raise ValueError("ame_load mode must be llm or deterministic")
244
+ profile = arguments.get("profile")
245
+ return self._load(corpus_id, Path(source_path).expanduser(), mode=mode, profile=str(profile) if profile else None)
246
+ if tool_name == "ame_connect":
247
+ corpus_id = str(arguments.get("corpus_id") or "").strip()
248
+ if not corpus_id:
249
+ raise ValueError("ame_connect requires corpus_id")
250
+ return self._connect(corpus_id, client=str(arguments.get("client") or "generic"))
251
+ if tool_name == "ame_corpora":
252
+ return self._corpora()
253
+ if self.corpus_root is not None:
254
+ return LocalMcpToolbox(self.corpus_root).call(tool_name, arguments)
255
+
256
+ corpus_id = str(arguments.get("corpus_id") or "").strip()
257
+ if not corpus_id:
258
+ raise ValueError(f"{tool_name} requires corpus_id when AME MCP is running in bootstrap mode")
259
+ local_arguments = {key: value for key, value in arguments.items() if key != "corpus_id"}
260
+ return LocalMcpToolbox(require_corpus(corpus_id)).call(tool_name, local_arguments)
261
+
262
+ def _doctor(self) -> dict[str, Any]:
263
+ home = ensure_runtime_layout()
264
+ config = load_config()
265
+ profile = HardwareProfiler().profile(home)
266
+ plan = ModelRouter(load_default_registry()).plan(profile)
267
+ install_plan = OllamaModelInstaller(host=config.lightrag.ollama_host, allow_cli_list=True).install_plan(plan, profile)
268
+ return {
269
+ "ame_home": str(home),
270
+ "runtime": "local filesystem",
271
+ "hardware": profile.model_dump(mode="json"),
272
+ "model_plan": plan.model_dump(mode="json"),
273
+ "install_plan": install_plan.model_dump(mode="json"),
274
+ "next_steps": [
275
+ "Ask the user before running ame_setup with execute=true because it downloads local models.",
276
+ "After models are ready, call ame_load with corpus_id and source_path.",
277
+ "After memory is built, answer questions with memory_search or memory_query using that corpus_id.",
278
+ ],
279
+ }
280
+
281
+ def _setup(self, *, execute: bool) -> dict[str, Any]:
282
+ home = ensure_runtime_layout()
283
+ config = load_config()
284
+ profile = HardwareProfiler().profile(home)
285
+ plan = ModelRouter(load_default_registry()).plan(profile)
286
+ installer = OllamaModelInstaller(host=config.lightrag.ollama_host, allow_cli_list=True)
287
+ install_plan = installer.install_plan(plan, profile)
288
+ payload: dict[str, Any] = {
289
+ "ame_home": str(home),
290
+ "execute": execute,
291
+ "install_plan": install_plan.model_dump(mode="json"),
292
+ }
293
+ if not profile.ollama_installed:
294
+ payload["status"] = "blocked"
295
+ payload["message"] = "Ollama is not installed. Install Ollama first, then run setup again."
296
+ return payload
297
+ if not install_plan.missing_models:
298
+ payload["status"] = "ready"
299
+ payload["message"] = "Recommended local models are already installed."
300
+ return payload
301
+ if not execute:
302
+ payload["status"] = "planned"
303
+ payload["message"] = "Ask the user for approval before running ame_setup with execute=true."
304
+ return payload
305
+ results = installer.pull(install_plan.missing_models, execute=True, installed=install_plan.installed_models)
306
+ payload["status"] = "executed"
307
+ payload["results"] = [result.model_dump(mode="json") for result in results]
308
+ return payload
309
+
310
+ def _load(self, corpus_id: str, source_path: Path, *, mode: str, profile: str | None) -> dict[str, Any]:
311
+ ensure_runtime_layout()
312
+ create_corpus(corpus_id)
313
+ report = MemoryPipeline().ingest(corpus_id, source_path, mode=mode, profile=profile)
314
+ return {
315
+ "corpus_id": corpus_id,
316
+ "source_path": str(source_path),
317
+ "report": report.model_dump(mode="json"),
318
+ "next_steps": [
319
+ f"Use memory_search with corpus_id={corpus_id!r} to answer grounded questions.",
320
+ f"Use ame_connect with corpus_id={corpus_id!r} if the user wants a corpus-bound MCP config.",
321
+ ],
322
+ }
323
+
324
+ def _connect(self, corpus_id: str, *, client: str) -> dict[str, Any]:
325
+ require_corpus(corpus_id)
326
+ server = {
327
+ "command": "ame",
328
+ "args": ["mcp", "stdio", corpus_id],
329
+ }
330
+ env = _current_mcp_env()
331
+ if env:
332
+ server["env"] = env
333
+ if client == "generic":
334
+ return server
335
+ if client not in {"codex", "claude"}:
336
+ raise ValueError("client must be generic, codex, or claude")
337
+ return {"mcpServers": {"adaptive-memory-engine": server}}
338
+
339
+ def _corpora(self) -> dict[str, Any]:
340
+ home = ensure_runtime_layout()
341
+ corpora_root = home / "corpora"
342
+ corpora = []
343
+ for child in sorted(corpora_root.iterdir()):
344
+ if child.is_dir():
345
+ corpora.append({"corpus_id": child.name, "path": str(child)})
346
+ return {"ame_home": str(home), "corpora": corpora}
347
+
348
+
349
+ class McpStdioServer:
350
+ def __init__(self, corpus_root: Path | None = None):
351
+ self.corpus_root = corpus_root
352
+ self.toolbox = BootstrapMcpToolbox(corpus_root)
353
+ self.should_stop = False
354
+
355
+ def run(self, stdin: TextIO | None = None, stdout: TextIO | None = None) -> None:
356
+ stdin = stdin or sys.stdin
357
+ stdout = stdout or sys.stdout
358
+ for line in stdin:
359
+ response = self.handle_line(line)
360
+ if response is not None:
361
+ stdout.write(json.dumps(response, ensure_ascii=False, separators=(",", ":"), default=str) + "\n")
362
+ stdout.flush()
363
+ if self.should_stop:
364
+ break
365
+
366
+ def handle_line(self, line: str) -> dict[str, Any] | list[dict[str, Any]] | None:
367
+ if not line.strip():
368
+ return None
369
+ try:
370
+ payload = json.loads(line)
371
+ except json.JSONDecodeError as exc:
372
+ return self._error(None, -32700, f"Parse error: {exc}")
373
+ return self.handle(payload)
374
+
375
+ def handle(self, payload: Any) -> dict[str, Any] | list[dict[str, Any]] | None:
376
+ if isinstance(payload, list):
377
+ responses = [self.handle(item) for item in payload]
378
+ return [response for response in responses if response is not None] or None
379
+ if not isinstance(payload, dict):
380
+ return self._error(None, -32600, "Invalid request")
381
+
382
+ request_id = payload.get("id")
383
+ method = payload.get("method")
384
+ params = payload.get("params") or {}
385
+
386
+ if request_id is None:
387
+ self._handle_notification(method)
388
+ return None
389
+
390
+ try:
391
+ result = self._handle_request(str(method), params)
392
+ except ValueError as exc:
393
+ return self._error(request_id, -32602, str(exc))
394
+ except Exception as exc: # pragma: no cover - defensive JSON-RPC boundary.
395
+ return self._error(request_id, -32603, str(exc))
396
+
397
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
398
+
399
+ def _handle_notification(self, method: Any) -> None:
400
+ if method == "notifications/initialized":
401
+ return
402
+ if method == "notifications/cancelled":
403
+ return
404
+
405
+ def _handle_request(self, method: str, params: dict[str, Any]) -> dict[str, Any] | None:
406
+ if method == "initialize":
407
+ return {
408
+ "protocolVersion": "2024-11-05",
409
+ "capabilities": {"tools": {}},
410
+ "serverInfo": {"name": "adaptive-memory-engine", "version": SERVER_VERSION},
411
+ }
412
+ if method == "ping":
413
+ return {}
414
+ if method == "tools/list":
415
+ return {"tools": [self._tool_for_mcp(tool) for tool in _tools_for_mode(corpus_bound=self.corpus_root is not None)]}
416
+ if method == "tools/call":
417
+ return self._call_tool(params)
418
+ if method == "resources/list":
419
+ return {"resources": []}
420
+ if method == "prompts/list":
421
+ return {"prompts": []}
422
+ if method == "shutdown":
423
+ self.should_stop = True
424
+ return None
425
+ raise ValueError(f"Unsupported MCP method: {method}")
426
+
427
+ def _call_tool(self, params: dict[str, Any]) -> dict[str, Any]:
428
+ name = params.get("name")
429
+ if not isinstance(name, str) or not name:
430
+ raise ValueError("tools/call requires params.name")
431
+ arguments = params.get("arguments") or {}
432
+ if not isinstance(arguments, dict):
433
+ raise ValueError("tools/call params.arguments must be an object")
434
+
435
+ try:
436
+ result = self.toolbox.call(name, arguments)
437
+ return {
438
+ "content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2, default=str)}],
439
+ "isError": False,
440
+ }
441
+ except Exception as exc:
442
+ return {"content": [{"type": "text", "text": str(exc)}], "isError": True}
443
+
444
+ def _tool_for_mcp(self, tool: McpToolSpec) -> dict[str, Any]:
445
+ return {
446
+ "name": tool.name,
447
+ "description": tool.description,
448
+ "inputSchema": tool.input_schema,
449
+ }
450
+
451
+ def _error(self, request_id: Any, code: int, message: str) -> dict[str, Any]:
452
+ return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
453
+
454
+
455
+ def _tools_for_mode(*, corpus_bound: bool) -> list[McpToolSpec]:
456
+ if corpus_bound:
457
+ return READ_TOOLS
458
+ return BOOTSTRAP_TOOLS + [_with_corpus_argument(tool) for tool in READ_TOOLS]
459
+
460
+
461
+ def _with_corpus_argument(tool: McpToolSpec) -> McpToolSpec:
462
+ schema = json.loads(json.dumps(tool.input_schema))
463
+ properties = schema.setdefault("properties", {})
464
+ properties["corpus_id"] = {"type": "string", "description": "AME corpus id to query."}
465
+ required = schema.setdefault("required", [])
466
+ if "corpus_id" not in required:
467
+ required.append("corpus_id")
468
+ return McpToolSpec(name=tool.name, description=tool.description, input_schema=schema)
469
+
470
+
471
+ def _current_mcp_env() -> dict[str, str]:
472
+ if not os.environ.get("AME_HOME"):
473
+ return {}
474
+ return {"AME_HOME": str(ame_home().expanduser().resolve())}
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from ame.bronze.store import BronzeStore
6
+ from ame.gold.resolver import SupersedesResolver
7
+ from ame.gold.store import GoldStore
8
+ from ame.query.diff import MemoryDiff, MemoryDiffEngine
9
+ from ame.query.engine import QueryEngine
10
+ from ame.query.mql import MqlEngine, MqlResult
11
+ from ame.query.result import QueryResult
12
+ from ame.silver.store import SilverStore
13
+ from ame.agent.results import DecisionsResult, GraphResult, RetrieveResult, WriteResult
14
+ from ame.writeback import MemoryWriter
15
+
16
+
17
+ class AgentMemoryAPI:
18
+ def __init__(self, corpus_root: Path):
19
+ self.corpus_root = corpus_root
20
+ self.bronze = BronzeStore(corpus_root)
21
+ self.gold = GoldStore(corpus_root)
22
+
23
+ def search(self, query: str) -> QueryResult:
24
+ return QueryEngine(self.bronze, self.gold).query(query)
25
+
26
+ def retrieve(self, query: str, k: int = 8) -> RetrieveResult:
27
+ normalized = query.casefold()
28
+ matches: list[dict] = []
29
+ for node in self.gold.nodes():
30
+ if normalized in node.name.casefold() or normalized in node.type.casefold():
31
+ matches.append({"kind": "node", **node.model_dump()})
32
+ for edge in self.gold.edges():
33
+ haystack = f"{edge.source} {edge.relation} {edge.target}".casefold()
34
+ if normalized in haystack:
35
+ matches.append({"kind": "edge", **edge.model_dump()})
36
+ for document in self.bronze.list():
37
+ if normalized in document.content.casefold() or normalized in document.source_id.casefold():
38
+ matches.append(
39
+ {
40
+ "kind": "document",
41
+ "id": document.id,
42
+ "source_id": document.source_id,
43
+ "source_type": document.source_type,
44
+ "metadata": document.metadata,
45
+ }
46
+ )
47
+ return RetrieveResult(query=query, matches=matches[:k])
48
+
49
+ def graph(self, entity: str) -> GraphResult:
50
+ normalized = entity.casefold()
51
+ edges = [
52
+ edge
53
+ for edge in self.gold.edges()
54
+ if normalized in edge.source.casefold() or normalized in edge.target.casefold()
55
+ ]
56
+ names = {edge.source.casefold() for edge in edges} | {edge.target.casefold() for edge in edges}
57
+ nodes = [node for node in self.gold.nodes() if node.name.casefold() in names or normalized in node.name.casefold()]
58
+ return GraphResult(
59
+ entity=entity,
60
+ nodes=[node.model_dump() for node in nodes],
61
+ edges=[edge.model_dump() for edge in edges],
62
+ )
63
+
64
+ def current_decision(self) -> QueryResult:
65
+ current = [event for event in self._timeline() if event.current and event.status == "accepted"]
66
+ titles = ", ".join(event.title for event in current) or "없음"
67
+ answer = f"현재 current decision은 {titles}이다. superseded decision은 current=false로 제외한다."
68
+ return QueryResult(
69
+ answer=answer,
70
+ matches=[answer],
71
+ sources=[],
72
+ confidence=0.9,
73
+ raw={
74
+ "answer_builder": "agent_memory_api",
75
+ "api": "memory.current_decision",
76
+ "current_decisions": [event.title for event in current],
77
+ },
78
+ )
79
+
80
+ def decisions(
81
+ self,
82
+ project: str | None = None,
83
+ current_only: bool = True,
84
+ date_from: str | None = None,
85
+ date_to: str | None = None,
86
+ ) -> DecisionsResult:
87
+ events = self._timeline()
88
+ if project:
89
+ events = [event for event in events if event.project and project.casefold() in event.project.casefold()]
90
+ if current_only:
91
+ events = [event for event in events if event.current]
92
+ if date_from:
93
+ events = [event for event in events if event.valid_from and event.valid_from >= date_from]
94
+ if date_to:
95
+ events = [event for event in events if event.valid_from and event.valid_from <= date_to]
96
+ return DecisionsResult(
97
+ project=project,
98
+ current_only=current_only,
99
+ decisions=[event.model_dump() for event in events],
100
+ )
101
+
102
+ def timeline(self, project: str | None = None) -> MqlResult:
103
+ suffix = f' FOR project="{project}"' if project else ""
104
+ return MqlEngine().execute(f"SHOW timeline{suffix}", self._timeline())
105
+
106
+ def why(self, decision: str) -> MqlResult:
107
+ return MqlEngine().execute(f'WHY decision="{decision}"', self._timeline(), self._rationales())
108
+
109
+ def diff(self, days: int = 7) -> MemoryDiff:
110
+ return MemoryDiffEngine().diff_last_days(self._timeline(), days=days)
111
+
112
+ def write_decision(
113
+ self,
114
+ title: str,
115
+ rationale: str,
116
+ project: str | None = None,
117
+ participants: list[str] | None = None,
118
+ source: str = "writeback",
119
+ ) -> WriteResult:
120
+ path = MemoryWriter().write_decision(
121
+ self.corpus_root.name,
122
+ title,
123
+ rationale,
124
+ project=project,
125
+ participants=participants,
126
+ source=source,
127
+ )
128
+ return WriteResult(kind="decision", path=str(path), title=title)
129
+
130
+ def write_note(self, title: str, content: str) -> WriteResult:
131
+ path = MemoryWriter().write_note(self.corpus_root.name, title, content)
132
+ return WriteResult(kind="note", path=str(path), title=title)
133
+
134
+ def mql(self, query: str) -> MqlResult | None:
135
+ return MqlEngine().execute(query, self._timeline(), self._rationales())
136
+
137
+ def _timeline(self):
138
+ return SupersedesResolver().resolve(self.gold.timeline(), self.gold.edges())
139
+
140
+ def _rationales(self):
141
+ return SilverStore(self.corpus_root).rationales()
ame/agent/results.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class RetrieveResult(BaseModel):
9
+ query: str
10
+ matches: list[dict[str, Any]] = Field(default_factory=list)
11
+
12
+
13
+ class GraphResult(BaseModel):
14
+ entity: str
15
+ nodes: list[dict[str, Any]] = Field(default_factory=list)
16
+ edges: list[dict[str, Any]] = Field(default_factory=list)
17
+
18
+
19
+ class DecisionsResult(BaseModel):
20
+ project: str | None = None
21
+ current_only: bool = True
22
+ decisions: list[dict[str, Any]] = Field(default_factory=list)
23
+
24
+
25
+ class WriteResult(BaseModel):
26
+ kind: str
27
+ path: str
28
+ title: str
29
+ ingested: bool = True
30
+ raw: dict[str, Any] = Field(default_factory=dict)
ame/bronze/schema.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class BronzeDocument(BaseModel):
10
+ id: str
11
+ corpus_id: str
12
+ source_type: str
13
+ source_id: str
14
+ content: str
15
+ metadata: dict[str, Any] = Field(default_factory=dict)
16
+ content_hash: str
17
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))