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_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()
|
uall_server/__init__.py
ADDED
|
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)
|