claude-smart 0.2.43 → 0.2.45

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 (68) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +2 -2
  4. package/package.json +2 -2
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +21 -1
  8. package/plugin/pyproject.toml +3 -3
  9. package/plugin/scripts/_lib.sh +72 -28
  10. package/plugin/scripts/backend-service.sh +55 -7
  11. package/plugin/scripts/cli.sh +2 -1
  12. package/plugin/scripts/dashboard-service.sh +7 -5
  13. package/plugin/scripts/ensure-plugin-root.sh +1 -0
  14. package/plugin/scripts/hook_entry.sh +7 -5
  15. package/plugin/scripts/smart-install.sh +2 -2
  16. package/plugin/src/README.md +57 -0
  17. package/plugin/uv.lock +126 -5
  18. package/plugin/vendor/reflexio/.env.example +9 -0
  19. package/plugin/vendor/reflexio/pyproject.toml +5 -2
  20. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +0 -1
  21. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +7 -4
  22. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +4 -4
  23. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +2 -2
  24. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +13 -4
  25. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +3 -1
  26. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +59 -6
  27. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  28. package/plugin/vendor/reflexio/reflexio/server/README.md +7 -1
  29. package/plugin/vendor/reflexio/reflexio/server/api.py +188 -34
  30. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
  31. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +33 -11
  32. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +278 -29
  33. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +457 -181
  34. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +28 -0
  35. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +22 -12
  36. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +137 -9
  37. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +1 -1
  38. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +36 -3
  39. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +13 -3
  40. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +17 -0
  41. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +1 -1
  42. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
  43. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
  44. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +27 -23
  45. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
  46. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
  47. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +19 -9
  48. package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
  49. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +1 -5
  50. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +44 -2
  51. package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
  52. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +8 -1
  53. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +91 -24
  54. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +3 -1
  55. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +6 -3
  56. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +1 -1
  57. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +18 -5
  58. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +2 -2
  59. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +88 -3
  60. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +36 -5
  61. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +6 -3
  62. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +6 -3
  63. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +32 -22
  64. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +89 -4
  65. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +46 -1
  66. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +12 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +1 -1
  68. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +8 -4
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
2
  import inspect
3
3
  import logging
4
- import threading
4
+ import os
5
5
  from collections.abc import Callable
6
+ from concurrent.futures import ThreadPoolExecutor
6
7
  from datetime import UTC, datetime
7
8
  from typing import Any
8
9
 
@@ -21,6 +22,7 @@ from slowapi.errors import RateLimitExceeded
21
22
  from slowapi.util import get_remote_address
22
23
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
23
24
  from starlette.responses import Response
25
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
24
26
 
25
27
  from reflexio.models.api_schema.braintrust_schema import (
26
28
  BraintrustStatusResponse,
@@ -194,6 +196,51 @@ SYNC_REQUEST_TIMEOUT_SECONDS = (
194
196
  )
195
197
  SUSPICIOUS_USER_AGENTS = ["bot", "crawler", "spider", "scraper", "curl", "wget"]
196
198
  ALLOWED_EMPTY_UA_PATHS = ["/health", "/"] # Paths that allow empty user agents
199
+ DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
200
+ REGENERATE_MAX_WORKERS = 2
201
+ _regen_executor = ThreadPoolExecutor(
202
+ max_workers=REGENERATE_MAX_WORKERS,
203
+ thread_name_prefix="reflexio-regen",
204
+ )
205
+
206
+
207
+ def _resolve_cors_origins() -> list[str]:
208
+ """Resolve browser origins allowed to make credentialed CORS requests."""
209
+ configured_origins = os.getenv("REFLEXIO_ALLOWED_ORIGINS", "").strip()
210
+ if configured_origins:
211
+ origins = [
212
+ origin.strip().rstrip("/")
213
+ for origin in configured_origins.split(",")
214
+ if origin.strip()
215
+ ]
216
+ return origins or ["http://localhost:8080"]
217
+
218
+ frontend_url = os.getenv("FRONTEND_URL", "").strip()
219
+ if frontend_url:
220
+ return [frontend_url.rstrip("/")]
221
+
222
+ return ["http://localhost:8080"]
223
+
224
+
225
+ def _max_body_bytes_from_env() -> int:
226
+ raw_value = os.getenv("REFLEXIO_MAX_BODY_BYTES", str(DEFAULT_MAX_BODY_BYTES))
227
+ try:
228
+ max_bytes = int(raw_value)
229
+ except ValueError:
230
+ logger.warning(
231
+ "Ignoring invalid REFLEXIO_MAX_BODY_BYTES=%r; using %s",
232
+ raw_value,
233
+ DEFAULT_MAX_BODY_BYTES,
234
+ )
235
+ return DEFAULT_MAX_BODY_BYTES
236
+ if max_bytes <= 0:
237
+ logger.warning(
238
+ "Ignoring non-positive REFLEXIO_MAX_BODY_BYTES=%r; using %s",
239
+ raw_value,
240
+ DEFAULT_MAX_BODY_BYTES,
241
+ )
242
+ return DEFAULT_MAX_BODY_BYTES
243
+ return max_bytes
197
244
 
198
245
 
199
246
  def get_rate_limit_key(request: Request) -> str:
@@ -312,6 +359,82 @@ class TimeoutMiddleware(BaseHTTPMiddleware):
312
359
  )
