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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +1 -1
- package/bin/claude-smart.js +2 -2
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +21 -1
- package/plugin/pyproject.toml +3 -3
- package/plugin/scripts/_lib.sh +72 -28
- package/plugin/scripts/backend-service.sh +55 -7
- package/plugin/scripts/cli.sh +2 -1
- package/plugin/scripts/dashboard-service.sh +7 -5
- package/plugin/scripts/ensure-plugin-root.sh +1 -0
- package/plugin/scripts/hook_entry.sh +7 -5
- package/plugin/scripts/smart-install.sh +2 -2
- package/plugin/src/README.md +57 -0
- package/plugin/uv.lock +126 -5
- package/plugin/vendor/reflexio/.env.example +9 -0
- package/plugin/vendor/reflexio/pyproject.toml +5 -2
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +0 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +7 -4
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +4 -4
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +2 -2
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +13 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +3 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +59 -6
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +7 -1
- package/plugin/vendor/reflexio/reflexio/server/api.py +188 -34
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +33 -11
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +278 -29
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +457 -181
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +22 -12
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +137 -9
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +36 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +27 -23
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +19 -9
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +1 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +44 -2
- package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +91 -24
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +18 -5
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +88 -3
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +36 -5
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +32 -22
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +89 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +46 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +1 -1
- 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
|
|
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:
|
|
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=
|
|
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:
|
|
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(
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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=
|
|
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
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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))
|