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
uall/service.py ADDED
@@ -0,0 +1,572 @@
1
+ """Central UALL orchestrator — closed-loop learning cycle."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+
6
+ from uall.analytics.service import AnalyticsService
7
+ from uall.collector.service import EventCollector
8
+ from uall.distillation.distiller import KnowledgeDistiller
9
+ from uall.evaluation.engine import EvaluationEngine
10
+ from uall.experiments.manager import ExperimentManager
11
+ from uall.memory.graph import graph_to_dict
12
+ from uall.memory.policies import PolicyManager
13
+ from uall.memory.pruning import MemoryPruner
14
+ from uall.memory.retrieval import MemoryRetriever
15
+ from uall.memory.validator import MemoryValidator
16
+ from uall.optimization.optimizers import PromptOptimizer, WorkflowOptimizer
17
+ from uall.promotion.queue import PromotionQueue
18
+ from uall.recommendations.engine import PatternDetector, RecommendationEngine
19
+ from uall.reflection.engine import ReflectionEngine
20
+ from uall.rollback.manager import RollbackManager
21
+ from uall.skills.library import SkillLibrary
22
+ from uall.telemetry.retrieval import RetrievalTelemetryService
23
+ from uall_core.ports.storage import StoragePort
24
+ from uall_core.providers.heuristic import HeuristicLLMProvider
25
+ from uall_core.schemas.common import PolicyVersion, Skill
26
+ from uall_core.schemas.events import Event, EventType, Feedback, RunEnd, RunStart, StageMetadata
27
+ from uall_core.schemas.lesson import (
28
+ CandidateLesson,
29
+ Lesson,
30
+ MemorySearchRequest,
31
+ MemorySearchResult,
32
+ ValidationResult,
33
+ )
34
+
35
+
36
+ class UALLService:
37
+ def __init__(self, storage: StoragePort, llm: HeuristicLLMProvider | None = None):
38
+ self.storage = storage
39
+ self.llm = llm or HeuristicLLMProvider()
40
+ self.collector = EventCollector(storage)
41
+ self.reflection = ReflectionEngine(storage, self.llm)
42
+ self.distiller = KnowledgeDistiller(storage, self.llm)
43
+ self.validator = MemoryValidator(storage, self.llm)
44
+ self.policies = PolicyManager(storage)
45
+ self.promotion = PromotionQueue(storage, self.distiller, self.policies)
46
+ self.retriever = MemoryRetriever(storage, self.llm)
47
+ self.telemetry = RetrievalTelemetryService(storage)
48
+ self.evaluator = EvaluationEngine(storage)
49
+ self.recommendations = RecommendationEngine(storage)
50
+ self.patterns = PatternDetector(storage)
51
+ self.experiments = ExperimentManager(storage)
52
+ self.rollback = RollbackManager(storage)
53
+ self.skills = SkillLibrary(storage)
54
+ self.pruner = MemoryPruner(storage, self.llm)
55
+ self.prompt_optimizer = PromptOptimizer(storage)
56
+ self.workflow_optimizer = WorkflowOptimizer(storage)
57
+ self.analytics = AnalyticsService(storage)
58
+
59
+ async def init(self) -> None:
60
+ await self.storage.init()
61
+ await self.policies.get_active()
62
+
63
+ # --- Runs ---
64
+ async def start_run(self, data: RunStart) -> dict:
65
+ return await self.collector.start_run(data)
66
+
67
+ async def record_event(self, event: Event, *, auto_learn: bool = False) -> dict:
68
+ result = await self.collector.record_event(event)
69
+ if auto_learn and event.event_type.value in ("failure", "correction", "suggestion"):
70
+ await self._learning_pipeline_from_event(event)
71
+ return result
72
+
73
+ async def end_run(self, data: RunEnd) -> dict:
74
+ result = await self.collector.end_run(data)
75
+ eval_result = await self.evaluator.evaluate_run(data.run_id)
76
+ result["evaluation"] = eval_result
77
+ for lesson_id in data.lessons_used:
78
+ await self.telemetry.record_outcome(
79
+ telemetry_id=None,
80
+ lesson_id=lesson_id,
81
+ run_id=data.run_id,
82
+ used=True,
83
+ improved=data.success,
84
+ )
85
+ return result
86
+
87
+ async def record_feedback(self, feedback: Feedback) -> dict:
88
+ return await self.collector.record_feedback(feedback)
89
+
90
+ # --- Learning pipeline ---
91
+ async def _learning_pipeline_from_event(self, event: Event) -> None:
92
+ candidate = CandidateLesson(
93
+ reflection_id="",
94
+ failure=event.payload.get("snippet", str(event.payload)[:200]),
95
+ root_cause=event.payload.get("intent", ""),
96
+ fix=event.payload.get("after", event.payload.get("snippet", "")),
97
+ stage=event.stage,
98
+ run_id=event.run_id,
99
+ event_ids=[event.event_id],
100
+ evidence_payload=event.payload,
101
+ )
102
+ await self.reflect_and_queue(candidate, auto_promote=True)
103
+
104
+ async def reflect_and_queue(
105
+ self, candidate: CandidateLesson, *, auto_promote: bool = False
106
+ ) -> dict:
107
+ if not self.validator._has_evidence(candidate):
108
+ return {"status": "rejected", "reason": "No evidence in event payload"}
109
+ candidate = await self.reflection.reflect_from_candidate(candidate)
110
+ validation = await self.validator.validate(candidate)
111
+ if validation.action == "reject":
112
+ return {"status": "rejected", "reason": validation.reason}
113
+ pending_id = await self.promotion.enqueue(candidate, validation)
114
+ result: dict = {"status": "queued", "pending_id": pending_id, "reflection_id": candidate.reflection_id}
115
+ if auto_promote:
116
+ result["promotion"] = await self.process_promotion_queue()
117
+ return result
118
+
119
+ async def validate_lesson(self, candidate: CandidateLesson) -> ValidationResult:
120
+ return await self.validator.validate(candidate)
121
+
122
+ async def process_promotion_queue(self, limit: int = 50) -> dict:
123
+ return await self.promotion.process_queue(limit=limit)
124
+
125
+ async def record_mcp_event(
126
+ self,
127
+ event_type: str,
128
+ summary: str,
129
+ *,
130
+ workflow: str | None = None,
131
+ step: str | None = None,
132
+ tool: str | None = None,
133
+ agent: str | None = None,
134
+ domain: str | None = None,
135
+ language: str | None = None,
136
+ environment: str | None = None,
137
+ namespace: str = "global",
138
+ payload: dict | None = None,
139
+ ) -> dict:
140
+ ns_level, ns_id = _parse_namespace_string(namespace)
141
+ stage = StageMetadata(
142
+ workflow=workflow,
143
+ step=step,
144
+ tool=tool,
145
+ agent=agent,
146
+ domain=domain,
147
+ language=language,
148
+ environment=environment,
149
+ namespace=ns_level,
150
+ namespace_id=ns_id,
151
+ )
152
+ run_id = f"mcp_{uuid.uuid4().hex[:8]}"
153
+ etype = EventType(event_type) if event_type in EventType._value2member_map_ else EventType.FAILURE
154
+ event = Event(
155
+ event_id=f"event_{uuid.uuid4().hex[:8]}",
156
+ event_type=etype,
157
+ run_id=run_id,
158
+ stage=stage,
159
+ payload={"summary": summary[:800], **(payload or {})},
160
+ )
161
+ await self.collector.record_event(event)
162
+ return {
163
+ "id": event.event_id,
164
+ "type": event_type,
165
+ "summary": summary[:800],
166
+ "metadata": {
167
+ "workflow": workflow,
168
+ "step": step,
169
+ "tool": tool,
170
+ "agent": agent,
171
+ "domain": domain,
172
+ "language": language,
173
+ "environment": environment,
174
+ "namespace": namespace,
175
+ },
176
+ }
177
+
178
+ async def reflect_from_events(
179
+ self,
180
+ event_ids: list[str],
181
+ suggestion: str | None = None,
182
+ lesson_text: str | None = None,
183
+ ) -> dict:
184
+ events = []
185
+ for event_id in event_ids:
186
+ evt = await self.storage.get_event(event_id)
187
+ if evt:
188
+ events.append(evt)
189
+ if not events:
190
+ raise ValueError("Reflection requires at least one existing event_id")
191
+
192
+ stage = _stage_from_events(events)
193
+ summaries = [
194
+ evt.get("payload", {}).get("summary")
195
+ or evt.get("payload", {}).get("snippet")
196
+ or str(evt.get("payload", {}))[:200]
197
+ for evt in events
198
+ ]
199
+ failure = summaries[0][:300]
200
+ fix = lesson_text or suggestion or f"Address: {failure[:200]}"
201
+ if suggestion and not lesson_text:
202
+ fix = f"When this pattern appears, apply this fix: {suggestion[:500]}"
203
+ candidate = CandidateLesson(
204
+ reflection_id="",
205
+ failure=failure,
206
+ root_cause=suggestion or "Derived from recorded evidence",
207
+ fix=fix[:300],
208
+ stage=stage,
209
+ run_id=events[0].get("run_id"),
210
+ event_ids=[evt.get("event_id", "") for evt in events],
211
+ evidence_payload={"summaries": summaries, "suggestion": suggestion},
212
+ )
213
+ candidate = await self.reflection.reflect_from_candidate(candidate)
214
+ return {
215
+ "id": candidate.reflection_id,
216
+ "candidate_lesson": candidate.fix,
217
+ "event_ids": candidate.event_ids,
218
+ "metadata": {
219
+ "workflow": stage.workflow,
220
+ "step": stage.step,
221
+ "namespace": _namespace_string(stage.namespace, stage.namespace_id),
222
+ },
223
+ "suggestion": suggestion,
224
+ "status": "candidate",
225
+ }
226
+
227
+ async def validate_and_enqueue(
228
+ self,
229
+ reflection_id: str | None = None,
230
+ candidate_lesson: str | None = None,
231
+ event_ids: list[str] | None = None,
232
+ metadata: dict | None = None,
233
+ ) -> dict:
234
+ reflection = await self.storage.get_reflection(reflection_id) if reflection_id else None
235
+ text = candidate_lesson or (reflection or {}).get("fix")
236
+ evidence_ids = event_ids or (reflection or {}).get("event_ids", [])
237
+ if reflection and not text:
238
+ text = reflection.get("fix")
239
+ if not text:
240
+ return {"action": "reject", "reason": "Candidate lesson is empty.", "quality_score": 0.0}
241
+
242
+ events = []
243
+ for event_id in evidence_ids:
244
+ evt = await self.storage.get_event(event_id)
245
+ if evt:
246
+ events.append(evt)
247
+ if not events:
248
+ return {
249
+ "action": "reject",
250
+ "reason": "No durable evidence event supports this lesson.",
251
+ "quality_score": 0.0,
252
+ }
253
+ stage = _stage_from_events(events)
254
+ if metadata:
255
+ ns_level, ns_id = _parse_namespace_string(metadata.get("namespace", "global"))
256
+ stage = StageMetadata(
257
+ workflow=metadata.get("workflow") or stage.workflow,
258
+ step=metadata.get("step") or stage.step,
259
+ namespace=ns_level,
260
+ namespace_id=ns_id,
261
+ )
262
+ candidate = CandidateLesson(
263
+ reflection_id=reflection_id or (reflection or {}).get("reflection_id", ""),
264
+ failure=(reflection or {}).get("failure", text[:200]),
265
+ root_cause=(reflection or {}).get("root_cause", ""),
266
+ fix=text,
267
+ stage=stage,
268
+ run_id=(reflection or {}).get("run_id"),
269
+ event_ids=[evt.get("event_id", "") for evt in events] or evidence_ids,
270
+ evidence_payload={"summaries": [evt.get("payload", {}) for evt in events]},
271
+ )
272
+ validation = await self.validator.validate(candidate)
273
+ result = {
274
+ "action": validation.action,
275
+ "quality_score": validation.quality_score,
276
+ "reason": validation.reason,
277
+ }
278
+ if validation.action == "reject":
279
+ return result
280
+ if validation.action == "merge":
281
+ result["target_id"] = validation.merge_target_id
282
+ return result
283
+ if validation.rewritten_fix:
284
+ candidate.fix = validation.rewritten_fix
285
+ pending_id = await self.promotion.enqueue(candidate, validation)
286
+ result["pending_id"] = pending_id
287
+ result["lesson"] = candidate.fix
288
+ return result
289
+
290
+ async def retrieve_for_mcp(
291
+ self,
292
+ query: str,
293
+ *,
294
+ workflow: str | None = None,
295
+ step: str | None = None,
296
+ tool: str | None = None,
297
+ agent: str | None = None,
298
+ domain: str | None = None,
299
+ language: str | None = None,
300
+ environment: str | None = None,
301
+ namespace: str = "global",
302
+ top_k: int = 5,
303
+ max_tokens: int = 800,
304
+ ) -> dict:
305
+ ns_level, ns_id = _parse_namespace_string(namespace)
306
+ req = MemorySearchRequest(
307
+ query=query,
308
+ workflow=workflow,
309
+ step=step,
310
+ tool=tool,
311
+ agent=agent,
312
+ domain=domain,
313
+ namespace=ns_level,
314
+ namespace_id=ns_id,
315
+ top_k=top_k,
316
+ max_tokens=max_tokens,
317
+ )
318
+ results = await self.retriever.retrieve(req)
319
+ policies = await self.get_policies()
320
+ retrieval_id = f"retrieval_{uuid.uuid4().hex[:8]}"
321
+ return {
322
+ "retrieval_id": retrieval_id,
323
+ "policies": [_policy_to_mcp(p) for p in policies],
324
+ "lessons": [
325
+ {
326
+ "lesson_id": r.lesson.lesson_id,
327
+ "score": round(r.score, 4),
328
+ "lesson": r.lesson.fix,
329
+ "telemetry_id": r.telemetry_id,
330
+ }
331
+ for r in results
332
+ ],
333
+ }
334
+
335
+ async def report_outcome(
336
+ self,
337
+ lesson_id: str,
338
+ used: bool,
339
+ accepted: bool,
340
+ improved: bool,
341
+ retrieval_id: str | None = None,
342
+ run_id: str | None = None,
343
+ ) -> dict:
344
+ result = await self.telemetry.record_outcome(
345
+ retrieval_id,
346
+ lesson_id,
347
+ run_id,
348
+ used,
349
+ accepted,
350
+ improved,
351
+ )
352
+ lesson = await self.storage.get_lesson(lesson_id)
353
+ confidence = lesson.confidence.model_dump(mode="json") if lesson else {}
354
+ return {
355
+ "telemetry_id": result.get("telemetry_id"),
356
+ "lesson_id": lesson_id,
357
+ "confidence": confidence,
358
+ }
359
+
360
+ async def add_policy_rule(self, rule: str, namespace: str = "global", priority: int = 100) -> dict:
361
+ policy_id = f"policy_{uuid.uuid4().hex[:8]}"
362
+ policy = PolicyVersion(
363
+ policy_id=policy_id,
364
+ version="v1",
365
+ rules=[rule[:600]],
366
+ priority=priority,
367
+ )
368
+ await self.create_policy(policy)
369
+ return {"id": policy_id, "rule": rule[:600], "namespace": namespace, "priority": priority}
370
+
371
+ async def add_mcp_skill(
372
+ self,
373
+ name: str,
374
+ description: str,
375
+ steps: list[str],
376
+ workflow: str | None = None,
377
+ tools: list[str] | None = None,
378
+ namespace: str = "global",
379
+ version: str = "0.1.0",
380
+ metadata: dict | None = None,
381
+ ) -> dict:
382
+ skill = Skill(
383
+ skill_id=f"skill_{uuid.uuid4().hex[:8]}",
384
+ name=name[:120],
385
+ version=version[:40],
386
+ description=description[:1000],
387
+ steps=[step[:500] for step in steps],
388
+ tool_bindings=[tool[:120] for tool in (tools or [])],
389
+ metadata={"namespace": namespace, "workflow": workflow, **(metadata or {})},
390
+ )
391
+ skill_id = await self.create_skill(skill)
392
+ return {
393
+ "id": skill_id,
394
+ "name": skill.name,
395
+ "version": skill.version,
396
+ "namespace": namespace,
397
+ "workflow": workflow,
398
+ }
399
+
400
+ async def search_mcp_skills(
401
+ self,
402
+ query: str,
403
+ workflow: str | None = None,
404
+ namespace: str = "global",
405
+ top_k: int = 5,
406
+ ) -> list[dict]:
407
+ skills = await self.search_skills(query)
408
+ matches = []
409
+ for skill in skills:
410
+ skill_ns = skill.metadata.get("namespace", "global")
411
+ if skill_ns != "global" and skill_ns != namespace:
412
+ continue
413
+ if workflow and skill.metadata.get("workflow") and skill.metadata.get("workflow") != workflow:
414
+ continue
415
+ matches.append(
416
+ {
417
+ "skill_id": skill.skill_id,
418
+ "name": skill.name,
419
+ "version": skill.version,
420
+ "workflow": skill.metadata.get("workflow"),
421
+ "description": skill.description,
422
+ "score": 0.5,
423
+ }
424
+ )
425
+ return matches[:top_k]
426
+
427
+ async def get_mcp_skill(self, skill_id: str) -> dict | None:
428
+ skill = await self.skills.get(skill_id)
429
+ if not skill:
430
+ return None
431
+ return skill.model_dump(mode="json")
432
+
433
+ async def list_pending(self) -> list:
434
+ pending = await self.promotion.list_pending()
435
+ return [p.model_dump(mode="json") for p in pending]
436
+
437
+ # --- Memory ---
438
+ async def retrieve(self, request: MemorySearchRequest) -> list[MemorySearchResult]:
439
+ return await self.retriever.retrieve(request)
440
+
441
+ async def store_lesson(self, lesson: Lesson) -> str:
442
+ return await self.storage.save_lesson(lesson)
443
+
444
+ async def get_lesson(self, lesson_id: str) -> Lesson | None:
445
+ return await self.storage.get_lesson(lesson_id)
446
+
447
+ async def get_provenance(self, lesson_id: str) -> dict | None:
448
+ lesson = await self.storage.get_lesson(lesson_id)
449
+ if not lesson:
450
+ return None
451
+ return lesson.provenance.model_dump(mode="json")
452
+
453
+ async def get_graph(self, lesson_id: str) -> dict | None:
454
+ lesson = await self.storage.get_lesson(lesson_id)
455
+ if not lesson:
456
+ return None
457
+ return graph_to_dict(lesson)
458
+
459
+ async def prune_memory(self) -> dict:
460
+ return await self.pruner.prune()
461
+
462
+ # --- Telemetry ---
463
+ async def record_lesson_outcome(
464
+ self,
465
+ lesson_id: str,
466
+ telemetry_id: str | None = None,
467
+ run_id: str | None = None,
468
+ used: bool = False,
469
+ accepted: bool = False,
470
+ improved: bool | None = None,
471
+ ) -> dict:
472
+ return await self.telemetry.record_outcome(
473
+ telemetry_id, lesson_id, run_id, used, accepted, improved
474
+ )
475
+
476
+ # --- Policies ---
477
+ async def get_policies(self) -> list[PolicyVersion]:
478
+ return await self.policies.get_active()
479
+
480
+ async def create_policy(self, policy: PolicyVersion) -> str:
481
+ old = await self.policies.get_active_version_string()
482
+ result = await self.policies.create_policy(policy)
483
+ if old != "none":
484
+ await self.policies.flag_lessons_for_revalidation(old)
485
+ return result
486
+
487
+ async def list_policy_versions(self, policy_id: str) -> list[PolicyVersion]:
488
+ return await self.policies.list_versions(policy_id)
489
+
490
+ # --- Recommendations & analytics ---
491
+ async def get_recommendations(self, **kwargs) -> list[dict]:
492
+ return await self.recommendations.get_recommendations(**kwargs)
493
+
494
+ async def detect_patterns(self) -> list[dict]:
495
+ return await self.patterns.detect_patterns()
496
+
497
+ async def evaluate(self, run_id: str) -> dict:
498
+ return await self.evaluator.evaluate_run(run_id)
499
+
500
+ async def agent_score(self, agent_id: str | None = None) -> dict:
501
+ return await self.evaluator.agent_score(agent_id)
502
+
503
+ async def get_analytics(self) -> dict:
504
+ return await self.analytics.get_analytics()
505
+
506
+ async def workflow_health(self, workflow_id: str) -> dict:
507
+ return await self.analytics.workflow_health(workflow_id)
508
+
509
+ async def top_failures(self) -> list[dict]:
510
+ return await self.analytics.top_failures()
511
+
512
+ # --- Experiments & rollback ---
513
+ async def start_experiment(self, **kwargs):
514
+ return await self.experiments.start(**kwargs)
515
+
516
+ async def end_experiment(self, experiment_id: str):
517
+ return await self.experiments.conclude(experiment_id)
518
+
519
+ async def experiment_results(self, experiment_id: str):
520
+ return await self.experiments.get_results(experiment_id)
521
+
522
+ async def rollback_resource(self, resource_type: str, resource_id: str, target_version: str) -> dict:
523
+ return await self.rollback.rollback(resource_type, resource_id, target_version)
524
+
525
+ async def list_versions(self, resource_type: str, resource_id: str):
526
+ return await self.rollback.list_versions(resource_type, resource_id)
527
+
528
+ # --- Skills ---
529
+ async def create_skill(self, skill: Skill) -> str:
530
+ return await self.skills.create(skill)
531
+
532
+ async def search_skills(self, query: str) -> list[Skill]:
533
+ return await self.skills.search(query)
534
+
535
+ # --- Optimizers ---
536
+ async def prompt_suggestions(self, agent_id: str, step: str, current_prompt: str) -> dict:
537
+ return await self.prompt_optimizer.suggest(agent_id, step, current_prompt)
538
+
539
+ async def workflow_suggestions(self, workflow_id: str) -> dict:
540
+ return await self.workflow_optimizer.analyze(workflow_id)
541
+
542
+
543
+ def _parse_namespace_string(namespace: str) -> tuple[str | None, str | None]:
544
+ if namespace and ":" in namespace:
545
+ level, ns_id = namespace.split(":", 1)
546
+ return level, ns_id
547
+ if namespace == "global":
548
+ return "global", None
549
+ return None, namespace
550
+
551
+
552
+ def _namespace_string(level: str | None, ns_id: str | None) -> str:
553
+ if level and ns_id:
554
+ return f"{level}:{ns_id}"
555
+ return level or ns_id or "global"
556
+
557
+
558
+ def _stage_from_events(events: list[dict]) -> StageMetadata:
559
+ if not events:
560
+ return StageMetadata()
561
+ stage_data = events[0].get("stage", {})
562
+ return StageMetadata(**stage_data)
563
+
564
+
565
+ def _policy_to_mcp(policy: PolicyVersion) -> dict:
566
+ return {
567
+ "id": policy.policy_id,
568
+ "version": policy.version,
569
+ "rule": policy.rules[0] if policy.rules else "",
570
+ "priority": policy.priority,
571
+ "namespace": "global",
572
+ }
uall/skills/library.py ADDED
@@ -0,0 +1,19 @@
1
+ from uall_core.ports.storage import StoragePort
2
+ from uall_core.schemas.common import Skill
3
+
4
+
5
+ class SkillLibrary:
6
+ def __init__(self, storage: StoragePort):
7
+ self.storage = storage
8
+
9
+ async def create(self, skill: Skill) -> str:
10
+ return await self.storage.save_skill(skill)
11
+
12
+ async def get(self, skill_id: str) -> Skill | None:
13
+ return await self.storage.get_skill(skill_id)
14
+
15
+ async def search(self, query: str) -> list[Skill]:
16
+ return await self.storage.search_skills(query)
17
+
18
+ async def update(self, skill: Skill) -> str:
19
+ return await self.storage.save_skill(skill)
@@ -0,0 +1,40 @@
1
+ from datetime import datetime
2
+
3
+ from uall_core.ports.storage import StoragePort
4
+ from uall_core.schemas.common import RetrievalTelemetryEvent
5
+ from uall.memory.confidence import update_from_telemetry
6
+
7
+
8
+ class RetrievalTelemetryService:
9
+ def __init__(self, storage: StoragePort):
10
+ self.storage = storage
11
+
12
+ async def record_outcome(
13
+ self,
14
+ telemetry_id: str | None,
15
+ lesson_id: str,
16
+ run_id: str | None = None,
17
+ used: bool = False,
18
+ accepted: bool = False,
19
+ improved: bool | None = None,
20
+ ) -> dict:
21
+ event = RetrievalTelemetryEvent(
22
+ telemetry_id=telemetry_id or f"tel_{lesson_id}_{datetime.utcnow().timestamp():.0f}",
23
+ lesson_id=lesson_id,
24
+ run_id=run_id,
25
+ retrieved=True,
26
+ used=used,
27
+ accepted=accepted,
28
+ outcome_improved=improved,
29
+ )
30
+ await self.storage.save_telemetry(event)
31
+
32
+ lesson = await self.storage.get_lesson(lesson_id)
33
+ if lesson:
34
+ update_from_telemetry(lesson, used, accepted, improved)
35
+ await self.storage.update_lesson(lesson)
36
+
37
+ return {"telemetry_id": event.telemetry_id, "recorded": True}
38
+
39
+ async def list_for_lesson(self, lesson_id: str) -> list[RetrievalTelemetryEvent]:
40
+ return await self.storage.list_telemetry(lesson_id)
uall_core/__init__.py ADDED
File without changes