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.
- agentcache/__init__.py +29 -0
- agentcache/app.py +312 -0
- agentcache/cli.py +346 -0
- agentcache/connect.py +724 -0
- agentcache/core/__init__.py +19 -0
- agentcache/core/audit_log.py +104 -0
- agentcache/core/config.py +51 -0
- agentcache/core/context_builder.py +209 -0
- agentcache/core/graph.py +120 -0
- agentcache/core/image_store.py +111 -0
- agentcache/core/infer.py +142 -0
- agentcache/core/kv_scopes.py +86 -0
- agentcache/core/lessons.py +242 -0
- agentcache/core/llm.py +596 -0
- agentcache/core/memory_store.py +207 -0
- agentcache/core/observation_store.py +576 -0
- agentcache/core/privacy.py +34 -0
- agentcache/core/project_profile.py +625 -0
- agentcache/core/search_service.py +444 -0
- agentcache/core/session_store.py +382 -0
- agentcache/core/slots.py +504 -0
- agentcache/db.py +359 -0
- agentcache/import_data.py +86 -0
- agentcache/legacy.py +94 -0
- agentcache/mcp_stdio.py +141 -0
- agentcache/py.typed +0 -0
- agentcache/replay_import.py +680 -0
- agentcache/routes/__init__.py +29 -0
- agentcache/routes/_deps.py +46 -0
- agentcache/routes/auth.py +68 -0
- agentcache/routes/graph.py +81 -0
- agentcache/routes/health.py +149 -0
- agentcache/routes/mcp.py +614 -0
- agentcache/routes/memories.py +116 -0
- agentcache/routes/migration.py +32 -0
- agentcache/routes/observations.py +253 -0
- agentcache/routes/search.py +80 -0
- agentcache/search.py +935 -0
- agentcache/storage/__init__.py +29 -0
- agentcache/storage/images.py +102 -0
- agentcache/storage/paths.py +116 -0
- agentcache/storage/scopes.py +9 -0
- agentcache/viewer/favicon.svg +1 -0
- agentcache/viewer/index.html +4235 -0
- agentcache/viewer_helpers.py +61 -0
- agentcache/workers.py +192 -0
- agentcache_core-0.9.9.dist-info/METADATA +194 -0
- agentcache_core-0.9.9.dist-info/RECORD +52 -0
- agentcache_core-0.9.9.dist-info/WHEEL +5 -0
- agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
- agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
- 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 @@
|
|
|
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>
|