agmem 0.1.1__py3-none-any.whl → 0.1.2__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.
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
- agmem-0.1.2.dist-info/RECORD +86 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +35 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +77 -76
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +4 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +81 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +74 -0
- memvcs/commands/fsck.py +55 -61
- memvcs/commands/garden.py +28 -37
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +16 -28
- memvcs/commands/pack.py +129 -0
- memvcs/commands/pull.py +4 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +59 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/gardener.py +164 -132
- memvcs/core/hooks.py +48 -14
- memvcs/core/knowledge_graph.py +134 -138
- memvcs/core/merge.py +248 -171
- memvcs/core/objects.py +95 -96
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/refs.py +132 -115
- memvcs/core/repository.py +174 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +112 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/vector_store.py +41 -35
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -50,7 +50,9 @@ def create_app(repo_path: Path) -> FastAPI:
|
|
|
50
50
|
if not repo.is_valid_repo():
|
|
51
51
|
raise HTTPException(status_code=400, detail="Not an agmem repository")
|
|
52
52
|
|
|
53
|
-
resolved = repo.resolve_ref(commit_hash) or (
|
|
53
|
+
resolved = repo.resolve_ref(commit_hash) or (
|
|
54
|
+
commit_hash if _valid_commit_hash(commit_hash) else None
|
|
55
|
+
)
|
|
54
56
|
if not resolved:
|
|
55
57
|
raise HTTPException(status_code=400, detail="Invalid revision or hash")
|
|
56
58
|
c = Commit.load(repo.object_store, resolved)
|
|
@@ -87,13 +89,15 @@ def create_app(repo_path: Path) -> FastAPI:
|
|
|
87
89
|
tree_diff = engine.diff_commits(c1, c2)
|
|
88
90
|
files = []
|
|
89
91
|
for fd in tree_diff.files:
|
|
90
|
-
files.append(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
files.append(
|
|
93
|
+
{
|
|
94
|
+
"path": fd.path,
|
|
95
|
+
"diff_type": fd.diff_type.value,
|
|
96
|
+
"old_hash": fd.old_hash,
|
|
97
|
+
"new_hash": fd.new_hash,
|
|
98
|
+
"diff_lines": fd.diff_lines,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
97
101
|
return {
|
|
98
102
|
"base": base,
|
|
99
103
|
"head": head,
|
|
@@ -138,37 +142,32 @@ def create_app(repo_path: Path) -> FastAPI:
|
|
|
138
142
|
if include_similarity:
|
|
139
143
|
try:
|
|
140
144
|
from memvcs.core.vector_store import VectorStore
|
|
141
|
-
|
|
145
|
+
|
|
146
|
+
vector_store = VectorStore(_repo_path / ".mem")
|
|
142
147
|
except ImportError:
|
|
143
148
|
pass
|
|
144
149
|
|
|
145
150
|
builder = KnowledgeGraphBuilder(repo, vector_store)
|
|
146
151
|
graph_data = builder.build_graph(
|
|
147
|
-
include_similarity=include_similarity,
|
|
148
|
-
similarity_threshold=threshold
|
|
152
|
+
include_similarity=include_similarity, similarity_threshold=threshold
|
|
149
153
|
)
|
|
150
154
|
|
|
151
155
|
# Return D3-compatible format
|
|
152
156
|
return {
|
|
153
|
-
|
|
157
|
+
"nodes": [
|
|
154
158
|
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
"id": n.id,
|
|
160
|
+
"name": n.label,
|
|
161
|
+
"group": n.memory_type,
|
|
162
|
+
"size": min(20, max(5, n.size // 100)),
|
|
159
163
|
}
|
|
160
164
|
for n in graph_data.nodes
|
|
161
165
|
],
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
'source': e.source,
|
|
165
|
-
'target': e.target,
|
|
166
|
-
'type': e.edge_type,
|
|
167
|
-
'value': e.weight
|
|
168
|
-
}
|
|
166
|
+
"links": [
|
|
167
|
+
{"source": e.source, "target": e.target, "type": e.edge_type, "value": e.weight}
|
|
169
168
|
for e in graph_data.edges
|
|
170
169
|
],
|
|
171
|
-
|
|
170
|
+
"metadata": graph_data.metadata,
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
@app.get("/graph", response_class=HTMLResponse)
|
|
@@ -185,7 +184,7 @@ def create_app(repo_path: Path) -> FastAPI:
|
|
|
185
184
|
|
|
186
185
|
|
|
187
186
|
# Embedded graph viewer template
|
|
188
|
-
GRAPH_HTML_TEMPLATE =
|
|
187
|
+
GRAPH_HTML_TEMPLATE = """<!DOCTYPE html>
|
|
189
188
|
<html>
|
|
190
189
|
<head>
|
|
191
190
|
<title>agmem Knowledge Graph</title>
|
|
@@ -349,4 +348,4 @@ GRAPH_HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
349
348
|
</script>
|
|
350
349
|
</body>
|
|
351
350
|
</html>
|
|
352
|
-
|
|
351
|
+
"""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retrieval module for agmem - context-aware recall with pluggable strategies.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import RetrievalStrategy, RecallResult
|
|
6
|
+
from .strategies import (
|
|
7
|
+
RecencyStrategy,
|
|
8
|
+
ImportanceStrategy,
|
|
9
|
+
SimilarityStrategy,
|
|
10
|
+
HybridStrategy,
|
|
11
|
+
)
|
|
12
|
+
from .recaller import RecallEngine
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"RetrievalStrategy",
|
|
16
|
+
"RecallResult",
|
|
17
|
+
"RecencyStrategy",
|
|
18
|
+
"ImportanceStrategy",
|
|
19
|
+
"SimilarityStrategy",
|
|
20
|
+
"HybridStrategy",
|
|
21
|
+
"RecallEngine",
|
|
22
|
+
]
|
memvcs/retrieval/base.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base retrieval interfaces for agmem recall.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import List, Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class RecallResult:
|
|
12
|
+
"""Single recalled memory with metadata."""
|
|
13
|
+
|
|
14
|
+
path: str
|
|
15
|
+
content: str
|
|
16
|
+
relevance_score: float
|
|
17
|
+
source: dict # commit_hash, author, indexed_at, etc.
|
|
18
|
+
importance: Optional[float] = None
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict:
|
|
21
|
+
"""Convert to JSON-serializable dict."""
|
|
22
|
+
return {
|
|
23
|
+
"path": self.path,
|
|
24
|
+
"content": self.content,
|
|
25
|
+
"relevance_score": self.relevance_score,
|
|
26
|
+
"source": self.source,
|
|
27
|
+
"importance": self.importance,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RetrievalStrategy(ABC):
|
|
32
|
+
"""Abstract base for recall strategies."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def recall(
|
|
36
|
+
self,
|
|
37
|
+
context: str,
|
|
38
|
+
limit: int,
|
|
39
|
+
exclude: List[str],
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> List[RecallResult]:
|
|
42
|
+
"""
|
|
43
|
+
Retrieve and rank memories for the given context.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
context: Current task description
|
|
47
|
+
limit: Max results to return
|
|
48
|
+
exclude: Tag/branch patterns to exclude (e.g., "experiment/*")
|
|
49
|
+
**kwargs: Strategy-specific options
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Ranked list of RecallResult
|
|
53
|
+
"""
|
|
54
|
+
pass
|
memvcs/retrieval/pack.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pack engine - context window budget manager for agmem.
|
|
3
|
+
|
|
4
|
+
Fills token budget with most relevant memories, optionally with summarization cascade.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional, Any
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .base import RecallResult
|
|
11
|
+
from .recaller import RecallEngine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PackResult:
|
|
16
|
+
"""Result of packing memories into budget."""
|
|
17
|
+
|
|
18
|
+
content: str
|
|
19
|
+
total_tokens: int
|
|
20
|
+
budget: int
|
|
21
|
+
items_used: int
|
|
22
|
+
items_total: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PackEngine:
|
|
26
|
+
"""Packs recalled memories into a token budget."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
recall_engine: RecallEngine,
|
|
31
|
+
model: str = "gpt-4o-mini",
|
|
32
|
+
summarization_cascade: bool = False,
|
|
33
|
+
):
|
|
34
|
+
self.recall_engine = recall_engine
|
|
35
|
+
self.model = model
|
|
36
|
+
self.summarization_cascade = summarization_cascade
|
|
37
|
+
|
|
38
|
+
def _count_tokens(self, text: str) -> int:
|
|
39
|
+
"""Count tokens using tiktoken."""
|
|
40
|
+
try:
|
|
41
|
+
import tiktoken
|
|
42
|
+
|
|
43
|
+
enc = tiktoken.encoding_for_model(self.model)
|
|
44
|
+
return len(enc.encode(text))
|
|
45
|
+
except ImportError:
|
|
46
|
+
# Fallback: ~4 chars per token
|
|
47
|
+
return len(text) // 4
|
|
48
|
+
except Exception:
|
|
49
|
+
return len(text) // 4
|
|
50
|
+
|
|
51
|
+
def pack(
|
|
52
|
+
self,
|
|
53
|
+
context: str,
|
|
54
|
+
budget: int = 4000,
|
|
55
|
+
strategy: str = "relevance",
|
|
56
|
+
exclude: Optional[List[str]] = None,
|
|
57
|
+
) -> PackResult:
|
|
58
|
+
"""
|
|
59
|
+
Pack memories into token budget.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
context: Current task description for recall
|
|
63
|
+
budget: Max tokens to use
|
|
64
|
+
strategy: recall strategy (relevance=hybrid, recency, importance)
|
|
65
|
+
exclude: Path patterns to exclude
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
PackResult with packed content and metadata
|
|
69
|
+
"""
|
|
70
|
+
exclude = exclude or []
|
|
71
|
+
recall_strategy = "hybrid" if strategy == "relevance" else strategy
|
|
72
|
+
if recall_strategy not in ("hybrid", "recency", "importance", "similarity"):
|
|
73
|
+
recall_strategy = "hybrid"
|
|
74
|
+
|
|
75
|
+
results = self.recall_engine.recall(
|
|
76
|
+
context=context,
|
|
77
|
+
limit=50, # Get more candidates
|
|
78
|
+
strategy=recall_strategy,
|
|
79
|
+
exclude=exclude,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Sort by relevance (already sorted by recall)
|
|
83
|
+
# Add tokens, fill greedily
|
|
84
|
+
total_tokens = 0
|
|
85
|
+
packed_items: List[RecallResult] = []
|
|
86
|
+
separator = "\n\n---\n\n"
|
|
87
|
+
header = f"# Context for: {context}\n\n" if context else ""
|
|
88
|
+
header_tokens = self._count_tokens(header)
|
|
89
|
+
budget -= header_tokens
|
|
90
|
+
|
|
91
|
+
for r in results:
|
|
92
|
+
item_text = f"## {r.path}\n{r.content}"
|
|
93
|
+
item_tokens = self._count_tokens(item_text)
|
|
94
|
+
if total_tokens + item_tokens <= budget:
|
|
95
|
+
packed_items.append(r)
|
|
96
|
+
total_tokens += item_tokens
|
|
97
|
+
else:
|
|
98
|
+
# Try truncated
|
|
99
|
+
if total_tokens < budget and item_tokens > 0:
|
|
100
|
+
ratio = (budget - total_tokens) / item_tokens
|
|
101
|
+
if ratio > 0.2: # At least 20% of item
|
|
102
|
+
trunc_len = int(len(item_text) * ratio)
|
|
103
|
+
truncated = item_text[:trunc_len] + "\n..."
|
|
104
|
+
pack_tokens = self._count_tokens(truncated)
|
|
105
|
+
if total_tokens + pack_tokens <= budget:
|
|
106
|
+
r_trunc = RecallResult(
|
|
107
|
+
path=r.path,
|
|
108
|
+
content=r.content[: int(len(r.content) * ratio)] + "...",
|
|
109
|
+
relevance_score=r.relevance_score,
|
|
110
|
+
source=r.source,
|
|
111
|
+
importance=r.importance,
|
|
112
|
+
)
|
|
113
|
+
packed_items.append(r_trunc)
|
|
114
|
+
total_tokens += pack_tokens
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
content_parts = [r.content for r in packed_items]
|
|
118
|
+
body = separator.join(content_parts)
|
|
119
|
+
full_content = header + body
|
|
120
|
+
total_tokens = header_tokens + self._count_tokens(body)
|
|
121
|
+
|
|
122
|
+
return PackResult(
|
|
123
|
+
content=full_content,
|
|
124
|
+
total_tokens=total_tokens,
|
|
125
|
+
budget=budget + header_tokens,
|
|
126
|
+
items_used=len(packed_items),
|
|
127
|
+
items_total=len(results),
|
|
128
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recall engine - orchestrates strategies and access tracking.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Any
|
|
6
|
+
|
|
7
|
+
from .base import RetrievalStrategy, RecallResult
|
|
8
|
+
from .strategies import RecencyStrategy, ImportanceStrategy, SimilarityStrategy, HybridStrategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RecallEngine:
|
|
12
|
+
"""Orchestrates recall with pluggable strategies and access tracking."""
|
|
13
|
+
|
|
14
|
+
STRATEGIES = ["recency", "importance", "similarity", "hybrid"]
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
repo: Any,
|
|
19
|
+
vector_store: Optional[Any] = None,
|
|
20
|
+
access_index: Optional[Any] = None,
|
|
21
|
+
use_cache: bool = True,
|
|
22
|
+
):
|
|
23
|
+
self.repo = repo
|
|
24
|
+
self.vector_store = vector_store
|
|
25
|
+
self.access_index = access_index
|
|
26
|
+
self.use_cache = use_cache
|
|
27
|
+
|
|
28
|
+
def _get_strategy(self, strategy_name: str) -> RetrievalStrategy:
|
|
29
|
+
"""Get strategy instance by name."""
|
|
30
|
+
name = strategy_name.lower()
|
|
31
|
+
if name == "recency":
|
|
32
|
+
return RecencyStrategy(self.repo)
|
|
33
|
+
if name == "importance":
|
|
34
|
+
return ImportanceStrategy(self.repo)
|
|
35
|
+
if name == "similarity":
|
|
36
|
+
if not self.vector_store:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"Similarity strategy requires agmem[vector]. Install with: pip install agmem[vector]"
|
|
39
|
+
)
|
|
40
|
+
return SimilarityStrategy(self.repo, self.vector_store)
|
|
41
|
+
if name == "hybrid":
|
|
42
|
+
return HybridStrategy(self.repo, self.vector_store)
|
|
43
|
+
raise ValueError(f"Unknown strategy: {strategy_name}. Choose from {self.STRATEGIES}")
|
|
44
|
+
|
|
45
|
+
def recall(
|
|
46
|
+
self,
|
|
47
|
+
context: str,
|
|
48
|
+
limit: int = 10,
|
|
49
|
+
strategy: str = "hybrid",
|
|
50
|
+
exclude: Optional[List[str]] = None,
|
|
51
|
+
) -> List[RecallResult]:
|
|
52
|
+
"""
|
|
53
|
+
Recall memories for the given context.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
context: Current task description
|
|
57
|
+
limit: Max results
|
|
58
|
+
strategy: recency, importance, similarity, or hybrid
|
|
59
|
+
exclude: Tag/path patterns to exclude
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Ranked list of RecallResult
|
|
63
|
+
"""
|
|
64
|
+
exclude_list = [e.strip() for e in (exclude or []) if e.strip()]
|
|
65
|
+
|
|
66
|
+
cached = self._get_cached_results(context, strategy, limit, exclude_list)
|
|
67
|
+
if cached is not None:
|
|
68
|
+
return cached
|
|
69
|
+
|
|
70
|
+
effective_strategy = (
|
|
71
|
+
"recency" if (strategy == "hybrid" and not self.vector_store) else strategy
|
|
72
|
+
)
|
|
73
|
+
strat = self._get_strategy(effective_strategy)
|
|
74
|
+
results = strat.recall(context=context, limit=limit, exclude=exclude_list)
|
|
75
|
+
|
|
76
|
+
self._record_access_and_cache(context, effective_strategy, limit, exclude_list, results)
|
|
77
|
+
return results
|
|
78
|
+
|
|
79
|
+
def _get_cached_results(
|
|
80
|
+
self, context: str, strategy: str, limit: int, exclude: List[str]
|
|
81
|
+
) -> Optional[List[RecallResult]]:
|
|
82
|
+
if not (self.use_cache and self.access_index and context):
|
|
83
|
+
return None
|
|
84
|
+
cached = self.access_index.get_cached_recall(context, strategy, limit, exclude)
|
|
85
|
+
if not cached or not cached.get("results"):
|
|
86
|
+
return None
|
|
87
|
+
return [RecallResult(**r) if isinstance(r, dict) else r for r in cached["results"]]
|
|
88
|
+
|
|
89
|
+
def _record_access_and_cache(
|
|
90
|
+
self,
|
|
91
|
+
context: str,
|
|
92
|
+
strategy: str,
|
|
93
|
+
limit: int,
|
|
94
|
+
exclude: List[str],
|
|
95
|
+
results: List[RecallResult],
|
|
96
|
+
) -> None:
|
|
97
|
+
if self.access_index:
|
|
98
|
+
head = self.repo.get_head_commit()
|
|
99
|
+
commit_hash = head.store(self.repo.object_store) if head else ""
|
|
100
|
+
for r in results:
|
|
101
|
+
self.access_index.record_access(r.path, commit_hash)
|
|
102
|
+
if self.use_cache and self.access_index and context and results:
|
|
103
|
+
self.access_index.set_cached_recall(
|
|
104
|
+
context, strategy, limit, exclude, [r.to_dict() for r in results]
|
|
105
|
+
)
|