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_python/client.py ADDED
@@ -0,0 +1,337 @@
1
+ """UALL Python SDK — local and remote modes."""
2
+
3
+ import os
4
+ import sys
5
+ import uuid
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ ROOT = Path(__file__).resolve().parents[2]
11
+ if str(ROOT) not in sys.path:
12
+ sys.path.insert(0, str(ROOT))
13
+
14
+ import httpx
15
+
16
+ from storage.adapters.file import get_storage
17
+ from uall.service import UALLService
18
+ from uall_core.schemas.events import RunEnd, RunStart, StageMetadata
19
+ from uall_core.schemas.lesson import MemorySearchRequest
20
+
21
+
22
+ class RunContext:
23
+ def __init__(self, client: "UALLClient", run_id: str, workflow_id: str, stage: StageMetadata):
24
+ self.client = client
25
+ self.run_id = run_id
26
+ self.workflow_id = workflow_id
27
+ self.stage = stage
28
+ self._lessons_used: list[str] = []
29
+
30
+ def record_failure(self, snippet: str, tags: list[str] | None = None, **stage_kwargs) -> dict:
31
+ stage = self._merge_stage(stage_kwargs)
32
+ return self.client._record_failure(self.run_id, snippet, stage, tags)
33
+
34
+ def record_correction(
35
+ self, before: str, after: str, intent: str, **stage_kwargs
36
+ ) -> dict:
37
+ stage = self._merge_stage(stage_kwargs)
38
+ return self.client._record_correction(self.run_id, before, after, intent, stage)
39
+
40
+ def retrieve(self, query: str = "", step: str | None = None, max_tokens: int = 800) -> list[dict]:
41
+ stage = StageMetadata(
42
+ workflow=self.workflow_id,
43
+ step=step or self.stage.step,
44
+ agent=self.stage.agent,
45
+ namespace=self.stage.namespace,
46
+ namespace_id=self.stage.namespace_id,
47
+ )
48
+ results = self.client.retrieve(
49
+ query=query or f"{self.workflow_id} {step or self.stage.step}",
50
+ workflow=self.workflow_id,
51
+ step=step or self.stage.step,
52
+ namespace=self.stage.namespace,
53
+ namespace_id=self.stage.namespace_id,
54
+ max_tokens=max_tokens,
55
+ )
56
+ for r in results:
57
+ lid = r.get("lesson", {}).get("lesson_id")
58
+ if lid:
59
+ self._lessons_used.append(lid)
60
+ return results
61
+
62
+ def report_lesson_outcome(
63
+ self,
64
+ lesson_id: str,
65
+ *,
66
+ used: bool = True,
67
+ accepted: bool = False,
68
+ improved: bool | None = None,
69
+ telemetry_id: str | None = None,
70
+ ) -> dict:
71
+ return self.client.record_lesson_outcome(
72
+ lesson_id,
73
+ telemetry_id=telemetry_id,
74
+ run_id=self.run_id,
75
+ used=used,
76
+ accepted=accepted,
77
+ improved=improved,
78
+ )
79
+
80
+ def _merge_stage(self, kwargs: dict) -> StageMetadata:
81
+ data = self.stage.model_dump()
82
+ data.update({k: v for k, v in kwargs.items() if v is not None})
83
+ return StageMetadata(**data)
84
+
85
+ def end(self, success: bool, metrics: dict | None = None) -> dict:
86
+ return self.client._end_run(self.run_id, success, self._lessons_used, metrics)
87
+
88
+
89
+ class UALLClient:
90
+ """SDK supporting local (in-process) and remote (HTTP) modes."""
91
+
92
+ def __init__(
93
+ self,
94
+ storage: str = "file",
95
+ data_dir: str | None = None,
96
+ base_url: str | None = None,
97
+ api_key: str | None = None,
98
+ ):
99
+ self.base_url = base_url
100
+ self.api_key = api_key or os.environ.get("UALL_API_KEY", "dev-key-change-me")
101
+ self._service: UALLService | None = None
102
+ if not base_url:
103
+ os.environ.setdefault("UALL_STORAGE_BACKEND", storage)
104
+ if data_dir:
105
+ os.environ["UALL_DATA_DIR"] = data_dir
106
+ self._storage = get_storage(storage, data_dir)
107
+ self._service = UALLService(self._storage)
108
+ self._initialized = False
109
+
110
+ async def _ensure_init(self):
111
+ if self._service and not self._initialized:
112
+ await self._service.init()
113
+ self._initialized = True
114
+
115
+ def _sync_init(self):
116
+ import asyncio
117
+
118
+ if self._service and not getattr(self, "_initialized", False):
119
+ asyncio.get_event_loop().run_until_complete(self._ensure_init())
120
+ self._initialized = True
121
+
122
+ @contextmanager
123
+ def run(
124
+ self,
125
+ workflow_id: str,
126
+ step: str | None = None,
127
+ agents: list[str] | None = None,
128
+ namespace: str | None = None,
129
+ ):
130
+ import asyncio
131
+
132
+ run_id = f"run_{uuid.uuid4().hex[:8]}"
133
+ ns_level, ns_id = ("project", "default")
134
+ if namespace and ":" in namespace:
135
+ ns_level, ns_id = namespace.split(":", 1)
136
+ stage = StageMetadata(
137
+ workflow=workflow_id,
138
+ step=step,
139
+ namespace=ns_level,
140
+ namespace_id=ns_id,
141
+ )
142
+ start = RunStart(
143
+ run_id=run_id,
144
+ workflow_id=workflow_id,
145
+ agents=agents or [],
146
+ stage=stage,
147
+ )
148
+ if self._service:
149
+ loop = asyncio.new_event_loop()
150
+ try:
151
+ loop.run_until_complete(self._ensure_init())
152
+ loop.run_until_complete(self._service.start_run(start))
153
+ finally:
154
+ loop.close()
155
+ else:
156
+ self._http_post("/runs/start", start.model_dump(mode="json"))
157
+
158
+ ctx = RunContext(self, run_id, workflow_id, stage)
159
+ try:
160
+ yield ctx
161
+ finally:
162
+ pass
163
+
164
+ def retrieve(self, **kwargs) -> list[dict]:
165
+ import asyncio
166
+
167
+ req = MemorySearchRequest(
168
+ query=kwargs.get("query", ""),
169
+ workflow=kwargs.get("workflow"),
170
+ step=kwargs.get("step"),
171
+ namespace=kwargs.get("namespace"),
172
+ namespace_id=kwargs.get("namespace_id"),
173
+ max_tokens=kwargs.get("max_tokens", 800),
174
+ top_k=kwargs.get("top_k", 5),
175
+ )
176
+ if self._service:
177
+ loop = asyncio.new_event_loop()
178
+ try:
179
+ loop.run_until_complete(self._ensure_init())
180
+ results = loop.run_until_complete(self._service.retrieve(req))
181
+ return [
182
+ {
183
+ "lesson": r.lesson.model_dump(mode="json"),
184
+ "score": r.score,
185
+ "telemetry_id": r.telemetry_id,
186
+ }
187
+ for r in results
188
+ ]
189
+ finally:
190
+ loop.close()
191
+ return self._http_post("/memory/search", req.model_dump(mode="json"))
192
+
193
+ def get_policies(self) -> list[dict]:
194
+ import asyncio
195
+
196
+ if self._service:
197
+ loop = asyncio.new_event_loop()
198
+ try:
199
+ loop.run_until_complete(self._ensure_init())
200
+ policies = loop.run_until_complete(self._service.get_policies())
201
+ return [p.model_dump(mode="json") for p in policies]
202
+ finally:
203
+ loop.close()
204
+ return self._http_get("/policies")
205
+
206
+ def get_recommendations(self, **kwargs) -> list[dict]:
207
+ import asyncio
208
+
209
+ if self._service:
210
+ loop = asyncio.new_event_loop()
211
+ try:
212
+ loop.run_until_complete(self._ensure_init())
213
+ return loop.run_until_complete(self._service.get_recommendations(**kwargs))
214
+ finally:
215
+ loop.close()
216
+ return self._http_post("/recommendations", kwargs)
217
+
218
+ def record_lesson_outcome(
219
+ self,
220
+ lesson_id: str,
221
+ telemetry_id: str | None = None,
222
+ run_id: str | None = None,
223
+ *,
224
+ used: bool = False,
225
+ accepted: bool = False,
226
+ improved: bool | None = None,
227
+ ) -> dict:
228
+ import asyncio
229
+
230
+ if self._service:
231
+ loop = asyncio.new_event_loop()
232
+ try:
233
+ loop.run_until_complete(self._ensure_init())
234
+ return loop.run_until_complete(
235
+ self._service.record_lesson_outcome(
236
+ lesson_id, telemetry_id, run_id, used, accepted, improved
237
+ )
238
+ )
239
+ finally:
240
+ loop.close()
241
+ return self._http_post(
242
+ "/telemetry/lesson-outcome",
243
+ {
244
+ "lesson_id": lesson_id,
245
+ "telemetry_id": telemetry_id,
246
+ "run_id": run_id,
247
+ "used": used,
248
+ "accepted": accepted,
249
+ "improved": improved,
250
+ },
251
+ )
252
+
253
+ def experiment(self, prompt_id: str, variant_b: str, split: float = 0.1) -> dict:
254
+ import asyncio
255
+
256
+ payload = {
257
+ "resource_type": "prompt",
258
+ "resource_id": prompt_id,
259
+ "variant_a": "current",
260
+ "variant_b": variant_b,
261
+ "traffic_split": split,
262
+ }
263
+ if self._service:
264
+ loop = asyncio.new_event_loop()
265
+ try:
266
+ loop.run_until_complete(self._ensure_init())
267
+ exp = loop.run_until_complete(self._service.start_experiment(**payload))
268
+ return exp.model_dump(mode="json")
269
+ finally:
270
+ loop.close()
271
+ return self._http_post("/experiments/start", payload)
272
+
273
+ def _record_failure(self, run_id, snippet, stage, tags) -> dict:
274
+ import asyncio
275
+ from uall_core.schemas.events import Event, EventType
276
+
277
+ event = Event(
278
+ event_id=f"failure_{uuid.uuid4().hex[:8]}",
279
+ event_type=EventType.FAILURE,
280
+ run_id=run_id,
281
+ stage=stage,
282
+ tags=tags or [],
283
+ payload={"snippet": snippet[:500]},
284
+ )
285
+ if self._service:
286
+ loop = asyncio.new_event_loop()
287
+ try:
288
+ loop.run_until_complete(self._ensure_init())
289
+ return loop.run_until_complete(self._service.record_event(event))
290
+ finally:
291
+ loop.close()
292
+ return self._http_post("/runs/event", event.model_dump(mode="json"))
293
+
294
+ def _record_correction(self, run_id, before, after, intent, stage) -> dict:
295
+ import asyncio
296
+ from uall_core.schemas.events import Event, EventType
297
+
298
+ event = Event(
299
+ event_id=f"correction_{uuid.uuid4().hex[:8]}",
300
+ event_type=EventType.CORRECTION,
301
+ run_id=run_id,
302
+ stage=stage,
303
+ payload={"before": before[:300], "after": after[:300], "intent": intent},
304
+ )
305
+ if self._service:
306
+ loop = asyncio.new_event_loop()
307
+ try:
308
+ loop.run_until_complete(self._ensure_init())
309
+ return loop.run_until_complete(self._service.record_event(event))
310
+ finally:
311
+ loop.close()
312
+ return self._http_post("/runs/event", event.model_dump(mode="json"))
313
+
314
+ def _end_run(self, run_id, success, lessons_used, metrics) -> dict:
315
+ import asyncio
316
+
317
+ data = RunEnd(run_id=run_id, success=success, lessons_used=lessons_used, metrics=metrics or {})
318
+ if self._service:
319
+ loop = asyncio.new_event_loop()
320
+ try:
321
+ loop.run_until_complete(self._ensure_init())
322
+ return loop.run_until_complete(self._service.end_run(data))
323
+ finally:
324
+ loop.close()
325
+ return self._http_post("/runs/end", data.model_dump(mode="json"))
326
+
327
+ def _http_post(self, path: str, data: dict) -> Any:
328
+ with httpx.Client(base_url=self.base_url, headers={"X-UALL-Key": self.api_key}) as c:
329
+ r = c.post(path, json=data)
330
+ r.raise_for_status()
331
+ return r.json()
332
+
333
+ def _http_get(self, path: str) -> Any:
334
+ with httpx.Client(base_url=self.base_url, headers={"X-UALL-Key": self.api_key}) as c:
335
+ r = c.get(path)
336
+ r.raise_for_status()
337
+ return r.json()
File without changes
uall_server/main.py ADDED
@@ -0,0 +1,284 @@
1
+ import os
2
+ import sys
3
+ from contextlib import asynccontextmanager
4
+ from pathlib import Path
5
+
6
+ # Ensure project root is on path for storage adapters
7
+ ROOT = Path(__file__).resolve().parents[2]
8
+ if str(ROOT) not in sys.path:
9
+ sys.path.insert(0, str(ROOT))
10
+
11
+ from fastapi import Depends, FastAPI, Header, HTTPException
12
+ from pydantic import BaseModel, Field
13
+
14
+ from storage.adapters.file import get_storage
15
+ from uall.service import UALLService
16
+ from uall_core.schemas.common import PolicyVersion, Skill
17
+ from uall_core.schemas.events import Event, Feedback, RunEnd, RunStart
18
+ from uall_core.schemas.lesson import CandidateLesson, Lesson, MemorySearchRequest
19
+
20
+ API_KEY = os.environ.get("UALL_API_KEY", "dev-key-change-me")
21
+ service: UALLService | None = None
22
+
23
+
24
+ @asynccontextmanager
25
+ async def lifespan(app: FastAPI):
26
+ global service
27
+ storage = get_storage()
28
+ service = UALLService(storage)
29
+ await service.init()
30
+ yield
31
+
32
+
33
+ app = FastAPI(title="UALL — Universal Agent Learning Layer", version="0.1.0", lifespan=lifespan)
34
+
35
+
36
+ def verify_key(x_uall_key: str = Header(default="")):
37
+ if x_uall_key != API_KEY:
38
+ raise HTTPException(status_code=401, detail="Invalid API key")
39
+ return x_uall_key
40
+
41
+
42
+ def get_service() -> UALLService:
43
+ assert service is not None
44
+ return service
45
+
46
+
47
+ @app.get("/health")
48
+ async def health():
49
+ return {"status": "ok", "service": "uall"}
50
+
51
+
52
+ # --- Runs ---
53
+ @app.post("/runs/start", dependencies=[Depends(verify_key)])
54
+ async def runs_start(data: RunStart, svc: UALLService = Depends(get_service)):
55
+ return await svc.start_run(data)
56
+
57
+
58
+ @app.post("/runs/event", dependencies=[Depends(verify_key)])
59
+ async def runs_event(event: Event, svc: UALLService = Depends(get_service)):
60
+ return await svc.record_event(event)
61
+
62
+
63
+ @app.post("/runs/end", dependencies=[Depends(verify_key)])
64
+ async def runs_end(data: RunEnd, svc: UALLService = Depends(get_service)):
65
+ return await svc.end_run(data)
66
+
67
+
68
+ @app.post("/feedback", dependencies=[Depends(verify_key)])
69
+ async def feedback(data: Feedback, svc: UALLService = Depends(get_service)):
70
+ return await svc.record_feedback(data)
71
+
72
+
73
+ # --- Reflection & validation ---
74
+ class ReflectRequest(BaseModel):
75
+ candidate: CandidateLesson
76
+
77
+
78
+ @app.post("/reflection", dependencies=[Depends(verify_key)])
79
+ async def reflection(req: ReflectRequest, svc: UALLService = Depends(get_service)):
80
+ return await svc.reflect_and_queue(req.candidate)
81
+
82
+
83
+ @app.post("/memory/validate", dependencies=[Depends(verify_key)])
84
+ async def memory_validate(req: ReflectRequest, svc: UALLService = Depends(get_service)):
85
+ result = await svc.validate_lesson(req.candidate)
86
+ return result.model_dump(mode="json")
87
+
88
+
89
+ @app.post("/memory/store", dependencies=[Depends(verify_key)])
90
+ async def memory_store(lesson: Lesson, svc: UALLService = Depends(get_service)):
91
+ lid = await svc.store_lesson(lesson)
92
+ return {"lesson_id": lid}
93
+
94
+
95
+ @app.post("/memory/search", dependencies=[Depends(verify_key)])
96
+ async def memory_search(req: MemorySearchRequest, svc: UALLService = Depends(get_service)):
97
+ results = await svc.retrieve(req)
98
+ return [
99
+ {
100
+ "lesson": r.lesson.model_dump(mode="json"),
101
+ "score": r.score,
102
+ "telemetry_id": r.telemetry_id,
103
+ }
104
+ for r in results
105
+ ]
106
+
107
+
108
+ @app.get("/memory/{lesson_id}/provenance", dependencies=[Depends(verify_key)])
109
+ async def memory_provenance(lesson_id: str, svc: UALLService = Depends(get_service)):
110
+ prov = await svc.get_provenance(lesson_id)
111
+ if not prov:
112
+ raise HTTPException(404, "Lesson not found")
113
+ return prov
114
+
115
+
116
+ @app.get("/memory/{lesson_id}/graph", dependencies=[Depends(verify_key)])
117
+ async def memory_graph(lesson_id: str, svc: UALLService = Depends(get_service)):
118
+ graph = await svc.get_graph(lesson_id)
119
+ if not graph:
120
+ raise HTTPException(404, "Lesson not found")
121
+ return graph
122
+
123
+
124
+ @app.post("/memory/prune", dependencies=[Depends(verify_key)])
125
+ async def memory_prune(svc: UALLService = Depends(get_service)):
126
+ return await svc.prune_memory()
127
+
128
+
129
+ # --- Promotion ---
130
+ @app.get("/promotion/pending", dependencies=[Depends(verify_key)])
131
+ async def promotion_pending(svc: UALLService = Depends(get_service)):
132
+ return await svc.list_pending()
133
+
134
+
135
+ @app.post("/promotion/process", dependencies=[Depends(verify_key)])
136
+ async def promotion_process(svc: UALLService = Depends(get_service)):
137
+ return await svc.process_promotion_queue()
138
+
139
+
140
+ # --- Telemetry ---
141
+ class TelemetryRequest(BaseModel):
142
+ lesson_id: str
143
+ telemetry_id: str | None = None
144
+ run_id: str | None = None
145
+ used: bool = False
146
+ accepted: bool = False
147
+ improved: bool | None = None
148
+
149
+
150
+ @app.post("/telemetry/lesson-outcome", dependencies=[Depends(verify_key)])
151
+ async def telemetry_outcome(req: TelemetryRequest, svc: UALLService = Depends(get_service)):
152
+ return await svc.record_lesson_outcome(
153
+ req.lesson_id, req.telemetry_id, req.run_id, req.used, req.accepted, req.improved
154
+ )
155
+
156
+
157
+ # --- Policies ---
158
+ @app.get("/policies", dependencies=[Depends(verify_key)])
159
+ async def get_policies(svc: UALLService = Depends(get_service)):
160
+ policies = await svc.get_policies()
161
+ return [p.model_dump(mode="json") for p in policies]
162
+
163
+
164
+ @app.post("/policies", dependencies=[Depends(verify_key)])
165
+ async def create_policy(policy: PolicyVersion, svc: UALLService = Depends(get_service)):
166
+ pid = await svc.create_policy(policy)
167
+ return {"policy_id": pid}
168
+
169
+
170
+ @app.get("/policies/versions", dependencies=[Depends(verify_key)])
171
+ async def policy_versions(policy_id: str, svc: UALLService = Depends(get_service)):
172
+ versions = await svc.list_policy_versions(policy_id)
173
+ return [v.model_dump(mode="json") for v in versions]
174
+
175
+
176
+ # --- Evaluation & analytics ---
177
+ @app.post("/evaluate", dependencies=[Depends(verify_key)])
178
+ async def evaluate(run_id: str, svc: UALLService = Depends(get_service)):
179
+ return await svc.evaluate(run_id)
180
+
181
+
182
+ @app.get("/agent-score", dependencies=[Depends(verify_key)])
183
+ async def agent_score(agent_id: str | None = None, svc: UALLService = Depends(get_service)):
184
+ return await svc.agent_score(agent_id)
185
+
186
+
187
+ @app.get("/analytics", dependencies=[Depends(verify_key)])
188
+ async def analytics(svc: UALLService = Depends(get_service)):
189
+ return await svc.get_analytics()
190
+
191
+
192
+ @app.get("/workflow-health", dependencies=[Depends(verify_key)])
193
+ async def workflow_health(workflow_id: str, svc: UALLService = Depends(get_service)):
194
+ return await svc.workflow_health(workflow_id)
195
+
196
+
197
+ @app.get("/top-failures", dependencies=[Depends(verify_key)])
198
+ async def top_failures(svc: UALLService = Depends(get_service)):
199
+ return await svc.top_failures()
200
+
201
+
202
+ # --- Recommendations ---
203
+ class RecommendationRequest(BaseModel):
204
+ agent_id: str | None = None
205
+ workflow_id: str | None = None
206
+ context: str | None = None
207
+
208
+
209
+ @app.post("/recommendations", dependencies=[Depends(verify_key)])
210
+ async def recommendations(req: RecommendationRequest, svc: UALLService = Depends(get_service)):
211
+ return await svc.get_recommendations(
212
+ agent_id=req.agent_id, workflow_id=req.workflow_id, context=req.context
213
+ )
214
+
215
+
216
+ @app.post("/recommendations/patterns", dependencies=[Depends(verify_key)])
217
+ async def recommendation_patterns(svc: UALLService = Depends(get_service)):
218
+ return await svc.detect_patterns()
219
+
220
+
221
+ # --- Experiments ---
222
+ class ExperimentStartRequest(BaseModel):
223
+ resource_type: str
224
+ resource_id: str
225
+ variant_a: str
226
+ variant_b: str
227
+ traffic_split: float = 0.1
228
+
229
+
230
+ @app.post("/experiments/start", dependencies=[Depends(verify_key)])
231
+ async def experiments_start(req: ExperimentStartRequest, svc: UALLService = Depends(get_service)):
232
+ exp = await svc.start_experiment(**req.model_dump())
233
+ return exp.model_dump(mode="json")
234
+
235
+
236
+ @app.post("/experiments/end", dependencies=[Depends(verify_key)])
237
+ async def experiments_end(experiment_id: str, svc: UALLService = Depends(get_service)):
238
+ exp = await svc.end_experiment(experiment_id)
239
+ return exp.model_dump(mode="json")
240
+
241
+
242
+ @app.get("/experiments/{experiment_id}/results", dependencies=[Depends(verify_key)])
243
+ async def experiment_results(experiment_id: str, svc: UALLService = Depends(get_service)):
244
+ exp = await svc.experiment_results(experiment_id)
245
+ if not exp:
246
+ raise HTTPException(404, "Experiment not found")
247
+ return exp.model_dump(mode="json")
248
+
249
+
250
+ # --- Rollback ---
251
+ class RollbackRequest(BaseModel):
252
+ resource_type: str
253
+ resource_id: str
254
+ target_version: str
255
+
256
+
257
+ @app.post("/rollback", dependencies=[Depends(verify_key)])
258
+ async def rollback(req: RollbackRequest, svc: UALLService = Depends(get_service)):
259
+ return await svc.rollback_resource(req.resource_type, req.resource_id, req.target_version)
260
+
261
+
262
+ @app.get("/versions/{resource_type}/{resource_id}", dependencies=[Depends(verify_key)])
263
+ async def versions(resource_type: str, resource_id: str, svc: UALLService = Depends(get_service)):
264
+ records = await svc.list_versions(resource_type, resource_id)
265
+ return [r.model_dump(mode="json") for r in records]
266
+
267
+
268
+ # --- Skills ---
269
+ @app.post("/skills", dependencies=[Depends(verify_key)])
270
+ async def create_skill(skill: Skill, svc: UALLService = Depends(get_service)):
271
+ sid = await svc.create_skill(skill)
272
+ return {"skill_id": sid}
273
+
274
+
275
+ @app.get("/skills/search", dependencies=[Depends(verify_key)])
276
+ async def search_skills(query: str, svc: UALLService = Depends(get_service)):
277
+ skills = await svc.search_skills(query)
278
+ return [s.model_dump(mode="json") for s in skills]
279
+
280
+
281
+ if __name__ == "__main__":
282
+ import uvicorn
283
+
284
+ uvicorn.run("uall_server.main:app", host="0.0.0.0", port=8000, reload=False)