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,29 @@
1
+ """
2
+ src/storage/ — Storage utilities package for agentmemory-python (A2.3).
3
+
4
+ Sub-modules:
5
+ scopes — KV scope registry (class KV with all scope definitions)
6
+ paths — Path/ID utility functions (normalize_folder_path, validate_agent_id,
7
+ generate_id, fingerprint_id)
8
+ images — Image-on-disk helpers (save_image_to_disk, delete_image, touch_image,
9
+ is_managed_image_path)
10
+
11
+ These are compatibility copies — the originals in functions.py are kept intact
12
+ for backward compatibility.
13
+ """
14
+
15
+ from .images import delete_image, is_managed_image_path, save_image_to_disk, touch_image
16
+ from .paths import fingerprint_id, generate_id, normalize_folder_path, validate_agent_id
17
+ from .scopes import KV
18
+
19
+ __all__ = [
20
+ "KV",
21
+ "normalize_folder_path",
22
+ "validate_agent_id",
23
+ "generate_id",
24
+ "fingerprint_id",
25
+ "save_image_to_disk",
26
+ "delete_image",
27
+ "touch_image",
28
+ "is_managed_image_path",
29
+ ]
@@ -0,0 +1,102 @@
1
+ """
2
+ src/storage/images.py — Image-on-disk storage helpers (A2.3).
3
+
4
+ Copied from src/functions.py — do NOT delete the originals (backward compat).
5
+ """
6
+
7
+ import hashlib
8
+ import os
9
+ from typing import Optional, Tuple
10
+
11
+ IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".agentmemory", "images")
12
+
13
+
14
+ def get_max_bytes() -> int:
15
+ """Return the configured image-store byte limit (default 500 MB)."""
16
+ return int(os.getenv("AGENTMEMORY_IMAGE_STORE_MAX_BYTES", 500 * 1024 * 1024))
17
+
18
+
19
+ def is_managed_image_path(file_path: str) -> bool:
20
+ """Return True iff *file_path* lives inside the managed images directory."""
21
+ if not file_path:
22
+ return False
23
+ resolved = os.path.abspath(file_path)
24
+ normalized_images_dir = os.path.abspath(IMAGES_DIR)
25
+ return (
26
+ resolved.startswith(normalized_images_dir + os.sep)
27
+ or resolved == normalized_images_dir
28
+ )
29
+
30
+
31
+ def save_image_to_disk(base64_data: str) -> Tuple[str, int]:
32
+ """Decode *base64_data* and write to the managed images directory.
33
+
34
+ Returns:
35
+ (file_path, bytes_written) — bytes_written is 0 if the file already
36
+ existed (content-addressable dedup via SHA-256).
37
+ """
38
+ if not base64_data:
39
+ return "", 0
40
+
41
+ if not os.path.exists(IMAGES_DIR):
42
+ os.makedirs(IMAGES_DIR, exist_ok=True)
43
+
44
+ clean_base64 = base64_data
45
+ ext = "png"
46
+
47
+ if base64_data.startswith("data:image/"):
48
+ comma_idx = base64_data.find(",")
49
+ if comma_idx != -1:
50
+ meta = base64_data[:comma_idx]
51
+ if "jpeg" in meta or "jpg" in meta:
52
+ ext = "jpg"
53
+ elif "webp" in meta:
54
+ ext = "webp"
55
+ elif "gif" in meta:
56
+ ext = "gif"
57
+ clean_base64 = base64_data[comma_idx + 1 :]
58
+ elif base64_data.startswith("/9j/"):
59
+ ext = "jpg"
60
+
61
+ h = hashlib.sha256(clean_base64.encode("utf-8")).hexdigest()
62
+ file_path = os.path.join(IMAGES_DIR, f"{h}.{ext}")
63
+
64
+ if os.path.exists(file_path):
65
+ return file_path, 0
66
+
67
+ import base64
68
+
69
+ buffer = base64.b64decode(clean_base64)
70
+ with open(file_path, "wb") as f:
71
+ f.write(buffer)
72
+
73
+ size = os.path.getsize(file_path)
74
+ return file_path, size
75
+
76
+
77
+ def delete_image(file_path: Optional[str]) -> int:
78
+ """Delete a managed image file and return the number of bytes freed.
79
+
80
+ Returns 0 if the path is not managed or does not exist.
81
+ """
82
+ if not file_path or not is_managed_image_path(file_path):
83
+ return 0
84
+ try:
85
+ if os.path.exists(file_path):
86
+ size = os.path.getsize(file_path)
87
+ os.remove(file_path)
88
+ return size
89
+ except Exception as e:
90
+ print(f"[agentmemory] Failed to delete image context: {e}")
91
+ return 0
92
+
93
+
94
+ def touch_image(file_path: str) -> None:
95
+ """Update the mtime of a managed image (keeps it alive past LRU eviction)."""
96
+ if not file_path or not is_managed_image_path(file_path):
97
+ return
98
+ try:
99
+ if os.path.exists(file_path):
100
+ os.utime(file_path, None)
101
+ except Exception:
102
+ pass
@@ -0,0 +1,116 @@
1
+ """
2
+ src/storage/paths.py — Path normalisation and ID utilities (A2.3).
3
+
4
+ Copied from src/functions.py — do NOT delete the originals (backward compat).
5
+ """
6
+
7
+ import hashlib
8
+ import os
9
+ import time
10
+ import uuid
11
+
12
+ # Maximum allowed length for folder paths and agent IDs.
13
+ _MAX_PATH_LEN = 512
14
+
15
+
16
+ def generate_id(prefix: str) -> str:
17
+ """Generate a time-sortable unique ID with a human-readable prefix.
18
+
19
+ Format: ``{prefix}_{base36_timestamp}_{12_hex_chars}``
20
+ """
21
+ t = int(time.time() * 1000)
22
+ chars = "0123456789abcdefghijklmnopqrstuvwxyz"
23
+ ts_str = ""
24
+ while t > 0:
25
+ ts_str = chars[t % 36] + ts_str
26
+ t //= 36
27
+ if not ts_str:
28
+ ts_str = "0"
29
+ rand = uuid.uuid4().hex[:12]
30
+ return f"{prefix}_{ts_str}_{rand}"
31
+
32
+
33
+ def fingerprint_id(prefix: str, content: str) -> str:
34
+ """Generate a deterministic ID by SHA-256 fingerprinting *content*.
35
+
36
+ Format: ``{prefix}_{first_16_hex_chars_of_sha256}``
37
+ """
38
+ h = hashlib.sha256(content.strip().lower().encode("utf-8")).hexdigest()
39
+ return f"{prefix}_{h[:16]}"
40
+
41
+
42
+ def normalize_folder_path(path: str) -> str:
43
+ """Normalize a folder path for safe use in KV scope keys.
44
+
45
+ Steps applied in order:
46
+ 1. Cap the raw input at 512 characters (REQ-066).
47
+ 2. Apply ``os.path.normpath`` to collapse redundant separators and
48
+ resolve any ``..`` components at the OS level.
49
+ 3. Convert all OS-native separators to forward slashes.
50
+ 4. Strip any remaining leading or trailing slashes.
51
+
52
+ Raises:
53
+ ValueError: if *path* is empty (before or after normalization), or
54
+ if the normalized result still contains a ``..`` segment,
55
+ which would indicate an attempt at path traversal
56
+ (REQ-064).
57
+
58
+ Returns:
59
+ A non-empty, forward-slash-separated string with no leading/trailing
60
+ slashes and no ``..`` segments — safe for use as a KV scope fragment.
61
+
62
+ Property (REQ-074): idempotent — applying this function twice yields
63
+ the same result as applying it once.
64
+ """
65
+ if not path:
66
+ raise ValueError("folder_path must not be empty")
67
+
68
+ # 1. Length cap before any processing.
69
+ path = path[:_MAX_PATH_LEN]
70
+
71
+ # Pre-normalisation traversal check: reject any path that contains a ".."
72
+ # component in the raw input before normpath has a chance to resolve it.
73
+ raw_parts = path.replace("\\", "/").split("/")
74
+ if any(part == ".." for part in raw_parts):
75
+ raise ValueError(f"folder_path contains path traversal segment '..': {path!r}")
76
+
77
+ # 2. OS-level normalisation (resolves duplicate separators, etc.)
78
+ normalized = os.path.normpath(path)
79
+
80
+ # 3. Unify separators to forward slash.
81
+ normalized = normalized.replace("\\", "/")
82
+
83
+ # 4. Strip leading / trailing slashes.
84
+ normalized = normalized.strip("/")
85
+
86
+ # Guard: also reject any ".." that somehow survives normalisation.
87
+ parts = normalized.split("/")
88
+ if any(part == ".." for part in parts):
89
+ raise ValueError(f"folder_path contains path traversal segment '..': {path!r}")
90
+
91
+ if not normalized:
92
+ raise ValueError("folder_path is empty after normalization")
93
+
94
+ return normalized
95
+
96
+
97
+ def validate_agent_id(agent_id: str) -> str:
98
+ """Validate and sanitize an agent_id before use in KV scope keys.
99
+
100
+ Strips surrounding whitespace and caps at 512 characters (REQ-066).
101
+
102
+ Raises:
103
+ ValueError: if *agent_id* is empty after stripping.
104
+
105
+ Returns:
106
+ Sanitized agent_id string.
107
+ """
108
+ if not agent_id:
109
+ raise ValueError("agent_id must not be empty")
110
+
111
+ sanitized = agent_id.strip()[:_MAX_PATH_LEN]
112
+
113
+ if not sanitized:
114
+ raise ValueError("agent_id is empty after stripping whitespace")
115
+
116
+ return sanitized
@@ -0,0 +1,9 @@
1
+ """
2
+ src/storage/scopes.py — KV scope registry (A2.3).
3
+
4
+ Re-exports KV from core.kv_scopes for backward compatibility.
5
+ """
6
+
7
+ from ..core.kv_scopes import KV
8
+
9
+ __all__ = ["KV"]
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="10" fill="#111111"/><text x="32" y="41" text-anchor="middle" font-family="Arial,sans-serif" font-size="24" font-weight="700" fill="#2D6A4F">AM</text></svg>