agentcache-core 0.9.9__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 (52) hide show
  1. agentcache/__init__.py +29 -0
  2. agentcache/app.py +312 -0
  3. agentcache/cli.py +346 -0
  4. agentcache/connect.py +724 -0
  5. agentcache/core/__init__.py +19 -0
  6. agentcache/core/audit_log.py +104 -0
  7. agentcache/core/config.py +51 -0
  8. agentcache/core/context_builder.py +209 -0
  9. agentcache/core/graph.py +120 -0
  10. agentcache/core/image_store.py +111 -0
  11. agentcache/core/infer.py +142 -0
  12. agentcache/core/kv_scopes.py +86 -0
  13. agentcache/core/lessons.py +242 -0
  14. agentcache/core/llm.py +596 -0
  15. agentcache/core/memory_store.py +207 -0
  16. agentcache/core/observation_store.py +576 -0
  17. agentcache/core/privacy.py +34 -0
  18. agentcache/core/project_profile.py +625 -0
  19. agentcache/core/search_service.py +444 -0
  20. agentcache/core/session_store.py +382 -0
  21. agentcache/core/slots.py +504 -0
  22. agentcache/db.py +359 -0
  23. agentcache/import_data.py +86 -0
  24. agentcache/legacy.py +94 -0
  25. agentcache/mcp_stdio.py +141 -0
  26. agentcache/py.typed +0 -0
  27. agentcache/replay_import.py +680 -0
  28. agentcache/routes/__init__.py +29 -0
  29. agentcache/routes/_deps.py +46 -0
  30. agentcache/routes/auth.py +68 -0
  31. agentcache/routes/graph.py +81 -0
  32. agentcache/routes/health.py +149 -0
  33. agentcache/routes/mcp.py +614 -0
  34. agentcache/routes/memories.py +116 -0
  35. agentcache/routes/migration.py +32 -0
  36. agentcache/routes/observations.py +253 -0
  37. agentcache/routes/search.py +80 -0
  38. agentcache/search.py +935 -0
  39. agentcache/storage/__init__.py +29 -0
  40. agentcache/storage/images.py +102 -0
  41. agentcache/storage/paths.py +116 -0
  42. agentcache/storage/scopes.py +9 -0
  43. agentcache/viewer/favicon.svg +1 -0
  44. agentcache/viewer/index.html +4235 -0
  45. agentcache/viewer_helpers.py +61 -0
  46. agentcache/workers.py +192 -0
  47. agentcache_core-0.9.9.dist-info/METADATA +194 -0
  48. agentcache_core-0.9.9.dist-info/RECORD +52 -0
  49. agentcache_core-0.9.9.dist-info/WHEEL +5 -0
  50. agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
  51. agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
  52. agentcache_core-0.9.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,116 @@