313
360
 
314
361
 
362
+ class _RequestBodyTooLargeError(Exception):
363
+ """Raised when the streamed request body exceeds the configured limit."""
364
+
365
+
366
+ class BodySizeLimitMiddleware:
367
+ """Reject requests whose declared or streamed body size exceeds the limit."""
368
+
369
+ def __init__(self, app: ASGIApp) -> None:
370
+ self.app = app
371
+
372
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
373
+ from starlette.responses import JSONResponse
374
+
375
+ if scope["type"] != "http":
376
+ await self.app(scope, receive, send)
377
+ return
378
+
379
+ max_body_bytes = _max_body_bytes_from_env()
380
+ content_length = None
381
+ for name, value in scope.get("headers", []):
382
+ if name.lower() == b"content-length":
383
+ content_length = value.decode("latin-1")
384
+ break
385
+
386
+ if content_length is not None:
387
+ try:
388
+ body_bytes = int(content_length)
389
+ except ValueError:
390
+ body_bytes = 0
391
+ if body_bytes > max_body_bytes:
392
+ await JSONResponse(
393
+ status_code=status.HTTP_413_CONTENT_TOO_LARGE,
394
+ content={"detail": "Request body too large"},
395
+ )(scope, receive, send)
396
+ return
397
+
398
+ consumed_bytes = 0
399
+
400
+ async def limited_receive() -> Message:
401
+ nonlocal consumed_bytes
402
+ message = await receive()
403
+ if message["type"] == "http.request":
404
+ consumed_bytes += len(message.get("body", b""))
405
+ if consumed_bytes > max_body_bytes:
406
+ raise _RequestBodyTooLargeError
407
+ return message
408
+
409
+ try:
410
+ await self.app(scope, limited_receive, send)
411
+ except _RequestBodyTooLargeError:
412
+ await JSONResponse(
413
+ status_code=status.HTTP_413_CONTENT_TOO_LARGE,
414
+ content={"detail": "Request body too large"},
415
+ )(scope, receive, send)
416
+
417
+
418
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
419
+ """Attach conservative browser security headers to every response."""
420
+
421
+ async def dispatch(
422
+ self, request: Request, call_next: RequestResponseEndpoint
423
+ ) -> Response:
424
+ response = await call_next(request)
425
+ response.headers["X-Content-Type-Options"] = "nosniff"
426
+ response.headers["X-Frame-Options"] = "DENY"
427
+ response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
428
+ if (
429
+ request.url.scheme == "https"
430
+ or request.headers.get("x-forwarded-proto", "").lower() == "https"
431
+ ):
432
+ response.headers["Strict-Transport-Security"] = (
433
+ "max-age=31536000; includeSubDomains"
434
+ )
435
+ return response
436
+
437
+
315
438
  class CorrelationIdMiddleware(BaseHTTPMiddleware):
