supermemory-agent 0.2.3__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.
- storage/adapters/__init__.py +0 -0
- storage/adapters/file.py +397 -0
- storage/adapters/postgres_qdrant_redis.py +32 -0
- storage/adapters/sqlite_chroma.py +95 -0
- supermemory_agent-0.2.3.dist-info/METADATA +170 -0
- supermemory_agent-0.2.3.dist-info/RECORD +54 -0
- supermemory_agent-0.2.3.dist-info/WHEEL +4 -0
- supermemory_agent-0.2.3.dist-info/entry_points.txt +2 -0
- supermemory_agent-0.2.3.dist-info/licenses/LICENSE +21 -0
- supermemory_mcp/__init__.py +5 -0
- supermemory_mcp/bridge.py +35 -0
- supermemory_mcp/handlers.py +772 -0
- supermemory_mcp/server.py +522 -0
- supermemory_mcp/text.py +16 -0
- uall/__init__.py +0 -0
- uall/analytics/service.py +35 -0
- uall/collector/__init__.py +0 -0
- uall/collector/service.py +100 -0
- uall/distillation/distiller.py +80 -0
- uall/evaluation/engine.py +38 -0
- uall/experiments/manager.py +83 -0
- uall/memory/__init__.py +0 -0
- uall/memory/confidence.py +36 -0
- uall/memory/freshness.py +28 -0
- uall/memory/graph.py +24 -0
- uall/memory/namespaces.py +40 -0
- uall/memory/policies.py +44 -0
- uall/memory/provenance.py +23 -0
- uall/memory/pruning.py +55 -0
- uall/memory/retrieval.py +98 -0
- uall/memory/ttl.py +22 -0
- uall/memory/validator.py +144 -0
- uall/optimization/optimizers.py +59 -0
- uall/promotion/queue.py +107 -0
- uall/recommendations/engine.py +66 -0
- uall/reflection/engine.py +72 -0
- uall/rollback/manager.py +49 -0
- uall/service.py +572 -0
- uall/skills/library.py +19 -0
- uall/telemetry/retrieval.py +40 -0
- uall_core/__init__.py +0 -0
- uall_core/ports/storage.py +71 -0
- uall_core/providers/heuristic.py +58 -0
- uall_core/providers/llm.py +6 -0
- uall_core/schemas/__init__.py +0 -0
- uall_core/schemas/common.py +73 -0
- uall_core/schemas/events.py +75 -0
- uall_core/schemas/graph.py +32 -0
- uall_core/schemas/lesson.py +109 -0
- uall_core/schemas/namespace.py +76 -0
- uall_python/__init__.py +3 -0
- uall_python/client.py +337 -0
- uall_server/__init__.py +0 -0
- uall_server/main.py +284 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from uall_core.ports.storage import StoragePort
|
|
4
|
+
from uall_core.providers.heuristic import HeuristicLLMProvider, cosine_similarity
|
|
5
|
+
from uall_core.schemas.graph import GraphEdgeType, KnowledgeGraph
|
|
6
|
+
from uall_core.schemas.lesson import CandidateLesson, Lesson
|
|
7
|
+
from uall_core.schemas.namespace import ConfidenceDimensions, FreshnessMetrics, Provenance
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KnowledgeDistiller:
|
|
11
|
+
def __init__(self, storage: StoragePort, llm: HeuristicLLMProvider | None = None):
|
|
12
|
+
self.storage = storage
|
|
13
|
+
self.llm = llm or HeuristicLLMProvider()
|
|
14
|
+
self.merge_threshold = 0.92
|
|
15
|
+
|
|
16
|
+
async def find_merge_target(self, candidate: CandidateLesson) -> tuple[str | None, float]:
|
|
17
|
+
emb = await self.llm.embed(f"{candidate.root_cause} {candidate.fix}")
|
|
18
|
+
best_id, best_sim = None, 0.0
|
|
19
|
+
for lesson in await self.storage.list_lessons("active"):
|
|
20
|
+
lesson_emb = lesson.embedding or await self.llm.embed(lesson.to_search_text())
|
|
21
|
+
sim = cosine_similarity(emb, lesson_emb)
|
|
22
|
+
if sim > best_sim:
|
|
23
|
+
best_sim, best_id = sim, lesson.lesson_id
|
|
24
|
+
if best_sim >= self.merge_threshold:
|
|
25
|
+
return best_id, best_sim
|
|
26
|
+
return None, best_sim
|
|
27
|
+
|
|
28
|
+
async def merge_into_lesson(self, lesson_id: str, candidate: CandidateLesson) -> Lesson:
|
|
29
|
+
lesson = await self.storage.get_lesson(lesson_id)
|
|
30
|
+
if not lesson:
|
|
31
|
+
raise ValueError(f"Lesson {lesson_id} not found")
|
|
32
|
+
lesson.occurrence_count += 1
|
|
33
|
+
lesson.confidence.evidence = min(1.0, lesson.confidence.evidence + 0.02)
|
|
34
|
+
lesson.confidence.recalculate_overall()
|
|
35
|
+
lesson.freshness.last_confirmed = lesson.freshness.created_at
|
|
36
|
+
await self.storage.update_lesson(lesson)
|
|
37
|
+
return lesson
|
|
38
|
+
|
|
39
|
+
async def candidate_to_lesson(
|
|
40
|
+
self,
|
|
41
|
+
candidate: CandidateLesson,
|
|
42
|
+
policy_version: str | None = None,
|
|
43
|
+
validator_action: str = "approve",
|
|
44
|
+
) -> Lesson:
|
|
45
|
+
embedding = await self.llm.embed(f"{candidate.root_cause} {candidate.fix}")
|
|
46
|
+
lesson_id = f"lesson_{uuid.uuid4().hex[:8]}"
|
|
47
|
+
confidence = ConfidenceDimensions(
|
|
48
|
+
evidence=candidate.confidence,
|
|
49
|
+
retrieval_success=0.5,
|
|
50
|
+
human_verified=False,
|
|
51
|
+
)
|
|
52
|
+
confidence.recalculate_overall()
|
|
53
|
+
graph = KnowledgeGraph(lesson_id=lesson_id)
|
|
54
|
+
for eid in candidate.event_ids:
|
|
55
|
+
if eid.startswith("failure"):
|
|
56
|
+
graph.add_edge(GraphEdgeType.CAUSED_BY, eid)
|
|
57
|
+
if eid.startswith("correction"):
|
|
58
|
+
graph.add_edge(GraphEdgeType.FIXED_BY, eid)
|
|
59
|
+
return Lesson(
|
|
60
|
+
lesson_id=lesson_id,
|
|
61
|
+
failure=candidate.failure,
|
|
62
|
+
root_cause=candidate.root_cause,
|
|
63
|
+
fix=candidate.fix,
|
|
64
|
+
memory_type=candidate.memory_type,
|
|
65
|
+
stage=candidate.stage,
|
|
66
|
+
namespace=candidate.namespace,
|
|
67
|
+
confidence=confidence,
|
|
68
|
+
freshness=FreshnessMetrics(),
|
|
69
|
+
provenance=Provenance(
|
|
70
|
+
run_id=candidate.run_id,
|
|
71
|
+
reflection_id=candidate.reflection_id,
|
|
72
|
+
event_ids=candidate.event_ids,
|
|
73
|
+
policy_version=policy_version,
|
|
74
|
+
validator_action=validator_action,
|
|
75
|
+
promoted_at=__import__("datetime").datetime.utcnow(),
|
|
76
|
+
),
|
|
77
|
+
graph=graph,
|
|
78
|
+
embedding=embedding,
|
|
79
|
+
quality_score=0.7,
|
|
80
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from uall_core.ports.storage import StoragePort
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EvaluationEngine:
|
|
5
|
+
def __init__(self, storage: StoragePort):
|
|
6
|
+
self.storage = storage
|
|
7
|
+
|
|
8
|
+
async def evaluate_run(self, run_id: str) -> dict:
|
|
9
|
+
run = await self.storage.get_run(run_id)
|
|
10
|
+
if not run:
|
|
11
|
+
return {"error": "run not found"}
|
|
12
|
+
events = run.get("events", [])
|
|
13
|
+
failures = sum(1 for e in events if e.get("event_type") == "failure")
|
|
14
|
+
corrections = sum(1 for e in events if e.get("event_type") == "correction")
|
|
15
|
+
success = run.get("success", False)
|
|
16
|
+
score = 1.0 if success else 0.0
|
|
17
|
+
score -= failures * 0.15
|
|
18
|
+
score -= corrections * 0.1
|
|
19
|
+
score = max(0.0, min(1.0, score))
|
|
20
|
+
return {
|
|
21
|
+
"run_id": run_id,
|
|
22
|
+
"score": round(score, 3),
|
|
23
|
+
"success": success,
|
|
24
|
+
"failures": failures,
|
|
25
|
+
"corrections": corrections,
|
|
26
|
+
"retry_count": failures,
|
|
27
|
+
"human_intervention": corrections > 0,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async def agent_score(self, agent_id: str | None = None) -> dict:
|
|
31
|
+
runs = await self.storage.list_runs()
|
|
32
|
+
scores = []
|
|
33
|
+
for run in runs:
|
|
34
|
+
ev = await self.evaluate_run(run.get("run_id", ""))
|
|
35
|
+
if "score" in ev:
|
|
36
|
+
scores.append(ev["score"])
|
|
37
|
+
avg = sum(scores) / len(scores) if scores else 0.0
|
|
38
|
+
return {"agent_id": agent_id, "average_score": round(avg, 3), "run_count": len(scores)}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from uall_core.ports.storage import StoragePort
|
|
5
|
+
from uall_core.schemas.common import Experiment, ExperimentMetrics
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExperimentManager:
|
|
9
|
+
def __init__(self, storage: StoragePort):
|
|
10
|
+
self.storage = storage
|
|
11
|
+
self.min_sample_size = 30
|
|
12
|
+
|
|
13
|
+
async def start(
|
|
14
|
+
self,
|
|
15
|
+
resource_type: str,
|
|
16
|
+
resource_id: str,
|
|
17
|
+
variant_a: str,
|
|
18
|
+
variant_b: str,
|
|
19
|
+
traffic_split: float = 0.1,
|
|
20
|
+
) -> Experiment:
|
|
21
|
+
exp = Experiment(
|
|
22
|
+
experiment_id=f"exp_{uuid.uuid4().hex[:8]}",
|
|
23
|
+
resource_type=resource_type,
|
|
24
|
+
resource_id=resource_id,
|
|
25
|
+
variant_a=variant_a,
|
|
26
|
+
variant_b=variant_b,
|
|
27
|
+
traffic_split=traffic_split,
|
|
28
|
+
)
|
|
29
|
+
await self.storage.save_experiment(exp)
|
|
30
|
+
return exp
|
|
31
|
+
|
|
32
|
+
async def record_run_metrics(
|
|
33
|
+
self, experiment_id: str, variant: str, metrics: dict
|
|
34
|
+
) -> Experiment:
|
|
35
|
+
exp = await self.storage.get_experiment(experiment_id)
|
|
36
|
+
if not exp:
|
|
37
|
+
raise ValueError(f"Experiment {experiment_id} not found")
|
|
38
|
+
target = exp.metrics_b if variant == "b" else exp.metrics_a
|
|
39
|
+
target.sample_size += 1
|
|
40
|
+
if metrics.get("success"):
|
|
41
|
+
target.success_rate = (
|
|
42
|
+
target.success_rate * (target.sample_size - 1) + 1
|
|
43
|
+
) / target.sample_size
|
|
44
|
+
else:
|
|
45
|
+
target.success_rate = (
|
|
46
|
+
target.success_rate * (target.sample_size - 1)
|
|
47
|
+
) / target.sample_size
|
|
48
|
+
target.retry_count = metrics.get("retry_count", target.retry_count)
|
|
49
|
+
target.cost = metrics.get("cost", target.cost)
|
|
50
|
+
target.latency_p50 = metrics.get("latency_p50", target.latency_p50)
|
|
51
|
+
target.latency_p95 = metrics.get("latency_p95", target.latency_p95)
|
|
52
|
+
target.token_usage = metrics.get("token_usage", target.token_usage)
|
|
53
|
+
target.human_approval_rate = metrics.get("human_approval_rate", target.human_approval_rate)
|
|
54
|
+
target.downstream_failure_rate = metrics.get(
|
|
55
|
+
"downstream_failure_rate", target.downstream_failure_rate
|
|
56
|
+
)
|
|
57
|
+
await self.storage.update_experiment(exp)
|
|
58
|
+
return exp
|
|
59
|
+
|
|
60
|
+
async def conclude(self, experiment_id: str) -> Experiment:
|
|
61
|
+
exp = await self.storage.get_experiment(experiment_id)
|
|
62
|
+
if not exp:
|
|
63
|
+
raise ValueError(f"Experiment {experiment_id} not found")
|
|
64
|
+
if exp.metrics_b.sample_size < self.min_sample_size:
|
|
65
|
+
exp.status = "running"
|
|
66
|
+
exp.winner = None
|
|
67
|
+
elif exp.metrics_b.success_rate > exp.metrics_a.success_rate:
|
|
68
|
+
guardrail_ok = (
|
|
69
|
+
exp.metrics_b.latency_p95 <= exp.metrics_a.latency_p95 * 1.2
|
|
70
|
+
and exp.metrics_b.downstream_failure_rate
|
|
71
|
+
<= exp.metrics_a.downstream_failure_rate + 0.05
|
|
72
|
+
)
|
|
73
|
+
exp.winner = "b" if guardrail_ok else None
|
|
74
|
+
exp.status = "concluded" if guardrail_ok else "rolled_back"
|
|
75
|
+
else:
|
|
76
|
+
exp.winner = "a"
|
|
77
|
+
exp.status = "concluded"
|
|
78
|
+
exp.concluded_at = datetime.utcnow()
|
|
79
|
+
await self.storage.update_experiment(exp)
|
|
80
|
+
return exp
|
|
81
|
+
|
|
82
|
+
async def get_results(self, experiment_id: str) -> Experiment | None:
|
|
83
|
+
return await self.storage.get_experiment(experiment_id)
|
uall/memory/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from uall_core.schemas.lesson import Lesson
|
|
2
|
+
from uall_core.schemas.namespace import ConfidenceDimensions
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def recalculate_overall(confidence: ConfidenceDimensions) -> float:
|
|
6
|
+
return confidence.recalculate_overall()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def update_from_telemetry(
|
|
10
|
+
lesson: Lesson, used: bool, accepted: bool, improved: bool | None
|
|
11
|
+
) -> Lesson:
|
|
12
|
+
if used:
|
|
13
|
+
lesson.freshness.usage_count += 1
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
lesson.freshness.last_used = datetime.utcnow()
|
|
17
|
+
if improved is True:
|
|
18
|
+
lesson.freshness.success_after_use += 1
|
|
19
|
+
lesson.confidence.retrieval_success = min(1.0, lesson.confidence.retrieval_success + 0.01)
|
|
20
|
+
elif improved is False:
|
|
21
|
+
lesson.freshness.failure_after_use += 1
|
|
22
|
+
lesson.confidence.retrieval_success = max(0.0, lesson.confidence.retrieval_success - 0.03)
|
|
23
|
+
if accepted:
|
|
24
|
+
lesson.confidence.human_verified = True
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
lesson.freshness.last_confirmed = datetime.utcnow()
|
|
28
|
+
lesson.confidence.recalculate_overall()
|
|
29
|
+
return lesson
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def update_from_human_confirmation(lesson: Lesson) -> Lesson:
|
|
33
|
+
lesson.confidence.human_verified = True
|
|
34
|
+
lesson.confidence.evidence = min(1.0, lesson.confidence.evidence + 0.05)
|
|
35
|
+
lesson.confidence.recalculate_overall()
|
|
36
|
+
return lesson
|
uall/memory/freshness.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
from uall_core.schemas.lesson import Lesson
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_staleness(lesson: Lesson) -> float:
|
|
7
|
+
f = lesson.freshness
|
|
8
|
+
if f.usage_count == 0:
|
|
9
|
+
age_days = (datetime.utcnow() - f.created_at).days
|
|
10
|
+
return min(0.5, age_days / 365)
|
|
11
|
+
|
|
12
|
+
success_rate = f.success_after_use / max(f.usage_count, 1)
|
|
13
|
+
staleness = 1.0 - success_rate
|
|
14
|
+
|
|
15
|
+
if f.last_used:
|
|
16
|
+
days_since = (datetime.utcnow() - f.last_used).days
|
|
17
|
+
staleness += min(0.3, days_since / 180)
|
|
18
|
+
|
|
19
|
+
if f.last_confirmed:
|
|
20
|
+
days_since_confirm = (datetime.utcnow() - f.last_confirmed).days
|
|
21
|
+
staleness -= min(0.2, days_since_confirm / 90)
|
|
22
|
+
|
|
23
|
+
return round(max(0.0, min(1.0, staleness)), 4)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def freshness_weight(lesson: Lesson) -> float:
|
|
27
|
+
lesson.freshness.staleness_score = compute_staleness(lesson)
|
|
28
|
+
return 1.0 - lesson.freshness.staleness_score
|
uall/memory/graph.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from uall_core.schemas.graph import GraphEdgeType, KnowledgeGraph
|
|
2
|
+
from uall_core.schemas.lesson import Lesson
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_lesson_graph(lesson: Lesson) -> KnowledgeGraph:
|
|
6
|
+
if lesson.graph:
|
|
7
|
+
return lesson.graph
|
|
8
|
+
return KnowledgeGraph(lesson_id=lesson.lesson_id)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_edge(lesson: Lesson, edge_type: GraphEdgeType, target_id: str) -> Lesson:
|
|
12
|
+
graph = get_lesson_graph(lesson)
|
|
13
|
+
graph.add_edge(edge_type, target_id)
|
|
14
|
+
lesson.graph = graph
|
|
15
|
+
return lesson
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def graph_to_dict(lesson: Lesson) -> dict:
|
|
19
|
+
graph = get_lesson_graph(lesson)
|
|
20
|
+
result: dict[str, list[str]] = {}
|
|
21
|
+
for edge in graph.edges:
|
|
22
|
+
key = edge.edge_type.value
|
|
23
|
+
result.setdefault(key, []).append(edge.target_id)
|
|
24
|
+
return {"lesson_id": lesson.lesson_id, "edges": result}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from uall_core.schemas.namespace import NAMESPACE_PRIORITY, NamespaceLevel, NamespaceRef
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_namespace(namespace: str | None) -> NamespaceRef | None:
|
|
5
|
+
if not namespace:
|
|
6
|
+
return None
|
|
7
|
+
if ":" in namespace:
|
|
8
|
+
level_str, ns_id = namespace.split(":", 1)
|
|
9
|
+
try:
|
|
10
|
+
level = NamespaceLevel(level_str)
|
|
11
|
+
except ValueError:
|
|
12
|
+
level = NamespaceLevel.PROJECT
|
|
13
|
+
return NamespaceRef(level=level, namespace_id=ns_id)
|
|
14
|
+
return NamespaceRef(namespace_id=namespace)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def namespace_match_score(lesson_ns: NamespaceRef, query_ns: NamespaceRef | None) -> float:
|
|
18
|
+
if query_ns is None:
|
|
19
|
+
return 0.5
|
|
20
|
+
if lesson_ns.namespace_id == query_ns.namespace_id and lesson_ns.level == query_ns.level:
|
|
21
|
+
return 1.0
|
|
22
|
+
try:
|
|
23
|
+
lesson_pri = NAMESPACE_PRIORITY.index(lesson_ns.level)
|
|
24
|
+
query_pri = NAMESPACE_PRIORITY.index(query_ns.level)
|
|
25
|
+
if lesson_pri <= query_pri:
|
|
26
|
+
return 0.7
|
|
27
|
+
except ValueError:
|
|
28
|
+
pass
|
|
29
|
+
return 0.3
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
RETRIEVAL_PRIORITY = [
|
|
33
|
+
"policies",
|
|
34
|
+
NamespaceLevel.ORGANIZATION,
|
|
35
|
+
NamespaceLevel.TEAM,
|
|
36
|
+
NamespaceLevel.PROJECT,
|
|
37
|
+
NamespaceLevel.USER,
|
|
38
|
+
NamespaceLevel.SESSION,
|
|
39
|
+
NamespaceLevel.GLOBAL,
|
|
40
|
+
]
|
uall/memory/policies.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from uall_core.ports.storage import StoragePort
|
|
2
|
+
from uall_core.schemas.common import PolicyVersion
|
|
3
|
+
from uall_core.schemas.lesson import Lesson
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PolicyManager:
|
|
7
|
+
def __init__(self, storage: StoragePort):
|
|
8
|
+
self.storage = storage
|
|
9
|
+
|
|
10
|
+
async def get_active(self) -> list[PolicyVersion]:
|
|
11
|
+
policies = await self.storage.get_active_policies()
|
|
12
|
+
if not policies:
|
|
13
|
+
default = PolicyVersion(
|
|
14
|
+
policy_id="security_policy",
|
|
15
|
+
version="v1",
|
|
16
|
+
rules=[
|
|
17
|
+
"Never expose secrets in output",
|
|
18
|
+
"Prefer inspecting document structure before OCR",
|
|
19
|
+
],
|
|
20
|
+
)
|
|
21
|
+
await self.storage.save_policy(default)
|
|
22
|
+
return [default]
|
|
23
|
+
return policies
|
|
24
|
+
|
|
25
|
+
async def create_policy(self, policy: PolicyVersion) -> str:
|
|
26
|
+
return await self.storage.save_policy(policy)
|
|
27
|
+
|
|
28
|
+
async def list_versions(self, policy_id: str) -> list[PolicyVersion]:
|
|
29
|
+
return await self.storage.list_policy_versions(policy_id)
|
|
30
|
+
|
|
31
|
+
async def get_active_version_string(self) -> str:
|
|
32
|
+
policies = await self.get_active()
|
|
33
|
+
if policies:
|
|
34
|
+
return f"{policies[0].policy_id}:{policies[0].version}"
|
|
35
|
+
return "none"
|
|
36
|
+
|
|
37
|
+
async def flag_lessons_for_revalidation(self, old_version: str) -> list[str]:
|
|
38
|
+
flagged = []
|
|
39
|
+
for lesson in await self.storage.list_lessons("active"):
|
|
40
|
+
if lesson.provenance.policy_version == old_version:
|
|
41
|
+
lesson.status = "pending_revalidation"
|
|
42
|
+
await self.storage.update_lesson(lesson)
|
|
43
|
+
flagged.append(lesson.lesson_id)
|
|
44
|
+
return flagged
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from uall_core.schemas.lesson import Lesson
|
|
2
|
+
from uall_core.schemas.namespace import Provenance
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def attach_provenance(
|
|
6
|
+
lesson: Lesson,
|
|
7
|
+
run_id: str | None = None,
|
|
8
|
+
reflection_id: str | None = None,
|
|
9
|
+
event_ids: list[str] | None = None,
|
|
10
|
+
policy_version: str | None = None,
|
|
11
|
+
validator_action: str | None = None,
|
|
12
|
+
) -> Lesson:
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
lesson.provenance = Provenance(
|
|
16
|
+
run_id=run_id or lesson.provenance.run_id,
|
|
17
|
+
reflection_id=reflection_id or lesson.provenance.reflection_id,
|
|
18
|
+
event_ids=event_ids or lesson.provenance.event_ids,
|
|
19
|
+
policy_version=policy_version or lesson.provenance.policy_version,
|
|
20
|
+
validator_action=validator_action or lesson.provenance.validator_action,
|
|
21
|
+
promoted_at=datetime.utcnow(),
|
|
22
|
+
)
|
|
23
|
+
return lesson
|
uall/memory/pruning.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from uall_core.ports.storage import StoragePort
|
|
2
|
+
from uall_core.providers.heuristic import cosine_similarity, HeuristicLLMProvider
|
|
3
|
+
from uall.memory.freshness import compute_staleness
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MemoryPruner:
|
|
7
|
+
def __init__(self, storage: StoragePort, llm: HeuristicLLMProvider | None = None):
|
|
8
|
+
self.storage = storage
|
|
9
|
+
self.llm = llm or HeuristicLLMProvider()
|
|
10
|
+
|
|
11
|
+
async def prune(self) -> dict:
|
|
12
|
+
merged = await self._merge_duplicates()
|
|
13
|
+
archived = await self._archive_stale()
|
|
14
|
+
removed = await self._remove_low_value()
|
|
15
|
+
return {"merged": merged, "archived": archived, "removed": removed}
|
|
16
|
+
|
|
17
|
+
async def _merge_duplicates(self) -> int:
|
|
18
|
+
lessons = await self.storage.list_lessons("active")
|
|
19
|
+
count = 0
|
|
20
|
+
seen: set[str] = set()
|
|
21
|
+
for i, a in enumerate(lessons):
|
|
22
|
+
if a.lesson_id in seen:
|
|
23
|
+
continue
|
|
24
|
+
for b in lessons[i + 1 :]:
|
|
25
|
+
if b.lesson_id in seen:
|
|
26
|
+
continue
|
|
27
|
+
emb_a = a.embedding or await self.llm.embed(a.to_search_text())
|
|
28
|
+
emb_b = b.embedding or await self.llm.embed(b.to_search_text())
|
|
29
|
+
if cosine_similarity(emb_a, emb_b) > 0.95:
|
|
30
|
+
a.occurrence_count += b.occurrence_count
|
|
31
|
+
await self.storage.update_lesson(a)
|
|
32
|
+
b.status = "archived"
|
|
33
|
+
await self.storage.update_lesson(b)
|
|
34
|
+
seen.add(b.lesson_id)
|
|
35
|
+
count += 1
|
|
36
|
+
return count
|
|
37
|
+
|
|
38
|
+
async def _archive_stale(self) -> int:
|
|
39
|
+
count = 0
|
|
40
|
+
for lesson in await self.storage.list_lessons("active"):
|
|
41
|
+
lesson.freshness.staleness_score = compute_staleness(lesson)
|
|
42
|
+
if lesson.freshness.staleness_score > 0.8 and lesson.freshness.usage_count < 3:
|
|
43
|
+
lesson.status = "archived"
|
|
44
|
+
await self.storage.update_lesson(lesson)
|
|
45
|
+
count += 1
|
|
46
|
+
return count
|
|
47
|
+
|
|
48
|
+
async def _remove_low_value(self) -> int:
|
|
49
|
+
count = 0
|
|
50
|
+
for lesson in await self.storage.list_lessons("active"):
|
|
51
|
+
if lesson.quality_score < 0.3 and lesson.freshness.usage_count == 0:
|
|
52
|
+
lesson.status = "archived"
|
|
53
|
+
await self.storage.update_lesson(lesson)
|
|
54
|
+
count += 1
|
|
55
|
+
return count
|
uall/memory/retrieval.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from uall_core.ports.storage import StoragePort
|
|
5
|
+
from uall_core.providers.heuristic import HeuristicLLMProvider, cosine_similarity
|
|
6
|
+
from uall_core.schemas.common import PolicyVersion, RetrievalTelemetryEvent
|
|
7
|
+
from uall_core.schemas.lesson import Lesson, MemorySearchRequest, MemorySearchResult
|
|
8
|
+
from uall.memory.freshness import freshness_weight
|
|
9
|
+
from uall.memory.namespaces import namespace_match_score, parse_namespace
|
|
10
|
+
from uall.memory.ttl import is_expired, ttl_weight
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryRetriever:
|
|
14
|
+
"""Hybrid multi-stage retrieval pipeline."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, storage: StoragePort, llm: HeuristicLLMProvider | None = None):
|
|
17
|
+
self.storage = storage
|
|
18
|
+
self.llm = llm or HeuristicLLMProvider()
|
|
19
|
+
|
|
20
|
+
async def retrieve(self, request: MemorySearchRequest) -> list[MemorySearchResult]:
|
|
21
|
+
# Stage 1: policies (injected first, not ranked)
|
|
22
|
+
policies = await self.storage.get_active_policies()
|
|
23
|
+
policy_text = self._format_policies(policies)
|
|
24
|
+
|
|
25
|
+
# Stage 2: namespace + metadata filter via storage
|
|
26
|
+
candidates = await self.storage.search_lessons(request)
|
|
27
|
+
|
|
28
|
+
# Stage 3: vector similarity scoring
|
|
29
|
+
query_emb = await self.llm.embed(request.query)
|
|
30
|
+
query_ns = parse_namespace(request.namespace)
|
|
31
|
+
|
|
32
|
+
scored: list[tuple[float, Lesson]] = []
|
|
33
|
+
for lesson in candidates:
|
|
34
|
+
if is_expired(lesson):
|
|
35
|
+
continue
|
|
36
|
+
emb = lesson.embedding or await self.llm.embed(lesson.to_search_text())
|
|
37
|
+
semantic = cosine_similarity(query_emb, emb)
|
|
38
|
+
stage_boost = self._stage_boost(lesson, request)
|
|
39
|
+
ns_boost = namespace_match_score(lesson.namespace, query_ns)
|
|
40
|
+
fresh_w = freshness_weight(lesson)
|
|
41
|
+
ttl_w = ttl_weight(lesson)
|
|
42
|
+
conf_w = lesson.confidence.overall
|
|
43
|
+
final = semantic * stage_boost * ns_boost * fresh_w * ttl_w * conf_w
|
|
44
|
+
scored.append((final, lesson))
|
|
45
|
+
|
|
46
|
+
# Stage 4: rerank top candidates (lightweight sort)
|
|
47
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
48
|
+
top = scored[: request.top_k]
|
|
49
|
+
|
|
50
|
+
results = []
|
|
51
|
+
token_budget = request.max_tokens
|
|
52
|
+
policy_tokens = len(policy_text.split())
|
|
53
|
+
token_budget -= policy_tokens
|
|
54
|
+
|
|
55
|
+
for score, lesson in top:
|
|
56
|
+
text = f"- {lesson.fix}"
|
|
57
|
+
tokens = len(text.split())
|
|
58
|
+
if token_budget <= 0:
|
|
59
|
+
break
|
|
60
|
+
tid = f"tel_{uuid.uuid4().hex[:8]}"
|
|
61
|
+
await self.storage.save_telemetry(
|
|
62
|
+
RetrievalTelemetryEvent(
|
|
63
|
+
telemetry_id=tid,
|
|
64
|
+
lesson_id=lesson.lesson_id,
|
|
65
|
+
retrieved=True,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
results.append(MemorySearchResult(lesson=lesson, score=score, telemetry_id=tid))
|
|
69
|
+
token_budget -= tokens
|
|
70
|
+
|
|
71
|
+
if policy_text and results:
|
|
72
|
+
# Attach policy context to first result metadata
|
|
73
|
+
results[0].lesson.metadata["policy_prefix"] = policy_text
|
|
74
|
+
|
|
75
|
+
return results
|
|
76
|
+
|
|
77
|
+
def _stage_boost(self, lesson: Lesson, request: MemorySearchRequest) -> float:
|
|
78
|
+
boost = 0.1
|
|
79
|
+
if request.step and lesson.stage.step == request.step:
|
|
80
|
+
boost = 1.0
|
|
81
|
+
elif request.workflow and lesson.stage.workflow == request.workflow:
|
|
82
|
+
boost = 0.6
|
|
83
|
+
elif request.domain and lesson.stage.domain == request.domain:
|
|
84
|
+
boost = 0.3
|
|
85
|
+
if request.tool and lesson.stage.tool == request.tool:
|
|
86
|
+
boost = max(boost, 0.8)
|
|
87
|
+
if request.agent and lesson.stage.agent == request.agent:
|
|
88
|
+
boost = max(boost, 0.9)
|
|
89
|
+
return boost
|
|
90
|
+
|
|
91
|
+
def _format_policies(self, policies: list[PolicyVersion]) -> str:
|
|
92
|
+
if not policies:
|
|
93
|
+
return ""
|
|
94
|
+
lines = ["[ORG POLICIES]"]
|
|
95
|
+
for p in policies:
|
|
96
|
+
for rule in p.rules:
|
|
97
|
+
lines.append(f"- {rule}")
|
|
98
|
+
return "\n".join(lines)
|
uall/memory/ttl.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
from uall_core.schemas.lesson import Lesson
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_expired(lesson: Lesson) -> bool:
|
|
7
|
+
if lesson.ttl.expires_at and lesson.ttl.expires_at < datetime.utcnow():
|
|
8
|
+
return True
|
|
9
|
+
if lesson.ttl.auto_revalidate_after_days:
|
|
10
|
+
cutoff = lesson.freshness.created_at + timedelta(days=lesson.ttl.auto_revalidate_after_days)
|
|
11
|
+
if datetime.utcnow() > cutoff and not lesson.freshness.last_confirmed:
|
|
12
|
+
lesson.status = "pending_revalidation"
|
|
13
|
+
return True
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ttl_weight(lesson: Lesson) -> float:
|
|
18
|
+
if is_expired(lesson):
|
|
19
|
+
return 0.0
|
|
20
|
+
if lesson.status == "pending_revalidation":
|
|
21
|
+
return 0.1
|
|
22
|
+
return 1.0
|