1
+ """
2
+ Memory routes blueprint.
3
+
4
+ Handles:
5
+ POST /agentmemory/remember
6
+ POST /agentmemory/agent/remember
7
+ GET /agentmemory/memories
8
+ POST /agentmemory/forget
9
+ """
10
+
11
+ from flask import Blueprint, jsonify, request
12
+
13
+ from .. import legacy as functions
14
+ from ..core import KV
15
+ from ._deps import get_kv, get_observation_store
16
+ from .auth import require_auth
17
+
18
+
19
+ def create_memories_bp(kv=None):
20
+ """Blueprint factory — receives kv at registration time (falls back to get_kv())."""
21
+ bp = Blueprint("memories", __name__)
22
+
23
+ def _kv():
24
+ return kv if kv is not None else get_kv()
25
+
26
+ # ------------------------------------------------------------------
27
+ # POST /agentmemory/remember
28
+ # ------------------------------------------------------------------
29
+
30
+ @bp.route("/agentcache/remember", methods=["POST"])
31
+ @bp.route("/agentmemory/remember", methods=["POST"])
32
+ @require_auth
33
+ def api_remember():
34
+ try:
35
+ body = request.get_json(force=True) or {}
36
+ res = functions.remember(_kv(), body)
37
+ return jsonify(res), 201
38
+ except Exception as e:
39
+ return jsonify({"error": str(e)}), 400
40
+
41
+ # ------------------------------------------------------------------
42
+ # POST /agentmemory/agent/remember
43
+ # ------------------------------------------------------------------
44
+
45
+ @bp.route("/agentcache/agent/remember", methods=["POST"])
46
+ @bp.route("/agentmemory/agent/remember", methods=["POST"])
47
+ @require_auth
48
+ def api_agent_remember():
49
+ try:
50
+ body = request.get_json(force=True) or {}
51
+ content = body.get("content")
52
+ if not content:
53
+ return jsonify({"error": "content is required"}), 400
54
+
55
+ agent_id = body.get("agentId")
56
+ project = body.get("project")
57
+ mem_type = body.get("type") or "fact"
58
+
59
+ concepts = body.get("concepts") or []
60
+ if isinstance(concepts, str):
61
+ concepts = [c.strip() for c in concepts.split(",") if c.strip()]
62
+
63
+ files = body.get("files") or []
64
+ if isinstance(files, str):
65
+ files = [f.strip() for f in files.split(",") if f.strip()]
66
+
67
+ payload = {
68
+ "content": content,
69
+ "type": mem_type,
70
+ "concepts": concepts,
71
+ "files": files,
72
+ "project": project,
73
+ }
74
+ if agent_id:
75
+ payload["agentId"] = agent_id
76
+
77
+ res = functions.remember(_kv(), payload)
78
+ return jsonify(res), 201
79
+ except Exception as e:
80
+ return jsonify({"error": str(e)}), 400
81
+
82
+ # ------------------------------------------------------------------
83
+ # GET /agentmemory/memories
84
+ # ------------------------------------------------------------------
85
+
86
+ @bp.route("/agentcache/memories", methods=["GET"])
87
+ @bp.route("/agentmemory/memories", methods=["GET"])
88
+ @require_auth
89
+ def api_memories_list():
90
+ latest_only = request.args.get("latest", "false").lower() == "true"
91
+ limit = int(request.args.get("limit", "500"))
92
+ all_mems = _kv().list(KV.memories)
93
+ if latest_only:
94
+ all_mems = [m for m in all_mems if m.get("isLatest") is not False]
95
+ all_mems.sort(key=lambda m: m.get("createdAt", ""), reverse=True)
96
+ return jsonify({"memories": all_mems[:limit], "total": len(all_mems)}), 200
97
+
98
+ # ------------------------------------------------------------------
99
+ # POST /agentmemory/forget
100
+ # ------------------------------------------------------------------
101
+
102
+ @bp.route("/agentcache/forget", methods=["POST"])
103
+ @bp.route("/agentmemory/forget", methods=["POST"])
104
+ @require_auth
105
+ def api_forget():
106
+ try:
107
+ body = request.get_json(force=True) or {}
108
+ res = get_observation_store().forget(body)
109
+ return jsonify(res), 200
110
+ except Exception as e:
111
+ return jsonify({"error": str(e)}), 400
112
+
113
+ return bp
114
+
115
+
116
+ memories_bp = create_memories_bp(None)
@@ -0,0 +1,32 @@
1
+ """
2
+ Migration route blueprint.
3
+
4
+ Handles:
5
+ POST /agentmemory/migrate
6
+ """
7
+
8
+ from flask import Blueprint, jsonify, request
9
+
10
+ from .. import legacy as functions
11
+ from ._deps import get_kv
12
+ from .auth import require_auth
13
+
14
+ migration_bp = Blueprint("migration", __name__)
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # POST /agentcache/migrate
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ @migration_bp.route("/agentcache/migrate", methods=["POST"])
23
+ @migration_bp.route("/agentmemory/migrate", methods=["POST"])
24
+ @require_auth
25
+ def api_migrate():
26
+ try:
27
+ body = request.get_json(force=True) or {}
28
+ dry_run = bool(body.get("dry_run", False))
29
+ result = functions.migrate_sessions_to_folders(get_kv(), dry_run)
30
+ return jsonify(result), 200
31
+ except Exception as e:
32
+ return jsonify({"error": str(e)}), 400
@@ -0,0 +1,253 @@
1
+ """
2
+ Observation routes blueprint.
3
+
4
+ Handles:
5
+ POST /agentcache/observe
6
+ POST /agentcache/agent/observe
7
+ GET /agentcache/folder/observations
8
+ GET /agentcache/folders
9
+ POST /agentcache/folder/dedup
10
+ """
11
+
12
+ import datetime
13
+ from typing import Optional
14
+
15
+ from flask import Blueprint, jsonify, request
16
+
17
+ from ..core.kv_scopes import KV
18
+ from ..core.observation_store import ObservationStore
19
+ from ._deps import get_observation_store
20
+ from .auth import require_auth
21
+
22
+
23
+ def _datetime_now_iso() -> str:
24
+ return (
25
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
26
+ )
27
+
28
+
29
+ def create_observations_bp(
30
+ observation_store: Optional[ObservationStore] = None,
31
+ ) -> Blueprint:
32
+ bp = Blueprint("observations", __name__)
33
+
34
+ def get_store() -> ObservationStore:
35
+ return (
36
+ observation_store
37
+ if observation_store is not None
38
+ else get_observation_store()
39
+ )
40
+
41
+ def get_kv():
42
+ return get_store().kv
43
+
44
+ @bp.route("/agentcache/observe", methods=["POST"])
45
+ @bp.route("/agentmemory/observe", methods=["POST"])
46
+ @require_auth
47
+ def api_observe():
48
+ body = {}
49
+ try:
50
+ body = request.get_json(force=True) or {}
51
+ folder_path = body.get("folderPath")
52
+ agent_id = body.get("agentId")
53
+ text = body.get("text") or body.get("content") or ""
54
+
55
+ if not folder_path or not agent_id or not text:
56
+ return (
57
+ jsonify({"error": "folderPath, agentId, and text are required"}),
58
+ 400,
59
+ )
60
+
61
+ payload = {
62
+ "folderPath": folder_path,
63
+ "agentId": agent_id,
64
+ "text": text,
65
+ "timestamp": body.get("timestamp") or _datetime_now_iso(),
66
+ "type": body.get("type"),
67
+ "title": body.get("title"),
68
+ "concepts": body.get("concepts"),
69
+ "files": body.get("files"),
70
+ "importance": body.get("importance"),
71
+ }
72
+ res = get_store().ingest(payload)
73
+ return jsonify(res), 201
74
+ except Exception as e:
75
+ import traceback
76
+
77
+ tb = traceback.format_exc()
78
+ print(
79
+ f"[observe] 400 — keys={list(body.keys())} {type(e).__name__}: {e}\n{tb}"
80
+ )
81
+ return (
82
+ jsonify(
83
+ {
84
+ "error": str(e),
85
+ "detail": type(e).__name__,
86
+ "keys": list(body.keys()),
87
+ "tb": tb,
88
+ }
89
+ ),
90
+ 400,
91
+ )
92
+
93
+ @bp.route("/agentcache/agent/observe", methods=["POST"])
94
+ @bp.route("/agentmemory/agent/observe", methods=["POST"])
95
+ @require_auth
96
+ def api_agent_observe():
97
+ try:
98
+ body = request.get_json(force=True) or {}
99
+ folder_path = body.get("folderPath")
100
+ agent_id = body.get("agentId")
101
+ text = body.get("text") or body.get("content") or ""
102
+
103
+ if not folder_path or not agent_id or not text:
104
+ return (
105
+ jsonify({"error": "folderPath, agentId, and text are required"}),
106
+ 400,
107
+ )
108
+
109
+ timestamp = body.get("timestamp") or _datetime_now_iso()
110
+
111
+ payload = {
112
+ "folderPath": folder_path,
113
+ "agentId": agent_id,
114
+ "text": text,
115
+ "timestamp": timestamp,
116
+ "type": body.get("type"),
117
+ "title": body.get("title"),
118
+ "concepts": body.get("concepts"),
119
+ "files": body.get("files"),
120
+ "importance": body.get("importance"),
121
+ }
122
+
123
+ res = get_store().ingest(payload)
124
+ return jsonify(res), 201
125
+ except ValueError as e:
126
+ print(
127
+ f"[agent_observe] 400 ValueError — body keys: {list(body.keys())} — {e}"
128
+ )
129
+ return jsonify({"error": str(e)}), 400
130
+ except Exception as e:
131
+ import traceback
132
+
133
+ print(
134
+ f"[agent_observe] 400 error — body keys: {list(body.keys())} — {type(e).__name__}: {e}"
135
+ )
136
+ print(traceback.format_exc())
137
+ return jsonify({"error": str(e), "detail": type(e).__name__}), 400
138
+
139
+ @bp.route("/agentcache/folders", methods=["GET"])
140
+ @bp.route("/agentmemory/folders", methods=["GET"])
141
+ @require_auth
142
+ def api_folders():
143
+ from .. import legacy
144
+
145
+ folders = sorted(
146
+ get_kv().list(KV.folders),
147
+ key=lambda x: x.get("lastUpdated", ""),
148
+ reverse=True,
149
+ )
150
+ if legacy.is_agent_scope_isolated():
151
+ aid = legacy.get_agent_id()
152
+ if aid:
153
+ folders = [f for f in folders if f.get("agentId") == aid]
154
+ return jsonify({"folders": folders}), 200
155
+
156
+ @bp.route("/agentcache/folder/observations", methods=["GET"])
157
+ @bp.route("/agentmemory/folder/observations", methods=["GET"])
158
+ @require_auth
159
+ def api_folder_observations():
160
+ fp = request.args.get("folderPath")
161
+ aid = request.args.get("agentId")
162
+ if not fp or not aid:
163
+ return jsonify({"error": "folderPath and agentId are required"}), 400
164
+ from .. import legacy
165
+
166
+ if legacy.is_agent_scope_isolated():
167
+ current_aid = legacy.get_agent_id()
168
+
169
+ if current_aid and aid != current_aid:
170
+ return (
171
+ jsonify(
172
+ {
173
+ "error": "Unauthorized: Agent scope is isolated to another agent"
174
+ }
175
+ ),
176
+ 403,
177
+ )
178
+ observations = sorted(
179
+ get_kv().list(KV.folder_obs(fp, aid)),
180
+ key=lambda x: x.get("timestamp", ""),
181
+ reverse=True,
182
+ )
183
+ return (
184
+ jsonify({"observations": observations, "folderPath": fp, "agentId": aid}),
185
+ 200,
186
+ )
187
+
188
+ @bp.route("/agentcache/session/start", methods=["POST"])
189
+ @bp.route("/agentmemory/session/start", methods=["POST"])
190
+ @require_auth
191
+ def api_session_start():
192
+ import uuid
193
+
194
+ body = request.get_json(force=True) or {}
195
+ session_id = body.get("sessionId") or f"compat_{uuid.uuid4().hex[:16]}"
196
+ return (
197
+ jsonify(
198
+ {
199
+ "sessionId": session_id,
200
+ "status": "active",
201
+ "message": "Session model migrated to folder-based. Use /agentmemory/agent/observe.",
202
+ }
203
+ ),
204
+ 200,
205
+ )
206
+
207
+ @bp.route("/agentcache/session/end", methods=["POST"])
208
+ @bp.route("/agentmemory/session/end", methods=["POST"])
209
+ @require_auth
210
+ def api_session_end():
211
+ return (
212
+ jsonify({"success": True, "message": "Session model is now folder-based."}),
213
+ 200,
214
+ )
215
+
216
+ @bp.route("/agentcache/folder/dedup", methods=["POST"])
217
+ @bp.route("/agentmemory/folder/dedup", methods=["POST"])
218
+ @require_auth
219
+ def api_folder_dedup():
220
+ try:
221
+ body = request.get_json(force=True) or {}
222
+ folder_path = body.get("folderPath") or None
223
+ agent_id = body.get("agentId") or None
224
+ res = get_store().dedup(folder_path, agent_id)
225
+ return jsonify(res), 200
226
+ except Exception as e:
227
+ return jsonify({"error": str(e)}), 400
228
+
229
+ @bp.route("/agentcache/observations", methods=["GET"])
230
+ @bp.route("/agentmemory/observations", methods=["GET"])
231
+ @require_auth
232
+ def api_observations_legacy():
233
+ session_id = request.args.get("sessionId", "")
234
+ if not session_id:
235
+ return jsonify({"observations": [], "sessionId": ""}), 200
236
+
237
+ try:
238
+ obs = sorted(
239
+ get_kv().list(KV.observations(session_id)),
240
+ key=lambda x: x.get("timestamp", ""),
241
+ reverse=True,
242
+ )
243
+ return jsonify({"observations": obs, "sessionId": session_id}), 200
244
+ except Exception as e:
245
+ return (
246
+ jsonify({"observations": [], "sessionId": session_id, "error": str(e)}),
247
+ 200,
248
+ )
249
+
250
+ return bp
251
+
252
+
253
+ observations_bp = create_observations_bp()
@@ -0,0 +1,80 @@
1
+ """
2
+ Search and timeline routes blueprint.
3
+
4
+ Handles:
5
+ POST /agentmemory/search
6
+ POST /agentmemory/timeline
7
+ """
8
+
9
+ from flask import Blueprint, jsonify, request
10
+
11
+ from ._deps import get_kv, get_observation_store, get_search_service
12
+ from .auth import require_auth
13
+
14
+ search_bp = Blueprint("search", __name__)
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # POST /agentcache/search
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ @search_bp.route("/agentcache/search", methods=["POST"])
23
+ @search_bp.route("/agentmemory/search", methods=["POST"])
24
+ @require_auth
25
+ def api_search():
26
+ try:
27
+ body = request.get_json(force=True) or {}
28
+ query = body.get("query")
29
+ if not query or not query.strip():
30
+ return jsonify({"error": "query is required"}), 400
31
+ limit = body.get("limit") or 10
32
+ folder_path = body.get("folderPath")
33
+ agent_id = body.get("agentId")
34
+
35
+ search_svc = get_search_service()
36
+ if search_svc is not None:
37
+ res = search_svc.search(
38
+ query=query,
39
+ limit=limit,
40
+ folder_path=folder_path,
41
+ agent_id=agent_id,
42
+ kv=get_kv(),
43
+ )
44
+ else:
45
+ res = []
46
+ return jsonify(res), 200
47
+ except Exception as e:
48
+ return jsonify({"error": str(e)}), 400
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # POST /agentcache/timeline
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ @search_bp.route("/agentcache/timeline", methods=["POST"])
57
+ @search_bp.route("/agentmemory/timeline", methods=["POST"])
58
+ @require_auth
59
+ def api_timeline():
60
+ try:
61
+ body = request.get_json(force=True) or {}
62
+ folder_path = body.get("folderPath")
63
+ agent_id = body.get("agentId")
64
+ limit = body.get("limit") or 100
65
+ before = body.get("before")
66
+ after = body.get("after")
67
+ obs_store = get_observation_store()
68
+ if obs_store is not None:
69
+ result = obs_store.timeline(
70
+ limit=limit,
71
+ folder_path=folder_path,
72
+ agent_id=agent_id,
73
+ before=before,
74
+ after=after,
75
+ )
76
+ else:
77
+ result = []
78
+ return jsonify({"observations": result}), 200
79
+ except Exception as e:
80
+ return jsonify({"error": str(e)}), 400