316
439
  """Middleware that assigns a unique correlation ID to each request.
317
440
 
@@ -931,7 +1054,9 @@ def delete_user_playbooks_by_ids(
931
1054
  response_model=BulkDeleteResponse,
932
1055
  response_model_exclude_none=True,
933
1056
  )
1057
+ @limiter.limit("10/minute")
934
1058
  def delete_all_interactions(
1059
+ request: Request,
935
1060
  org_id: str = Depends(default_get_org_id),
936
1061
  ) -> BulkDeleteResponse:
937
1062
  """Delete all requests and their associated interactions.
@@ -950,7 +1075,9 @@ def delete_all_interactions(
950
1075
  response_model=BulkDeleteResponse,
951
1076
  response_model_exclude_none=True,
952
1077
  )
1078
+ @limiter.limit("10/minute")
953
1079
  def delete_all_profiles(
1080
+ request: Request,
954
1081
  org_id: str = Depends(default_get_org_id),
955
1082
  ) -> BulkDeleteResponse:
956
1083
  """Delete all profiles.
@@ -969,7 +1096,9 @@ def delete_all_profiles(
969
1096
  response_model=BulkDeleteResponse,
970
1097
  response_model_exclude_none=True,
971
1098
  )
1099
+ @limiter.limit("10/minute")
972
1100
  def delete_all_playbooks(
1101
+ request: Request,
973
1102
  org_id: str = Depends(default_get_org_id),
974
1103
  ) -> BulkDeleteResponse:
975
1104
  """Delete all playbooks (both user and agent).
@@ -988,7 +1117,9 @@ def delete_all_playbooks(
988
1117
  response_model=BulkDeleteResponse,
989
1118
  response_model_exclude_none=True,
990
1119
  )
1120
+ @limiter.limit("10/minute")
991
1121
  def delete_all_user_playbooks(
1122
+ request: Request,
992
1123
  org_id: str = Depends(default_get_org_id),
993
1124
  ) -> BulkDeleteResponse:
994
1125
  """Delete all user playbooks (user only, not agent).
@@ -1007,7 +1138,9 @@ def delete_all_user_playbooks(
1007
1138
  response_model=BulkDeleteResponse,
1008
1139
  response_model_exclude_none=True,
1009
1140
  )
1141
+ @limiter.limit("10/minute")
1010
1142
  def delete_all_agent_playbooks(
1143
+ request: Request,
1011
1144
  org_id: str = Depends(default_get_org_id),
1012
1145
  ) -> BulkDeleteResponse:
1013
1146
  """Delete all agent playbooks (agent only, not user).
@@ -1026,8 +1159,10 @@ def delete_all_agent_playbooks(
1026
1159
  response_model=ClearUserDataResponse,
1027
1160
  response_model_exclude_none=True,
1028
1161
  )
1162
+ @limiter.limit("10/minute")
1029
1163
  def clear_user_data(
1030
- request: ClearUserDataRequest,
1164
+ request: Request,
1165
+ payload: ClearUserDataRequest,
1031
1166
  org_id: str = Depends(default_get_org_id),
1032
1167
  ) -> ClearUserDataResponse:
1033
1168
  """Delete all rows scoped to a single ``user_id``.
@@ -1046,7 +1181,7 @@ def clear_user_data(
1046
1181
  Returns:
1047
1182
  ClearUserDataResponse: Response with per-entity deletion counts
1048
1183
  """
1049
- return publisher_api.clear_user_data(org_id=org_id, request=request)
1184
+ return publisher_api.clear_user_data(org_id=org_id, request=payload)
1050
1185
 
1051
1186
 
