claude-smart 0.2.47 → 0.2.49
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 +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- 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 +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- 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/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -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 +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -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/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 +320 -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 +3 -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/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- 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/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- 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 +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- 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 +216 -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/_agent.py +22 -10
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
- 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 +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -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 -8
- 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/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -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/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""Evaluation route handlers (extracted from api.py, Tier3 A2)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
from fastapi import (
|
|
12
|
+
APIRouter,
|
|
13
|
+
Depends,
|
|
14
|
+
HTTPException,
|
|
15
|
+
Request,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from reflexio.models.api_schema.eval_overview_schema import (
|
|
19
|
+
GetEvaluationOverviewRequest,
|
|
20
|
+
GetEvaluationOverviewResponse,
|
|
21
|
+
GetRecentShadowComparisonsResponse,
|
|
22
|
+
GradeOnDemandRequest,
|
|
23
|
+
GradeOnDemandResponse,
|
|
24
|
+
RegenerateFailure,
|
|
25
|
+
RegenerateRequest,
|
|
26
|
+
RegenerateStartResponse,
|
|
27
|
+
RegenerateStatusResponse,
|
|
28
|
+
)
|
|
29
|
+
from reflexio.models.api_schema.retriever_schema import (
|
|
30
|
+
GetAgentSuccessEvaluationResultsRequest,
|
|
31
|
+
GetEvaluationResultsViewResponse,
|
|
32
|
+
)
|
|
33
|
+
from reflexio.models.api_schema.ui.converters import (
|
|
34
|
+
to_evaluation_result_view,
|
|
35
|
+
)
|
|
36
|
+
from reflexio.models.config_schema import (
|
|
37
|
+
SINGLETON_AGENT_SUCCESS_EVALUATION_NAME,
|
|
38
|
+
)
|
|
39
|
+
from reflexio.server.auth import (
|
|
40
|
+
default_get_org_id,
|
|
41
|
+
)
|
|
42
|
+
from reflexio.server.cache import reflexio_cache
|
|
43
|
+
from reflexio.server.rate_limit import limiter
|
|
44
|
+
from reflexio.server.services.agent_success_evaluation.regen_jobs import (
|
|
45
|
+
REGEN_JOBS,
|
|
46
|
+
run_regen,
|
|
47
|
+
)
|
|
48
|
+
from reflexio.server.services.agent_success_evaluation.runner import (
|
|
49
|
+
run_group_evaluation,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
router = APIRouter()
|
|
54
|
+
|
|
55
|
+
_GRADE_ON_DEMAND_CACHE_TTL_SECONDS = 24 * 60 * 60
|
|
56
|
+
|
|
57
|
+
_GRADE_ON_DEMAND_CACHE_KEY_PREFIX = "grade_on_demand"
|
|
58
|
+
|
|
59
|
+
_RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS = 30 * 24 * 60 * 60
|
|
60
|
+
|
|
61
|
+
_RECENT_SHADOW_COMPARISONS_MAX_LIMIT = 100
|
|
62
|
+
|
|
63
|
+
REGENERATE_MAX_WORKERS = 2
|
|
64
|
+
|
|
65
|
+
_regen_executor = ThreadPoolExecutor(
|
|
66
|
+
max_workers=REGENERATE_MAX_WORKERS,
|
|
67
|
+
thread_name_prefix="reflexio-regen",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _grade_on_demand_cache_key(
|
|
72
|
+
org_id: str, session_id: str, agent_version: str, evaluation_name: str
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Build the operation_state key for the on-demand grading cache.
|
|
75
|
+
|
|
76
|
+
The key embeds every active singleton dimension that could change the
|
|
77
|
+
verdict: org_id (multi-tenant scope), session_id (the unit of work),
|
|
78
|
+
agent_version (eval results are versioned), and evaluation_name (kept as a
|
|
79
|
+
compatibility/readback discriminator for historical multi-evaluator rows).
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
org_id (str): Tenant identifier from the auth context.
|
|
83
|
+
session_id (str): Target session.
|
|
84
|
+
agent_version (str): Agent version filter.
|
|
85
|
+
evaluation_name (str): Evaluator/result namespace to isolate cache rows.
|
|
86
|
+
Returns:
|
|
87
|
+
str: A namespaced key suitable for ``storage.upsert_operation_state``.
|
|
88
|
+
"""
|
|
89
|
+
# Length-prefix each free-form component (``f"{len(s)}:{s}"``) so the join is
|
|
90
|
+
# injective: distinct component tuples can never collapse to the same key even
|
|
91
|
+
# when a component itself contains the ``::`` delimiter. Keeps the prefix intact
|
|
92
|
+
# for prefix-based filtering and the key human-readable for inspection.
|
|
93
|
+
parts = "::".join(
|
|
94
|
+
f"{len(s)}:{s}" for s in (org_id, session_id, agent_version, evaluation_name)
|
|
95
|
+
)
|
|
96
|
+
return f"{_GRADE_ON_DEMAND_CACHE_KEY_PREFIX}::{parts}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_grade_on_demand_cache(
|
|
100
|
+
storage: Any, cache_key: str, *, now: int
|
|
101
|
+
) -> int | None:
|
|
102
|
+
"""Return the cached ``result_id`` if a valid entry exists, else None.
|
|
103
|
+
|
|
104
|
+
Returns None on three conditions: no entry, malformed entry, or entry
|
|
105
|
+
whose ``last_graded_at`` is older than the 24h TTL. Keeps the handler
|
|
106
|
+
body focused on the happy path.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
storage: The request's storage backend.
|
|
110
|
+
cache_key (str): Key produced by ``_grade_on_demand_cache_key``.
|
|
111
|
+
now (int): Current Unix-seconds wall-clock timestamp.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
int | None: Cached result_id when fresh, else None.
|
|
115
|
+
"""
|
|
116
|
+
cached_state = storage.get_operation_state(cache_key)
|
|
117
|
+
if not cached_state:
|
|
118
|
+
return None
|
|
119
|
+
state = cached_state.get("operation_state")
|
|
120
|
+
if not isinstance(state, dict):
|
|
121
|
+
return None
|
|
122
|
+
last_graded_at = state.get("last_graded_at")
|
|
123
|
+
if not isinstance(last_graded_at, int):
|
|
124
|
+
return None
|
|
125
|
+
if (now - last_graded_at) >= _GRADE_ON_DEMAND_CACHE_TTL_SECONDS:
|
|
126
|
+
return None
|
|
127
|
+
cached_result_id = state.get("result_id")
|
|
128
|
+
return cached_result_id if isinstance(cached_result_id, int) else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
|
|
132
|
+
"""Look up the user_id that owns a session_id without requiring it as input.
|
|
133
|
+
|
|
134
|
+
Uses the first-request bulk helper even for this single-session path so the
|
|
135
|
+
lookup can use the same indexed query shape as evaluation overview.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
storage: The request's storage backend.
|
|
139
|
+
session_id (str): The target session whose owner to resolve.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
str | None: The user_id of the earliest request in the session,
|
|
143
|
+
or None when no requests exist.
|
|
144
|
+
"""
|
|
145
|
+
first_requests = storage.get_first_requests_by_session_ids([session_id])
|
|
146
|
+
first = first_requests.get(session_id)
|
|
147
|
+
if first is None:
|
|
148
|
+
return None
|
|
149
|
+
return first.user_id
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _find_fresh_result_id(
|
|
153
|
+
storage: Any,
|
|
154
|
+
*,
|
|
155
|
+
user_id: str,
|
|
156
|
+
session_id: str,
|
|
157
|
+
agent_version: str,
|
|
158
|
+
evaluation_name: str,
|
|
159
|
+
previous_result_ids: set[int],
|
|
160
|
+
) -> int | None:
|
|
161
|
+
"""Locate the result_id written by the most-recent grade for this session.
|
|
162
|
+
|
|
163
|
+
The runner writes rows but doesn't return the id. Use the targeted result-id
|
|
164
|
+
lookup so this path does not scan broad evaluation windows.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
storage: The request's storage backend.
|
|
168
|
+
user_id (str): The user whose session slice was graded.
|
|
169
|
+
session_id (str): The graded session.
|
|
170
|
+
agent_version (str): The version dimension.
|
|
171
|
+
evaluation_name (str): Evaluator/result namespace to isolate readback.
|
|
172
|
+
previous_result_ids (set[int]): Matching rows observed before grading.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
int | None: result_id of the latest matching row, or None if the
|
|
176
|
+
runner wrote nothing.
|
|
177
|
+
"""
|
|
178
|
+
result_ids = storage.get_agent_success_evaluation_result_ids(
|
|
179
|
+
user_id=user_id,
|
|
180
|
+
session_id=session_id,
|
|
181
|
+
evaluation_name=evaluation_name,
|
|
182
|
+
agent_version=agent_version,
|
|
183
|
+
)
|
|
184
|
+
fresh_result_ids = [rid for rid in result_ids if rid not in previous_result_ids]
|
|
185
|
+
if not fresh_result_ids:
|
|
186
|
+
return None
|
|
187
|
+
return max(fresh_result_ids)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.post(
|
|
191
|
+
"/api/get_agent_success_evaluation_results",
|
|
192
|
+
response_model=GetEvaluationResultsViewResponse,
|
|
193
|
+
response_model_exclude_none=True,
|
|
194
|
+
)
|
|
195
|
+
def get_agent_success_evaluation_results(
|
|
196
|
+
request: GetAgentSuccessEvaluationResultsRequest,
|
|
197
|
+
org_id: str = Depends(default_get_org_id),
|
|
198
|
+
) -> GetEvaluationResultsViewResponse:
|
|
199
|
+
"""Get agent success evaluation results.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
request (GetAgentSuccessEvaluationResultsRequest): The get request
|
|
203
|
+
org_id (str): Organization ID
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
GetEvaluationResultsViewResponse: Response containing agent success evaluation results
|
|
207
|
+
"""
|
|
208
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
209
|
+
response = reflexio.get_agent_success_evaluation_results(request)
|
|
210
|
+
return GetEvaluationResultsViewResponse(
|
|
211
|
+
success=response.success,
|
|
212
|
+
agent_success_evaluation_results=[
|
|
213
|
+
to_evaluation_result_view(r)
|
|
214
|
+
for r in response.agent_success_evaluation_results
|
|
215
|
+
],
|
|
216
|
+
msg=response.msg,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@router.post(
|
|
221
|
+
"/api/get_evaluation_overview",
|
|
222
|
+
response_model=GetEvaluationOverviewResponse,
|
|
223
|
+
response_model_exclude_none=True,
|
|
224
|
+
)
|
|
225
|
+
def get_evaluation_overview(
|
|
226
|
+
request: GetEvaluationOverviewRequest,
|
|
227
|
+
org_id: str = Depends(default_get_org_id),
|
|
228
|
+
) -> GetEvaluationOverviewResponse:
|
|
229
|
+
"""Return the redesigned /evaluations page payload.
|
|
230
|
+
|
|
231
|
+
Aggregates hero state, four context tiles with deltas, top rule
|
|
232
|
+
attribution, and a corrections-per-session distribution into a single
|
|
233
|
+
response shaped exactly as the frontend renders it.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
request (GetEvaluationOverviewRequest): Window + bucket granularity.
|
|
237
|
+
org_id (str): Organization ID resolved by the auth dependency.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
GetEvaluationOverviewResponse: Full overview payload.
|
|
241
|
+
"""
|
|
242
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
243
|
+
return reflexio.get_evaluation_overview(request)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@router.post(
|
|
247
|
+
"/api/evaluations/regenerate",
|
|
248
|
+
response_model=RegenerateStartResponse,
|
|
249
|
+
response_model_exclude_none=True,
|
|
250
|
+
)
|
|
251
|
+
@limiter.limit("5/minute")
|
|
252
|
+
def start_regenerate(
|
|
253
|
+
request: Request,
|
|
254
|
+
payload: RegenerateRequest,
|
|
255
|
+
org_id: str = Depends(default_get_org_id),
|
|
256
|
+
) -> RegenerateStartResponse:
|
|
257
|
+
"""Start a singleton regenerate job over a time window.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
payload (RegenerateRequest): Window bounds plus optional legacy evaluator name.
|
|
261
|
+
org_id (str): Organization ID resolved by the auth dependency.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
RegenerateStartResponse: ``job_id`` to poll/cancel and ``total``
|
|
265
|
+
tuples queued.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
HTTPException: 409 when a regenerate for the same org is already
|
|
269
|
+
running. 503 when storage is not configured.
|
|
270
|
+
"""
|
|
271
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
272
|
+
storage = reflexio.request_context.storage
|
|
273
|
+
if storage is None:
|
|
274
|
+
raise HTTPException(status_code=503, detail="Storage not configured")
|
|
275
|
+
descriptors = storage.get_session_ids_in_window(
|
|
276
|
+
from_ts=payload.from_ts, to_ts=payload.to_ts
|
|
277
|
+
)
|
|
278
|
+
try:
|
|
279
|
+
job = REGEN_JOBS.create(
|
|
280
|
+
org_id=org_id,
|
|
281
|
+
from_ts=payload.from_ts,
|
|
282
|
+
to_ts=payload.to_ts,
|
|
283
|
+
total=len(descriptors),
|
|
284
|
+
)
|
|
285
|
+
except RuntimeError as e:
|
|
286
|
+
raise HTTPException(status_code=409, detail=str(e)) from e
|
|
287
|
+
_regen_executor.submit(
|
|
288
|
+
run_regen,
|
|
289
|
+
job=job,
|
|
290
|
+
request_context=reflexio.request_context,
|
|
291
|
+
llm_client=reflexio.llm_client,
|
|
292
|
+
)
|
|
293
|
+
return RegenerateStartResponse(job_id=job.job_id, total=job.total)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@router.get(
|
|
297
|
+
"/api/evaluations/regenerate/{job_id}",
|
|
298
|
+
response_model=RegenerateStatusResponse,
|
|
299
|
+
response_model_exclude_none=True,
|
|
300
|
+
)
|
|
301
|
+
def get_regenerate_status(
|
|
302
|
+
job_id: str,
|
|
303
|
+
org_id: str = Depends(default_get_org_id),
|
|
304
|
+
) -> RegenerateStatusResponse:
|
|
305
|
+
"""Poll the status of a regenerate job.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
job_id (str): Opaque handle returned by POST /api/evaluations/regenerate.
|
|
309
|
+
org_id (str): Organization ID resolved by the auth dependency.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
RegenerateStatusResponse: Counters, status, and failure list.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
HTTPException: 404 when ``job_id`` is unknown or belongs to a
|
|
316
|
+
different org.
|
|
317
|
+
"""
|
|
318
|
+
job = REGEN_JOBS.get(job_id)
|
|
319
|
+
if job is None or job.org_id != org_id:
|
|
320
|
+
raise HTTPException(status_code=404, detail="Unknown job_id")
|
|
321
|
+
return RegenerateStatusResponse(
|
|
322
|
+
job_id=job.job_id,
|
|
323
|
+
status=job.status,
|
|
324
|
+
total=job.total,
|
|
325
|
+
completed=job.completed,
|
|
326
|
+
failed=job.failed,
|
|
327
|
+
failures=[
|
|
328
|
+
RegenerateFailure(session_id=f.session_id, reason=f.reason)
|
|
329
|
+
for f in job.failures
|
|
330
|
+
],
|
|
331
|
+
started_at=job.started_at,
|
|
332
|
+
finished_at=job.finished_at,
|
|
333
|
+
# F3 informational counters — surface sampler + concurrency facts
|
|
334
|
+
# so the dashboard can render "n_sampled / total_candidates" and
|
|
335
|
+
# the configured worker cap without a second round-trip.
|
|
336
|
+
total_candidates=job.total_candidates,
|
|
337
|
+
sampled_count=job.sampled_count,
|
|
338
|
+
concurrency_limit=job.concurrency_limit,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@router.delete("/api/evaluations/regenerate/{job_id}")
|
|
343
|
+
def cancel_regenerate(
|
|
344
|
+
job_id: str,
|
|
345
|
+
org_id: str = Depends(default_get_org_id),
|
|
346
|
+
) -> dict[str, str]:
|
|
347
|
+
"""Request cancellation of a running regenerate job.
|
|
348
|
+
|
|
349
|
+
Sets the worker's cancel event; the worker checks the flag between
|
|
350
|
+
sessions and transitions to ``"cancelled"`` on its next iteration.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
job_id (str): Opaque handle returned by POST /api/evaluations/regenerate.
|
|
354
|
+
org_id (str): Organization ID resolved by the auth dependency.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
dict[str, str]: ``{"status": "cancelled"}`` on successful flag set.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
HTTPException: 404 when ``job_id`` is unknown or belongs to a
|
|
361
|
+
different org.
|
|
362
|
+
"""
|
|
363
|
+
job = REGEN_JOBS.get(job_id)
|
|
364
|
+
if job is None or job.org_id != org_id:
|
|
365
|
+
raise HTTPException(status_code=404, detail="Unknown job_id")
|
|
366
|
+
REGEN_JOBS.cancel(job_id)
|
|
367
|
+
return {"status": "cancelled"}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@router.post(
|
|
371
|
+
"/api/evaluations/grade_on_demand",
|
|
372
|
+
response_model=GradeOnDemandResponse,
|
|
373
|
+
response_model_exclude_none=False,
|
|
374
|
+
)
|
|
375
|
+
def grade_on_demand(
|
|
376
|
+
payload: GradeOnDemandRequest,
|
|
377
|
+
org_id: str = Depends(default_get_org_id),
|
|
378
|
+
) -> GradeOnDemandResponse:
|
|
379
|
+
"""Grade a single session synchronously; serve cached results within 24h.
|
|
380
|
+
|
|
381
|
+
Flow:
|
|
382
|
+
1. Read the operation_state cache; if a fresh entry exists, return it
|
|
383
|
+
with ``cached=True``.
|
|
384
|
+
2. Resolve the session's user_id from storage (skip with ``NO_REQUESTS``
|
|
385
|
+
when the session is unknown — surfaced as 200 + ``skipped_reason``
|
|
386
|
+
so the frontend's bounded-list click-through can handle stale ids
|
|
387
|
+
locally without polluting 5xx telemetry).
|
|
388
|
+
3. Invoke ``run_group_evaluation(force_regenerate=True)`` so the
|
|
389
|
+
"already evaluated" short-circuit doesn't suppress a customer's
|
|
390
|
+
explicit click.
|
|
391
|
+
4. Find the freshly-written result_id and persist it in the cache
|
|
392
|
+
with ``last_graded_at`` so future calls within 24h short-circuit.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
payload (GradeOnDemandRequest): Session + version plus optional legacy evaluator name.
|
|
396
|
+
org_id (str): Tenant identifier resolved by the auth dependency.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
GradeOnDemandResponse: Echoes ``session_id`` and carries either
|
|
400
|
+
a fresh ``result_id`` (``cached=False``), a cached one
|
|
401
|
+
(``cached=True``), or a ``skipped_reason`` (NO_REQUESTS).
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
HTTPException: 503 when storage is not configured.
|
|
405
|
+
"""
|
|
406
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
407
|
+
storage = reflexio.request_context.storage
|
|
408
|
+
if storage is None:
|
|
409
|
+
raise HTTPException(status_code=503, detail="Storage not configured")
|
|
410
|
+
|
|
411
|
+
evaluation_name = payload.evaluation_name or SINGLETON_AGENT_SUCCESS_EVALUATION_NAME
|
|
412
|
+
cache_key = _grade_on_demand_cache_key(
|
|
413
|
+
org_id,
|
|
414
|
+
payload.session_id,
|
|
415
|
+
payload.agent_version,
|
|
416
|
+
evaluation_name,
|
|
417
|
+
)
|
|
418
|
+
now = int(datetime.now(UTC).timestamp())
|
|
419
|
+
|
|
420
|
+
cached_result_id = _read_grade_on_demand_cache(storage, cache_key, now=now)
|
|
421
|
+
if cached_result_id is not None:
|
|
422
|
+
return GradeOnDemandResponse(
|
|
423
|
+
session_id=payload.session_id,
|
|
424
|
+
result_id=cached_result_id,
|
|
425
|
+
cached=True,
|
|
426
|
+
skipped_reason=None,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
user_id = _resolve_session_user_id(storage, payload.session_id)
|
|
430
|
+
if user_id is None:
|
|
431
|
+
return GradeOnDemandResponse(
|
|
432
|
+
session_id=payload.session_id,
|
|
433
|
+
result_id=None,
|
|
434
|
+
cached=False,
|
|
435
|
+
skipped_reason="NO_REQUESTS",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
previous_result_ids = set(
|
|
439
|
+
storage.get_agent_success_evaluation_result_ids(
|
|
440
|
+
user_id=user_id,
|
|
441
|
+
session_id=payload.session_id,
|
|
442
|
+
evaluation_name=evaluation_name,
|
|
443
|
+
agent_version=payload.agent_version,
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Two operation_state rows are intentionally written for this session:
|
|
448
|
+
# 1) `grade_on_demand::{len:org}::{len:session}::{len:version}::{len:eval}`
|
|
449
|
+
# (each component length-prefixed by `_grade_on_demand_cache_key` so the
|
|
450
|
+
# key is injective) — our 24h cache, set below after result_id resolves.
|
|
451
|
+
# 2) `agent_success_group_eval::{org_id}::{user_id}::{session_id}`
|
|
452
|
+
# — the runner's own "evaluated" marker, written by
|
|
453
|
+
# run_group_evaluation. Future background runs without
|
|
454
|
+
# force_regenerate will skip this session as a result.
|
|
455
|
+
# The cache key namespaces are distinct so the two markers do not
|
|
456
|
+
# interfere; the explicit force_regenerate=True here is what makes
|
|
457
|
+
# an on-demand grade always do real work on a cache miss.
|
|
458
|
+
run_group_evaluation(
|
|
459
|
+
org_id=org_id,
|
|
460
|
+
user_id=user_id,
|
|
461
|
+
session_id=payload.session_id,
|
|
462
|
+
agent_version=payload.agent_version,
|
|
463
|
+
source=None,
|
|
464
|
+
request_context=reflexio.request_context,
|
|
465
|
+
llm_client=reflexio.llm_client,
|
|
466
|
+
force_regenerate=True,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
result_id = _find_fresh_result_id(
|
|
470
|
+
storage,
|
|
471
|
+
user_id=user_id,
|
|
472
|
+
session_id=payload.session_id,
|
|
473
|
+
agent_version=payload.agent_version,
|
|
474
|
+
evaluation_name=evaluation_name,
|
|
475
|
+
previous_result_ids=previous_result_ids,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
storage.upsert_operation_state(
|
|
479
|
+
cache_key,
|
|
480
|
+
{"last_graded_at": now, "result_id": result_id},
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
return GradeOnDemandResponse(
|
|
484
|
+
session_id=payload.session_id,
|
|
485
|
+
result_id=result_id,
|
|
486
|
+
cached=False,
|
|
487
|
+
skipped_reason=None,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@router.get(
|
|
492
|
+
"/api/evaluations/shadow_comparisons/recent",
|
|
493
|
+
response_model=GetRecentShadowComparisonsResponse,
|
|
494
|
+
)
|
|
495
|
+
def get_recent_shadow_comparisons(
|
|
496
|
+
limit: int = 10,
|
|
497
|
+
org_id: str = Depends(default_get_org_id),
|
|
498
|
+
) -> GetRecentShadowComparisonsResponse:
|
|
499
|
+
"""Return the N most recent shadow comparison verdicts for the pinned rubric.
|
|
500
|
+
|
|
501
|
+
Filters to the org's currently pinned
|
|
502
|
+
``Config.shadow_comparison_judge_prompt_version`` so verdicts produced
|
|
503
|
+
under an older rubric do not mix into the drawer or the Top 10
|
|
504
|
+
disagreements widget. Storage returns verdicts newest-first and caps the
|
|
505
|
+
read at ``limit``.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
limit (int): Max verdicts to return. Clamped to ``[1, 100]``.
|
|
509
|
+
Default 10 — matches the size of the drawer and Top 10 widget.
|
|
510
|
+
org_id (str): Tenant identifier resolved by the auth dependency.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
GetRecentShadowComparisonsResponse: Verdicts in newest-first order.
|
|
514
|
+
Empty list when the backend does not support the
|
|
515
|
+
``shadow_comparison_verdicts`` storage feature, when no verdicts
|
|
516
|
+
exist in the 30-day window, or when no verdicts match the pinned
|
|
517
|
+
prompt version.
|
|
518
|
+
|
|
519
|
+
Raises:
|
|
520
|
+
HTTPException: 503 when storage is not configured.
|
|
521
|
+
"""
|
|
522
|
+
clamped_limit = max(1, min(int(limit), _RECENT_SHADOW_COMPARISONS_MAX_LIMIT))
|
|
523
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
524
|
+
storage = reflexio.request_context.storage
|
|
525
|
+
if storage is None:
|
|
526
|
+
raise HTTPException(status_code=503, detail="Storage not configured")
|
|
527
|
+
|
|
528
|
+
config = reflexio.request_context.configurator.get_config()
|
|
529
|
+
pinned_version = (
|
|
530
|
+
config.shadow_comparison_judge_prompt_version
|
|
531
|
+
if config is not None
|
|
532
|
+
else "v1.0.0"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
now = int(datetime.now(UTC).timestamp())
|
|
536
|
+
try:
|
|
537
|
+
verdicts = storage.get_recent_shadow_comparison_verdicts(
|
|
538
|
+
from_ts=now - _RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS,
|
|
539
|
+
to_ts=now,
|
|
540
|
+
judge_prompt_version=pinned_version,
|
|
541
|
+
limit=clamped_limit,
|
|
542
|
+
)
|
|
543
|
+
except NotImplementedError:
|
|
544
|
+
# Backends that don't support shadow verdicts (e.g., Disk) should
|
|
545
|
+
# quietly return empty rather than 5xx — the surface degrades to
|
|
546
|
+
# "no data yet" in the UI.
|
|
547
|
+
return GetRecentShadowComparisonsResponse(verdicts=[])
|
|
548
|
+
|
|
549
|
+
return GetRecentShadowComparisonsResponse(verdicts=verdicts)
|