claude-smart 0.2.48 → 0.2.50
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 +2 -2
- package/README.md +11 -40
- package/bin/claude-smart.js +393 -55
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- 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 +255 -7
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +766 -123
- package/plugin/scripts/codex-hook.js +79 -229
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +118 -137
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +116 -21
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +93 -29
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +56 -11
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- 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 +262 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- 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 +57 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- 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/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
22
22
|
|
|
23
23
|
from reflexio.models.api_schema.retriever_schema import (
|
|
24
24
|
ConversationTurn,
|
|
25
|
+
ReformulationResult,
|
|
25
26
|
SearchAgentPlaybookRequest,
|
|
26
27
|
SearchUserPlaybookRequest,
|
|
27
28
|
SearchUserProfileRequest,
|
|
@@ -50,6 +51,15 @@ from reflexio.server.services.retrieval.recency import (
|
|
|
50
51
|
multiplicative_factor,
|
|
51
52
|
)
|
|
52
53
|
from reflexio.server.services.retrieval.relevance_floor import apply_relevance_floors
|
|
54
|
+
from reflexio.server.services.retrieval.session_dedup import (
|
|
55
|
+
EntityKey,
|
|
56
|
+
session_seen_cache,
|
|
57
|
+
)
|
|
58
|
+
from reflexio.server.services.retrieval.temporal import (
|
|
59
|
+
freshness_collapse,
|
|
60
|
+
sort_by_recency,
|
|
61
|
+
window_bounds,
|
|
62
|
+
)
|
|
53
63
|
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
54
64
|
from reflexio.server.tracing import profile_step, set_span_data
|
|
55
65
|
|
|
@@ -76,6 +86,20 @@ _SEARCH_FANOUT_EXECUTOR = ThreadPoolExecutor(
|
|
|
76
86
|
thread_name_prefix="reflexio-search",
|
|
77
87
|
)
|
|
78
88
|
_ENV_SINGLE_RPC = "REFLEXIO_UNIFIED_SEARCH_SINGLE_RPC"
|
|
89
|
+
# Working-pool floor for temporal reordering (freshness collapse / recency
|
|
90
|
+
# sort): matches the RecencyConfig default pool so the fresh version of a
|
|
91
|
+
# fact can be promoted even when it ranks below top_k on text relevance.
|
|
92
|
+
_TEMPORAL_POOL_SIZE = 20
|
|
93
|
+
# Session dedup widens the fetch pool so filtered-out (already served) items
|
|
94
|
+
# can be backfilled, but never by more than this many extra candidates.
|
|
95
|
+
_SESSION_DEDUP_FETCH_BUMP_CAP = 50
|
|
96
|
+
# Singular dedup kinds (session_dedup EntityKey vocabulary) — deliberately
|
|
97
|
+
# distinct from the plural request-level ``entity_types`` values.
|
|
98
|
+
_ENTITY_ID_ATTRS = {
|
|
99
|
+
"profile": "profile_id",
|
|
100
|
+
"user_playbook": "user_playbook_id",
|
|
101
|
+
"agent_playbook": "agent_playbook_id",
|
|
102
|
+
}
|
|
79
103
|
_EMBEDDING_CACHE_TTL_SECONDS = max(
|
|
80
104
|
0, int(os.getenv("REFLEXIO_QUERY_EMBEDDING_CACHE_TTL_SECONDS", "300") or "300")
|
|
81
105
|
)
|
|
@@ -139,14 +163,9 @@ def run_unified_search(
|
|
|
139
163
|
floor_cfg = retrieval_floor or RetrievalFloorConfig()
|
|
140
164
|
floor_on = floor_cfg.enabled
|
|
141
165
|
recency_on = bool(recency and recency.enabled)
|
|
142
|
-
fetch_k = max(
|
|
143
|
-
top_k,
|
|
144
|
-
floor_cfg.pool_size if floor_on else 0,
|
|
145
|
-
recency.pool_size if recency_on and recency is not None else 0,
|
|
146
|
-
)
|
|
147
166
|
|
|
148
|
-
# --- Phase A: query reformulation + embedding
|
|
149
|
-
|
|
167
|
+
# --- Phase A: query reformulation (+ temporal signals) + embedding ---
|
|
168
|
+
reformulation, embedding, embedding_failed = _run_phase_a(
|
|
150
169
|
query=request.query,
|
|
151
170
|
storage=storage,
|
|
152
171
|
llm_client=llm_client,
|
|
@@ -157,6 +176,50 @@ def run_unified_search(
|
|
|
157
176
|
pre_retrieval_model_name=pre_retrieval_model_name,
|
|
158
177
|
search_mode=request.search_mode,
|
|
159
178
|
)
|
|
179
|
+
reformulated_query = reformulation.standalone_query
|
|
180
|
+
|
|
181
|
+
# Degrade-to-FTS: when embedding GENERATION failed (distinct from the
|
|
182
|
+
# benign None cases), force the effective mode to FTS for the entire
|
|
183
|
+
# Phase B fan-out. Storage then takes the zero-placeholder branch and
|
|
184
|
+
# never re-embeds — closing the historical second-embed leak where a
|
|
185
|
+
# None embedding + a vector/hybrid mode caused the storage layer to retry
|
|
186
|
+
# the embed and raise unguarded. Benign None (FTS-only, unsupported)
|
|
187
|
+
# leaves the requested mode untouched.
|
|
188
|
+
effective_search_mode = request.search_mode
|
|
189
|
+
if embedding_failed:
|
|
190
|
+
effective_search_mode = SearchMode.FTS
|
|
191
|
+
logger.warning(
|
|
192
|
+
"event=search_degraded_to_fts reason=embedding_generation_failed "
|
|
193
|
+
"backend=%s requested_mode=%s",
|
|
194
|
+
_storage_backend_name(storage),
|
|
195
|
+
request.search_mode.value,
|
|
196
|
+
)
|
|
197
|
+
window_start, window_end = window_bounds(
|
|
198
|
+
reformulation.start_days_ago, reformulation.end_days_ago
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Temporal reordering (freshness collapse / recency sort) happens after
|
|
202
|
+
# relevance ranking, so it needs a wider working pool: the fresh version
|
|
203
|
+
# of a fact may rank below top_k on text relevance alone. When signals
|
|
204
|
+
# are present, fetch and rank a larger pool and cut to top_k only after
|
|
205
|
+
# the temporal pass.
|
|
206
|
+
temporal_reorder = reformulation.wants_current or reformulation.recency_dominant
|
|
207
|
+
fetch_k = max(
|
|
208
|
+
top_k,
|
|
209
|
+
floor_cfg.pool_size if floor_on else 0,
|
|
210
|
+
recency.pool_size if recency_on and recency is not None else 0,
|
|
211
|
+
_TEMPORAL_POOL_SIZE if temporal_reorder else 0,
|
|
212
|
+
)
|
|
213
|
+
result_cap = max(top_k, _TEMPORAL_POOL_SIZE) if temporal_reorder else top_k
|
|
214
|
+
|
|
215
|
+
# Session dedup: a request that carries a session_id skips items already
|
|
216
|
+
# served to that (org, session) and backfills from a widened pool.
|
|
217
|
+
session_id = (request.session_id or "").strip()
|
|
218
|
+
seen_keys = (
|
|
219
|
+
session_seen_cache.seen(org_id, session_id) if session_id else frozenset()
|
|
220
|
+
)
|
|
221
|
+
if seen_keys:
|
|
222
|
+
fetch_k += min(len(seen_keys), _SESSION_DEDUP_FETCH_BUMP_CAP)
|
|
160
223
|
|
|
161
224
|
# --- Phase B: parallel searches across all entity types ---
|
|
162
225
|
profiles, agent_playbooks, user_playbooks = _run_phase_b(
|
|
@@ -168,41 +231,65 @@ def run_unified_search(
|
|
|
168
231
|
top_k=fetch_k,
|
|
169
232
|
threshold=threshold,
|
|
170
233
|
recency_on=recency_on,
|
|
234
|
+
start_time=window_start,
|
|
235
|
+
end_time=window_end,
|
|
236
|
+
search_mode=effective_search_mode,
|
|
171
237
|
)
|
|
172
238
|
|
|
173
239
|
if profiles is None:
|
|
174
240
|
return UnifiedSearchResponse(success=False, msg="Search failed")
|
|
175
241
|
|
|
242
|
+
if seen_keys:
|
|
243
|
+
# Drop before the floors so seen items spend no cross-encoder budget.
|
|
244
|
+
profiles = _drop_seen_items(profiles or [], "profile", seen_keys)
|
|
245
|
+
agent_playbooks = _drop_seen_items(
|
|
246
|
+
agent_playbooks or [], "agent_playbook", seen_keys
|
|
247
|
+
)
|
|
248
|
+
user_playbooks = _drop_seen_items(
|
|
249
|
+
user_playbooks or [], "user_playbook", seen_keys
|
|
250
|
+
)
|
|
251
|
+
|
|
176
252
|
if floor_on:
|
|
177
253
|
profiles, agent_playbooks, user_playbooks = _apply_floors(
|
|
178
254
|
query=reformulated_query,
|
|
179
255
|
profiles=profiles,
|
|
180
256
|
agent_playbooks=agent_playbooks, # type: ignore[arg-type]
|
|
181
257
|
user_playbooks=user_playbooks, # type: ignore[arg-type]
|
|
182
|
-
top_k=
|
|
258
|
+
top_k=result_cap,
|
|
183
259
|
cfg=floor_cfg,
|
|
184
260
|
recency=recency if recency_on else None,
|
|
185
261
|
)
|
|
186
262
|
elif recency_on and recency is not None:
|
|
187
263
|
profiles = _apply_combined_score_recency(
|
|
188
|
-
profiles or [], entity_type="profiles", top_k=
|
|
264
|
+
profiles or [], entity_type="profiles", top_k=result_cap, cfg=recency
|
|
189
265
|
)
|
|
190
266
|
agent_playbooks = _apply_combined_score_recency(
|
|
191
267
|
agent_playbooks or [],
|
|
192
268
|
entity_type="agent_playbooks",
|
|
193
|
-
top_k=
|
|
269
|
+
top_k=result_cap,
|
|
194
270
|
cfg=recency,
|
|
195
271
|
)
|
|
196
272
|
user_playbooks = _apply_combined_score_recency(
|
|
197
273
|
user_playbooks or [],
|
|
198
274
|
entity_type="user_playbooks",
|
|
199
|
-
top_k=
|
|
275
|
+
top_k=result_cap,
|
|
200
276
|
cfg=recency,
|
|
201
277
|
)
|
|
202
278
|
else:
|
|
203
|
-
profiles = _unwrap_items(profiles or [])[:
|
|
204
|
-
agent_playbooks = _unwrap_items(agent_playbooks or [])[:
|
|
205
|
-
user_playbooks = _unwrap_items(user_playbooks or [])[:
|
|
279
|
+
profiles = _unwrap_items(profiles or [])[:result_cap]
|
|
280
|
+
agent_playbooks = _unwrap_items(agent_playbooks or [])[:result_cap]
|
|
281
|
+
user_playbooks = _unwrap_items(user_playbooks or [])[:result_cap]
|
|
282
|
+
|
|
283
|
+
# --- Temporal post-processing from query-derived signals ---
|
|
284
|
+
if temporal_reorder:
|
|
285
|
+
arms = [profiles or [], agent_playbooks or [], user_playbooks or []]
|
|
286
|
+
if reformulation.wants_current:
|
|
287
|
+
arms = [freshness_collapse(arm) for arm in arms]
|
|
288
|
+
if reformulation.recency_dominant:
|
|
289
|
+
arms = [sort_by_recency(arm) for arm in arms]
|
|
290
|
+
# Final cut to top_k only after the temporal pass, so items promoted
|
|
291
|
+
# from the wider pool can take the top slots.
|
|
292
|
+
profiles, agent_playbooks, user_playbooks = (arm[:top_k] for arm in arms)
|
|
206
293
|
|
|
207
294
|
user_playbooks = _suppress_source_user_playbooks(
|
|
208
295
|
storage=storage,
|
|
@@ -218,7 +305,11 @@ def run_unified_search(
|
|
|
218
305
|
reformulated_query=reformulated_query
|
|
219
306
|
if reformulated_query != request.query
|
|
220
307
|
else None,
|
|
308
|
+
degraded=embedding_failed,
|
|
309
|
+
search_mode_effective=effective_search_mode.value if embedding_failed else None,
|
|
221
310
|
)
|
|
311
|
+
if session_id:
|
|
312
|
+
session_seen_cache.record(org_id, session_id, _served_entity_keys(response))
|
|
222
313
|
_maybe_capture_final_response(
|
|
223
314
|
request=request,
|
|
224
315
|
response=response,
|
|
@@ -257,7 +348,7 @@ def _run_phase_a(
|
|
|
257
348
|
enable_reformulation: bool = False,
|
|
258
349
|
pre_retrieval_model_name: str | None = None,
|
|
259
350
|
search_mode: SearchMode = SearchMode.HYBRID,
|
|
260
|
-
) -> tuple[
|
|
351
|
+
) -> tuple[ReformulationResult, list[float] | None, bool]:
|
|
261
352
|
"""Run query reformulation and embedding generation sequentially.
|
|
262
353
|
|
|
263
354
|
Args:
|
|
@@ -273,7 +364,16 @@ def _run_phase_a(
|
|
|
273
364
|
search_mode (SearchMode): Search mode; FTS-only mode skips embedding generation entirely
|
|
274
365
|
|
|
275
366
|
Returns:
|
|
276
|
-
tuple[
|
|
367
|
+
tuple[ReformulationResult, Optional[list[float]], bool]: The
|
|
368
|
+
reformulation result (standalone query + temporal signals;
|
|
369
|
+
signal-free pass-through of the original query when reformulation
|
|
370
|
+
is disabled), the query embedding (None when unsupported, when the
|
|
371
|
+
mode is FTS-only, or when generation failed), and
|
|
372
|
+
``embedding_failed`` — True *only* when an embedding attempt was
|
|
373
|
+
made and raised. The benign None cases (FTS-only mode,
|
|
374
|
+
``supports_embedding`` False) leave ``embedding_failed`` False so
|
|
375
|
+
callers can distinguish a real failure from a mode that never
|
|
376
|
+
needed an embedding.
|
|
277
377
|
"""
|
|
278
378
|
reformulator = QueryReformulator(
|
|
279
379
|
llm_client=llm_client,
|
|
@@ -288,14 +388,14 @@ def _run_phase_a(
|
|
|
288
388
|
has_conversation_history=bool(conversation_history),
|
|
289
389
|
):
|
|
290
390
|
if enable_reformulation:
|
|
291
|
-
|
|
292
|
-
standalone_query = result.standalone_query
|
|
391
|
+
reformulation = reformulator.rewrite(query, conversation_history)
|
|
293
392
|
else:
|
|
294
|
-
|
|
393
|
+
reformulation = ReformulationResult(standalone_query=query)
|
|
295
394
|
|
|
296
395
|
# Embedding generation (uses reformulated query for semantic accuracy).
|
|
297
396
|
# FTS-only search has no use for an embedding, so skip the call entirely.
|
|
298
397
|
embedding = None
|
|
398
|
+
embedding_failed = False
|
|
299
399
|
if supports_embedding and search_mode != SearchMode.FTS:
|
|
300
400
|
with profile_step(
|
|
301
401
|
"search.embedding",
|
|
@@ -303,13 +403,24 @@ def _run_phase_a(
|
|
|
303
403
|
purpose="query",
|
|
304
404
|
) as span:
|
|
305
405
|
try:
|
|
306
|
-
embedding = _get_cached_query_embedding(
|
|
406
|
+
embedding = _get_cached_query_embedding(
|
|
407
|
+
storage, reformulation.standalone_query
|
|
408
|
+
)
|
|
409
|
+
# A falsy result (None or []) means the embedder produced no
|
|
410
|
+
# usable query vector — e.g. a storage backend that swallows
|
|
411
|
+
# EmbeddingUnavailableError and returns [] on a provider outage.
|
|
412
|
+
# Treat it the same as a raised failure: degrade to FTS rather
|
|
413
|
+
# than run a vector search with an empty embedding.
|
|
414
|
+
if not embedding:
|
|
415
|
+
embedding = None
|
|
416
|
+
embedding_failed = True
|
|
307
417
|
span.set_data("embedding_generated", embedding is not None)
|
|
308
418
|
except Exception as e:
|
|
309
419
|
span.set_data("embedding_generated", False)
|
|
310
|
-
|
|
420
|
+
embedding_failed = True
|
|
421
|
+
logger.exception("Embedding generation failed: %s", e)
|
|
311
422
|
|
|
312
|
-
return
|
|
423
|
+
return reformulation, embedding, embedding_failed
|
|
313
424
|
|
|
314
425
|
|
|
315
426
|
def _run_phase_b(
|
|
@@ -321,6 +432,9 @@ def _run_phase_b(
|
|
|
321
432
|
top_k: int,
|
|
322
433
|
threshold: float,
|
|
323
434
|
recency_on: bool = False,
|
|
435
|
+
start_time: datetime | None = None,
|
|
436
|
+
end_time: datetime | None = None,
|
|
437
|
+
search_mode: SearchMode = SearchMode.HYBRID,
|
|
324
438
|
) -> tuple[
|
|
325
439
|
list[Any] | None,
|
|
326
440
|
list[Any] | None,
|
|
@@ -336,11 +450,19 @@ def _run_phase_b(
|
|
|
336
450
|
query (str): Query string (possibly rewritten) for FTS
|
|
337
451
|
top_k (int): Maximum results per entity type
|
|
338
452
|
threshold (float): Minimum match threshold
|
|
453
|
+
start_time (Optional[datetime]): Query-derived time-window lower bound
|
|
454
|
+
(from reformulation temporal signals); forces the per-arm fan-out
|
|
455
|
+
path since the combined single RPC has no time parameters.
|
|
456
|
+
end_time (Optional[datetime]): Query-derived time-window upper bound.
|
|
457
|
+
search_mode (SearchMode): The EFFECTIVE search mode for this fan-out —
|
|
458
|
+
equal to ``request.search_mode`` normally, but forced to FTS by the
|
|
459
|
+
caller when embedding generation failed (degrade-to-FTS). Threaded
|
|
460
|
+
into every arm so storage never re-embeds a failed query.
|
|
339
461
|
|
|
340
462
|
Returns:
|
|
341
463
|
tuple: (profiles, agent_playbooks, user_playbooks) — all None on timeout/failure
|
|
342
464
|
"""
|
|
343
|
-
options = SearchOptions(query_embedding=embedding, search_mode=
|
|
465
|
+
options = SearchOptions(query_embedding=embedding, search_mode=search_mode)
|
|
344
466
|
|
|
345
467
|
entity_types = set(request.entity_types or _DEFAULT_ENTITY_TYPES)
|
|
346
468
|
allowed_agent_statuses = request.agent_playbook_status_filter
|
|
@@ -360,9 +482,16 @@ def _run_phase_b(
|
|
|
360
482
|
wants_scored_single_rpc = recency_on and callable(
|
|
361
483
|
getattr(storage, "unified_hybrid_search_scored", None)
|
|
362
484
|
)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
485
|
+
# A query-derived time window forces the per-arm fan-out: the
|
|
486
|
+
# combined single RPC has no time parameters.
|
|
487
|
+
has_time_window = start_time is not None or end_time is not None
|
|
488
|
+
if (
|
|
489
|
+
not has_time_window
|
|
490
|
+
and _unified_single_rpc_enabled()
|
|
491
|
+
and (
|
|
492
|
+
getattr(storage, "supports_unified_hybrid_search", False)
|
|
493
|
+
or wants_scored_single_rpc
|
|
494
|
+
)
|
|
366
495
|
):
|
|
367
496
|
combined = _run_phase_b_single_rpc(
|
|
368
497
|
request=request,
|
|
@@ -374,6 +503,7 @@ def _run_phase_b(
|
|
|
374
503
|
entity_types=entity_types,
|
|
375
504
|
allowed_agent_statuses=allowed_agent_statuses,
|
|
376
505
|
recency_on=recency_on,
|
|
506
|
+
search_mode=search_mode,
|
|
377
507
|
)
|
|
378
508
|
if combined is not None:
|
|
379
509
|
profiles, agent_playbooks, user_playbooks = combined
|
|
@@ -398,7 +528,9 @@ def _run_phase_b(
|
|
|
398
528
|
threshold,
|
|
399
529
|
request.user_id,
|
|
400
530
|
embedding,
|
|
401
|
-
|
|
531
|
+
search_mode,
|
|
532
|
+
start_time,
|
|
533
|
+
end_time,
|
|
402
534
|
)
|
|
403
535
|
if "profiles" in entity_types
|
|
404
536
|
else None
|
|
@@ -415,6 +547,8 @@ def _run_phase_b(
|
|
|
415
547
|
request.playbook_name,
|
|
416
548
|
allowed_agent_statuses,
|
|
417
549
|
options,
|
|
550
|
+
start_time,
|
|
551
|
+
end_time,
|
|
418
552
|
)
|
|
419
553
|
if "agent_playbooks" in entity_types
|
|
420
554
|
else None
|
|
@@ -428,7 +562,9 @@ def _run_phase_b(
|
|
|
428
562
|
status_filter=None,
|
|
429
563
|
threshold=threshold,
|
|
430
564
|
top_k=top_k,
|
|
431
|
-
search_mode=
|
|
565
|
+
search_mode=search_mode,
|
|
566
|
+
start_time=start_time,
|
|
567
|
+
end_time=end_time,
|
|
432
568
|
)
|
|
433
569
|
user_playbooks_future = _submit_with_current_context(
|
|
434
570
|
_SEARCH_FANOUT_EXECUTOR,
|
|
@@ -485,6 +621,7 @@ def _run_phase_b_single_rpc(
|
|
|
485
621
|
entity_types: set[str],
|
|
486
622
|
allowed_agent_statuses: list[PlaybookStatus] | None,
|
|
487
623
|
recency_on: bool = False,
|
|
624
|
+
search_mode: SearchMode = SearchMode.HYBRID,
|
|
488
625
|
) -> tuple[list[Any], list[Any], list[Any]] | None:
|
|
489
626
|
"""Run all Phase B arms through one combined storage round trip.
|
|
490
627
|
|
|
@@ -529,7 +666,7 @@ def _run_phase_b_single_rpc(
|
|
|
529
666
|
agent_version=request.agent_version,
|
|
530
667
|
playbook_name=request.playbook_name,
|
|
531
668
|
agent_playbook_statuses=statuses,
|
|
532
|
-
search_mode=
|
|
669
|
+
search_mode=search_mode,
|
|
533
670
|
include_profiles="profiles" in entity_types and bool(request.user_id),
|
|
534
671
|
include_agent_playbooks="agent_playbooks" in entity_types,
|
|
535
672
|
include_user_playbooks="user_playbooks" in entity_types,
|
|
@@ -664,6 +801,34 @@ def _unwrap_items(items: list[Any]) -> list[Any]:
|
|
|
664
801
|
return [_unwrap_item(item) for item in items]
|
|
665
802
|
|
|
666
803
|
|
|
804
|
+
def _entity_key(kind: str, item: Any) -> EntityKey | None:
|
|
805
|
+
"""Return the session-dedup key for a (possibly score-wrapped) item."""
|
|
806
|
+
raw_id = getattr(_unwrap_item(item), _ENTITY_ID_ATTRS[kind], None)
|
|
807
|
+
if raw_id in (None, "", 0):
|
|
808
|
+
return None
|
|
809
|
+
return (kind, str(raw_id))
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _drop_seen_items(
|
|
813
|
+
items: list[Any], kind: str, seen: frozenset[EntityKey]
|
|
814
|
+
) -> list[Any]:
|
|
815
|
+
return [item for item in items if _entity_key(kind, item) not in seen]
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _served_entity_keys(response: UnifiedSearchResponse) -> list[EntityKey]:
|
|
819
|
+
arms: tuple[tuple[str, list[Any]], ...] = (
|
|
820
|
+
("profile", response.profiles or []),
|
|
821
|
+
("user_playbook", response.user_playbooks or []),
|
|
822
|
+
("agent_playbook", response.agent_playbooks or []),
|
|
823
|
+
)
|
|
824
|
+
return [
|
|
825
|
+
key
|
|
826
|
+
for kind, items in arms
|
|
827
|
+
for item in items
|
|
828
|
+
if (key := _entity_key(kind, item)) is not None
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
|
|
667
832
|
def _suppress_source_user_playbooks(
|
|
668
833
|
*,
|
|
669
834
|
storage: BaseStorage,
|
|
@@ -764,12 +929,17 @@ def _search_agent_playbooks_via_storage(
|
|
|
764
929
|
playbook_name: str | None,
|
|
765
930
|
allowed_statuses: list[PlaybookStatus] | None,
|
|
766
931
|
options: SearchOptions,
|
|
932
|
+
start_time: datetime | None = None,
|
|
933
|
+
end_time: datetime | None = None,
|
|
767
934
|
) -> list[AgentPlaybook]:
|
|
768
935
|
"""Search agent playbooks, restricted to one or more approval statuses.
|
|
769
936
|
|
|
770
937
|
When ``allowed_statuses`` is None or empty, falls back to
|
|
771
938
|
``_DEFAULT_AGENT_PLAYBOOK_STATUSES`` (APPROVED + PENDING). Callers that
|
|
772
939
|
genuinely want REJECTED playbooks must opt in by passing the full list.
|
|
940
|
+
``start_time``/``end_time`` bound ``created_at`` (query-derived time
|
|
941
|
+
windows from reformulation temporal signals; unset when the query names
|
|
942
|
+
no window).
|
|
773
943
|
"""
|
|
774
944
|
with profile_step(
|
|
775
945
|
"search.branch.agent_playbooks",
|
|
@@ -790,6 +960,8 @@ def _search_agent_playbooks_via_storage(
|
|
|
790
960
|
threshold=threshold,
|
|
791
961
|
top_k=top_k,
|
|
792
962
|
search_mode=options.search_mode,
|
|
963
|
+
start_time=start_time,
|
|
964
|
+
end_time=end_time,
|
|
793
965
|
)
|
|
794
966
|
results: list[AgentPlaybook] = []
|
|
795
967
|
seen_ids: set[str] = set()
|
|
@@ -812,6 +984,8 @@ def _search_profiles_via_storage(
|
|
|
812
984
|
user_id: str | None,
|
|
813
985
|
embedding: list[float] | None,
|
|
814
986
|
search_mode: SearchMode,
|
|
987
|
+
start_time: datetime | None = None,
|
|
988
|
+
end_time: datetime | None = None,
|
|
815
989
|
) -> list[UserProfile]:
|
|
816
990
|
"""Search profiles via storage.search_user_profile, returning [] on error or missing user_id.
|
|
817
991
|
|
|
@@ -823,6 +997,10 @@ def _search_profiles_via_storage(
|
|
|
823
997
|
user_id (Optional[str]): User ID filter (required for profile search)
|
|
824
998
|
embedding (Optional[list[float]]): Pre-computed query embedding, or None for text-only search
|
|
825
999
|
search_mode (SearchMode): Search mode (hybrid/vector/fts)
|
|
1000
|
+
start_time (Optional[datetime]): Lower bound on last_modified_timestamp
|
|
1001
|
+
(query-derived time window from reformulation temporal signals;
|
|
1002
|
+
unset when the query names no window)
|
|
1003
|
+
end_time (Optional[datetime]): Upper bound on last_modified_timestamp
|
|
826
1004
|
|
|
827
1005
|
Returns:
|
|
828
1006
|
list[UserProfile]: Matching profiles, or [] on error/missing user_id
|
|
@@ -843,6 +1021,8 @@ def _search_profiles_via_storage(
|
|
|
843
1021
|
top_k=top_k,
|
|
844
1022
|
threshold=threshold,
|
|
845
1023
|
search_mode=search_mode,
|
|
1024
|
+
start_time=start_time,
|
|
1025
|
+
end_time=end_time,
|
|
846
1026
|
),
|
|
847
1027
|
status_filter=[None],
|
|
848
1028
|
query_embedding=embedding,
|
|
@@ -27,6 +27,7 @@ class UsageEvent:
|
|
|
27
27
|
pipeline: str | None = None
|
|
28
28
|
entity_type: str | None = None
|
|
29
29
|
entity_id: str | None = None
|
|
30
|
+
event_key: str | None = None
|
|
30
31
|
extractor_name: str | None = None
|
|
31
32
|
playbook_name: str | None = None
|
|
32
33
|
source: str | None = None
|
|
@@ -72,6 +73,7 @@ def record_usage_event(
|
|
|
72
73
|
pipeline: str | None = None,
|
|
73
74
|
entity_type: str | None = None,
|
|
74
75
|
entity_id: str | None = None,
|
|
76
|
+
event_key: str | None = None,
|
|
75
77
|
extractor_name: str | None = None,
|
|
76
78
|
playbook_name: str | None = None,
|
|
77
79
|
source: str | None = None,
|
|
@@ -109,6 +111,7 @@ def record_usage_event(
|
|
|
109
111
|
pipeline=pipeline,
|
|
110
112
|
entity_type=entity_type,
|
|
111
113
|
entity_id=entity_id,
|
|
114
|
+
event_key=event_key,
|
|
112
115
|
extractor_name=extractor_name,
|
|
113
116
|
playbook_name=playbook_name,
|
|
114
117
|
source=source,
|
|
@@ -34,6 +34,7 @@ from __future__ import annotations
|
|
|
34
34
|
|
|
35
35
|
import json
|
|
36
36
|
import os
|
|
37
|
+
import re
|
|
37
38
|
from collections.abc import Iterator
|
|
38
39
|
from contextlib import contextmanager
|
|
39
40
|
from typing import Any
|
|
@@ -42,6 +43,28 @@ from unittest.mock import MagicMock, patch
|
|
|
42
43
|
from reflexio.models.structured_output import find_schema_keyword as _find_schema_key
|
|
43
44
|
from reflexio.test_support.llm_model_registry import get_model_registry
|
|
44
45
|
|
|
46
|
+
_LEARNING_REF_PATTERN = re.compile(r'"learning_ref":\s*"([^"]+)"')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_learning_refs(prompt_content: str) -> list[str]:
|
|
50
|
+
"""Extract unique learning_refs from a judge prompt, preserving order.
|
|
51
|
+
|
|
52
|
+
The retrieved-learning judges validate exact per-ref coverage, so the
|
|
53
|
+
mock must echo the refs actually listed in the prompt rather than a
|
|
54
|
+
static payload. Only the ``[Retrieved Learnings]`` payload section is
|
|
55
|
+
scanned — the prompt's ``[Output]`` example also contains a
|
|
56
|
+
``"learning_ref"`` placeholder line that must not be echoed.
|
|
57
|
+
"""
|
|
58
|
+
start = prompt_content.find("[Retrieved Learnings]")
|
|
59
|
+
if start != -1:
|
|
60
|
+
end = prompt_content.find("[Output]", start)
|
|
61
|
+
prompt_content = prompt_content[start : end if end != -1 else None]
|
|
62
|
+
seen: list[str] = []
|
|
63
|
+
for ref in _LEARNING_REF_PATTERN.findall(prompt_content):
|
|
64
|
+
if ref not in seen:
|
|
65
|
+
seen.append(ref)
|
|
66
|
+
return seen
|
|
67
|
+
|
|
45
68
|
|
|
46
69
|
def _create_mock_completion(
|
|
47
70
|
prompt_content: str, parse_structured_output: bool = False
|
|
@@ -55,6 +78,40 @@ def _create_mock_completion(
|
|
|
55
78
|
|
|
56
79
|
if "Output just a boolean value" in prompt_content:
|
|
57
80
|
content = str(registry["boolean_evaluation"].minimal_valid)
|
|
81
|
+
elif "[Retrieved Learning Relevance Evaluation]" in prompt_content:
|
|
82
|
+
# Echo the refs listed in the prompt — the judge validates exact
|
|
83
|
+
# coverage, so a static payload would always fail validation.
|
|
84
|
+
refs = _extract_learning_refs(prompt_content)
|
|
85
|
+
content = json.dumps(
|
|
86
|
+
{
|
|
87
|
+
"verdicts": [
|
|
88
|
+
{
|
|
89
|
+
"learning_ref": ref,
|
|
90
|
+
"is_relevant": True,
|
|
91
|
+
"relevance_reason": "Applies to the session task.",
|
|
92
|
+
}
|
|
93
|
+
for ref in refs
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
if refs
|
|
97
|
+
else registry["retrieved_learning_relevance"].minimal_valid
|
|
98
|
+
)
|
|
99
|
+
elif "[Retrieved Learning Impact Evaluation]" in prompt_content:
|
|
100
|
+
refs = _extract_learning_refs(prompt_content)
|
|
101
|
+
content = json.dumps(
|
|
102
|
+
{
|
|
103
|
+
"verdicts": [
|
|
104
|
+
{
|
|
105
|
+
"learning_ref": ref,
|
|
106
|
+
"impact": "positive",
|
|
107
|
+
"impact_reason": "The response followed this learning.",
|
|
108
|
+
}
|
|
109
|
+
for ref in refs
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
if refs
|
|
113
|
+
else registry["retrieved_learning_impact"].minimal_valid
|
|
114
|
+
)
|
|
58
115
|
elif "policy consolidation" in prompt_content:
|
|
59
116
|
content = json.dumps(registry["playbook_aggregation"].minimal_valid)
|
|
60
117
|
elif (
|
|
@@ -72,6 +129,10 @@ def _create_mock_completion(
|
|
|
72
129
|
content = json.dumps(registry["playbook_extraction"].minimal_valid)
|
|
73
130
|
elif '"tags"' in prompt_content:
|
|
74
131
|
content = json.dumps(registry["tagging"].minimal_valid)
|
|
132
|
+
elif "standalone_query" in prompt_content:
|
|
133
|
+
# Query-reformulation prompt (v2+, structured) — anchored on its
|
|
134
|
+
# primary output field name, which the prompt must describe.
|
|
135
|
+
content = json.dumps(registry["query_reformulation"].minimal_valid)
|
|
75
136
|
elif parse_structured_output:
|
|
76
137
|
content = json.dumps(registry["profile_extraction"].minimal_valid)
|
|
77
138
|
else:
|
|
@@ -30,9 +30,14 @@ class ModelRegistryEntry:
|
|
|
30
30
|
def _build_registry() -> dict[str, ModelRegistryEntry]:
|
|
31
31
|
"""Build the model registry with lazy imports to avoid circular dependencies."""
|
|
32
32
|
from reflexio.models.api_schema.eval_overview_schema import ShadowComparisonOutput
|
|
33
|
+
from reflexio.models.api_schema.retriever_schema import ReformulationResult
|
|
33
34
|
from reflexio.server.services.agent_success_evaluation.agent_success_evaluation_constants import (
|
|
34
35
|
AgentSuccessEvaluationOutput,
|
|
35
36
|
)
|
|
37
|
+
from reflexio.server.services.agent_success_evaluation.components.retrieved_learning_evaluator import (
|
|
38
|
+
RetrievedLearningImpactOutput,
|
|
39
|
+
RetrievedLearningRelevanceOutput,
|
|
40
|
+
)
|
|
36
41
|
from reflexio.server.services.playbook.components.consolidator import (
|
|
37
42
|
PlaybookConsolidationOutput,
|
|
38
43
|
)
|
|
@@ -114,6 +119,30 @@ def _build_registry() -> dict[str, ModelRegistryEntry]:
|
|
|
114
119
|
"is_escalated": False,
|
|
115
120
|
},
|
|
116
121
|
),
|
|
122
|
+
"retrieved_learning_relevance": ModelRegistryEntry(
|
|
123
|
+
model_class=RetrievedLearningRelevanceOutput,
|
|
124
|
+
minimal_valid={
|
|
125
|
+
"verdicts": [
|
|
126
|
+
{
|
|
127
|
+
"learning_ref": "profile:mock-profile",
|
|
128
|
+
"is_relevant": True,
|
|
129
|
+
"relevance_reason": "Applies to the session task.",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
"retrieved_learning_impact": ModelRegistryEntry(
|
|
135
|
+
model_class=RetrievedLearningImpactOutput,
|
|
136
|
+
minimal_valid={
|
|
137
|
+
"verdicts": [
|
|
138
|
+
{
|
|
139
|
+
"learning_ref": "profile:mock-profile",
|
|
140
|
+
"impact": "positive",
|
|
141
|
+
"impact_reason": "The response followed this learning.",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
),
|
|
117
146
|
"tagging": ModelRegistryEntry(
|
|
118
147
|
model_class=TagsOutput,
|
|
119
148
|
minimal_valid={"tags": ["example_tag"]},
|
|
@@ -152,6 +181,10 @@ def _build_registry() -> dict[str, ModelRegistryEntry]:
|
|
|
152
181
|
model_class=None,
|
|
153
182
|
minimal_valid="true",
|
|
154
183
|
),
|
|
184
|
+
"query_reformulation": ModelRegistryEntry(
|
|
185
|
+
model_class=ReformulationResult,
|
|
186
|
+
minimal_valid={"standalone_query": "example reformulated query"},
|
|
187
|
+
),
|
|
155
188
|
}
|
|
156
189
|
|
|
157
190
|
|
|
File without changes
|