claude-smart 0.2.49 → 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 +10 -43
- package/bin/claude-smart.js +105 -0
- 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/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +197 -1
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +757 -119
- package/plugin/scripts/codex-hook.js +63 -225
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +117 -136
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/smart-install.sh +15 -1
- package/plugin/src/claude_smart/cli.py +14 -0
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +4 -1
- 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 +1 -1
- 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/client/client.py +126 -3
- 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 +177 -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 +66 -3
- 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 +62 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- 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 +20 -5
- 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/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/worker.py +85 -16
- 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_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
- 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 +122 -44
- 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 +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
- 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 +36 -1
- 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/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 +43 -0
- 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/_user.py +45 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
- 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 +50 -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 +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -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
|
@@ -11,6 +11,7 @@ from reflexio.server.api_endpoints.request_context import RequestContext
|
|
|
11
11
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
12
12
|
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
13
13
|
from reflexio.server.llm.token_accounting import RunTokenTotals, sum_trace_tokens
|
|
14
|
+
from reflexio.server.services.deferred_learning_plan import ExtractorBookmarkAdvance
|
|
14
15
|
from reflexio.server.services.extraction.outcome import ExtractionOutcome
|
|
15
16
|
from reflexio.server.services.extraction.resumable_agent import (
|
|
16
17
|
run_resumable_extraction_agent,
|
|
@@ -172,25 +173,6 @@ class PlaybookExtractor:
|
|
|
172
173
|
)
|
|
173
174
|
return session_data_models
|
|
174
175
|
|
|
175
|
-
def _update_operation_state(
|
|
176
|
-
self, request_interaction_data_models: list[RequestInteractionDataModel]
|
|
177
|
-
) -> None:
|
|
178
|
-
"""
|
|
179
|
-
Update operation state after processing interactions.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
request_interaction_data_models: The interactions that were processed
|
|
183
|
-
"""
|
|
184
|
-
all_interactions = extract_interactions_from_request_interaction_data_models(
|
|
185
|
-
request_interaction_data_models
|
|
186
|
-
)
|
|
187
|
-
mgr = self._create_state_manager()
|
|
188
|
-
mgr.update_extractor_bookmark(
|
|
189
|
-
extractor_name=get_extractor_name(self.config),
|
|
190
|
-
processed_interactions=all_interactions,
|
|
191
|
-
user_id=self.service_config.user_id,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
176
|
# ===============================
|
|
195
177
|
# public methods
|
|
196
178
|
# ===============================
|
|
@@ -202,10 +184,12 @@ class PlaybookExtractor:
|
|
|
202
184
|
This extractor handles its own data collection:
|
|
203
185
|
1. Gets interactions based on its config (window size, source filtering)
|
|
204
186
|
2. Applies time range filter for rerun flows
|
|
205
|
-
3.
|
|
187
|
+
3. Defers the stride-bookmark advance onto the outcome (applied in persist)
|
|
206
188
|
|
|
207
189
|
Returns:
|
|
208
|
-
list
|
|
190
|
+
An empty list when there are no interactions to process; otherwise an
|
|
191
|
+
ExtractionOutcome carrying the extracted playbook entries, the
|
|
192
|
+
resumable run_id (when set), and the deferred bookmark advance.
|
|
209
193
|
"""
|
|
210
194
|
# Collect interactions using extractor's own window_size/stride_size settings
|
|
211
195
|
request_interaction_data_models = self._get_interactions()
|
|
@@ -217,17 +201,30 @@ class PlaybookExtractor:
|
|
|
217
201
|
|
|
218
202
|
user_playbooks = self.extract_playbook_entries(request_interaction_data_models)
|
|
219
203
|
|
|
220
|
-
#
|
|
204
|
+
# Defer the stride-bookmark advance onto the outcome instead of
|
|
205
|
+
# self-advancing here (F1): applied downstream — inside the persist
|
|
206
|
+
# fence on the durable path, or in ``.run()``'s persist half — so it
|
|
207
|
+
# stays atomic with the playbook row writes. Only produced when output
|
|
208
|
+
# was generated (bookmark-iff-rows).
|
|
209
|
+
bookmark_advance: ExtractorBookmarkAdvance | None = None
|
|
221
210
|
if user_playbooks:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
token_totals=self._last_resumable_token_totals,
|
|
211
|
+
bookmark_advance = ExtractorBookmarkAdvance(
|
|
212
|
+
extractor_name=get_extractor_name(self.config),
|
|
213
|
+
processed_interactions=extract_interactions_from_request_interaction_data_models(
|
|
214
|
+
request_interaction_data_models
|
|
215
|
+
),
|
|
216
|
+
user_id=self.service_config.user_id,
|
|
229
217
|
)
|
|
230
|
-
|
|
218
|
+
|
|
219
|
+
# Always return an ExtractionOutcome so the bookmark advance rides along
|
|
220
|
+
# even in the non-resumable case; a resumable run also surfaces its
|
|
221
|
+
# run_id for _agent_runs finalization.
|
|
222
|
+
return ExtractionOutcome.completed(
|
|
223
|
+
user_playbooks,
|
|
224
|
+
run_id=self._last_resumable_run_id,
|
|
225
|
+
token_totals=self._last_resumable_token_totals,
|
|
226
|
+
bookmark_advance=bookmark_advance,
|
|
227
|
+
)
|
|
231
228
|
|
|
232
229
|
def extract_playbook_entries(
|
|
233
230
|
self, request_interaction_data_models: list[RequestInteractionDataModel]
|
|
@@ -19,6 +19,7 @@ def apply_playbook_edit(
|
|
|
19
19
|
new_playbook: UserPlaybook,
|
|
20
20
|
source: str,
|
|
21
21
|
request_id: str,
|
|
22
|
+
skip_embedding: bool = False,
|
|
22
23
|
) -> int:
|
|
23
24
|
"""Insert a replacement playbook then atomically supersede the incumbent.
|
|
24
25
|
|
|
@@ -43,6 +44,12 @@ def apply_playbook_edit(
|
|
|
43
44
|
non-empty; use the reflection run id (``ReflectionServiceRequest.request_id``)
|
|
44
45
|
or another operation-scoped id. Raises ``ValueError`` immediately
|
|
45
46
|
(before any storage write) when empty, preventing orphaned successor rows.
|
|
47
|
+
skip_embedding: Forwarded to ``save_user_playbooks``. Defaults to
|
|
48
|
+
``False`` (recompute the embedding at write time — what every online
|
|
49
|
+
/ offline-tuner caller relies on). The durable reflection persist
|
|
50
|
+
path passes ``True`` because the successor's embedding was already
|
|
51
|
+
precomputed off the writer transaction, so no embedding runs inside
|
|
52
|
+
the fence.
|
|
46
53
|
|
|
47
54
|
Returns:
|
|
48
55
|
The ``user_playbook_id`` of the newly inserted playbook, or ``-1`` if
|
|
@@ -56,7 +63,7 @@ def apply_playbook_edit(
|
|
|
56
63
|
"apply_playbook_edit: request_id must be non-empty (operation-run correlation id)"
|
|
57
64
|
)
|
|
58
65
|
new_playbook.source = source
|
|
59
|
-
storage.save_user_playbooks([new_playbook])
|
|
66
|
+
storage.save_user_playbooks([new_playbook], skip_embedding=skip_embedding)
|
|
60
67
|
new_id: int = new_playbook.user_playbook_id
|
|
61
68
|
|
|
62
69
|
ctx = LineageContext(op_kind="revise", actor=source, request_id=request_id)
|
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
10
10
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
11
|
+
from reflexio.server.services.deferred_learning_plan import GenerationComputePlan
|
|
11
12
|
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
12
13
|
|
|
13
14
|
from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
|
|
@@ -22,22 +23,19 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
22
23
|
UserPlaybook,
|
|
23
24
|
)
|
|
24
25
|
from reflexio.models.config_schema import PlaybookConfig
|
|
25
|
-
from reflexio.server.extensions import get_service
|
|
26
|
-
from reflexio.server.operation_limiter import run_with_operation_limit
|
|
27
26
|
from reflexio.server.services.base_generation_service import (
|
|
28
27
|
BaseGenerationService,
|
|
29
28
|
StatusChangeOperation,
|
|
30
29
|
)
|
|
31
|
-
from reflexio.server.services.
|
|
32
|
-
|
|
30
|
+
from reflexio.server.services.deferred_learning_plan import PlaybookWritePlan
|
|
31
|
+
from reflexio.server.services.playbook.aggregation_trigger import (
|
|
32
|
+
maybe_trigger_user_playbook_aggregation,
|
|
33
33
|
)
|
|
34
|
-
from reflexio.server.services.playbook.components.aggregator import PlaybookAggregator
|
|
35
34
|
from reflexio.server.services.playbook.components.extractor import PlaybookExtractor
|
|
36
35
|
from reflexio.server.services.playbook.playbook_service_constants import (
|
|
37
36
|
PlaybookServiceConstants,
|
|
38
37
|
)
|
|
39
38
|
from reflexio.server.services.playbook.playbook_service_utils import (
|
|
40
|
-
PlaybookAggregatorRequest,
|
|
41
39
|
PlaybookGenerationRequest,
|
|
42
40
|
format_expert_comparison_pairs,
|
|
43
41
|
has_expert_content,
|
|
@@ -154,8 +152,9 @@ class PlaybookGenerationService(
|
|
|
154
152
|
Returns:
|
|
155
153
|
PlaybookGenerationServiceConfig object
|
|
156
154
|
"""
|
|
155
|
+
generation_request_id = request.request_id
|
|
157
156
|
return PlaybookGenerationServiceConfig(
|
|
158
|
-
request_id=
|
|
157
|
+
request_id=generation_request_id,
|
|
159
158
|
agent_version=request.agent_version,
|
|
160
159
|
user_id=request.user_id,
|
|
161
160
|
source=request.source,
|
|
@@ -290,12 +289,30 @@ class PlaybookGenerationService(
|
|
|
290
289
|
all_playbooks.extend(result)
|
|
291
290
|
self._finalize_extracted_items(all_playbooks)
|
|
292
291
|
|
|
293
|
-
def
|
|
294
|
-
|
|
292
|
+
def _resolve_write_plan(
|
|
293
|
+
self, results: list[list[UserPlaybook]]
|
|
294
|
+
) -> PlaybookWritePlan | None:
|
|
295
|
+
"""Compute-half of playbook finalization — NO learning DB write.
|
|
296
|
+
|
|
297
|
+
Flattens + ``dedupe_and_drop_empty``, runs the deduplicator (its 2nd
|
|
298
|
+
LLM call + reads of existing rows), assigns ``source``/``status``, and
|
|
299
|
+
**precomputes embeddings** on the survivors. Returns a
|
|
300
|
+
:class:`PlaybookWritePlan` for the persist half, or ``None`` when there
|
|
301
|
+
is nothing to write (matching the pre-split ``if all_playbooks:`` gate,
|
|
302
|
+
which suppressed save/lineage/schedulers alike). Issues no
|
|
303
|
+
``save_user_playbooks``/``merge_records``/``supersede_*`` — the write is
|
|
304
|
+
the persist half's job (compute is write-free).
|
|
305
|
+
"""
|
|
295
306
|
from reflexio.server.services.playbook.playbook_service_utils import (
|
|
296
307
|
dedupe_and_drop_empty,
|
|
297
308
|
)
|
|
298
309
|
|
|
310
|
+
generation_request_id = self.service_config.request_id # type: ignore[reportOptionalMemberAccess]
|
|
311
|
+
|
|
312
|
+
all_playbooks: list[UserPlaybook] = []
|
|
313
|
+
for result in results:
|
|
314
|
+
if isinstance(result, list):
|
|
315
|
+
all_playbooks.extend(result)
|
|
299
316
|
all_playbooks = dedupe_and_drop_empty(all_playbooks)
|
|
300
317
|
|
|
301
318
|
# Deduplicate against existing entries in DB when deduplicator is enabled
|
|
@@ -324,7 +341,7 @@ class PlaybookGenerationService(
|
|
|
324
341
|
merge_groups,
|
|
325
342
|
) = consolidator.deduplicate(
|
|
326
343
|
[all_playbooks],
|
|
327
|
-
|
|
344
|
+
generation_request_id,
|
|
328
345
|
self.service_config.agent_version, # type: ignore[reportOptionalMemberAccess]
|
|
329
346
|
user_id=self.service_config.user_id, # type: ignore[reportOptionalMemberAccess]
|
|
330
347
|
)
|
|
@@ -342,41 +359,115 @@ class PlaybookGenerationService(
|
|
|
342
359
|
|
|
343
360
|
logger.info("All user playbook entries: %s", all_playbooks)
|
|
344
361
|
|
|
362
|
+
if not all_playbooks:
|
|
363
|
+
return None
|
|
364
|
+
|
|
345
365
|
logger.info(
|
|
346
366
|
"Successfully completed %d %s playbook generation for request id: %s",
|
|
347
367
|
len(all_playbooks),
|
|
348
368
|
self._get_service_name(),
|
|
349
|
-
|
|
369
|
+
generation_request_id,
|
|
350
370
|
)
|
|
351
371
|
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
372
|
+
# Precompute embeddings on the survivors (compute-side, NO DB write). The
|
|
373
|
+
# persist half passes skip_embedding=True so no embedding runs in the fence.
|
|
374
|
+
self.storage.precompute_user_playbook_embeddings(all_playbooks) # type: ignore[reportOptionalMemberAccess]
|
|
375
|
+
|
|
376
|
+
return PlaybookWritePlan(
|
|
377
|
+
request_id=generation_request_id,
|
|
378
|
+
output_pending_status=self.output_pending_status,
|
|
379
|
+
skip_aggregation=self.skip_aggregation,
|
|
380
|
+
new_playbooks=all_playbooks,
|
|
381
|
+
superseded_ids=existing_ids_to_delete,
|
|
382
|
+
merge_groups=merge_groups,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def _persist_write_plan(self, plan: PlaybookWritePlan) -> None:
|
|
386
|
+
"""Persist-half of playbook finalization — apply the resolved write-plan.
|
|
387
|
+
|
|
388
|
+
Issues only the fence-critical row writes: saves the new playbooks
|
|
389
|
+
(``skip_embedding=True`` — embeddings were precomputed in compute; the
|
|
390
|
+
save also assigns survivor ids) then materializes the consolidation
|
|
391
|
+
lineage, which MUST see those survivor ids and so runs AFTER the save.
|
|
392
|
+
NO LLM / embedding / dedup. The off-thread optimization/aggregation
|
|
393
|
+
schedulers are NOT here — they fire post-commit in
|
|
394
|
+
``emit_generation_side_effects`` (durable / ``.run()``) or right after
|
|
395
|
+
persist in ``_finalize_extracted_items`` (resume/manual).
|
|
396
|
+
"""
|
|
397
|
+
if not plan.new_playbooks:
|
|
398
|
+
return
|
|
399
|
+
try:
|
|
400
|
+
self.storage.save_user_playbooks( # type: ignore[reportOptionalMemberAccess]
|
|
401
|
+
plan.new_playbooks, skip_embedding=True
|
|
402
|
+
)
|
|
403
|
+
self._apply_consolidation_lineage(
|
|
404
|
+
plan.new_playbooks,
|
|
405
|
+
plan.merge_groups,
|
|
406
|
+
plan.superseded_ids,
|
|
407
|
+
request_id=plan.request_id,
|
|
408
|
+
)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(
|
|
411
|
+
"Failed to save %s results for request id: %s due to %s, exception type: %s",
|
|
412
|
+
self._get_service_name(),
|
|
413
|
+
plan.request_id,
|
|
414
|
+
str(e),
|
|
415
|
+
type(e).__name__,
|
|
416
|
+
)
|
|
417
|
+
raise
|
|
418
|
+
|
|
419
|
+
def _dispatch_playbook_schedulers(self, plan: PlaybookWritePlan) -> None:
|
|
420
|
+
"""Fire the off-thread optimization + aggregation schedulers post-persist.
|
|
369
421
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
422
|
+
Phantom-billing gate: on the durable / ``.run()`` path this is invoked
|
|
423
|
+
from ``emit_generation_side_effects`` (post-commit), so a fence-lost
|
|
424
|
+
(superseded) job never enqueues optimization or triggers aggregation. On
|
|
425
|
+
the synchronous resume/manual path the permanent
|
|
426
|
+
``_finalize_extracted_items`` wrapper invokes it right after persist,
|
|
427
|
+
keeping that path identical to the pre-split monolith. The two callers
|
|
428
|
+
are mutually exclusive, so the schedulers fire exactly once per run.
|
|
429
|
+
"""
|
|
430
|
+
self._enqueue_user_playbook_optimization(plan.new_playbooks)
|
|
431
|
+
if not plan.output_pending_status and not plan.skip_aggregation:
|
|
432
|
+
logger.info("Trigger playbook aggregation")
|
|
433
|
+
self._trigger_playbook_aggregation()
|
|
434
|
+
|
|
435
|
+
def emit_generation_side_effects(self, plan: GenerationComputePlan) -> None:
|
|
436
|
+
"""Post-commit side-effects — base telemetry/billing + playbook schedulers.
|
|
437
|
+
|
|
438
|
+
Extends the base emit (``generation_succeeded`` + ② Learning billing)
|
|
439
|
+
with the off-thread optimization/aggregation schedulers, which move here
|
|
440
|
+
so they fire only for a fence-winning durable job (never for a
|
|
441
|
+
superseded one) — the phantom-billing gate.
|
|
442
|
+
"""
|
|
443
|
+
super().emit_generation_side_effects(plan)
|
|
444
|
+
write_plan = plan.write_plan
|
|
445
|
+
if write_plan is not None:
|
|
446
|
+
self._dispatch_playbook_schedulers(write_plan)
|
|
447
|
+
|
|
448
|
+
def _finalize_extracted_items(self, all_playbooks: list[UserPlaybook]) -> None:
|
|
449
|
+
"""Permanent V3 wrapper: compute→persist→schedulers together (no fence).
|
|
450
|
+
|
|
451
|
+
Kept for the synchronous resume/manual callers
|
|
452
|
+
(``ExtractionResumeWorker`` calls this directly). Routes them through the
|
|
453
|
+
same ``_resolve_write_plan`` (compute) + ``_persist_write_plan``
|
|
454
|
+
(persist) split the durable worker uses — with no external
|
|
455
|
+
``commit_scope`` — then dispatches the same off-thread schedulers, so the
|
|
456
|
+
result is identical to the pre-split monolith.
|
|
457
|
+
"""
|
|
458
|
+
plan = self._resolve_write_plan([all_playbooks])
|
|
459
|
+
if plan is None:
|
|
460
|
+
return
|
|
461
|
+
self._persist_write_plan(plan)
|
|
462
|
+
self._dispatch_playbook_schedulers(plan)
|
|
374
463
|
|
|
375
464
|
def _apply_consolidation_lineage(
|
|
376
465
|
self,
|
|
377
466
|
saved_playbooks: list[UserPlaybook],
|
|
378
467
|
merge_groups: list[tuple[int, list[int]]],
|
|
379
468
|
existing_ids_to_delete: list[int],
|
|
469
|
+
*,
|
|
470
|
+
request_id: str,
|
|
380
471
|
) -> None:
|
|
381
472
|
"""Materialize consolidation merges as lineage tombstones.
|
|
382
473
|
|
|
@@ -393,9 +484,14 @@ class PlaybookGenerationService(
|
|
|
393
484
|
saved_playbooks: The just-persisted entries (survivor ids assigned).
|
|
394
485
|
merge_groups: ``(survivor_index, source_existing_ids)`` per merge.
|
|
395
486
|
existing_ids_to_delete: ALL archived ids (merge sources + leftovers).
|
|
487
|
+
request_id: Generation request id — the lineage key recorded on the
|
|
488
|
+
merge/supersede events. Passed explicitly (from the write-plan)
|
|
489
|
+
rather than read off ``self.service_config`` so persist stays
|
|
490
|
+
decoupled from the mutable service config on the fenced path.
|
|
396
491
|
"""
|
|
397
492
|
from reflexio.models.api_schema.domain.entities import LineageContext
|
|
398
493
|
|
|
494
|
+
generation_request_id = request_id
|
|
399
495
|
merged_source_ids: set[int] = set()
|
|
400
496
|
for survivor_idx, source_ids in merge_groups:
|
|
401
497
|
survivor_id = saved_playbooks[survivor_idx].user_playbook_id
|
|
@@ -409,7 +505,7 @@ class PlaybookGenerationService(
|
|
|
409
505
|
actor="consolidator",
|
|
410
506
|
source_ids=[str(s) for s in source_ids],
|
|
411
507
|
reason="dedup-merge",
|
|
412
|
-
request_id=
|
|
508
|
+
request_id=generation_request_id,
|
|
413
509
|
),
|
|
414
510
|
)
|
|
415
511
|
|
|
@@ -423,7 +519,7 @@ class PlaybookGenerationService(
|
|
|
423
519
|
try:
|
|
424
520
|
superseded_count = self.storage.supersede_user_playbooks_by_ids( # type: ignore[reportOptionalMemberAccess]
|
|
425
521
|
leftover_ids,
|
|
426
|
-
|
|
522
|
+
generation_request_id,
|
|
427
523
|
)
|
|
428
524
|
logger.info(
|
|
429
525
|
"Superseded %d split-source existing entries", superseded_count
|
|
@@ -528,42 +624,12 @@ class PlaybookGenerationService(
|
|
|
528
624
|
Trigger playbook aggregation for playbook types that have aggregator config.
|
|
529
625
|
This is called after raw user playbook entries are saved to check if aggregation should run.
|
|
530
626
|
"""
|
|
531
|
-
|
|
532
|
-
if not playbook_config or not playbook_config.aggregation_config:
|
|
533
|
-
return
|
|
534
|
-
|
|
535
|
-
logger.info("Triggering aggregation")
|
|
536
|
-
|
|
537
|
-
# Create aggregator request. Aggregation is singleton — it operates on the
|
|
538
|
-
# user's whole playbook set, so no name selector is threaded.
|
|
539
|
-
aggregator_request = PlaybookAggregatorRequest(
|
|
540
|
-
agent_version=self.service_config.agent_version, # type: ignore[reportOptionalMemberAccess]
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
# Initialize and run aggregator (synchronous)
|
|
544
|
-
aggregation_prompt_processor = get_service(AGGREGATION_PROMPT_PROCESSOR)
|
|
545
|
-
aggregator_kwargs = {}
|
|
546
|
-
if aggregation_prompt_processor is not None:
|
|
547
|
-
aggregator_kwargs["aggregation_prompt_processor"] = (
|
|
548
|
-
aggregation_prompt_processor
|
|
549
|
-
)
|
|
550
|
-
aggregator = PlaybookAggregator(
|
|
551
|
-
llm_client=self.client,
|
|
627
|
+
maybe_trigger_user_playbook_aggregation(
|
|
552
628
|
request_context=self.request_context,
|
|
629
|
+
llm_client=self.client,
|
|
553
630
|
agent_version=self.service_config.agent_version, # type: ignore[reportOptionalMemberAccess]
|
|
554
|
-
|
|
631
|
+
reason="playbook_generation",
|
|
555
632
|
)
|
|
556
|
-
try:
|
|
557
|
-
run_with_operation_limit(
|
|
558
|
-
org_id=self.request_context.org_id,
|
|
559
|
-
operation="aggregation",
|
|
560
|
-
fn=lambda: aggregator.run(aggregator_request),
|
|
561
|
-
)
|
|
562
|
-
except TimeoutError:
|
|
563
|
-
logger.info(
|
|
564
|
-
"Skipping inline aggregation for agent_version=%s: aggregation limiter is saturated",
|
|
565
|
-
self.service_config.agent_version, # type: ignore[reportOptionalMemberAccess]
|
|
566
|
-
)
|
|
567
633
|
|
|
568
634
|
# ===============================
|
|
569
635
|
# Rerun hook implementations (override base class methods)
|
|
@@ -646,8 +712,9 @@ class PlaybookGenerationService(
|
|
|
646
712
|
"""
|
|
647
713
|
# Handle rerun requests (have start_time/end_time datetime objects)
|
|
648
714
|
if isinstance(request, RerunPlaybookGenerationRequest):
|
|
715
|
+
operation_request_id = f"rerun_playbook_{uuid.uuid4().hex[:8]}"
|
|
649
716
|
return PlaybookGenerationRequest(
|
|
650
|
-
request_id=
|
|
717
|
+
request_id=operation_request_id,
|
|
651
718
|
agent_version=request.agent_version,
|
|
652
719
|
user_id=user_id,
|
|
653
720
|
source=request.source,
|
|
@@ -660,8 +727,9 @@ class PlaybookGenerationService(
|
|
|
660
727
|
auto_run=False,
|
|
661
728
|
)
|
|
662
729
|
# Handle manual requests (ManualPlaybookGenerationRequest)
|
|
730
|
+
operation_request_id = f"manual_{uuid.uuid4().hex[:8]}"
|
|
663
731
|
return PlaybookGenerationRequest(
|
|
664
|
-
request_id=
|
|
732
|
+
request_id=operation_request_id,
|
|
665
733
|
agent_version=request.agent_version,
|
|
666
734
|
user_id=user_id,
|
|
667
735
|
source=request.source,
|
|
@@ -20,6 +20,9 @@ from reflexio.models.api_schema.domain.entities import LineageContext
|
|
|
20
20
|
from reflexio.models.config_schema import PlaybookOptimizerConfig
|
|
21
21
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
22
22
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
23
|
+
from reflexio.server.services.playbook.aggregation_trigger import (
|
|
24
|
+
maybe_trigger_user_playbook_aggregation,
|
|
25
|
+
)
|
|
23
26
|
from reflexio.server.tracing import sentry_tags
|
|
24
27
|
|
|
25
28
|
from .assistant_webhook import AssistantCallable, LocalScriptAssistant, WebhookAssistant
|
|
@@ -488,13 +491,29 @@ class PlaybookOptimizer:
|
|
|
488
491
|
# the rule wording, so writing ``best_content`` is sufficient — there
|
|
489
492
|
# is no derived polarity label or separate polarity field to keep in
|
|
490
493
|
# sync.
|
|
491
|
-
|
|
494
|
+
successor_id = _supersede_user_playbook(
|
|
492
495
|
self.storage,
|
|
493
496
|
current_user,
|
|
494
497
|
best_content,
|
|
495
498
|
"playbook_optimizer",
|
|
496
499
|
request_id=run_request_id,
|
|
497
500
|
)
|
|
501
|
+
if successor_id is not None:
|
|
502
|
+
try:
|
|
503
|
+
successor = self.storage.get_user_playbook_by_id(successor_id)
|
|
504
|
+
if successor is not None and successor.agent_version:
|
|
505
|
+
maybe_trigger_user_playbook_aggregation(
|
|
506
|
+
request_context=self.request_context,
|
|
507
|
+
llm_client=self.llm_client,
|
|
508
|
+
agent_version=successor.agent_version,
|
|
509
|
+
reason="playbook_optimizer",
|
|
510
|
+
)
|
|
511
|
+
except Exception: # noqa: BLE001
|
|
512
|
+
logger.exception(
|
|
513
|
+
"playbook_optimizer aggregation trigger failed after successor commit",
|
|
514
|
+
extra={"successor_id": successor_id},
|
|
515
|
+
)
|
|
516
|
+
return successor_id
|
|
498
517
|
|
|
499
518
|
|
|
500
519
|
def _agent_like_playbook(playbook: UserPlaybook) -> AgentPlaybook:
|
|
@@ -5,6 +5,9 @@ import logging
|
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
+
from functools import partial
|
|
9
|
+
|
|
10
|
+
from reflexio.server.callback_executor import submit_callback
|
|
8
11
|
|
|
9
12
|
from .optimizer import PlaybookOptimizationRunStatus, PlaybookOptimizationTarget
|
|
10
13
|
|
|
@@ -110,12 +113,16 @@ class PlaybookOptimizationScheduler:
|
|
|
110
113
|
continue
|
|
111
114
|
_, callback, abort_threshold, cooldown_seconds = current
|
|
112
115
|
del self._scheduled[key]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
submit_callback(
|
|
117
|
+
f"playbook-opt-{key[1]}-{key[2]}",
|
|
118
|
+
partial(
|
|
119
|
+
self._run_callback,
|
|
120
|
+
key,
|
|
121
|
+
callback,
|
|
122
|
+
abort_threshold,
|
|
123
|
+
cooldown_seconds,
|
|
124
|
+
),
|
|
125
|
+
)
|
|
119
126
|
except Exception:
|
|
120
127
|
logger.exception("Playbook optimization scheduler loop failed")
|
|
121
128
|
time.sleep(1)
|
package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"""Independent query reformulation module for pre-retrieval optimization.
|
|
2
2
|
|
|
3
|
-
Reformulates search queries into clean, normalized natural language
|
|
4
|
-
|
|
3
|
+
Reformulates search queries into clean, normalized natural language AND
|
|
4
|
+
extracts the query's temporal signals (relative time window,
|
|
5
|
+
recency_dominant, wants_current) in the same structured LLM call — so
|
|
6
|
+
retrieval can be time-sensitive with no additional pre-search latency.
|
|
5
7
|
No FTS expansion -- that is handled by document-side expansion at storage time.
|
|
6
8
|
|
|
7
9
|
Provides two interfaces:
|
|
8
|
-
- rewrite(): pure query transformation (no search)
|
|
10
|
+
- rewrite(): pure query transformation + temporal signals (no search)
|
|
9
11
|
- search(): rewrite + execute search via callable + merge results
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import logging
|
|
13
15
|
from collections.abc import Callable
|
|
16
|
+
from datetime import UTC, datetime
|
|
14
17
|
from typing import Generic, TypeVar
|
|
15
18
|
|
|
16
19
|
from pydantic import BaseModel
|
|
@@ -84,13 +87,15 @@ class QueryReformulator:
|
|
|
84
87
|
query: str,
|
|
85
88
|
conversation_history: list[ConversationTurn] | None = None,
|
|
86
89
|
) -> ReformulationResult:
|
|
87
|
-
"""Reformulate a
|
|
90
|
+
"""Reformulate a query and extract its temporal signals.
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
One structured LLM call resolves conversation context (pronouns,
|
|
90
93
|
ellipsis, implicit references), expands abbreviations, fixes grammar,
|
|
91
|
-
and
|
|
94
|
+
normalizes terminology — and reads the question's time semantics
|
|
95
|
+
(relative window, recency_dominant, wants_current).
|
|
92
96
|
|
|
93
|
-
Falls back to the original query on any
|
|
97
|
+
Falls back to the original query with no temporal signals on any
|
|
98
|
+
LLM/parse failure.
|
|
94
99
|
|
|
95
100
|
Args:
|
|
96
101
|
query (str): The original user search query
|
|
@@ -98,11 +103,11 @@ class QueryReformulator:
|
|
|
98
103
|
context-aware reformulation.
|
|
99
104
|
|
|
100
105
|
Returns:
|
|
101
|
-
ReformulationResult:
|
|
106
|
+
ReformulationResult: Reformulated standalone query + temporal
|
|
107
|
+
signals.
|
|
102
108
|
"""
|
|
103
109
|
try:
|
|
104
|
-
|
|
105
|
-
return ReformulationResult(standalone_query=reformulated)
|
|
110
|
+
return self._reformulate(query, conversation_history)
|
|
106
111
|
except Exception as e:
|
|
107
112
|
logger.warning("Query reformulation failed, using original: %s", e)
|
|
108
113
|
return ReformulationResult(standalone_query=query)
|
|
@@ -150,18 +155,21 @@ class QueryReformulator:
|
|
|
150
155
|
self,
|
|
151
156
|
query: str,
|
|
152
157
|
conversation_history: list[ConversationTurn] | None = None,
|
|
153
|
-
) ->
|
|
154
|
-
"""
|
|
158
|
+
) -> ReformulationResult:
|
|
159
|
+
"""One structured LLM call: rewritten query + temporal signals.
|
|
155
160
|
|
|
156
161
|
Args:
|
|
157
162
|
query (str): The original search query
|
|
158
163
|
conversation_history (list, optional): Prior conversation turns
|
|
159
164
|
|
|
160
165
|
Returns:
|
|
161
|
-
|
|
166
|
+
ReformulationResult: Validated result. A suspect rewritten-query
|
|
167
|
+
field degrades the whole result to the original query with no
|
|
168
|
+
signals (a model that mangles the rewrite is not trusted for
|
|
169
|
+
the signals either).
|
|
162
170
|
|
|
163
171
|
Raises:
|
|
164
|
-
Exception: If LLM call
|
|
172
|
+
Exception: If the LLM call fails
|
|
165
173
|
"""
|
|
166
174
|
conversation_context = self._format_conversation_context(conversation_history)
|
|
167
175
|
conversation_context_block = (
|
|
@@ -171,29 +179,36 @@ class QueryReformulator:
|
|
|
171
179
|
)
|
|
172
180
|
prompt = self.prompt_manager.render_prompt(
|
|
173
181
|
"query_reformulation",
|
|
174
|
-
{
|
|
182
|
+
{
|
|
183
|
+
"query": query,
|
|
184
|
+
"conversation_context_block": conversation_context_block,
|
|
185
|
+
"today_iso": datetime.now(UTC).date().isoformat(),
|
|
186
|
+
},
|
|
175
187
|
)
|
|
176
188
|
logger.debug("Query reformulation prompt: %s", prompt)
|
|
177
189
|
model_kwargs = {}
|
|
178
190
|
if self.model_name:
|
|
179
191
|
model_kwargs["model"] = self.model_name
|
|
180
|
-
result = self.llm_client.
|
|
181
|
-
prompt,
|
|
192
|
+
result = self.llm_client.generate_chat_response(
|
|
193
|
+
messages=[{"role": "user", "content": prompt}],
|
|
194
|
+
response_format=ReformulationResult,
|
|
182
195
|
timeout=self.LLM_TIMEOUT,
|
|
183
196
|
max_retries=self.LLM_MAX_RETRIES,
|
|
184
197
|
**model_kwargs,
|
|
185
198
|
)
|
|
186
199
|
log_model_response(logger, "Query reformulation model response", result)
|
|
187
200
|
|
|
188
|
-
if isinstance(result,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return extracted
|
|
192
|
-
logger.warning("LLM returned invalid reformulation: %s", result)
|
|
193
|
-
return query
|
|
201
|
+
if not isinstance(result, ReformulationResult):
|
|
202
|
+
logger.warning("LLM returned non-structured reformulation: %r", result)
|
|
203
|
+
return ReformulationResult(standalone_query=query)
|
|
194
204
|
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
extracted = self._extract_reformulated_query(result.standalone_query)
|
|
206
|
+
if not extracted:
|
|
207
|
+
logger.warning(
|
|
208
|
+
"LLM returned invalid reformulation: %s", result.standalone_query
|
|
209
|
+
)
|
|
210
|
+
return ReformulationResult(standalone_query=query)
|
|
211
|
+
return result.model_copy(update={"standalone_query": extracted})
|
|
197
212
|
|
|
198
213
|
@classmethod
|
|
199
214
|
def _extract_reformulated_query(cls, output: str) -> str | None:
|