agent-memory-engine 0.1.0__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 (39) hide show
  1. agent_memory/__init__.py +33 -0
  2. agent_memory/cli.py +142 -0
  3. agent_memory/client.py +355 -0
  4. agent_memory/config.py +28 -0
  5. agent_memory/controller/__init__.py +15 -0
  6. agent_memory/controller/conflict.py +95 -0
  7. agent_memory/controller/consolidation.py +136 -0
  8. agent_memory/controller/forgetting.py +29 -0
  9. agent_memory/controller/router.py +62 -0
  10. agent_memory/controller/trust.py +31 -0
  11. agent_memory/embedding/__init__.py +5 -0
  12. agent_memory/embedding/base.py +11 -0
  13. agent_memory/embedding/local_provider.py +38 -0
  14. agent_memory/embedding/openai_provider.py +11 -0
  15. agent_memory/extraction/__init__.py +5 -0
  16. agent_memory/extraction/entity_extractor.py +13 -0
  17. agent_memory/extraction/pipeline.py +123 -0
  18. agent_memory/extraction/prompts.py +40 -0
  19. agent_memory/governance/__init__.py +6 -0
  20. agent_memory/governance/audit.py +14 -0
  21. agent_memory/governance/export.py +72 -0
  22. agent_memory/governance/health.py +40 -0
  23. agent_memory/interfaces/__init__.py +14 -0
  24. agent_memory/interfaces/mcp_server.py +128 -0
  25. agent_memory/interfaces/rest_api.py +71 -0
  26. agent_memory/llm/__init__.py +5 -0
  27. agent_memory/llm/base.py +23 -0
  28. agent_memory/llm/ollama_client.py +64 -0
  29. agent_memory/llm/openai_client.py +94 -0
  30. agent_memory/models.py +149 -0
  31. agent_memory/storage/__init__.py +4 -0
  32. agent_memory/storage/base.py +59 -0
  33. agent_memory/storage/schema.sql +125 -0
  34. agent_memory/storage/sqlite_backend.py +762 -0
  35. agent_memory_engine-0.1.0.dist-info/METADATA +228 -0
  36. agent_memory_engine-0.1.0.dist-info/RECORD +39 -0
  37. agent_memory_engine-0.1.0.dist-info/WHEEL +4 -0
  38. agent_memory_engine-0.1.0.dist-info/entry_points.txt +2 -0
  39. agent_memory_engine-0.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+
