severian 0.3.13__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 (90) hide show
  1. severian/__init__.py +0 -0
  2. severian/api/__init__.py +12 -0
  3. severian/api/app.py +419 -0
  4. severian/api/schemas.py +124 -0
  5. severian/application/__init__.py +0 -0
  6. severian/application/anticipation.py +397 -0
  7. severian/application/compactcites.py +42 -0
  8. severian/application/conflicts.py +204 -0
  9. severian/application/context_v2.py +145 -0
  10. severian/application/datestamps.py +96 -0
  11. severian/application/derivation.py +326 -0
  12. severian/application/dreaming.py +760 -0
  13. severian/application/emptyitems.py +72 -0
  14. severian/application/episodes.py +155 -0
  15. severian/application/eventtime.py +333 -0
  16. severian/application/extraction.py +253 -0
  17. severian/application/factanchors.py +78 -0
  18. severian/application/factslots.py +356 -0
  19. severian/application/fillbudget.py +192 -0
  20. severian/application/graph.py +218 -0
  21. severian/application/ingestion.py +114 -0
  22. severian/application/ports.py +430 -0
  23. severian/application/projections.py +211 -0
  24. severian/application/replytrim.py +82 -0
  25. severian/application/rerank.py +116 -0
  26. severian/application/retrieval.py +604 -0
  27. severian/application/service.py +905 -0
  28. severian/application/statements.py +167 -0
  29. severian/application/sufficiency.py +171 -0
  30. severian/application/truthresolution.py +505 -0
  31. severian/application/userfacts.py +409 -0
  32. severian/application/worker.py +465 -0
  33. severian/auth/__init__.py +24 -0
  34. severian/auth/principal_resolver.py +114 -0
  35. severian/auth/store.py +226 -0
  36. severian/backends.py +104 -0
  37. severian/benchmarks/__init__.py +48 -0
  38. severian/benchmarks/adapters/__init__.py +24 -0
  39. severian/benchmarks/adapters/baseline.py +85 -0
  40. severian/benchmarks/adapters/chroma.py +271 -0
  41. severian/benchmarks/adapters/scope.py +18 -0
  42. severian/benchmarks/adapters/severian.py +434 -0
  43. severian/benchmarks/checkpoint_merge.py +151 -0
  44. severian/benchmarks/cli.py +45 -0
  45. severian/benchmarks/embedding_runner.py +680 -0
  46. severian/benchmarks/manifest.py +292 -0
  47. severian/benchmarks/quantiles.py +57 -0
  48. severian/benchmarks/runner.py +398 -0
  49. severian/cli/__init__.py +13 -0
  50. severian/cli/__main__.py +5 -0
  51. severian/cli/app.py +774 -0
  52. severian/cli/path_policy.py +103 -0
  53. severian/composition.py +204 -0
  54. severian/domain/__init__.py +0 -0
  55. severian/domain/errors.py +42 -0
  56. severian/domain/hits.py +25 -0
  57. severian/domain/models.py +362 -0
  58. severian/domain/text.py +38 -0
  59. severian/infrastructure/__init__.py +0 -0
  60. severian/infrastructure/audit.py +411 -0
  61. severian/infrastructure/embedding_daemon.py +305 -0
  62. severian/infrastructure/embedding_metadata.py +137 -0
  63. severian/infrastructure/embedding_registry.py +177 -0
  64. severian/infrastructure/embedding_resolver.py +170 -0
  65. severian/infrastructure/fts.py +289 -0
  66. severian/infrastructure/granite_embedding.py +100 -0
  67. severian/infrastructure/native_accel.py +148 -0
  68. severian/infrastructure/pgvector_index.py +343 -0
  69. severian/infrastructure/postgresql.py +2176 -0
  70. severian/infrastructure/qwen_embedding.py +147 -0
  71. severian/infrastructure/rebuild.py +469 -0
  72. severian/infrastructure/remote_qwen_embedding.py +254 -0
  73. severian/infrastructure/snapshot.py +237 -0
  74. severian/infrastructure/sqlite.py +2036 -0
  75. severian/infrastructure/stemming.py +207 -0
  76. severian/infrastructure/task_notes.py +107 -0
  77. severian/infrastructure/vector.py +520 -0
  78. severian/integrations/__init__.py +9 -0
  79. severian/integrations/hermes.py +1115 -0
  80. severian/mcp/__init__.py +18 -0
  81. severian/mcp/__main__.py +73 -0
  82. severian/mcp/server.py +931 -0
  83. severian/py.typed +0 -0
  84. severian/sdk/__init__.py +17 -0
  85. severian/sdk/client.py +250 -0
  86. severian-0.3.13.dist-info/METADATA +278 -0
  87. severian-0.3.13.dist-info/RECORD +90 -0
  88. severian-0.3.13.dist-info/WHEEL +4 -0
  89. severian-0.3.13.dist-info/entry_points.txt +3 -0
  90. severian-0.3.13.dist-info/licenses/LICENSE +21 -0