1052
1187
  @core_router.post(
@@ -1071,7 +1206,9 @@ def get_interactions(
1071
1206
  response_model=GetInteractionsViewResponse,
1072
1207
  response_model_exclude_none=True,
1073
1208
  )
1209
+ @limiter.limit("30/minute")
1074
1210
  def get_all_interactions(
1211
+ request: Request,
1075
1212
  limit: int = 100,
1076
1213
  org_id: str = Depends(default_get_org_id),
1077
1214
  ) -> GetInteractionsViewResponse:
@@ -1154,7 +1291,9 @@ def get_profiles(
1154
1291
  response_model=GetProfilesViewResponse,
1155
1292
  response_model_exclude_none=True,
1156
1293
  )
1294
+ @limiter.limit("30/minute")
1157
1295
  def get_all_profiles(
1296
+ request: Request,
1158
1297
  limit: int = 100,
1159
1298
  status_filter: str | None = None,
1160
1299
  org_id: str = Depends(default_get_org_id),
@@ -1230,7 +1369,9 @@ def run_playbook_aggregation(
1230
1369
 
1231
1370
 
1232
1371
  @core_router.post("/api/set_config")
1372
+ @limiter.limit("10/minute")
1233
1373
  def set_config(
1374
+ request: Request,
1234
1375
  config: dict[str, Any],
1235
1376
  org_id: str = Depends(default_get_org_id),
1236
1377
  ) -> SetConfigResponse:
@@ -1257,7 +1398,9 @@ def set_config(
1257
1398
 
1258
1399
 
1259
1400
  @core_router.post("/api/update_config")
1401
+ @limiter.limit("10/minute")
1260
1402
  def update_config(
1403
+ request: Request,
1261
1404
  partial: dict[str, Any],
1262
1405
  org_id: str = Depends(default_get_org_id),
1263
1406
  ) -> SetConfigResponse:
@@ -1566,8 +1709,10 @@ def update_user_profile_endpoint(
1566
1709
  response_model=GetDashboardStatsResponse,
1567
1710
  response_model_exclude_none=True,
1568
1711
  )
1712
+ @limiter.limit("30/minute")
1569
1713
  def get_dashboard_stats(
1570
- request: GetDashboardStatsRequest,
1714
+ request: Request,
1715
+ payload: GetDashboardStatsRequest,
1571
1716
  org_id: str = Depends(default_get_org_id),
1572
1717
  ) -> GetDashboardStatsResponse:
1573
1718
  """Get comprehensive dashboard statistics including counts and time-series data.
@@ -1583,7 +1728,7 @@ def get_dashboard_stats(
1583
1728
  reflexio = get_reflexio(org_id=org_id)
1584
1729
 
1585
1730
  # Get dashboard stats using Reflexio's method
1586
- return reflexio.get_dashboard_stats(request)
1731
+ return reflexio.get_dashboard_stats(payload)
1587
1732
 
1588
1733
 
1589
1734
  @core_router.post(
@@ -1755,7 +1900,9 @@ def get_evaluation_overview(
1755
1900
  response_model=RegenerateStartResponse,
1756
1901
  response_model_exclude_none=True,
1757
1902
  )
1903
+ @limiter.limit("5/minute")
1758
1904
  def start_regenerate(
1905
+ request: Request,
1759
1906
  payload: RegenerateRequest,
1760
1907
  org_id: str = Depends(default_get_org_id),
1761
1908
  ) -> RegenerateStartResponse:
@@ -1789,15 +1936,12 @@ def start_regenerate(
1789
1936
  )
1790
1937
  except RuntimeError as e:
1791
1938
  raise HTTPException(status_code=409, detail=str(e)) from e
1792
- threading.Thread(
1793
- target=run_regen,
1794
- kwargs={
1795
- "job": job,
1796
- "request_context": reflexio.request_context,
1797
- "llm_client": reflexio.llm_client,
1798
- },
1799
- daemon=True,
1800
- ).start()
1939
+ _regen_executor.submit(
1940
+ run_regen,
1941
+ job=job,
1942
+ request_context=reflexio.request_context,
1943
+ llm_client=reflexio.llm_client,
1944
+ )
1801
1945
  return RegenerateStartResponse(job_id=job.job_id, total=job.total)
1802
1946
 
1803
1947
 
@@ -2612,24 +2756,16 @@ def create_app(
2612
2756
  @asynccontextmanager
2613
2757
  async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
2614
2758
  validate_llm_availability()
2615
- bootstrap_org_id = _lifespan_org_id()
2616
- bootstrap_context = RequestContext(org_id=bootstrap_org_id)
2617
- retrieval_floor = bootstrap_context.configurator.get_config().retrieval_floor
2618
- if retrieval_floor.enabled:
2619
- from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
2620
-
2621
- _prewarm_cross_encoder()
2622
- else:
2623
- logger.info(
2624
- "Cross-encoder pre-warm skipped because retrieval_floor.enabled=false"
2625
- )
2759
+ from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
2760
+
2761
+ _prewarm_cross_encoder()
2626
2762
  # The scheduler discovers every org with resumable work each tick and
2627
2763
  # drives a per-org worker with org-scoped claims, so it is not limited
2628
2764
  # to the bootstrap org. The bootstrap org is only used to read config
2629
2765
  # and to seed cross-org discovery.
2630
2766
  scheduler = maybe_start_resume_scheduler(
2631
2767
  lambda org_id: RequestContext(org_id=org_id),
2632
- bootstrap_org_id=bootstrap_org_id,
2768
+ bootstrap_org_id=_lifespan_org_id(),
2633
2769
  )
2634
2770
  try:
2635
2771
  yield
@@ -2661,14 +2797,32 @@ def create_app(
2661
2797
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore[reportArgumentType]
2662
2798
 
2663
2799
  # CORS
2664
- origins = ["*"]
2665
- app.add_middleware(
2666
- CORSMiddleware,
2667
- allow_origins=origins,
2668
- allow_credentials=True,
2669
- allow_methods=["*"],
2670
- allow_headers=["*"],
2671
- )
2800
+ # The locked-down, credentialed allowlist is an enterprise concern: only
2801
+ # hosts that wire in auth (``require_auth=True``) restrict browser origins.
2802
+ # OSS/local runs have no auth and bundle their own docs playground on a
2803
+ # separate port, so they allow any origin (no credentials needed).
2804
+ if require_auth:
2805
+ app.add_middleware(
2806
+ CORSMiddleware,
2807
+ allow_origins=_resolve_cors_origins(),
2808
+ allow_credentials=True,
2809
+ allow_methods=["*"],
2810
+ allow_headers=["*"],
2811
+ )
2812
+ else:
2813
+ app.add_middleware(
2814
+ CORSMiddleware,
2815
+ allow_origins=["*"],
2816
+ allow_credentials=False,
2817
+ allow_methods=["*"],
2818
+ allow_headers=["*"],
2819
+ )
2820
+
2821
+ # Reject oversized requests before they reach endpoint handlers.
2822
+ app.add_middleware(BodySizeLimitMiddleware)
2823
+
2824
+ # Security headers
2825
+ app.add_middleware(SecurityHeadersMiddleware)
2672
2826
 
2673
2827
  # Timeout middleware
2674
2828
  app.add_middleware(TimeoutMiddleware)
@@ -0,0 +1,34 @@
1
+ # server/api_endpoints
2
+ Description: Bridge between FastAPI routes and business logic — builds `RequestContext`, validates requests, and delegates into `Reflexio`. Most endpoints are registered on the `core_router` in `../api.py`; the files here are the handlers/helpers it calls.
3
+
4
+ > For the complete endpoint list (publish, retrieval, search, profile/playbook lifecycle, evaluation, Braintrust, operations), see the parent [server README](../README.md#api-endpoints).
5
+
6
+ ## Files
7
+
8
+ | File | Purpose |
9
+ |------|---------|
10
+ | `request_context.py` | `RequestContext` — bundles `org_id`, `storage`, `configurator`, `prompt_manager`. Built per request via `get_request_context()` (FastAPI `Depends`). The one object every handler reads storage/config/prompts through. |
11
+ | `publisher_api.py` | Publishing + direct CRUD helpers: `add_user_interaction/profile/playbook`, `update_*`, and the full family of single / by-ids / bulk delete helpers for interactions, profiles, playbooks, requests, and sessions; plus `run_playbook_aggregation()` and `clear_user_data()`. |
12
+ | `account_api.py` | Identity/config helpers behind `/api/whoami`, `/api/my_config`. |
13
+ | `health_api.py` | `GET /`, `/health`, `/healthz`, `/healthz/eval`; `install()` adds response-time + liveness tracking. |
14
+ | `pending_tool_call_api.py` | Router for resumable-extraction human clarification: list/get, `resolve`, `answer`, `not_applicable`, `cancel`; HMAC signature verify + migration-retry helpers. |
15
+ | `stall_state_api.py` | `GET /api/stall_state`, `POST /api/stall_state/notified` — extraction-agent waiting state. |
16
+ | `precondition_checks.py` | `precondition_checks()` — shared request validation. |
17
+
18
+ ## Architecture Pattern
19
+
20
+ ```
21
+ api.py (core_router + sub-routers)
22
+ -> Depends(get_request_context) -> RequestContext(org_id, storage, configurator, prompt_manager)
23
+ -> get_reflexio(org_id) -> Reflexio (reflexio_lib) -> services/
24
+ ```
25
+
26
+ - **`RequestContext` is the context-passing contract** — handlers receive it via `Depends` and never reach for storage/config/prompts globally.
27
+ - **Route handlers call `Reflexio` through `get_reflexio(org_id)`** — these helper files do **not** instantiate `Reflexio()` directly.
28
+ - **Auth is injected, not implemented here** — the OS app uses `default_get_org_id` / `DEFAULT_ORG_ID`; the enterprise extension swaps in authenticated org resolution (see `reflexio_ext/server/api_endpoints/`).
29
+
30
+ ## Requirements / Problems to Avoid
31
+
32
+ - **NEVER instantiate `Reflexio()` in a handler** — use `get_reflexio(org_id)` from `server/cache/`.
33
+ - **Keep business logic in `services/`** — endpoints validate, build context, and delegate; they don't embed extraction/evaluation logic.
34
+ - **Pending-tool-call writes can race** — use the migration-retry + HMAC-verify helpers already in `pending_tool_call_api.py` rather than writing raw.
@@ -155,7 +155,9 @@ def delete_user_profile(
155
155
  try:
156
156
  return reflexio.delete_profile(request)
157
157
  except Exception as e:
158
- with sentry_tags(subsystem="publisher_api", action="delete_user_profile", org_id=org_id):
158
+ with sentry_tags(
159
+ subsystem="publisher_api", action="delete_user_profile", org_id=org_id
160
+ ):
159
161
  logger.exception("Failed to delete user profile")
160
162
  return DeleteUserProfileResponse(success=False, message=str(e))
161
163
 
@@ -176,7 +178,9 @@ def delete_user_interaction(
176
178
  try:
177
179
  return reflexio.delete_interaction(request)
178
180
  except Exception as e:
179
- with sentry_tags(subsystem="publisher_api", action="delete_user_interaction", org_id=org_id):
181
+ with sentry_tags(
182
+ subsystem="publisher_api", action="delete_user_interaction", org_id=org_id
183
+ ):
180
184
  logger.exception("Failed to delete user interaction")
181
185
  return DeleteUserInteractionResponse(success=False, message=str(e))
182
186
 
@@ -195,7 +199,9 @@ def delete_request(org_id: str, request: DeleteRequestRequest) -> DeleteRequestR
195
199
  try:
196
200
  return reflexio.delete_request(request)
197
201
  except Exception as e:
198
- with sentry_tags(subsystem="publisher_api", action="delete_request", org_id=org_id):
202
+ with sentry_tags(
203
+ subsystem="publisher_api", action="delete_request", org_id=org_id
204
+ ):
199
205
  logger.exception("Failed to delete request")
200
206
  return DeleteRequestResponse(success=False, message=str(e))
201
207
 
@@ -214,7 +220,9 @@ def delete_session(org_id: str, request: DeleteSessionRequest) -> DeleteSessionR
214
220
  try:
215
221
  return reflexio.delete_session(request)
216
222
  except Exception as e:
217
- with sentry_tags(subsystem="publisher_api", action="delete_session", org_id=org_id):
223
+ with sentry_tags(
224
+ subsystem="publisher_api", action="delete_session", org_id=org_id
225
+ ):
218
226
  logger.exception("Failed to delete session")
219
227
  return DeleteSessionResponse(success=False, message=str(e))
220
228
 
@@ -235,7 +243,9 @@ def delete_agent_playbook(
235
243
  try:
236
244
  return reflexio.delete_agent_playbook(request)
237
245
  except Exception as e:
238
- with sentry_tags(subsystem="publisher_api", action="delete_agent_playbook", org_id=org_id):
246
+ with sentry_tags(
247
+ subsystem="publisher_api", action="delete_agent_playbook", org_id=org_id
248
+ ):
239
249
  logger.exception("Failed to delete agent playbook")
240
250
  return DeleteAgentPlaybookResponse(success=False, message=str(e))
241
251
 
@@ -256,7 +266,9 @@ def delete_user_playbook(
256
266
  try:
257
267
  return reflexio.delete_user_playbook(request)
258
268
  except Exception as e:
259
- with sentry_tags(subsystem="publisher_api", action="delete_user_playbook", org_id=org_id):
269
+ with sentry_tags(
270
+ subsystem="publisher_api", action="delete_user_playbook", org_id=org_id
271
+ ):
260
272
  logger.exception("Failed to delete user playbook")
261
273
  return DeleteUserPlaybookResponse(success=False, message=str(e))
262
274
 
@@ -434,7 +446,9 @@ def run_playbook_aggregation(
434
446
  request.agent_version, request.playbook_name
435
447
  )
436
448
  except Exception as e:
437
- with sentry_tags(subsystem="publisher_api", action="run_playbook_aggregation", org_id=org_id):
449
+ with sentry_tags(
450
+ subsystem="publisher_api", action="run_playbook_aggregation", org_id=org_id
451
+ ):
438
452
  logger.exception("Failed to run playbook aggregation")
439
453
  return RunPlaybookAggregationResponse(success=False, message=str(e))
440
454
 
@@ -472,7 +486,9 @@ def update_agent_playbook_status(
472
486
  try:
473
487
  return reflexio.update_agent_playbook_status(request)
474
488
  except Exception as e:
475
- with sentry_tags(subsystem="publisher_api", action="update_playbook_status", org_id=org_id):
489
+ with sentry_tags(
490
+ subsystem="publisher_api", action="update_playbook_status", org_id=org_id
491
+ ):
476
492
  logger.exception("Failed to update playbook status")
477
493
  return UpdatePlaybookStatusResponse(success=False, msg=str(e))
478
494
 
@@ -493,7 +509,9 @@ def update_agent_playbook(
493
509
  try:
494
510
  return reflexio.update_agent_playbook(request)
495
511
  except Exception as e:
496
- with sentry_tags(subsystem="publisher_api", action="update_agent_playbook", org_id=org_id):
512
+ with sentry_tags(
513
+ subsystem="publisher_api", action="update_agent_playbook", org_id=org_id
514
+ ):
497
515
  logger.exception("Failed to update agent playbook")
498
516
  return UpdateAgentPlaybookResponse(success=False, msg=str(e))
499
517
 
@@ -514,7 +532,9 @@ def update_user_playbook(
514
532
  try:
515
533
  return reflexio.update_user_playbook(request)
516
534
  except Exception as e:
517
- with sentry_tags(subsystem="publisher_api", action="update_user_playbook", org_id=org_id):
535
+ with sentry_tags(
536
+ subsystem="publisher_api", action="update_user_playbook", org_id=org_id
537
+ ):
518
538
  logger.exception("Failed to update user playbook")
519
539
  return UpdateUserPlaybookResponse(success=False, msg=str(e))
520
540
 
@@ -535,6 +555,8 @@ def update_user_profile(
535
555
  try:
536
556
  return reflexio.update_user_profile(request)
537
557
  except Exception as e:
538
- with sentry_tags(subsystem="publisher_api", action="update_user_profile", org_id=org_id):
558
+ with sentry_tags(
559
+ subsystem="publisher_api", action="update_user_profile", org_id=org_id
560
+ ):
539
561
  logger.exception("Failed to update user profile")
540
562
  return UpdateUserProfileResponse(success=False, msg=str(e))