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.
Files changed (54) hide show
  1. storage/adapters/__init__.py +0 -0
  2. storage/adapters/file.py +397 -0
  3. storage/adapters/postgres_qdrant_redis.py +32 -0
  4. storage/adapters/sqlite_chroma.py +95 -0
  5. supermemory_agent-0.2.3.dist-info/METADATA +170 -0
  6. supermemory_agent-0.2.3.dist-info/RECORD +54 -0
  7. supermemory_agent-0.2.3.dist-info/WHEEL +4 -0
  8. supermemory_agent-0.2.3.dist-info/entry_points.txt +2 -0
  9. supermemory_agent-0.2.3.dist-info/licenses/LICENSE +21 -0
  10. supermemory_mcp/__init__.py +5 -0
  11. supermemory_mcp/bridge.py +35 -0
  12. supermemory_mcp/handlers.py +772 -0
  13. supermemory_mcp/server.py +522 -0
  14. supermemory_mcp/text.py +16 -0
  15. uall/__init__.py +0 -0
  16. uall/analytics/service.py +35 -0
  17. uall/collector/__init__.py +0 -0
  18. uall/collector/service.py +100 -0
  19. uall/distillation/distiller.py +80 -0
  20. uall/evaluation/engine.py +38 -0
  21. uall/experiments/manager.py +83 -0
  22. uall/memory/__init__.py +0 -0
  23. uall/memory/confidence.py +36 -0
  24. uall/memory/freshness.py +28 -0
  25. uall/memory/graph.py +24 -0
  26. uall/memory/namespaces.py +40 -0
  27. uall/memory/policies.py +44 -0
  28. uall/memory/provenance.py +23 -0
  29. uall/memory/pruning.py +55 -0
  30. uall/memory/retrieval.py +98 -0
  31. uall/memory/ttl.py +22 -0
  32. uall/memory/validator.py +144 -0
  33. uall/optimization/optimizers.py +59 -0
  34. uall/promotion/queue.py +107 -0
  35. uall/recommendations/engine.py +66 -0
  36. uall/reflection/engine.py +72 -0
  37. uall/rollback/manager.py +49 -0
  38. uall/service.py +572 -0
  39. uall/skills/library.py +19 -0
  40. uall/telemetry/retrieval.py +40 -0
  41. uall_core/__init__.py +0 -0
  42. uall_core/ports/storage.py +71 -0
  43. uall_core/providers/heuristic.py +58 -0
  44. uall_core/providers/llm.py +6 -0
  45. uall_core/schemas/__init__.py +0 -0
  46. uall_core/schemas/common.py +73 -0
  47. uall_core/schemas/events.py +75 -0
  48. uall_core/schemas/graph.py +32 -0
  49. uall_core/schemas/lesson.py +109 -0
  50. uall_core/schemas/namespace.py +76 -0
  51. uall_python/__init__.py +3 -0
  52. uall_python/client.py +337 -0
  53. uall_server/__init__.py +0 -0
  54. 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)
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
@@ -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
+ ]
@@ -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
@@ -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