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.
- adaptive_memory_engine-0.1.6.dist-info/METADATA +228 -0
- adaptive_memory_engine-0.1.6.dist-info/RECORD +72 -0
- adaptive_memory_engine-0.1.6.dist-info/WHEEL +4 -0
- adaptive_memory_engine-0.1.6.dist-info/entry_points.txt +3 -0
- adaptive_memory_engine-0.1.6.dist-info/licenses/LICENSE +21 -0
- ame/__init__.py +1 -0
- ame/agent/__init__.py +1 -0
- ame/agent/mcp.py +474 -0
- ame/agent/memory_api.py +141 -0
- ame/agent/results.py +30 -0
- ame/bronze/schema.py +17 -0
- ame/bronze/store.py +38 -0
- ame/cli/__init__.py +1 -0
- ame/cli/main.py +903 -0
- ame/connectors/base.py +30 -0
- ame/connectors/contract.py +199 -0
- ame/connectors/github.py +66 -0
- ame/connectors/google.py +464 -0
- ame/connectors/google_oauth.py +156 -0
- ame/connectors/jira.py +66 -0
- ame/connectors/json_helpers.py +43 -0
- ame/connectors/markdown.py +116 -0
- ame/connectors/notion.py +59 -0
- ame/connectors/oauth_callback.py +102 -0
- ame/connectors/oauth_provider.py +250 -0
- ame/connectors/obsidian.py +19 -0
- ame/connectors/router.py +155 -0
- ame/connectors/slack.py +66 -0
- ame/connectors/slack_oauth.py +417 -0
- ame/connectors/sync_history.py +73 -0
- ame/context_budget.py +106 -0
- ame/core/config.py +77 -0
- ame/core/corpus.py +17 -0
- ame/core/errors.py +18 -0
- ame/core/paths.py +111 -0
- ame/core/state.py +57 -0
- ame/export/obsidian.py +123 -0
- ame/gold/builder.py +300 -0
- ame/gold/ontology.py +80 -0
- ame/gold/resolver.py +91 -0
- ame/gold/schema.py +40 -0
- ame/gold/store.py +45 -0
- ame/hardware/profiler.py +85 -0
- ame/hardware/tier.py +27 -0
- ame/hermes/__init__.py +3 -0
- ame/hermes/memory.py +209 -0
- ame/models/download.py +243 -0
- ame/models/ollama.py +60 -0
- ame/models/registry.py +101 -0
- ame/models/router.py +22 -0
- ame/pipeline.py +155 -0
- ame/query/diff.py +40 -0
- ame/query/engine.py +919 -0
- ame/query/memory_os.py +313 -0
- ame/query/mql.py +84 -0
- ame/query/multihop.py +264 -0
- ame/query/result.py +20 -0
- ame/sdk.py +52 -0
- ame/security.py +145 -0
- ame/silver/extractor.py +414 -0
- ame/silver/llm_extractor.py +181 -0
- ame/silver/prompts.py +56 -0
- ame/silver/rationale.py +140 -0
- ame/silver/schema.py +51 -0
- ame/silver/store.py +59 -0
- ame/storage/custom_kg.py +33 -0
- ame/storage/lightrag_adapter.py +362 -0
- ame/validation/confidence.py +5 -0
- ame/validation/grounding.py +10 -0
- ame/validation/type_gate.py +22 -0
- ame/writeback.py +173 -0
- 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())}
|
ame/agent/memory_api.py
ADDED
|
@@ -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))
|