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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +11 -6
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- 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()
|