claude-smart 0.2.46 → 0.2.48

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 (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,98 @@
1
+ """Billing money-path metering helpers shared by search + playbook routes (Tier3 A2).
2
+
3
+ These emit the ③ Application / search-request billing signals. They resolve
4
+ ``get_reflexio`` through the ``reflexio_cache`` module (not a bound import) so a
5
+ single ``patch("reflexio.server.cache.reflexio_cache.get_reflexio")`` at the
6
+ source intercepts the lookup regardless of which route module calls them.
7
+ """
8
+
9
+ import logging
10
+ import time
11
+
12
+ from fastapi import Request
13
+
14
+ from reflexio.server.cache import reflexio_cache
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _stamp_search_dependencies_done(request: Request) -> None:
20
+ """Stamp when dependency resolution reaches its final search dependency."""
21
+ request.state.search_deps_done_monotonic = time.monotonic()
22
+
23
+
24
+ def _meter_applied_learnings(
25
+ *,
26
+ org_id: str,
27
+ caller_type: str,
28
+ surfaced_count: int,
29
+ request_id: str | None = None,
30
+ session_id: str | None = None,
31
+ ) -> None:
32
+ """Emit the ③ Application event via the OSS emission helper.
33
+
34
+ No-op unless a production-agent caller surfaced >= 1 result. The cheap
35
+ caller-type and count guard runs first so the get_reflexio / config lookup
36
+ is skipped on the free paths (dashboard / empty result).
37
+
38
+ Args:
39
+ org_id: Organization ID for the requesting caller.
40
+ caller_type: Resolved caller classification (e.g. ``"production_agent"``).
41
+ surfaced_count: Total number of learnings returned to the caller.
42
+ request_id: Optional request correlation ID from the payload.
43
+ session_id: Optional session ID from the payload.
44
+ """
45
+ if caller_type != "production_agent" or surfaced_count <= 0:
46
+ return
47
+ try:
48
+ from reflexio.server.billing_meter import record_applied_learnings
49
+ from reflexio.server.billing_signals import platform_llm_from_config
50
+
51
+ config = reflexio_cache.get_reflexio(
52
+ org_id=org_id
53
+ ).request_context.configurator.get_config()
54
+ record_applied_learnings(
55
+ org_id=org_id,
56
+ surfaced_count=surfaced_count,
57
+ caller_type=caller_type,
58
+ platform_llm=platform_llm_from_config(config),
59
+ platform_storage=None, # resolved enterprise-side at rollup (Phase 1)
60
+ request_id=request_id,
61
+ session_id=session_id,
62
+ )
63
+ except Exception:
64
+ logger.warning(
65
+ "applied-learnings metering failed for org %s", org_id, exc_info=True
66
+ )
67
+
68
+
69
+ def _meter_search_request(
70
+ *,
71
+ org_id: str,
72
+ caller_type: str,
73
+ request_id: str | None = None,
74
+ session_id: str | None = None,
75
+ ) -> None:
76
+ """Emit one production-agent search request metric.
77
+
78
+ Args:
79
+ org_id: Organization ID for the requesting caller.
80
+ caller_type: Resolved caller classification.
81
+ request_id: Optional request correlation ID from the payload.
82
+ session_id: Optional session ID from the payload.
83
+ """
84
+ if caller_type != "production_agent":
85
+ return
86
+ try:
87
+ from reflexio.server.billing_meter import record_search_request
88
+
89
+ record_search_request(
90
+ org_id=org_id,
91
+ caller_type=caller_type,
92
+ request_id=request_id,
93
+ session_id=session_id,
94
+ )
95
+ except Exception:
96
+ logger.warning(
97
+ "search-request metering failed for org %s", org_id, exc_info=True
98
+ )
@@ -0,0 +1,129 @@
1
+ """Braintrust connector route handlers (extracted from api.py, Tier3 A2)."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ pass
8
+
9
+ from fastapi import (
10
+ APIRouter,
11
+ Depends,
12
+ Request,
13
+ )
14
+
15
+ from reflexio.models.api_schema.braintrust_schema import (
16
+ BraintrustStatusResponse,
17
+ ConnectBraintrustRequest,
18
+ ConnectBraintrustResponse,
19
+ SelectProjectsRequest,
20
+ SelectProjectsResponse,
21
+ SyncBraintrustResponse,
22
+ )
23
+ from reflexio.server.auth import (
24
+ default_get_org_id,
25
+ )
26
+ from reflexio.server.cache import reflexio_cache
27
+ from reflexio.server.rate_limit import limiter
28
+
29
+ logger = logging.getLogger(__name__)
30
+ router = APIRouter()
31
+
32
+
33
+ @router.post(
34
+ "/api/braintrust/connect",
35
+ response_model=ConnectBraintrustResponse,
36
+ response_model_exclude_none=True,
37
+ )
38
+ @limiter.limit("10/minute")
39
+ def braintrust_connect(
40
+ request: Request,
41
+ payload: ConnectBraintrustRequest,
42
+ org_id: str = Depends(default_get_org_id),
43
+ ) -> ConnectBraintrustResponse:
44
+ """Step 1: validate the Braintrust API key and list workspaces/projects.
45
+
46
+ Persists nothing — call `/api/braintrust/select_projects` to commit.
47
+
48
+ Args:
49
+ request (Request): The HTTP request object for rate limiting.
50
+ payload (ConnectBraintrustRequest): Customer's Braintrust API key.
51
+ org_id (str): Resolved by auth dependency.
52
+
53
+ Returns:
54
+ ConnectBraintrustResponse: Workspaces tree on success; `success=False`
55
+ with a message when the key is rejected.
56
+ """
57
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
58
+ return reflexio.braintrust_connect(payload)
59
+
60
+
61
+ @router.post(
62
+ "/api/braintrust/select_projects",
63
+ response_model=SelectProjectsResponse,
64
+ response_model_exclude_none=True,
65
+ )
66
+ @limiter.limit("10/minute")
67
+ def braintrust_select_projects(
68
+ request: Request,
69
+ payload: SelectProjectsRequest,
70
+ org_id: str = Depends(default_get_org_id),
71
+ ) -> SelectProjectsResponse:
72
+ """Step 2: commit the Braintrust connection with selected projects.
73
+
74
+ The API key is encrypted at rest. Subsequent syncs use the persisted
75
+ connection until the customer calls DELETE /api/braintrust/connection.
76
+ """
77
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
78
+ return reflexio.braintrust_select_projects(payload)
79
+
80
+
81
+ @router.get(
82
+ "/api/braintrust/status",
83
+ response_model=BraintrustStatusResponse,
84
+ response_model_exclude_none=True,
85
+ )
86
+ def braintrust_status(
87
+ org_id: str = Depends(default_get_org_id),
88
+ ) -> BraintrustStatusResponse:
89
+ """Return Braintrust connection state. Never echoes the API key."""
90
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
91
+ return reflexio.braintrust_status()
92
+
93
+
94
+ @router.delete("/api/braintrust/connection")
95
+ @limiter.limit("10/minute")
96
+ def braintrust_disconnect(
97
+ request: Request,
98
+ org_id: str = Depends(default_get_org_id),
99
+ ) -> dict:
100
+ """Delete the persisted Braintrust connection for the org.
101
+
102
+ Args:
103
+ org_id (str): Resolved by auth dependency.
104
+
105
+ Returns:
106
+ dict: ``{"success": True}`` on completion.
107
+ """
108
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
109
+ reflexio.braintrust_disconnect()
110
+ return {"success": True}
111
+
112
+
113
+ @router.post(
114
+ "/api/braintrust/sync",
115
+ response_model=SyncBraintrustResponse,
116
+ response_model_exclude_none=True,
117
+ )
118
+ @limiter.limit("10/minute")
119
+ def braintrust_sync(
120
+ request: Request,
121
+ org_id: str = Depends(default_get_org_id),
122
+ ) -> SyncBraintrustResponse:
123
+ """Trigger a one-shot sync of Braintrust scorer outputs.
124
+
125
+ Scheduled (cron) sync is a follow-up; for now the endpoint exists so
126
+ operators can drive a manual import.
127
+ """
128
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
129
+ return reflexio.braintrust_sync()
@@ -0,0 +1,210 @@
1
+ """Config route handlers (extracted from api.py, Tier3 A2)."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ pass
8
+
9
+ from fastapi import (
10
+ APIRouter,
11
+ Depends,
12
+ HTTPException,
13
+ Request,
14
+ status,
15
+ )
16
+
17
+ from reflexio.models.api_schema.retriever_schema import (
18
+ SetConfigResponse,
19
+ )
20
+ from reflexio.models.api_schema.service_schemas import (
21
+ MyConfigResponse,
22
+ )
23
+ from reflexio.models.config_schema import (
24
+ Config,
25
+ )
26
+ from reflexio.server.api_endpoints import (
27
+ account_api,
28
+ )
29
+ from reflexio.server.auth import (
30
+ default_get_org_id,
31
+ )
32
+ from reflexio.server.cache import reflexio_cache
33
+ from reflexio.server.rate_limit import limiter
34
+
35
+ logger = logging.getLogger(__name__)
36
+ router = APIRouter()
37
+
38
+
39
+ @router.get(
40
+ "/api/my_config",
41
+ response_model=MyConfigResponse,
42
+ response_model_exclude_none=True,
43
+ )
44
+ def my_config_endpoint(
45
+ request: Request,
46
+ org_id: str = Depends(default_get_org_id),
47
+ ) -> MyConfigResponse:
48
+ """Return raw storage credentials for the caller's org.
49
+
50
+ Enablement is controlled by two independent opt-ins so the endpoint
51
+ is closed by default on unauthenticated self-host deployments:
52
+
53
+ - ``request.app.state.my_config_enabled`` — set to True by
54
+ :func:`create_app` when the host wires in a Bearer-auth
55
+ ``get_org_id`` dependency, so enterprise callers are always
56
+ authenticated before they reach this route.
57
+ - ``REFLEXIO_ALLOW_MY_CONFIG=true`` — OS self-host escape hatch.
58
+
59
+ If neither is set we return a closed response instead of a 404 so
60
+ the CLI can display an actionable hint.
61
+ """
62
+ app_state_enabled = bool(getattr(request.app.state, "my_config_enabled", False))
63
+ if not (app_state_enabled or account_api.my_config_allowed()):
64
+ return MyConfigResponse(
65
+ success=False,
66
+ message=(
67
+ "GET /api/my_config is disabled. Set "
68
+ "REFLEXIO_ALLOW_MY_CONFIG=true to enable."
69
+ ),
70
+ )
71
+ return account_api.my_config(org_id=org_id)
72
+
73
+
74
+ @router.post("/api/set_config")
75
+ @limiter.limit("10/minute")
76
+ def set_config(
77
+ request: Request,
78
+ config: dict[str, Any],
79
+ org_id: str = Depends(default_get_org_id),
80
+ ) -> SetConfigResponse:
81
+ """Set configuration for the organization.
82
+
83
+ Args:
84
+ config (dict[str, Any]): The configuration payload to set
85
+ org_id (str): Organization ID
86
+
87
+ Returns:
88
+ dict: Response containing success status and message
89
+ """
90
+ from pydantic import ValidationError
91
+
92
+ # Create Reflexio instance to access the configurator through request_context
93
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
94
+ configurator = reflexio.request_context.configurator
95
+ normalized_config = configurator.normalize_config_payload(config)
96
+ if not isinstance(normalized_config, dict):
97
+ normalized_config = config
98
+ try:
99
+ Config.model_validate(normalized_config)
100
+ except ValidationError as exc:
101
+ raise HTTPException(
102
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
103
+ detail=exc.errors(),
104
+ ) from exc
105
+
106
+ # Set the config using Reflexio's set_config method
107
+ response = reflexio.set_config(normalized_config)
108
+
109
+ # Invalidate cache on successful config change to ensure fresh instance next request
110
+ if response.success:
111
+ reflexio_cache.invalidate_reflexio_cache(org_id=org_id)
112
+
113
+ return response
114
+
115
+
116
+ @router.post("/api/update_config")
117
+ @limiter.limit("10/minute")
118
+ def update_config(
119
+ request: Request,
120
+ partial: dict[str, Any],
121
+ org_id: str = Depends(default_get_org_id),
122
+ ) -> SetConfigResponse:
123
+ """Apply a partial update to the org's config (PATCH semantics).
124
+
125
+ Performs a **top-level shallow merge** of *partial* over the existing
126
+ config and round-trips through ``Config(**merged)`` so Pydantic
127
+ validates the result and rejects bogus top-level fields.
128
+
129
+ .. warning::
130
+ Nested objects (e.g. ``storage_config``, ``profile_extractor_config``,
131
+ ``user_playbook_extractor_config``) are **replaced wholesale**.
132
+ Deep merging is intentionally not supported -- the discriminator on
133
+ ``storage_config`` would be lost on partial updates, and merging nested
134
+ dicts has ambiguous semantics.
135
+
136
+ To update a field inside an extractor config you must resend that
137
+ extractor object fully populated (including ``extractor_name``,
138
+ ``extraction_definition_prompt``, etc.). For one-off mutations prefer
139
+ ``GET /api/get_config`` followed by ``POST /api/set_config`` with the
140
+ modified full config.
141
+
142
+ Unlike :func:`set_config`, callers do not need to re-send the full
143
+ config (including required fields like ``storage_config``) just to
144
+ flip a single top-level boolean. The merge happens server-side
145
+ atomically within the request, eliminating the read-modify-write
146
+ race a client would otherwise hit.
147
+
148
+ Args:
149
+ partial: Top-level fields to overlay on the existing config.
150
+ org_id: Organization ID resolved by the auth layer.
151
+
152
+ Returns:
153
+ SetConfigResponse: Success status and message from
154
+ :meth:`Reflexio.set_config`.
155
+ """
156
+ from pydantic import ValidationError
157
+
158
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
159
+ existing_config = reflexio.request_context.configurator.get_config()
160
+ existing = existing_config.model_dump(mode="python")
161
+ merged = {**existing, **partial}
162
+ # Pydantic validates the merged shape and rejects unknown / malformed
163
+ # fields here, before storage validation in reflexio.set_config.
164
+ # Convert ValidationError into 422 so callers passing a partial that
165
+ # would replace a nested extractor object with an incomplete dict (e.g.
166
+ # {"user_playbook_extractor_config": {"aggregation_config": {...}}})
167
+ # get a clean client-error response instead of a 500.
168
+ try:
169
+ merged_config = Config(**merged)
170
+ except ValidationError as exc:
171
+ raise HTTPException(
172
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
173
+ detail={
174
+ "error": "Invalid partial config (top-level shallow merge)",
175
+ "hint": (
176
+ "Nested objects (e.g. user_playbook_extractor_config) "
177
+ "are replaced wholesale, not deep-merged. To mutate a "
178
+ "single nested field, fetch the full config via "
179
+ "/api/get_config, edit, and POST it back via "
180
+ "/api/set_config."
181
+ ),
182
+ "validation_errors": exc.errors(),
183
+ },
184
+ ) from exc
185
+ if merged_config.model_dump(mode="python") == existing:
186
+ logger.info("Skipping no-op config update for org %s", org_id)
187
+ return SetConfigResponse(success=True, msg="Configuration unchanged")
188
+
189
+ response = reflexio.set_config(merged_config)
190
+ if response.success:
191
+ reflexio_cache.invalidate_reflexio_cache(org_id=org_id)
192
+ return response
193
+
194
+
195
+ @router.get("/api/get_config")
196
+ def get_config(
197
+ org_id: str = Depends(default_get_org_id),
198
+ ) -> dict[str, Any]:
199
+ """Get configuration for the organization.
200
+
201
+ Args:
202
+ org_id (str): Organization ID
203
+
204
+ Returns:
205
+ Config: The current configuration
206
+ """
207
+ # Create Reflexio instance to access the configurator through request_context
208
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
209
+
210
+ return reflexio.request_context.configurator.get_config_for_response()