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,46 @@
1
+ """
2
+ Shared service accessors for route blueprints.
3
+
4
+ Route handlers call these to reach the process-wide singletons that
5
+ ``app.py:init_services`` populates on the Flask ``extensions`` dict.
6
+ Kept here so no route file has to re-derive the "extensions → app-module
7
+ globals → raise" fallback chain that used to live in six copies.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from flask import current_app
15
+
16
+
17
+ def _from_extensions(name: str) -> Any:
18
+ ext = getattr(current_app, "extensions", {}) or {}
19
+ value = ext.get(name)
20
+ if value is not None:
21
+ return value
22
+
23
+ from .. import app as app_module
24
+
25
+ return getattr(app_module, name, None)
26
+
27
+
28
+ def get_kv() -> Any:
29
+ """Return the process StateKV. Raises RuntimeError if uninitialised."""
30
+ kv = _from_extensions("kv")
31
+ if kv is None:
32
+ raise RuntimeError("StateKV is not initialized")
33
+ return kv
34
+
35
+
36
+ def get_search_service() -> Any:
37
+ """Return the SearchService singleton (or None if search is disabled)."""
38
+ return _from_extensions("search_service")
39
+
40
+
41
+ def get_observation_store() -> Any:
42
+ """Return the ObservationStore. Raises RuntimeError if uninitialised."""
43
+ store = _from_extensions("observation_store")
44
+ if store is None:
45
+ raise RuntimeError("ObservationStore is not initialized")
46
+ return store
@@ -0,0 +1,68 @@
1
+ """
2
+ Shared authentication utilities for all route blueprints.
3
+
4
+ Public API
5
+ ----------
6
+ verify_token(provided, secret) -> bool
7
+ Raw HMAC comparison. Used directly by the WebSocket handler in app.py.
8
+
9
+ require_auth
10
+ Flask route decorator. Reads AGENTCACHE_SECRET / AGENTMEMORY_SECRET from
11
+ the environment, validates the Authorization: Bearer <token> header, and
12
+ aborts with 401 JSON if the check fails. Routes with no secret configured
13
+ are always allowed through.
14
+
15
+ Usage in a blueprint
16
+ --------------------
17
+ from .auth import require_auth
18
+
19
+ @bp.route("/agentcache/something", methods=["GET"])
20
+ @require_auth
21
+ def api_something():
22
+ ...
23
+ """
24
+
25
+ import hmac
26
+ import os
27
+ from functools import wraps
28
+
29
+ from flask import abort, jsonify, make_response, request
30
+
31
+
32
+ def verify_token(provided: str, secret: str) -> bool:
33
+ """Return True if *provided* matches *secret* via constant-time comparison."""
34
+ return hmac.compare_digest(
35
+ provided.encode("utf-8"),
36
+ secret.encode("utf-8"),
37
+ )
38
+
39
+
40
+ def require_auth(f):
41
+ """Decorator that enforces Bearer-token authentication on a Flask route.
42
+
43
+ If no secret is configured the request is passed through unchanged —
44
+ consistent with the previous per-blueprint behaviour.
45
+ """
46
+
47
+ @wraps(f)
48
+ def _wrapped(*args, **kwargs):
49
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
50
+ if not secret:
51
+ # No secret configured → open access (matches legacy behaviour).
52
+ return f(*args, **kwargs)
53
+
54
+ auth_header = request.headers.get("Authorization") or request.headers.get(
55
+ "authorization"
56
+ )
57
+ if not auth_header or not auth_header.startswith("Bearer "):
58
+ resp = make_response(jsonify({"error": "unauthorized"}), 401)
59
+ abort(resp)
60
+
61
+ provided_token = auth_header[7:].strip()
62
+ if not verify_token(provided_token, secret):
63
+ resp = make_response(jsonify({"error": "unauthorized"}), 401)
64
+ abort(resp)
65
+
66
+ return f(*args, **kwargs)
67
+
68
+ return _wrapped
@@ -0,0 +1,81 @@
1
+ """
2
+ Graph routes blueprint.
3
+
4
+ Handles:
5
+ GET /agentmemory/graph
6
+ GET /agentmemory/graph/stats
7
+ POST /agentmemory/graph/query
8
+ POST /agentmemory/graph/build
9
+ """
10
+
11
+ from flask import Blueprint, jsonify, request
12
+
13
+ from .. import legacy as functions
14
+ from ._deps import get_kv
15
+ from .auth import require_auth
16
+
17
+
18
+ def create_graph_bp(kv=None):
19
+ """Blueprint factory — receives kv at registration time (falls back to get_kv())."""
20
+ bp = Blueprint("graph", __name__)
21
+
22
+ def _kv():
23
+ return kv if kv is not None else get_kv()
24
+
25
+ # ------------------------------------------------------------------
26
+ # GET /agentcache/graph
27
+ # ------------------------------------------------------------------
28
+
29
+ @bp.route("/agentcache/graph", methods=["GET"])
30
+ @bp.route("/agentmemory/graph", methods=["GET"])
31
+ @require_auth
32
+ def api_graph():
33
+ result = functions.folder_graph_build(_kv())
34
+ return jsonify(result), 200
35
+
36
+ # ------------------------------------------------------------------
37
+ # GET /agentcache/graph/stats
38
+ # ------------------------------------------------------------------
39
+
40
+ @bp.route("/agentcache/graph/stats", methods=["GET"])
41
+ @bp.route("/agentmemory/graph/stats", methods=["GET"])
42
+ @require_auth
43
+ def api_graph_stats():
44
+ g = functions.folder_graph_build(_kv())
45
+ node_count = len(g.get("nodes", []))
46
+ edge_count = len(g.get("edges", []))
47
+ return jsonify({"nodes": node_count, "edges": edge_count, "success": True}), 200
48
+
49
+ # ------------------------------------------------------------------
50
+ # POST /agentcache/graph/query
51
+ # ------------------------------------------------------------------
52
+
53
+ @bp.route("/agentcache/graph/query", methods=["POST"])
54
+ @bp.route("/agentmemory/graph/query", methods=["POST"])
55
+ @require_auth
56
+ def api_graph_query():
57
+ try:
58
+ request.get_json(force=True) or {}
59
+ return jsonify({"nodes": [], "edges": [], "success": True}), 200
60
+ except Exception as e:
61
+ return jsonify({"error": str(e)}), 400
62
+
63
+ # ------------------------------------------------------------------
64
+ # POST /agentcache/graph/build
65
+ # ------------------------------------------------------------------
66
+
67
+ @bp.route("/agentcache/graph/build", methods=["POST"])
68
+ @bp.route("/agentmemory/graph/build", methods=["POST"])
69
+ @require_auth
70
+ def api_graph_build():
71
+ try:
72
+ if functions.is_consolidation_enabled():
73
+ functions.consolidate(_kv())
74
+ return jsonify({"success": True}), 200
75
+ except Exception as e:
76
+ return jsonify({"error": str(e)}), 400
77
+
78
+ return bp
79
+
80
+
81
+ graph_bp = create_graph_bp(None)
@@ -0,0 +1,149 @@
1
+ """
2
+ Health and audit routes blueprint.
3
+
4
+ Handles:
5
+ GET /agentmemory/livez
6
+ GET /agentmemory/health
7
+ GET /agentmemory/audit
8
+ GET /agentmemory/config/flags
9
+ """
10
+
11
+ import os
12
+
13
+ from flask import Blueprint, Response, jsonify, request
14
+
15
+ from .. import legacy as functions
16
+ from ..legacy import query_audit
17
+ from ._deps import get_kv
18
+ from .auth import require_auth
19
+
20
+
21
+ def create_health_bp(kv=None, embedding_provider=None):
22
+ """Blueprint factory — receives kv and embedding_provider at registration time."""
23
+ bp = Blueprint("health", __name__)
24
+
25
+ def _kv():
26
+ return kv if kv is not None else get_kv()
27
+
28
+ # ------------------------------------------------------------------
29
+ # GET /auth.md (no auth required)
30
+ # ------------------------------------------------------------------
31
+
32
+ @bp.route("/auth.md", methods=["GET"])
33
+ def auth_docs():
34
+ """Serve the agent onboarding documentation in Markdown format."""
35
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
36
+ auth_md_path = os.path.join(base_dir, "auth.md")
37
+
38
+ if os.path.exists(auth_md_path):
39
+ try:
40
+ with open(auth_md_path, "r", encoding="utf-8") as f:
41
+ content = f.read()
42
+ return Response(
43
+ content,
44
+ mimetype="text/markdown",
45
+ headers={"Content-Type": "text/markdown; charset=utf-8"},
46
+ )
47
+ except Exception as e:
48
+ return jsonify({"error": f"failed to read auth.md: {e}"}), 500
49
+
50
+ return jsonify({"error": "auth.md not found"}), 404
51
+
52
+ # ------------------------------------------------------------------
53
+ # GET /agentcache/livez (no auth required)
54
+ # ------------------------------------------------------------------
55
+
56
+ @bp.route("/agentcache/livez", methods=["GET"])
57
+ @bp.route("/agentmemory/livez", methods=["GET"])
58
+ def livez():
59
+ port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
60
+ return jsonify(
61
+ {
62
+ "status": "ok",
63
+ "service": "agentcache",
64
+ "viewerPort": port,
65
+ "viewerSkipped": False,
66
+ }
67
+ )
68
+
69
+ # ------------------------------------------------------------------
70
+ # GET /agentcache/health
71
+ # ------------------------------------------------------------------
72
+
73
+ @bp.route("/agentcache/health", methods=["GET"])
74
+ @bp.route("/agentmemory/health", methods=["GET"])
75
+ def health():
76
+ return jsonify(functions.health_check(_kv()))
77
+
78
+ # ------------------------------------------------------------------
79
+ # GET /agentcache/audit
80
+ # ------------------------------------------------------------------
81
+
82
+ @bp.route("/agentcache/audit", methods=["GET"])
83
+ @bp.route("/agentmemory/audit", methods=["GET"])
84
+ @require_auth
85
+ def api_audit():
86
+ op = request.args.get("operation")
87
+ limit = int(request.args.get("limit", "50"))
88
+ res = query_audit(_kv(), {"operation": op, "limit": limit})
89
+ return jsonify({"entries": res, "success": True}), 200
90
+
91
+ # ------------------------------------------------------------------
92
+ # GET /agentcache/config/flags
93
+ # ------------------------------------------------------------------
94
+
95
+ @bp.route("/agentcache/config/flags", methods=["GET"])
96
+ @bp.route("/agentmemory/config/flags", methods=["GET"])
97
+ @require_auth
98
+ def config_flags():
99
+ provider_kind = "llm" if embedding_provider else "noop"
100
+ embedding_prov = "gemini" if embedding_provider else "none"
101
+
102
+ flags = [
103
+ {
104
+ "key": "GRAPH_EXTRACTION_ENABLED",
105
+ "label": "Knowledge graph extraction",
106
+ "enabled": functions.is_graph_extraction_enabled(),
107
+ "default": False,
108
+ "affects": ["Graph", "Dashboard"],
109
+ "needsLlm": True,
110
+ "description": "Extracts entities and relations from observations into a knowledge graph.",
111
+ "enableHow": "Set GRAPH_EXTRACTION_ENABLED=true and restart.",
112
+ "docsHref": "https://github.com/rohitg00/agentmemory#knowledge-graph",
113
+ },
114
+ {
115
+ "key": "CONSOLIDATION_ENABLED",
116
+ "label": "Memory consolidation",
117
+ "enabled": functions.is_consolidation_enabled(),
118
+ "default": False,
119
+ "affects": ["Dashboard", "Memories", "Crystals"],
120
+ "needsLlm": True,
121
+ "description": "Periodically summarizes sessions into semantic facts + procedures.",
122
+ "enableHow": "Set CONSOLIDATION_ENABLED=true and restart.",
123
+ "docsHref": "https://github.com/rohitg00/agentmemory#consolidation",
124
+ },
125
+ {
126
+ "key": "AGENTCACHE_AUTO_COMPRESS",
127
+ "label": "LLM-powered observation compression",
128
+ "enabled": functions.is_auto_compress_enabled(),
129
+ "default": False,
130
+ "affects": ["Memories", "Timeline"],
131
+ "needsLlm": True,
132
+ "description": "Every observation is compressed by the LLM for richer summaries. OFF uses synthetic compression.",
133
+ "enableHow": "Set AGENTCACHE_AUTO_COMPRESS=true.",
134
+ "docsHref": "https://github.com/rohitg00/agentmemory/issues/138",
135
+ },
136
+ ]
137
+ return jsonify(
138
+ {
139
+ "version": "0.9.9",
140
+ "provider": provider_kind,
141
+ "embeddingProvider": embedding_prov,
142
+ "flags": flags,
143
+ }
144
+ )
145
+
146
+ return bp
147
+
148
+
149
+ health_bp = create_health_bp(None)