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
uall/memory/validator.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from uall_core.ports.storage import StoragePort
|
|
2
|
+
from uall_core.providers.heuristic import HeuristicLLMProvider
|
|
3
|
+
from uall_core.schemas.common import PolicyVersion
|
|
4
|
+
from uall_core.schemas.lesson import CandidateLesson, ValidationResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MemoryValidator:
|
|
8
|
+
def __init__(self, storage: StoragePort, llm: HeuristicLLMProvider | None = None):
|
|
9
|
+
self.storage = storage
|
|
10
|
+
self.llm = llm or HeuristicLLMProvider()
|
|
11
|
+
self.min_quality = 0.4
|
|
12
|
+
|
|
13
|
+
async def validate(self, candidate: CandidateLesson) -> ValidationResult:
|
|
14
|
+
quality = await self._score_quality(candidate)
|
|
15
|
+
policies = await self.storage.get_active_policies()
|
|
16
|
+
|
|
17
|
+
if not self._has_evidence(candidate):
|
|
18
|
+
return ValidationResult(
|
|
19
|
+
action="reject",
|
|
20
|
+
candidate=candidate,
|
|
21
|
+
quality_score=quality,
|
|
22
|
+
reason="No evidence in event payload",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
for policy in policies:
|
|
26
|
+
if await self._contradicts_policy(candidate, policy):
|
|
27
|
+
return ValidationResult(
|
|
28
|
+
action="reject",
|
|
29
|
+
candidate=candidate,
|
|
30
|
+
quality_score=quality,
|
|
31
|
+
reason=f"Contradicts policy {policy.policy_id} v{policy.version}",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
merge_id, sim = await self._find_duplicate(candidate)
|
|
35
|
+
if merge_id:
|
|
36
|
+
return ValidationResult(
|
|
37
|
+
action="merge",
|
|
38
|
+
candidate=candidate,
|
|
39
|
+
quality_score=quality,
|
|
40
|
+
merge_target_id=merge_id,
|
|
41
|
+
reason=f"Near-duplicate similarity {sim:.2f}",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
conflict = await self._find_conflict(candidate)
|
|
45
|
+
if conflict:
|
|
46
|
+
return ValidationResult(
|
|
47
|
+
action="reject",
|
|
48
|
+
candidate=candidate,
|
|
49
|
+
quality_score=quality,
|
|
50
|
+
reason=f"Conflicts with lesson {conflict}",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if quality < self.min_quality:
|
|
54
|
+
return ValidationResult(
|
|
55
|
+
action="reject",
|
|
56
|
+
candidate=candidate,
|
|
57
|
+
quality_score=quality,
|
|
58
|
+
reason=f"Quality score {quality:.2f} below threshold",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if len(candidate.fix) < 20 or not any(
|
|
62
|
+
w in candidate.fix.lower() for w in ("verify", "check", "inspect", "validate", "use", "before", "first")
|
|
63
|
+
):
|
|
64
|
+
rewritten = await self._rewrite(candidate)
|
|
65
|
+
return ValidationResult(
|
|
66
|
+
action="rewrite",
|
|
67
|
+
candidate=candidate,
|
|
68
|
+
quality_score=quality,
|
|
69
|
+
rewritten_fix=rewritten,
|
|
70
|
+
reason="Rewritten for clarity and actionability",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return ValidationResult(
|
|
74
|
+
action="approve",
|
|
75
|
+
candidate=candidate,
|
|
76
|
+
quality_score=quality,
|
|
77
|
+
reason="Passed all validation checks",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def _score_quality(self, candidate: CandidateLesson) -> float:
|
|
81
|
+
score = 0.3
|
|
82
|
+
if candidate.failure:
|
|
83
|
+
score += 0.15
|
|
84
|
+
if candidate.root_cause and len(candidate.root_cause) > 10:
|
|
85
|
+
score += 0.2
|
|
86
|
+
if candidate.fix and len(candidate.fix) > 15:
|
|
87
|
+
score += 0.25
|
|
88
|
+
if candidate.evidence_payload:
|
|
89
|
+
score += 0.1
|
|
90
|
+
return min(1.0, score)
|
|
91
|
+
|
|
92
|
+
def _has_evidence(self, candidate: CandidateLesson) -> bool:
|
|
93
|
+
if candidate.event_ids:
|
|
94
|
+
return True
|
|
95
|
+
if candidate.failure and candidate.failure.strip():
|
|
96
|
+
return True
|
|
97
|
+
if candidate.fix and len(candidate.fix.strip()) > 10:
|
|
98
|
+
return True
|
|
99
|
+
payload = candidate.evidence_payload or {}
|
|
100
|
+
snippet = payload.get("snippet") or payload.get("after") or payload.get("intent")
|
|
101
|
+
return bool(snippet and str(snippet).strip())
|
|
102
|
+
|
|
103
|
+
async def _contradicts_policy(self, candidate: CandidateLesson, policy: PolicyVersion) -> bool:
|
|
104
|
+
fix_lower = candidate.fix.lower()
|
|
105
|
+
for rule in policy.rules:
|
|
106
|
+
rule_lower = rule.lower()
|
|
107
|
+
if "never expose secrets" in rule_lower and "secret" in fix_lower:
|
|
108
|
+
if "never" not in fix_lower and "don't" not in fix_lower:
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
async def _find_duplicate(self, candidate: CandidateLesson) -> tuple[str | None, float]:
|
|
113
|
+
from uall_core.providers.heuristic import cosine_similarity
|
|
114
|
+
|
|
115
|
+
emb = await self.llm.embed(f"{candidate.root_cause} {candidate.fix}")
|
|
116
|
+
best_id, best_sim = None, 0.0
|
|
117
|
+
for lesson in await self.storage.list_lessons("active"):
|
|
118
|
+
lesson_emb = lesson.embedding or await self.llm.embed(lesson.to_search_text())
|
|
119
|
+
sim = cosine_similarity(emb, lesson_emb)
|
|
120
|
+
if sim > best_sim:
|
|
121
|
+
best_sim, best_id = sim, lesson.lesson_id
|
|
122
|
+
if best_sim >= 0.92:
|
|
123
|
+
return best_id, best_sim
|
|
124
|
+
return None, best_sim
|
|
125
|
+
|
|
126
|
+
async def _find_conflict(self, candidate: CandidateLesson) -> str | None:
|
|
127
|
+
for lesson in await self.storage.list_lessons("active"):
|
|
128
|
+
if lesson.graph:
|
|
129
|
+
conflicts = lesson.graph.get_targets(
|
|
130
|
+
__import__(
|
|
131
|
+
"uall_core.schemas.graph", fromlist=["GraphEdgeType"]
|
|
132
|
+
).GraphEdgeType.CONFLICTS_WITH
|
|
133
|
+
)
|
|
134
|
+
if candidate.reflection_id in conflicts:
|
|
135
|
+
return lesson.lesson_id
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
async def _rewrite(self, candidate: CandidateLesson) -> str:
|
|
139
|
+
prompt = f"Rewrite this fix to be actionable: {candidate.fix}"
|
|
140
|
+
response = await self.llm.complete(prompt)
|
|
141
|
+
for line in response.splitlines():
|
|
142
|
+
if "FIX:" in line.upper():
|
|
143
|
+
return line.split(":", 1)[1].strip()
|
|
144
|
+
return f"Before acting: validate. {candidate.fix}"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from uall_core.ports.storage import StoragePort
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PromptOptimizer:
|
|
5
|
+
def __init__(self, storage: StoragePort):
|
|
6
|
+
self.storage = storage
|
|
7
|
+
|
|
8
|
+
async def suggest(self, agent_id: str, step: str, current_prompt: str) -> dict:
|
|
9
|
+
from uall.recommendations.engine import RecommendationEngine
|
|
10
|
+
|
|
11
|
+
recs = RecommendationEngine(self.storage)
|
|
12
|
+
recommendations = await recs.get_recommendations(agent_id=agent_id)
|
|
13
|
+
additions = [r["text"] for r in recommendations if r.get("step") == step][:3]
|
|
14
|
+
if not additions:
|
|
15
|
+
additions = [r["text"] for r in recommendations[:2]]
|
|
16
|
+
suggested = current_prompt
|
|
17
|
+
if additions:
|
|
18
|
+
suggested += "\n\n[UALL Recommendations]\n" + "\n".join(f"- {a}" for a in additions)
|
|
19
|
+
return {
|
|
20
|
+
"agent_id": agent_id,
|
|
21
|
+
"step": step,
|
|
22
|
+
"current": current_prompt,
|
|
23
|
+
"recommended": suggested,
|
|
24
|
+
"additions": additions,
|
|
25
|
+
"confidence": recommendations[0]["confidence"] if recommendations else 0.0,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WorkflowOptimizer:
|
|
30
|
+
def __init__(self, storage: StoragePort):
|
|
31
|
+
self.storage = storage
|
|
32
|
+
|
|
33
|
+
async def analyze(self, workflow_id: str) -> dict:
|
|
34
|
+
runs = await self.storage.list_runs()
|
|
35
|
+
step_failures: dict[str, int] = {}
|
|
36
|
+
for run in runs:
|
|
37
|
+
if run.get("workflow_id") != workflow_id:
|
|
38
|
+
continue
|
|
39
|
+
for evt in run.get("events", []):
|
|
40
|
+
if evt.get("event_type") == "failure":
|
|
41
|
+
step = evt.get("stage", {}).get("step", "unknown")
|
|
42
|
+
step_failures[step] = step_failures.get(step, 0) + 1
|
|
43
|
+
suggestions = []
|
|
44
|
+
if step_failures:
|
|
45
|
+
worst = max(step_failures, key=step_failures.get)
|
|
46
|
+
suggestions.append(
|
|
47
|
+
{
|
|
48
|
+
"type": "workflow_reorder",
|
|
49
|
+
"message": f"Add validation before '{worst}' step",
|
|
50
|
+
"high_failure_step": worst,
|
|
51
|
+
"failure_count": step_failures[worst],
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
return {
|
|
55
|
+
"workflow_id": workflow_id,
|
|
56
|
+
"step_failures": step_failures,
|
|
57
|
+
"health": "degraded" if step_failures else "healthy",
|
|
58
|
+
"suggestions": suggestions,
|
|
59
|
+
}
|
uall/promotion/queue.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from uall_core.ports.storage import StoragePort
|
|
5
|
+
from uall_core.schemas.lesson import CandidateLesson, PendingLesson, ValidationResult
|
|
6
|
+
from uall.distillation.distiller import KnowledgeDistiller
|
|
7
|
+
from uall.memory.policies import PolicyManager
|
|
8
|
+
from uall.memory.provenance import attach_provenance
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PromotionQueue:
|
|
12
|
+
def __init__(self, storage: StoragePort, distiller: KnowledgeDistiller, policies: PolicyManager):
|
|
13
|
+
self.storage = storage
|
|
14
|
+
self.distiller = distiller
|
|
15
|
+
self.policies = policies
|
|
16
|
+
|
|
17
|
+
async def enqueue(self, candidate: CandidateLesson, validation: ValidationResult) -> str:
|
|
18
|
+
pending = PendingLesson(
|
|
19
|
+
pending_id=f"pending_{uuid.uuid4().hex[:8]}",
|
|
20
|
+
candidate=candidate,
|
|
21
|
+
validation_result=validation,
|
|
22
|
+
status="pending",
|
|
23
|
+
)
|
|
24
|
+
await self.storage.save_pending(pending)
|
|
25
|
+
return pending.pending_id
|
|
26
|
+
|
|
27
|
+
async def list_pending(self, status: str = "pending") -> list[PendingLesson]:
|
|
28
|
+
return await self.storage.list_pending(status)
|
|
29
|
+
|
|
30
|
+
async def process_queue(self, limit: int = 50) -> dict:
|
|
31
|
+
pending_list = (await self.storage.list_pending("pending"))[:limit]
|
|
32
|
+
promoted: list[str] = []
|
|
33
|
+
discarded: list[str] = []
|
|
34
|
+
merged: list[str] = []
|
|
35
|
+
|
|
36
|
+
for pending in pending_list:
|
|
37
|
+
pending.status = "evaluating"
|
|
38
|
+
await self.storage.update_pending(pending)
|
|
39
|
+
result, lesson_id = await self._process_one(pending)
|
|
40
|
+
if result == "promoted" and lesson_id:
|
|
41
|
+
promoted.append(lesson_id)
|
|
42
|
+
elif result == "merged" and lesson_id:
|
|
43
|
+
merged.append(lesson_id)
|
|
44
|
+
else:
|
|
45
|
+
discarded.append(pending.pending_id)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"promoted": promoted,
|
|
49
|
+
"discarded": discarded,
|
|
50
|
+
"merged": merged,
|
|
51
|
+
"processed": len(pending_list),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async def _process_one(self, pending: PendingLesson) -> tuple[str, str | None]:
|
|
55
|
+
validation = pending.validation_result
|
|
56
|
+
candidate = pending.candidate
|
|
57
|
+
|
|
58
|
+
if validation.action == "reject":
|
|
59
|
+
pending.status = "discarded"
|
|
60
|
+
pending.processed_at = datetime.utcnow()
|
|
61
|
+
await self.storage.update_pending(pending)
|
|
62
|
+
return "discarded", None
|
|
63
|
+
|
|
64
|
+
if validation.action == "merge" and validation.merge_target_id:
|
|
65
|
+
await self.distiller.merge_into_lesson(validation.merge_target_id, candidate)
|
|
66
|
+
pending.status = "promoted"
|
|
67
|
+
pending.processed_at = datetime.utcnow()
|
|
68
|
+
await self.storage.update_pending(pending)
|
|
69
|
+
return "merged", validation.merge_target_id
|
|
70
|
+
|
|
71
|
+
if validation.action == "rewrite" and validation.rewritten_fix:
|
|
72
|
+
candidate.fix = validation.rewritten_fix
|
|
73
|
+
|
|
74
|
+
# Shadow test: simple heuristic — fix must be actionable
|
|
75
|
+
if not await self._shadow_test(candidate):
|
|
76
|
+
pending.status = "discarded"
|
|
77
|
+
pending.processed_at = datetime.utcnow()
|
|
78
|
+
await self.storage.update_pending(pending)
|
|
79
|
+
return "discarded", None
|
|
80
|
+
|
|
81
|
+
policy_version = await self.policies.get_active_version_string()
|
|
82
|
+
lesson = await self.distiller.candidate_to_lesson(
|
|
83
|
+
candidate,
|
|
84
|
+
policy_version=policy_version,
|
|
85
|
+
validator_action=validation.action,
|
|
86
|
+
)
|
|
87
|
+
lesson.quality_score = validation.quality_score
|
|
88
|
+
attach_provenance(
|
|
89
|
+
lesson,
|
|
90
|
+
run_id=candidate.run_id,
|
|
91
|
+
reflection_id=candidate.reflection_id,
|
|
92
|
+
event_ids=candidate.event_ids,
|
|
93
|
+
policy_version=policy_version,
|
|
94
|
+
validator_action=validation.action,
|
|
95
|
+
)
|
|
96
|
+
await self.storage.save_lesson(lesson)
|
|
97
|
+
pending.status = "promoted"
|
|
98
|
+
pending.processed_at = datetime.utcnow()
|
|
99
|
+
await self.storage.update_pending(pending)
|
|
100
|
+
return "promoted", lesson.lesson_id
|
|
101
|
+
|
|
102
|
+
async def _shadow_test(self, candidate: CandidateLesson) -> bool:
|
|
103
|
+
if len(candidate.fix) < 10:
|
|
104
|
+
return False
|
|
105
|
+
if not candidate.root_cause:
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
|
|
3
|
+
from uall_core.ports.storage import StoragePort
|
|
4
|
+
from uall_core.schemas.lesson import MemorySearchRequest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RecommendationEngine:
|
|
8
|
+
def __init__(self, storage: StoragePort):
|
|
9
|
+
self.storage = storage
|
|
10
|
+
|
|
11
|
+
async def get_recommendations(
|
|
12
|
+
self,
|
|
13
|
+
agent_id: str | None = None,
|
|
14
|
+
workflow_id: str | None = None,
|
|
15
|
+
context: str | None = None,
|
|
16
|
+
) -> list[dict]:
|
|
17
|
+
lessons = await self.storage.list_lessons("active")
|
|
18
|
+
recs = []
|
|
19
|
+
for lesson in lessons:
|
|
20
|
+
if lesson.occurrence_count < 1:
|
|
21
|
+
continue
|
|
22
|
+
if lesson.confidence.overall < 0.5:
|
|
23
|
+
continue
|
|
24
|
+
if workflow_id and lesson.stage.workflow and lesson.stage.workflow != workflow_id:
|
|
25
|
+
continue
|
|
26
|
+
if agent_id and lesson.stage.agent and lesson.stage.agent != agent_id:
|
|
27
|
+
continue
|
|
28
|
+
impact = lesson.occurrence_count * lesson.confidence.overall
|
|
29
|
+
recs.append(
|
|
30
|
+
{
|
|
31
|
+
"type": "prompt_recommendation",
|
|
32
|
+
"lesson_id": lesson.lesson_id,
|
|
33
|
+
"text": lesson.fix,
|
|
34
|
+
"confidence": lesson.confidence.overall,
|
|
35
|
+
"observed_over": lesson.occurrence_count,
|
|
36
|
+
"estimated_impact": f"Reduce failures by ~{int(impact * 3)}%",
|
|
37
|
+
"workflow": lesson.stage.workflow,
|
|
38
|
+
"step": lesson.stage.step,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
recs.sort(key=lambda x: x["confidence"] * x["observed_over"], reverse=True)
|
|
42
|
+
return recs[:10]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PatternDetector:
|
|
46
|
+
def __init__(self, storage: StoragePort):
|
|
47
|
+
self.storage = storage
|
|
48
|
+
self.threshold = 3
|
|
49
|
+
|
|
50
|
+
async def detect_patterns(self) -> list[dict]:
|
|
51
|
+
lessons = await self.storage.list_lessons("active")
|
|
52
|
+
causes = Counter()
|
|
53
|
+
for lesson in lessons:
|
|
54
|
+
key = lesson.root_cause[:80] if lesson.root_cause else lesson.failure[:80]
|
|
55
|
+
causes[key] += lesson.occurrence_count
|
|
56
|
+
patterns = []
|
|
57
|
+
for cause, count in causes.most_common():
|
|
58
|
+
if count >= self.threshold:
|
|
59
|
+
patterns.append(
|
|
60
|
+
{
|
|
61
|
+
"pattern": cause,
|
|
62
|
+
"occurrences": count,
|
|
63
|
+
"recommendation": f"Address recurring issue: {cause}",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
return patterns
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from uall_core.ports.storage import StoragePort
|
|
6
|
+
from uall_core.providers.heuristic import HeuristicLLMProvider
|
|
7
|
+
from uall_core.schemas.lesson import CandidateLesson, Reflection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReflectionEngine:
|
|
11
|
+
def __init__(self, storage: StoragePort, llm: HeuristicLLMProvider | None = None):
|
|
12
|
+
self.storage = storage
|
|
13
|
+
self.llm = llm or HeuristicLLMProvider()
|
|
14
|
+
|
|
15
|
+
async def reflect(self, candidate: CandidateLesson) -> Reflection:
|
|
16
|
+
prompt = (
|
|
17
|
+
f"Analyze this agent failure and produce a lesson.\n"
|
|
18
|
+
f"FAILURE: {candidate.failure}\n"
|
|
19
|
+
f"CONTEXT: {candidate.evidence_payload}\n"
|
|
20
|
+
f"Provide FAILURE, ROOT_CAUSE, FIX, CONFIDENCE."
|
|
21
|
+
)
|
|
22
|
+
response = await self.llm.complete(prompt)
|
|
23
|
+
failure, root, fix, confidence = _parse_reflection(response, candidate)
|
|
24
|
+
reflection = Reflection(
|
|
25
|
+
reflection_id=f"reflection_{uuid.uuid4().hex[:8]}",
|
|
26
|
+
failure=failure,
|
|
27
|
+
root_cause=root,
|
|
28
|
+
fix=fix,
|
|
29
|
+
confidence=confidence,
|
|
30
|
+
memory_type=candidate.memory_type,
|
|
31
|
+
run_id=candidate.run_id,
|
|
32
|
+
event_ids=candidate.event_ids,
|
|
33
|
+
)
|
|
34
|
+
await self.storage.save_reflection(reflection.model_dump(mode="json"))
|
|
35
|
+
return reflection
|
|
36
|
+
|
|
37
|
+
async def reflect_from_candidate(self, candidate: CandidateLesson) -> CandidateLesson:
|
|
38
|
+
reflection = await self.reflect(candidate)
|
|
39
|
+
return CandidateLesson(
|
|
40
|
+
reflection_id=reflection.reflection_id,
|
|
41
|
+
failure=reflection.failure,
|
|
42
|
+
root_cause=reflection.root_cause,
|
|
43
|
+
fix=reflection.fix,
|
|
44
|
+
memory_type=reflection.memory_type,
|
|
45
|
+
stage=candidate.stage,
|
|
46
|
+
namespace=candidate.namespace,
|
|
47
|
+
confidence=reflection.confidence,
|
|
48
|
+
run_id=reflection.run_id,
|
|
49
|
+
event_ids=reflection.event_ids,
|
|
50
|
+
evidence_payload=candidate.evidence_payload,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_reflection(response: str, candidate: CandidateLesson) -> tuple[str, str, str, float]:
|
|
55
|
+
failure = candidate.failure
|
|
56
|
+
root = candidate.root_cause or "Unknown root cause"
|
|
57
|
+
fix = candidate.fix or "Add validation step"
|
|
58
|
+
confidence = 0.8
|
|
59
|
+
for line in response.splitlines():
|
|
60
|
+
upper = line.upper()
|
|
61
|
+
if "FAILURE:" in upper:
|
|
62
|
+
failure = line.split(":", 1)[1].strip()
|
|
63
|
+
elif "ROOT_CAUSE:" in upper or "ROOT CAUSE:" in upper:
|
|
64
|
+
root = line.split(":", 1)[1].strip()
|
|
65
|
+
elif "FIX:" in upper:
|
|
66
|
+
fix = line.split(":", 1)[1].strip()
|
|
67
|
+
elif "CONFIDENCE:" in upper:
|
|
68
|
+
try:
|
|
69
|
+
confidence = float(re.findall(r"[\d.]+", line)[0])
|
|
70
|
+
except (IndexError, ValueError):
|
|
71
|
+
pass
|
|
72
|
+
return failure[:300], root[:300], fix[:300], min(max(confidence, 0.0), 1.0)
|
uall/rollback/manager.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from uall_core.ports.storage import StoragePort
|
|
4
|
+
from uall_core.schemas.common import VersionRecord
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RollbackManager:
|
|
8
|
+
def __init__(self, storage: StoragePort):
|
|
9
|
+
self.storage = storage
|
|
10
|
+
|
|
11
|
+
async def save_version(
|
|
12
|
+
self, resource_type: str, resource_id: str, version: str, content: dict, promoted: bool = False
|
|
13
|
+
) -> str:
|
|
14
|
+
record = VersionRecord(
|
|
15
|
+
resource_type=resource_type,
|
|
16
|
+
resource_id=resource_id,
|
|
17
|
+
version=version,
|
|
18
|
+
content=content,
|
|
19
|
+
promoted=promoted,
|
|
20
|
+
)
|
|
21
|
+
return await self.storage.save_version(record)
|
|
22
|
+
|
|
23
|
+
async def rollback(self, resource_type: str, resource_id: str, target_version: str) -> dict:
|
|
24
|
+
versions = await self.storage.list_versions(resource_type, resource_id)
|
|
25
|
+
target = next((v for v in versions if v.version == target_version), None)
|
|
26
|
+
if not target:
|
|
27
|
+
raise ValueError(f"Version {target_version} not found for {resource_id}")
|
|
28
|
+
for v in versions:
|
|
29
|
+
v.promoted = v.version == target_version
|
|
30
|
+
await self.storage.save_version(v)
|
|
31
|
+
return {
|
|
32
|
+
"resource_type": resource_type,
|
|
33
|
+
"resource_id": resource_id,
|
|
34
|
+
"rolled_back_to": target_version,
|
|
35
|
+
"content": target.content,
|
|
36
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async def list_versions(self, resource_type: str, resource_id: str) -> list[VersionRecord]:
|
|
40
|
+
return await self.storage.list_versions(resource_type, resource_id)
|
|
41
|
+
|
|
42
|
+
async def auto_rollback_on_regression(
|
|
43
|
+
self, resource_type: str, resource_id: str, current_version: str, previous_version: str
|
|
44
|
+
) -> dict | None:
|
|
45
|
+
versions = await self.storage.list_versions(resource_type, resource_id)
|
|
46
|
+
current = next((v for v in versions if v.version == current_version), None)
|
|
47
|
+
if current and current.promoted:
|
|
48
|
+
return await self.rollback(resource_type, resource_id, previous_version)
|
|
49
|
+
return None
|