severian/__init__.py ADDED
File without changes
@@ -0,0 +1,12 @@
1
+ """Public API package for Severian.
2
+
3
+ The application is constructed via :func:`severian.api.app.create_app`
4
+ so tests and the runtime uvicorn entrypoint share the same composition
5
+ root. Adapters (CLI/SDK) MUST NOT bypass the ``MemoryService`` wired
6
+ here.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from severian.api.app import create_app
11
+
12
+ __all__ = ["create_app"]
severian/api/app.py ADDED
@@ -0,0 +1,419 @@
1
+ """FastAPI application factory.
2
+
3
+ The API is intentionally thin: it validates request shapes, maps the
4
+ validated payload to the canonical application service, and returns
5
+ Pydantic response models. There is no direct repository, projection
6
+ or index access in this layer.
7
+
8
+ ``create_app`` is the single composition root used by both production
9
+ (``uvicorn severian.api.app:app``) and tests (``TestClient(create_app(...))``).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from fastapi import Depends, FastAPI, HTTPException, Path, Request, status
17
+ from fastapi.responses import JSONResponse
18
+
19
+ from severian.api.schemas import (
20
+ HealthResponse,
21
+ IngestRequest,
22
+ IngestResponse,
23
+ MemoryResponse,
24
+ ObservationResponse,
25
+ ScopeModel,
26
+ SearchHit,
27
+ SearchRequest,
28
+ SearchResponse,
29
+ )
30
+ from severian.application.derivation import DerivationGateway
31
+ from severian.application.service import MemoryService
32
+ from severian.auth.principal_resolver import PrincipalResolver
33
+ from severian.domain.errors import ForbiddenError, NotFoundError
34
+ from severian.domain.models import Scope
35
+
36
+ # Constants kept here so URL paths and tests share the same source of
37
+ # truth. Versioned under ``/api/v1`` per Phase 3 plan.
38
+ API_PREFIX = "/api/v1"
39
+ OBSERVATIONS_PATH = f"{API_PREFIX}/observations"
40
+ MEMORIES_PATH = f"{API_PREFIX}/memories"
41
+ SEARCH_PATH = f"{API_PREFIX}/search"
42
+
43
+
44
+ GatewayFactory = Callable[[], DerivationGateway]
45
+
46
+
47
+ def _scope_from_model(model: ScopeModel) -> Scope:
48
+ return Scope(
49
+ tenant_id=model.tenant_id,
50
+ profile_id=model.profile_id,
51
+ collection_id=model.collection_id,
52
+ agent_id=model.agent_id,
53
+ session_id=model.session_id,
54
+ )
55
+
56
+
57
+ def _bind_principal(requested: ScopeModel, principal: Scope) -> Scope:
58
+ """Derive the effective scope from the authenticated principal.
59
+
60
+ P0-6: the security-sensitive dimensions (tenant/profile) are NOT
61
+ client-selectable when a principal is bound. A request that claims
62
+ a different tenant or profile than the principal is an
63
+ authorization violation (403) — it is NOT a validation error, so
64
+ callers cannot probe for cross-tenant existence via 422 vs 403.
65
+ """
66
+ if requested.tenant_id != principal.tenant_id or requested.profile_id != principal.profile_id:
67
+ raise ForbiddenError("requested scope is outside the authenticated principal")
68
+ return Scope(
69
+ tenant_id=principal.tenant_id,
70
+ profile_id=principal.profile_id,
71
+ collection_id=requested.collection_id,
72
+ agent_id=requested.agent_id,
73
+ session_id=requested.session_id,
74
+ )
75
+
76
+
77
+ def _view_to_observation_response(observation: Any) -> ObservationResponse:
78
+ return ObservationResponse(
79
+ observation_id=observation.id,
80
+ content=observation.content,
81
+ content_hash=observation.content_hash,
82
+ scope=ScopeModel(
83
+ tenant_id=observation.scope.tenant_id,
84
+ profile_id=observation.scope.profile_id,
85
+ collection_id=observation.scope.collection_id,
86
+ agent_id=observation.scope.agent_id,
87
+ session_id=observation.scope.session_id,
88
+ ),
89
+ source=dict(observation.source),
90
+ created_at=observation.created_at.isoformat(),
91
+ )
92
+
93
+
94
+ def _hit_to_response(hit: Any) -> SearchHit:
95
+ return SearchHit(
96
+ memory_id=hit.memory_id,
97
+ content=hit.content,
98
+ source_observation_ids=list(hit.source_observation_ids),
99
+ lexical_score=hit.lexical_score,
100
+ vector_score=hit.vector_score,
101
+ fused_score=hit.fused_score,
102
+ scope=ScopeModel(
103
+ tenant_id=hit.scope.tenant_id,
104
+ profile_id=hit.scope.profile_id,
105
+ collection_id=hit.scope.collection_id,
106
+ agent_id=hit.scope.agent_id,
107
+ session_id=hit.scope.session_id,
108
+ ),
109
+ )
110
+
111
+
112
+ def create_app(
113
+ *,
114
+ service: MemoryService,
115
+ gateway: GatewayFactory | DerivationGateway,
116
+ principal: Scope | None = None,
117
+ principal_resolver: PrincipalResolver | None = None,
118
+ ) -> FastAPI:
119
+ """Build a FastAPI app bound to a ``MemoryService`` instance.
120
+
121
+ ``gateway`` may be either a callable returning a fresh gateway per
122
+ request (preferred — derivation may hold per-request state) or a
123
+ fixed gateway instance used for every request (suitable for
124
+ deterministic local fixtures).
125
+
126
+ ``principal`` optionally binds a fixed authenticated scope. When
127
+ set, the security-sensitive dimensions (``tenant_id`` and
128
+ ``profile_id``) are derived server-side from the principal and any
129
+ request body/token that disagrees with them is rejected with 403 —
130
+ scope becomes authorization, not a client-supplied selector. When
131
+ unset (local/dev mode), the client must supply a fully-populated
132
+ scope as before.
133
+
134
+ ``principal_resolver`` optionally enables per-request API-key
135
+ authentication (SEC-01). When set, each request must carry a
136
+ valid ``Authorization: Bearer <key>`` header; the resolved
137
+ principal overrides the static ``principal`` (if any). Fail-closed:
138
+ unauthenticated requests are rejected with 401. Health endpoints
139
+ (``/healthz``, ``/readyz``) are exempt.
140
+ """
141
+
142
+ def _get_gateway() -> DerivationGateway:
143
+ if callable(gateway) and not hasattr(gateway, "derive"):
144
+ return gateway()
145
+ return gateway # type: ignore[return-value]
146
+
147
+ app = FastAPI(
148
+ title="Severian Memory API",
149
+ version="0.1.0",
150
+ description="Versioned HTTP API for the canonical memory platform.",
151
+ )
152
+ app.state.service = service
153
+
154
+ def get_service() -> MemoryService:
155
+ return service
156
+
157
+ def get_gateway() -> DerivationGateway:
158
+ return _get_gateway()
159
+
160
+ # SEC-01: when no principal_resolver is configured (dev/loopback mode),
161
+ # use a no-op dependency that returns None so FastAPI's parameter
162
+ # analysis treats ``payload`` as the body. When a resolver is
163
+ # configured, it is a callable that returns the authenticated Scope
164
+ # (or raises 401).
165
+ def _no_auth() -> Scope | None:
166
+ return None
167
+
168
+ auth_dependency = Depends(principal_resolver) if principal_resolver is not None else Depends(_no_auth)
169
+
170
+ def resolve_scope(scope_model: ScopeModel, request_principal: Scope | None = None) -> Scope:
171
+ """Resolve the effective scope for a request.
172
+
173
+ With a bound principal, tenant/profile are derived server-side
174
+ and any disagreement is a 403. Without one, the client-supplied
175
+ scope is used as-is (local/dev mode).
176
+
177
+ ``request_principal`` (from the auth resolver) takes precedence
178
+ over the static ``principal`` — it is the per-request authenticated
179
+ identity.
180
+ """
181
+ effective = request_principal if request_principal is not None else principal
182
+ if effective is not None:
183
+ return _bind_principal(scope_model, effective)
184
+ return _scope_from_model(scope_model)
185
+
186
+ @app.get("/healthz", response_model=HealthResponse)
187
+ def healthz() -> HealthResponse:
188
+ # Liveness: always 200 while the process serves requests.
189
+ return HealthResponse(ready=True, components={})
190
+
191
+ @app.get("/readyz", response_model=HealthResponse)
192
+ def readyz(svc: MemoryService = Depends(get_service)) -> HealthResponse:
193
+ # Readiness: dependency gate. P1-2: a service whose
194
+ # dependencies are down must NOT report ready — callers (load
195
+ # balancers, orchestrators) get 503 so traffic is routed away.
196
+ report = svc.health()
197
+ response = HealthResponse(ready=report.ready, components=report.components)
198
+ if not report.ready:
199
+ raise HTTPException(
200
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
201
+ detail={
202
+ "ready": False,
203
+ "components": report.components,
204
+ "type": "not_ready",
205
+ },
206
+ )
207
+ return response
208
+
209
+ @app.post(
210
+ OBSERVATIONS_PATH,
211
+ response_model=IngestResponse,
212
+ status_code=status.HTTP_201_CREATED,
213
+ )
214
+ def ingest(
215
+ payload: IngestRequest,
216
+ svc: MemoryService = Depends(get_service),
217
+ request_principal: Scope | None = auth_dependency,
218
+ ) -> IngestResponse:
219
+ try:
220
+ view = svc.ingest_observation(
221
+ resolve_scope(payload.scope, request_principal),
222
+ content=payload.content,
223
+ source=payload.source,
224
+ )
225
+ except ForbiddenError as exc:
226
+ raise HTTPException(
227
+ status_code=status.HTTP_403_FORBIDDEN,
228
+ detail=str(exc),
229
+ ) from exc
230
+ except ValueError as exc:
231
+ # Pydantic surface for client errors — never leak internals.
232
+ raise HTTPException(
233
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
234
+ detail=str(exc),
235
+ ) from exc
236
+ return IngestResponse(
237
+ observation_id=view.observation_id,
238
+ content=view.content,
239
+ content_hash=view.content_hash,
240
+ scope=ScopeModel(
241
+ tenant_id=view.scope.tenant_id,
242
+ profile_id=view.scope.profile_id,
243
+ collection_id=view.scope.collection_id,
244
+ agent_id=view.scope.agent_id,
245
+ session_id=view.scope.session_id,
246
+ ),
247
+ source=dict(view.source),
248
+ job_id=view.job_id,
249
+ job_kind=view.job_kind,
250
+ )
251
+
252
+ @app.get(
253
+ f"{OBSERVATIONS_PATH}/{{observation_id}}",
254
+ response_model=ObservationResponse,
255
+ )
256
+ def get_observation(
257
+ observation_id: str = Path(..., min_length=1),
258
+ tenant_id: str = "",
259
+ profile_id: str = "",
260
+ collection_id: str = "",
261
+ agent_id: str = "",
262
+ session_id: str = "",
263
+ svc: MemoryService = Depends(get_service),
264
+ request_principal: Scope | None = auth_dependency,
265
+ ) -> ObservationResponse:
266
+ # P1-4: ScopeModel construction lives INSIDE the try so pydantic
267
+ # validation failures surface as 422, not 500. Missing/invalid
268
+ # scope dimensions are client errors, never server errors.
269
+ try:
270
+ scope = ScopeModel(
271
+ tenant_id=tenant_id,
272
+ profile_id=profile_id,
273
+ collection_id=collection_id,
274
+ agent_id=agent_id,
275
+ session_id=session_id,
276
+ )
277
+ scope_obj = resolve_scope(scope, request_principal)
278
+ except ForbiddenError as exc:
279
+ raise HTTPException(
280
+ status_code=status.HTTP_403_FORBIDDEN,
281
+ detail=str(exc),
282
+ ) from exc
283
+ except ValueError as exc:
284
+ raise HTTPException(
285
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
286
+ detail=str(exc),
287
+ ) from exc
288
+ try:
289
+ observation = svc.get_observation(scope_obj, observation_id)
290
+ except NotFoundError as exc:
291
+ # 404 is correct: the request was well-formed but the scope
292
+ # cannot see the requested observation. The body MUST NOT
293
+ # disclose whether the ID exists in another scope.
294
+ raise HTTPException(
295
+ status_code=status.HTTP_404_NOT_FOUND,
296
+ detail="observation not found in scope",
297
+ ) from exc
298
+ return _view_to_observation_response(observation)
299
+
300
+ @app.get(
301
+ f"{MEMORIES_PATH}/{{memory_id}}",
302
+ response_model=MemoryResponse,
303
+ )
304
+ def get_memory(
305
+ memory_id: str = Path(..., min_length=1),
306
+ tenant_id: str = "",
307
+ profile_id: str = "",
308
+ collection_id: str = "",
309
+ agent_id: str = "",
310
+ session_id: str = "",
311
+ svc: MemoryService = Depends(get_service),
312
+ request_principal: Scope | None = auth_dependency,
313
+ ) -> MemoryResponse:
314
+ # P1-4: memory-detail endpoint. Search returns memory ids (both
315
+ # observation-derived and derived memories); the dashboard needs
316
+ # to open any hit. Scope construction lives INSIDE the try so
317
+ # validation failures are 422, not 500.
318
+ try:
319
+ scope = ScopeModel(
320
+ tenant_id=tenant_id,
321
+ profile_id=profile_id,
322
+ collection_id=collection_id,
323
+ agent_id=agent_id,
324
+ session_id=session_id,
325
+ )
326
+ scope_obj = resolve_scope(scope, request_principal)
327
+ except ForbiddenError as exc:
328
+ raise HTTPException(
329
+ status_code=status.HTTP_403_FORBIDDEN,
330
+ detail=str(exc),
331
+ ) from exc
332
+ except ValueError as exc:
333
+ raise HTTPException(
334
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
335
+ detail=str(exc),
336
+ ) from exc
337
+ try:
338
+ memory = svc.get_memory(scope_obj, memory_id)
339
+ except NotFoundError as exc:
340
+ # Same discipline as observations: a well-formed request for
341
+ # an id the scope cannot see is 404; the body never
342
+ # discloses cross-scope existence.
343
+ raise HTTPException(
344
+ status_code=status.HTTP_404_NOT_FOUND,
345
+ detail="memory not found in scope",
346
+ ) from exc
347
+ return MemoryResponse(
348
+ memory_id=memory.id,
349
+ content=memory.content,
350
+ kind=memory.kind,
351
+ source_observation_ids=list(memory.source_observation_ids),
352
+ scope=ScopeModel.from_domain(memory.scope),
353
+ created_at=str(memory.created_at),
354
+ )
355
+
356
+ @app.post(SEARCH_PATH, response_model=SearchResponse)
357
+ def search(
358
+ payload: SearchRequest,
359
+ gw: DerivationGateway = Depends(get_gateway),
360
+ svc: MemoryService = Depends(get_service),
361
+ request_principal: Scope | None = auth_dependency,
362
+ ) -> SearchResponse:
363
+ try:
364
+ scope = resolve_scope(payload.scope, request_principal)
365
+ except ForbiddenError as exc:
366
+ raise HTTPException(
367
+ status_code=status.HTTP_403_FORBIDDEN,
368
+ detail=str(exc),
369
+ ) from exc
370
+ except ValueError as exc:
371
+ raise HTTPException(
372
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
373
+ detail=str(exc),
374
+ ) from exc
375
+ try:
376
+ hits = svc.search(
377
+ scope,
378
+ query=payload.query,
379
+ limit=payload.limit,
380
+ derive=payload.derive,
381
+ gateway=gw,
382
+ )
383
+ except ValueError as exc:
384
+ raise HTTPException(
385
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
386
+ detail=str(exc),
387
+ ) from exc
388
+ return SearchResponse(hits=[_hit_to_response(hit) for hit in hits])
389
+
390
+ @app.exception_handler(Exception)
391
+ async def safe_exception_handler(
392
+ request: Request, exc: Exception
393
+ ) -> JSONResponse:
394
+ # HTTPException (404/403/422/503) must pass through with its
395
+ # intended status; only unexpected exceptions become 500.
396
+ # Defence-in-depth: never leak stack traces or canonical
397
+ # payloads. QA gate SEC-03 requires no sensitive content in
398
+ # error responses.
399
+ if isinstance(exc, HTTPException):
400
+ detail = exc.detail if isinstance(exc.detail, (str, dict, list)) else "error"
401
+ return JSONResponse(
402
+ status_code=exc.status_code,
403
+ content={"detail": detail, "type": "http_error"},
404
+ )
405
+ return JSONResponse(
406
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
407
+ content={"detail": "internal error", "type": "server_error"},
408
+ )
409
+
410
+ return app
411
+
412
+
413
+ __all__ = [
414
+ "API_PREFIX",
415
+ "OBSERVATIONS_PATH",
416
+ "MEMORIES_PATH",
417
+ "SEARCH_PATH",
418
+ "create_app",
419
+ ]
@@ -0,0 +1,124 @@
1
+ """Pydantic request/response models for the versioned HTTP API.
2
+
3
+ All scope fields are mandatory on every request: there is no implicit
4
+ scope on the server. The model rejects empty scope dimensions, which
5
+ mirrors the domain invariant in :class:`severian.domain.models.Scope`.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
12
+
13
+
14
+ class ScopeModel(BaseModel):
15
+ model_config = ConfigDict(extra="forbid")
16
+
17
+ tenant_id: str
18
+ profile_id: str
19
+ collection_id: str
20
+ agent_id: str
21
+ session_id: str
22
+
23
+ @classmethod
24
+ def from_domain(cls, scope: Any) -> ScopeModel:
25
+ return cls(
26
+ tenant_id=scope.tenant_id,
27
+ profile_id=scope.profile_id,
28
+ collection_id=scope.collection_id,
29
+ agent_id=scope.agent_id,
30
+ session_id=scope.session_id,
31
+ )
32
+
33
+ @field_validator(
34
+ "tenant_id", "profile_id", "collection_id", "agent_id", "session_id"
35
+ )
36
+ @classmethod
37
+ def _nonempty(cls, value: str) -> str:
38
+ if not value.strip():
39
+ raise ValueError("scope dimensions must not be empty")
40
+ return value
41
+
42
+
43
+ class IngestRequest(BaseModel):
44
+ model_config = ConfigDict(extra="forbid")
45
+
46
+ scope: ScopeModel
47
+ content: str = Field(min_length=1, max_length=1_000_000)
48
+ source: dict[str, Any] = Field(default_factory=dict)
49
+
50
+
51
+ class IngestResponse(BaseModel):
52
+ model_config = ConfigDict(extra="forbid")
53
+
54
+ observation_id: str
55
+ content: str
56
+ content_hash: str
57
+ scope: ScopeModel
58
+ source: dict[str, Any]
59
+ job_id: str
60
+ job_kind: str
61
+
62
+
63
+ class ObservationResponse(BaseModel):
64
+ model_config = ConfigDict(extra="forbid")
65
+
66
+ observation_id: str
67
+ content: str
68
+ content_hash: str
69
+ scope: ScopeModel
70
+ source: dict[str, Any]
71
+ created_at: str
72
+
73
+
74
+ class SearchRequest(BaseModel):
75
+ model_config = ConfigDict(extra="forbid")
76
+
77
+ scope: ScopeModel
78
+ query: str = Field(min_length=1, max_length=10_000)
79
+ limit: int = Field(default=20, ge=1, le=200)
80
+ derive: bool = False
81
+
82
+
83
+ class SearchHit(BaseModel):
84
+ model_config = ConfigDict(extra="forbid")
85
+
86
+ memory_id: str
87
+ content: str
88
+ source_observation_ids: list[str]
89
+ lexical_score: float
90
+ vector_score: float
91
+ fused_score: float
92
+ scope: ScopeModel
93
+
94
+
95
+ class MemoryResponse(BaseModel):
96
+ model_config = ConfigDict(extra="forbid")
97
+
98
+ memory_id: str
99
+ content: str
100
+ kind: str
101
+ source_observation_ids: list[str]
102
+ scope: ScopeModel
103
+ created_at: str
104
+
105
+
106
+ class SearchResponse(BaseModel):
107
+ model_config = ConfigDict(extra="forbid")
108
+
109
+ hits: list[SearchHit]
110
+
111
+
112
+ class HealthComponent(BaseModel):
113
+ model_config = ConfigDict(extra="forbid")
114
+
115
+ name: str
116
+ status: str
117
+ detail: str = ""
118
+
119
+
120
+ class HealthResponse(BaseModel):
121
+ model_config = ConfigDict(extra="forbid")
122
+
123
+ ready: bool
124
+ components: dict[str, str]
File without changes