5
+ from agent_memory.client import MemoryClient
6
+ from agent_memory.config import AgentMemoryConfig
7
+
8
+
9
+ class _FallbackMCPServer:
10
+ def __init__(self, name: str) -> None:
11
+ self.name = name
12
+ self.tools: dict[str, object] = {}
13
+
14
+ def tool(self, name: str | None = None):
15
+ def decorator(func):
16
+ self.tools[name or func.__name__] = func
17
+ return func
18
+
19
+ return decorator
20
+
21
+
22
+ def _serialize_trace(report) -> dict[str, object]:
23
+ return {
24
+ "focus": {"id": report.focus.id, "content": report.focus.content},
25
+ "ancestors": [{"id": item.id, "content": item.content} for item in report.ancestors],
26
+ "descendants": [{"id": item.id, "content": item.content} for item in report.descendants],
27
+ "relations": [
28
+ {
29
+ "source_id": edge.source_id,
30
+ "target_id": edge.target_id,
31
+ "relation_type": edge.relation_type.value,
32
+ }
33
+ for edge in report.relations
34
+ ],
35
+ "evolution_events": report.evolution_events,
36
+ }
37
+
38
+
39
+ def _make_server() -> object:
40
+ try:
41
+ from fastmcp import FastMCP
42
+
43
+ return FastMCP("agent-memory")
44
+ except ImportError:
45
+ return _FallbackMCPServer("agent-memory")
46
+
47
+
48
+ def create_mcp_server(client: MemoryClient) -> object:
49
+ server = _make_server()
50
+
51
+ @server.tool("memory_store")
52
+ def memory_store(content: str, source_id: str, memory_type: str = "semantic") -> dict[str, object]:
53
+ item = client.add(content, source_id=source_id, memory_type=memory_type) # type: ignore[arg-type]
54
+ return {"id": item.id, "content": item.content, "trust_score": item.trust_score}
55
+
56
+ @server.tool("memory_search")
57
+ def memory_search(query: str, limit: int = 5) -> list[dict[str, object]]:
58
+ return [
59
+ {
60
+ "id": result.item.id,
61
+ "content": result.item.content,
62
+ "score": result.score,
63
+ "matched_by": result.matched_by,
64
+ }
65
+ for result in client.search(query, limit=limit)
66
+ ]
67
+
68
+ @server.tool("memory_ingest_conversation")
69
+ def memory_ingest_conversation(turns: list[dict[str, str]], source_id: str) -> list[dict[str, object]]:
70
+ items = client.ingest_conversation(
71
+ [client.turn_model(**turn) for turn in turns],
72
+ source_id=source_id,
73
+ )
74
+ return [{"id": item.id, "content": item.content} for item in items]
75
+
76
+ @server.tool("memory_trace")
77
+ def memory_trace(memory_id: str, max_depth: int = 10) -> dict[str, object]:
78
+ return _serialize_trace(client.trace_graph(memory_id, max_depth=max_depth))
79
+
80
+ @server.tool("memory_health")
81
+ def memory_health() -> dict[str, object]:
82
+ return asdict(client.health())
83
+
84
+ @server.tool("memory_audit")
85
+ def memory_audit(limit: int = 50) -> list[dict[str, object]]:
86
+ return client.audit_events(limit=limit)
87
+
88
+ @server.tool("memory_evolution")
89
+ def memory_evolution(memory_id: str, limit: int = 50) -> list[dict[str, object]]:
90
+ return client.evolution_events(memory_id=memory_id, limit=limit)
91
+
92
+ @server.tool("memory_update")
93
+ def memory_update(memory_id: str, content: str) -> dict[str, object]:
94
+ item = client.get(memory_id)
95
+ if item is None:
96
+ raise ValueError(f"Memory {memory_id} not found")
97
+ updated = client.update(item, content=content)
98
+ return {"id": updated.id, "content": updated.content}
99
+
100
+ @server.tool("memory_delete")
101
+ def memory_delete(memory_id: str) -> dict[str, object]:
102
+ return {"deleted": client.delete(memory_id)}
103
+
104
+ @server.tool("memory_maintain")
105
+ def memory_maintain() -> dict[str, object]:
106
+ return asdict(client.maintain())
107
+
108
+ @server.tool("memory_export")
109
+ def memory_export(path: str) -> dict[str, object]:
110
+ exported = client.export_jsonl(path)
111
+ return {"path": path, "exported": exported}
112
+
113
+ return server
114
+
115
+
116
+ def main() -> int:
117
+ client = MemoryClient(config=AgentMemoryConfig.from_env())
118
+ server = create_mcp_server(client)
119
+ run = getattr(server, "run", None)
120
+ if callable(run):
121
+ run()
122
+ return 0
123
+ print("FastMCP is not installed. Install with `pip install -e .[mcp]`.")
124
+ return 1
125
+
126
+
127
+ if __name__ == "__main__":
128
+ raise SystemExit(main())
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+
5
+ from agent_memory.client import MemoryClient
6
+ from agent_memory.interfaces.mcp_server import _serialize_trace
7
+
8
+
9
+ class _FallbackRestApp:
10
+ def __init__(self) -> None:
11
+ self.routes: dict[str, object] = {}
12
+
13
+ def add_api_route(self, path: str, endpoint, methods: list[str]) -> None:
14
+ for method in methods:
15
+ self.routes[f"{method.upper()} {path}"] = endpoint
16
+
17
+
18
+ def _make_app() -> object:
19
+ try:
20
+ from fastapi import FastAPI
21
+
22
+ return FastAPI(title="agent-memory")
23
+ except ImportError:
24
+ return _FallbackRestApp()
25
+
26
+
27
+ def create_rest_app(client: MemoryClient) -> object:
28
+ app = _make_app()
29
+
30
+ def create_memory(payload: dict[str, object]) -> dict[str, object]:
31
+ item = client.add(
32
+ str(payload["content"]),
33
+ source_id=str(payload.get("source_id", "rest")),
34
+ memory_type=str(payload.get("memory_type", "semantic")), # type: ignore[arg-type]
35
+ )
36
+ return {"id": item.id, "content": item.content}
37
+
38
+ def search_memory(query: str, limit: int = 5) -> list[dict[str, object]]:
39
+ return [
40
+ {"id": result.item.id, "content": result.item.content, "score": result.score}
41
+ for result in client.search(query, limit=limit)
42
+ ]
43
+
44
+ def read_health() -> dict[str, object]:
45
+ return asdict(client.health())
46
+
47
+ def read_audit(limit: int = 50) -> list[dict[str, object]]:
48
+ return client.audit_events(limit=limit)
49
+
50
+ def read_evolution(memory_id: str, limit: int = 50) -> list[dict[str, object]]:
51
+ return client.evolution_events(memory_id=memory_id, limit=limit)
52
+
53
+ def trace_memory(memory_id: str, max_depth: int = 10) -> dict[str, object]:
54
+ return _serialize_trace(client.trace_graph(memory_id, max_depth=max_depth))
55
+
56
+ def run_maintenance() -> dict[str, object]:
57
+ return asdict(client.maintain())
58
+
59
+ def export_memory(path: str) -> dict[str, object]:
60
+ exported = client.export_jsonl(path)
61
+ return {"path": path, "exported": exported}
62
+
63
+ app.add_api_route("/memories", create_memory, methods=["POST"])
64
+ app.add_api_route("/search", search_memory, methods=["GET"])
65
+ app.add_api_route("/health", read_health, methods=["GET"])
66
+ app.add_api_route("/audit", read_audit, methods=["GET"])
67
+ app.add_api_route("/evolution", read_evolution, methods=["GET"])
68
+ app.add_api_route("/trace", trace_memory, methods=["GET"])
69
+ app.add_api_route("/maintain", run_maintenance, methods=["POST"])
70
+ app.add_api_route("/export", export_memory, methods=["POST"])
71
+ return app
@@ -0,0 +1,5 @@
1
+ from agent_memory.llm.ollama_client import OllamaClient
2
+ from agent_memory.llm.openai_client import OpenAIClient
3
+
4
+ __all__ = ["OllamaClient", "OpenAIClient"]
5
+
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from typing import Protocol
5
+
6
+
7
+ class LLMClient(Protocol):
8
+ def complete(self, prompt: str, system_prompt: str | None = None) -> str:
9
+ ...
10
+
11
+ def generate_json(
12
+ self,
13
+ *,
14
+ prompt: str,
15
+ schema: dict[str, Any],
16
+ schema_name: str,
17
+ system_prompt: str | None = None,
18
+ ) -> dict[str, Any]:
19
+ ...
20
+
21
+
22
+ class LLMClientError(RuntimeError):
23
+ pass
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+ from urllib import request
7
+
8
+ from agent_memory.llm.base import LLMClientError
9
+
10
+
11
+ class OllamaClient:
12
+ def __init__(self, model: str = "llama3.1", base_url: str | None = None) -> None:
13
+ self.model = model
14
+ self.base_url = (base_url or os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")).rstrip("/")
15
+
16
+ def complete(self, prompt: str, system_prompt: str | None = None) -> str:
17
+ payload: dict[str, Any] = {
18
+ "model": self.model,
19
+ "prompt": prompt,
20
+ "stream": False,
21
+ }
22
+ if system_prompt:
23
+ payload["system"] = system_prompt
24
+ response = self._request_json("/api/generate", payload)
25
+ if "response" not in response:
26
+ raise LLMClientError(f"Ollama response missing text: {response}")
27
+ return str(response["response"])
28
+
29
+ def generate_json(
30
+ self,
31
+ *,
32
+ prompt: str,
33
+ schema: dict[str, Any],
34
+ schema_name: str,
35
+ system_prompt: str | None = None,
36
+ ) -> dict[str, Any]:
37
+ payload: dict[str, Any] = {
38
+ "model": self.model,
39
+ "prompt": prompt,
40
+ "stream": False,
41
+ "format": schema,
42
+ }
43
+ if system_prompt:
44
+ payload["system"] = system_prompt
45
+ response = self._request_json("/api/generate", payload)
46
+ text = str(response.get("response", "")).strip()
47
+ try:
48
+ return json.loads(text)
49
+ except json.JSONDecodeError as exc:
50
+ raise LLMClientError(f"Ollama response was not valid JSON: {text}") from exc
51
+
52
+ def _request_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
53
+ body = json.dumps(payload).encode("utf-8")
54
+ req = request.Request(
55
+ f"{self.base_url}{path}",
56
+ data=body,
57
+ method="POST",
58
+ headers={"Content-Type": "application/json"},
59
+ )
60
+ try:
61
+ with request.urlopen(req) as response:
62
+ return json.loads(response.read().decode("utf-8"))
63
+ except Exception as exc:
64
+ raise LLMClientError(f"Ollama request failed: {exc}") from exc
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+ from urllib import request
7
+
8
+ from agent_memory.llm.base import LLMClientError
9
+
10
+
11
+ class OpenAIClient:
12
+ def __init__(
13
+ self,
14
+ model: str = "gpt-4o-mini",
15
+ api_key: str | None = None,
16
+ base_url: str = "https://api.openai.com/v1",
17
+ ) -> None:
18
+ self.model = model
19
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
20
+ self.base_url = base_url.rstrip("/")
21
+
22
+ def complete(self, prompt: str, system_prompt: str | None = None) -> str:
23
+ payload = {
24
+ "model": self.model,
25
+ "input": self._build_input(prompt=prompt, system_prompt=system_prompt),
26
+ }
27
+ response = self._request_json("/responses", payload)
28
+ return self._extract_output_text(response)
29
+
30
+ def generate_json(
31
+ self,
32
+ *,
33
+ prompt: str,
34
+ schema: dict[str, Any],
35
+ schema_name: str,
36
+ system_prompt: str | None = None,
37
+ ) -> dict[str, Any]:
38
+ payload = {
39
+ "model": self.model,
40
+ "input": self._build_input(prompt=prompt, system_prompt=system_prompt),
41
+ "text": {
42
+ "format": {
43
+ "type": "json_schema",
44
+ "name": schema_name,
45
+ "strict": True,
46
+ "schema": schema,
47
+ }
48
+ },
49
+ }
50
+ response = self._request_json("/responses", payload)
51
+ text = self._extract_output_text(response)
52
+ try:
53
+ return json.loads(text)
54
+ except json.JSONDecodeError as exc:
55
+ raise LLMClientError(f"OpenAI response was not valid JSON: {text}") from exc
56
+
57
+ def _build_input(self, *, prompt: str, system_prompt: str | None) -> list[dict[str, str]]:
58
+ messages: list[dict[str, str]] = []
59
+ if system_prompt:
60
+ messages.append({"role": "system", "content": system_prompt})
61
+ messages.append({"role": "user", "content": prompt})
62
+ return messages
63
+
64
+ def _request_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
65
+ if not self.api_key:
66
+ raise LLMClientError("OPENAI_API_KEY is required to use OpenAIClient.")
67
+ body = json.dumps(payload).encode("utf-8")
68
+ req = request.Request(
69
+ f"{self.base_url}{path}",
70
+ data=body,
71
+ method="POST",
72
+ headers={
73
+ "Authorization": f"Bearer {self.api_key}",
74
+ "Content-Type": "application/json",
75
+ },
76
+ )
77
+ try:
78
+ with request.urlopen(req) as response:
79
+ return json.loads(response.read().decode("utf-8"))
80
+ except Exception as exc:
81
+ raise LLMClientError(f"OpenAI request failed: {exc}") from exc
82
+
83
+ def _extract_output_text(self, response: dict[str, Any]) -> str:
84
+ for output in response.get("output", []):
85
+ if output.get("type") != "message":
86
+ continue
87
+ for item in output.get("content", []):
88
+ if item.get("type") == "refusal":
89
+ raise LLMClientError(item.get("refusal", "Model refused request."))
90
+ if item.get("type") in {"output_text", "text"} and item.get("text"):
91
+ return str(item["text"])
92
+ if response.get("output_text"):
93
+ return str(response["output_text"])
94
+ raise LLMClientError(f"Could not extract text from OpenAI response: {response}")
agent_memory/models.py ADDED
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+
9
+ def utc_now() -> datetime:
10
+ return datetime.now(timezone.utc)
11
+
12
+
13
+ class MemoryType(str, Enum):
14
+ SEMANTIC = "semantic"
15
+ EPISODIC = "episodic"
16
+ PROCEDURAL = "procedural"
17
+
18
+
19
+ class MemoryLayer(str, Enum):
20
+ SHORT_TERM = "short_term"
21
+ LONG_TERM = "long_term"
22
+
23
+
24
+ class QueryIntent(str, Enum):
25
+ FACTUAL = "factual"
26
+ TEMPORAL = "temporal"
27
+ CAUSAL = "causal"
28
+ EXPLORATORY = "exploratory"
29
+ PROCEDURAL = "procedural"
30
+ GENERAL = "general"
31
+
32
+
33
+ class RelationType(str, Enum):
34
+ DERIVED_FROM = "derived_from"
35
+ SUPERSEDES = "supersedes"
36
+ SUPPORTS = "supports"
37
+ CONTRADICTS = "contradicts"
38
+ RELATED_TO = "related_to"
39
+
40
+
41
+ class ConflictResolution(str, Enum):
42
+ SUPERSEDE = "supersede"
43
+ KEEP_BOTH = "keep_both"
44
+ MANUAL = "manual"
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class MemoryItem:
49
+ id: str
50
+ content: str
51
+ memory_type: MemoryType
52
+ embedding: list[float]
53
+ created_at: datetime = field(default_factory=utc_now)
54
+ last_accessed: datetime = field(default_factory=utc_now)
55
+ access_count: int = 0
56
+ valid_from: datetime | None = None
57
+ valid_until: datetime | None = None
58
+ trust_score: float = 0.75
59
+ importance: float = 0.5
60
+ layer: MemoryLayer = MemoryLayer.SHORT_TERM
61
+ decay_rate: float = 0.1
62
+ source_id: str = "manual"
63
+ causal_parent_id: str | None = None
64
+ supersedes_id: str | None = None
65
+ entity_refs: list[str] = field(default_factory=list)
66
+ tags: list[str] = field(default_factory=list)
67
+ deleted_at: datetime | None = None
68
+
69
+
70
+ @dataclass(slots=True)
71
+ class MemoryDraft:
72
+ content: str
73
+ memory_type: MemoryType = MemoryType.SEMANTIC
74
+ importance: float = 0.5
75
+ trust_score: float = 0.7
76
+ source_id: str = "conversation"
77
+ causal_parent_id: str | None = None
78
+ supersedes_id: str | None = None
79
+ entity_refs: list[str] = field(default_factory=list)
80
+ tags: list[str] = field(default_factory=list)
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class ConversationTurn:
85
+ role: str
86
+ content: str
87
+ timestamp: datetime | None = None
88
+
89
+
90
+ @dataclass(slots=True)
91
+ class SearchResult:
92
+ item: MemoryItem
93
+ score: float
94
+ matched_by: list[str]
95
+
96
+
97
+ @dataclass(slots=True)
98
+ class RetrievalPlan:
99
+ intent: QueryIntent
100
+ strategies: list[str]
101
+ filters: dict[str, Any] = field(default_factory=dict)
102
+
103
+
104
+ @dataclass(slots=True)
105
+ class RelationEdge:
106
+ source_id: str
107
+ target_id: str
108
+ relation_type: RelationType
109
+ created_at: datetime = field(default_factory=utc_now)
110
+
111
+
112
+ @dataclass(slots=True)
113
+ class ConflictRecord:
114
+ existing_id: str
115
+ candidate_id: str | None
116
+ confidence: float
117
+ resolution: ConflictResolution
118
+ reason: str
119
+
120
+
121
+ @dataclass(slots=True)
122
+ class MaintenanceReport:
123
+ promoted: int = 0
124
+ demoted: int = 0
125
+ decayed: int = 0
126
+ conflicts_found: int = 0
127
+ conflicts_resolved: int = 0
128
+ consolidated: int = 0
129
+
130
+
131
+ @dataclass(slots=True)
132
+ class HealthReport:
133
+ total_memories: int
134
+ stale_ratio: float
135
+ orphan_ratio: float
136
+ unresolved_conflicts: int
137
+ average_trust_score: float
138
+ database_size_bytes: int
139
+ audit_events: int
140
+ suggestions: list[str] = field(default_factory=list)
141
+
142
+
143
+ @dataclass(slots=True)
144
+ class TraceReport:
145
+ focus: MemoryItem
146
+ ancestors: list[MemoryItem] = field(default_factory=list)
147
+ descendants: list[MemoryItem] = field(default_factory=list)
148
+ relations: list[RelationEdge] = field(default_factory=list)
149
+ evolution_events: list[dict[str, Any]] = field(default_factory=list)
@@ -0,0 +1,4 @@
1
+ from agent_memory.storage.sqlite_backend import SQLiteBackend
2
+
3
+ __all__ = ["SQLiteBackend"]
4
+
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from agent_memory.models import MemoryItem, RelationEdge
6
+
7
+
8
+ class StorageBackend(Protocol):
9
+ def add_memory(self, item: MemoryItem) -> MemoryItem:
10
+ ...
11
+
12
+ def get_memory(self, memory_id: str) -> MemoryItem | None:
13
+ ...
14
+
15
+ def update_memory(self, item: MemoryItem) -> MemoryItem:
16
+ ...
17
+
18
+ def soft_delete_memory(self, memory_id: str) -> bool:
19
+ ...
20
+
21
+ def search_full_text(self, query: str, limit: int = 10, memory_type: str | None = None) -> list[tuple[MemoryItem, float]]:
22
+ ...
23
+
24
+ def search_by_entities(self, entities: list[str], limit: int = 10, memory_type: str | None = None) -> list[tuple[MemoryItem, float]]:
25
+ ...
26
+
27
+ def search_by_vector(
28
+ self,
29
+ embedding: list[float],
30
+ limit: int = 10,
31
+ memory_type: str | None = None,
32
+ ) -> list[tuple[MemoryItem, float]]:
33
+ ...
34
+
35
+ def touch_memory(self, memory_id: str) -> None:
36
+ ...
37
+
38
+ def trace_ancestors(self, memory_id: str, max_depth: int = 10) -> list[MemoryItem]:
39
+ ...
40
+
41
+ def list_memories(self, include_deleted: bool = False) -> list[MemoryItem]:
42
+ ...
43
+
44
+ def add_relation(self, edge: RelationEdge) -> None:
45
+ ...
46
+
47
+ def list_relations(self, memory_id: str | None = None) -> list[RelationEdge]:
48
+ ...
49
+
50
+ def trace_descendants(self, memory_id: str, max_depth: int = 10) -> list[MemoryItem]:
51
+ ...
52
+
53
+ def relation_exists_between(
54
+ self,
55
+ left_id: str,
56
+ right_id: str,
57
+ relation_types: list[str] | None = None,
58
+ ) -> bool:
59
+ ...