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,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)