claude-smart 0.2.46 → 0.2.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +11 -6
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Should-run precheck helpers for ``BaseGenerationService`` (Tier-1b decomposition).
|
|
2
|
+
|
|
3
|
+
``ShouldRunPrecheckMixin`` holds the pre-extraction ``should_run`` gate and its
|
|
4
|
+
template hooks: ``_should_run_before_extraction`` (the gate), the overridable
|
|
5
|
+
``_build_should_run_prompt`` / ``_collect_scoped_interactions_for_precheck`` /
|
|
6
|
+
``_get_precheck_interaction_query_kwargs`` hooks, and ``_resolve_should_run_model``
|
|
7
|
+
(the enterprise string-path patch target — it stays MRO-resolvable on the base
|
|
8
|
+
because the base is ahead of this mixin in every subclass MRO and the module path
|
|
9
|
+
``base_generation_service`` + the ``BaseGenerationService`` class name are unchanged).
|
|
10
|
+
|
|
11
|
+
``_collect_scoped_interactions_for_precheck`` writes ``self._last_precheck_sessions``
|
|
12
|
+
— the Precheck→Billing (INV-3) seam that ``_extraction_input_text`` later reads. The
|
|
13
|
+
cheap pre-filter helper ``_cheap_should_run_reject`` remains a module function in
|
|
14
|
+
``base_generation_service`` (tests pin it there); the gate imports it function-locally
|
|
15
|
+
to avoid a circular import and to keep the ``base_generation_service`` string-path
|
|
16
|
+
patch effective. Method bodies are moved verbatim from the former monolithic
|
|
17
|
+
``base_generation_service.py``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
24
|
+
|
|
25
|
+
from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
|
|
26
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
27
|
+
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
28
|
+
from reflexio.server.services.extractor_config_utils import get_extractor_name
|
|
29
|
+
from reflexio.server.services.extractor_interaction_utils import (
|
|
30
|
+
get_effective_source_filter,
|
|
31
|
+
get_extractor_window_params,
|
|
32
|
+
)
|
|
33
|
+
from reflexio.server.services.service_utils import log_llm_messages, log_model_response
|
|
34
|
+
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
TExtractorConfig = TypeVar("TExtractorConfig")
|
|
39
|
+
TGenerationServiceConfig = TypeVar("TGenerationServiceConfig")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ShouldRunPrecheckMixin(Generic[TExtractorConfig, TGenerationServiceConfig]): # noqa: UP046
|
|
43
|
+
"""Pre-extraction ``should_run`` gate + precheck template hooks.
|
|
44
|
+
|
|
45
|
+
Mixed into ``BaseGenerationService`` ahead of ``ABC``; the per-run ``self``
|
|
46
|
+
attributes these methods read (``service_config``, ``storage``, ``client``,
|
|
47
|
+
``request_context``, ``_last_precheck_sessions``) are initialised on the base
|
|
48
|
+
``__init__`` — the annotation-only stubs below give pyright the types without
|
|
49
|
+
introducing shared class-level mutable state.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Annotation-only stubs for base-owned attributes these helpers read (init'd in
|
|
53
|
+
# the base ``__init__``). NEVER assign here — a class-level default would be a
|
|
54
|
+
# shared-state footgun on a mixin.
|
|
55
|
+
service_config: TGenerationServiceConfig | None
|
|
56
|
+
storage: BaseStorage | None
|
|
57
|
+
client: LiteLLMClient
|
|
58
|
+
request_context: RequestContext
|
|
59
|
+
# The Precheck→Billing (INV-3) seam: written by
|
|
60
|
+
# ``_collect_scoped_interactions_for_precheck``, read by ``_extraction_input_text``.
|
|
61
|
+
_last_precheck_sessions: list[Any] | None
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
# Abstract on the base ABC (stays there per SINK-2); declared here type-only
|
|
65
|
+
# so pyright can resolve the ``self._get_service_name()`` call. No runtime
|
|
66
|
+
# attribute is added, so ``__abstractmethods__`` is unaffected.
|
|
67
|
+
def _get_service_name(self) -> str: ...
|
|
68
|
+
|
|
69
|
+
def _should_run_before_extraction(self, extractor_config: TExtractorConfig) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Pre-extraction check called before extractor execution.
|
|
72
|
+
|
|
73
|
+
Template method that:
|
|
74
|
+
1. Skips for non-auto runs and mock mode
|
|
75
|
+
2. Returns True immediately when service_config.force_extraction=True
|
|
76
|
+
(bypasses cheap pre-filter and LLM should_run vote)
|
|
77
|
+
3. Collects scoped interactions via _collect_scoped_interactions_for_precheck
|
|
78
|
+
4. Delegates prompt building to _build_should_run_prompt (subclass hook)
|
|
79
|
+
5. Makes a single LLM call to determine if extraction should proceed
|
|
80
|
+
|
|
81
|
+
Override _build_should_run_prompt in subclasses to provide service-specific
|
|
82
|
+
criteria and prompt construction. Default returns True (always run) when
|
|
83
|
+
no prompt hook is provided.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
extractor_config: Enabled extractor config that will be run
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: True if extraction should proceed, False to skip
|
|
90
|
+
"""
|
|
91
|
+
# Skip for non-auto runs (rerun/manual flows always run)
|
|
92
|
+
if not getattr(self.service_config, "auto_run", True):
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Skip for mock mode
|
|
96
|
+
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
# `force_extraction=True` is the caller's explicit "no gates" signal —
|
|
100
|
+
# corrections, manual /learn, anything time-sensitive. Bypass the
|
|
101
|
+
# cheap pre-filter (slash-only / too-short rejects) and the LLM
|
|
102
|
+
# should_run vote so the extractor always runs on this batch.
|
|
103
|
+
if getattr(self.service_config, "force_extraction", False):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Skip if org config disables the pre-extraction check
|
|
107
|
+
root_config = self.request_context.configurator.get_config()
|
|
108
|
+
if root_config and root_config.skip_should_run_check:
|
|
109
|
+
logger.info(
|
|
110
|
+
"skip_should_run_check is enabled for %s, bypassing pre-extraction check",
|
|
111
|
+
self._get_service_name(),
|
|
112
|
+
)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
# Collect scoped interactions
|
|
116
|
+
session_data_models, scoped_config = (
|
|
117
|
+
self._collect_scoped_interactions_for_precheck(extractor_config)
|
|
118
|
+
)
|
|
119
|
+
if not session_data_models:
|
|
120
|
+
logger.info(
|
|
121
|
+
"No interactions found for consolidated should_generate check for %s",
|
|
122
|
+
self._get_service_name(),
|
|
123
|
+
)
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
# Cheap pre-filter: reject batches that are structurally unable
|
|
127
|
+
# to yield signal (slash-commands only, too-short user turns,
|
|
128
|
+
# extractor-prompt echoes) without burning a 5–7s LLM call. See
|
|
129
|
+
# _cheap_should_run_reject for the rule set.
|
|
130
|
+
from reflexio.server.services.base_generation_service import (
|
|
131
|
+
_cheap_should_run_reject,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
reject_reason = _cheap_should_run_reject(session_data_models)
|
|
135
|
+
if reject_reason is not None:
|
|
136
|
+
logger.info(
|
|
137
|
+
"Cheap pre-filter rejected %s should_run: reason=%s identifier=%s",
|
|
138
|
+
self._get_service_name(),
|
|
139
|
+
reject_reason,
|
|
140
|
+
getattr(self.service_config, "user_id", None) or "unknown",
|
|
141
|
+
)
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
# Build prompt via subclass hook
|
|
145
|
+
prompt = self._build_should_run_prompt(scoped_config, session_data_models)
|
|
146
|
+
if not prompt:
|
|
147
|
+
return True # No prompt means no check needed, proceed
|
|
148
|
+
|
|
149
|
+
# Resolve model and make LLM call
|
|
150
|
+
should_run_model = self._resolve_should_run_model()
|
|
151
|
+
identifier = getattr(self.service_config, "user_id", None) or "unknown"
|
|
152
|
+
try:
|
|
153
|
+
should_start = time.perf_counter()
|
|
154
|
+
logger.info(
|
|
155
|
+
"event=consolidated_should_run_start service=%s identifier=%s model=%s extractor=%s",
|
|
156
|
+
self._get_service_name(),
|
|
157
|
+
identifier,
|
|
158
|
+
should_run_model,
|
|
159
|
+
get_extractor_name(extractor_config),
|
|
160
|
+
)
|
|
161
|
+
log_llm_messages(
|
|
162
|
+
logger,
|
|
163
|
+
"Should extract check",
|
|
164
|
+
[{"role": "user", "content": prompt}],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
content = self.client.generate_chat_response(
|
|
168
|
+
messages=[{"role": "user", "content": prompt}],
|
|
169
|
+
model=should_run_model,
|
|
170
|
+
)
|
|
171
|
+
log_model_response(
|
|
172
|
+
logger,
|
|
173
|
+
f"Consolidated {self._get_service_name()} should_run response",
|
|
174
|
+
content,
|
|
175
|
+
)
|
|
176
|
+
decision = bool(content and "true" in content.lower()) # type: ignore[reportAttributeAccessIssue]
|
|
177
|
+
logger.info(
|
|
178
|
+
"event=consolidated_should_run_end service=%s identifier=%s elapsed_seconds=%.3f decision=%s",
|
|
179
|
+
self._get_service_name(),
|
|
180
|
+
identifier,
|
|
181
|
+
time.perf_counter() - should_start,
|
|
182
|
+
decision,
|
|
183
|
+
)
|
|
184
|
+
return decision
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
logger.error(
|
|
187
|
+
"Consolidated should_generate check failed for %s: %s, defaulting to run",
|
|
188
|
+
self._get_service_name(),
|
|
189
|
+
str(exc),
|
|
190
|
+
)
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
def _build_should_run_prompt(
|
|
194
|
+
self,
|
|
195
|
+
scoped_config: TExtractorConfig, # noqa: ARG002
|
|
196
|
+
session_data_models: list[RequestInteractionDataModel], # noqa: ARG002
|
|
197
|
+
) -> str | None:
|
|
198
|
+
"""
|
|
199
|
+
Build the prompt for the consolidated should_run LLM check.
|
|
200
|
+
|
|
201
|
+
Override in subclasses to provide service-specific criteria building
|
|
202
|
+
and prompt rendering. Return None if no check is needed (always proceed).
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
scoped_config: Extractor config that had scoped interactions
|
|
206
|
+
session_data_models: Deduplicated request interaction data models
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Optional[str]: The rendered prompt string, or None to skip the check
|
|
210
|
+
"""
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _collect_scoped_interactions_for_precheck(
|
|
214
|
+
self, extractor_config: TExtractorConfig
|
|
215
|
+
) -> tuple[list[RequestInteractionDataModel], TExtractorConfig]:
|
|
216
|
+
"""
|
|
217
|
+
Collect interactions for consolidated pre-check using extractor-scoped filters.
|
|
218
|
+
|
|
219
|
+
Mirrors each extractor's source/window scope so the consolidated gate
|
|
220
|
+
does not skip valid extraction because of an unrelated fixed interaction slice.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
extractor_config: Enabled extractor config after request-level filtering
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
tuple: (session data models, extractor config)
|
|
227
|
+
"""
|
|
228
|
+
root_config = self.request_context.configurator.get_config()
|
|
229
|
+
global_window_size = (
|
|
230
|
+
getattr(root_config, "window_size", None) if root_config else None
|
|
231
|
+
)
|
|
232
|
+
global_stride_size = (
|
|
233
|
+
getattr(root_config, "stride_size", None) if root_config else None
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
extra_kwargs = self._get_precheck_interaction_query_kwargs()
|
|
237
|
+
|
|
238
|
+
should_skip, effective_source = get_effective_source_filter(
|
|
239
|
+
extractor_config, getattr(self.service_config, "source", None)
|
|
240
|
+
)
|
|
241
|
+
if should_skip:
|
|
242
|
+
# Stash for the billing path to reuse (same window the gate saw).
|
|
243
|
+
self._last_precheck_sessions = []
|
|
244
|
+
return [], extractor_config
|
|
245
|
+
|
|
246
|
+
window_size, _ = get_extractor_window_params(
|
|
247
|
+
extractor_config, global_window_size, global_stride_size
|
|
248
|
+
)
|
|
249
|
+
session_data_models, _ = self.storage.get_last_k_interactions_grouped( # type: ignore[reportOptionalMemberAccess]
|
|
250
|
+
user_id=getattr(self.service_config, "user_id", None),
|
|
251
|
+
k=window_size,
|
|
252
|
+
sources=effective_source,
|
|
253
|
+
start_time=getattr(self.service_config, "rerun_start_time", None),
|
|
254
|
+
end_time=getattr(self.service_config, "rerun_end_time", None),
|
|
255
|
+
**extra_kwargs,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Stash for the billing path (_extraction_input_text) to reuse instead of
|
|
259
|
+
# re-querying storage for billing_input_tokens.
|
|
260
|
+
self._last_precheck_sessions = session_data_models
|
|
261
|
+
return session_data_models, extractor_config
|
|
262
|
+
|
|
263
|
+
def _get_precheck_interaction_query_kwargs(self) -> dict:
|
|
264
|
+
"""
|
|
265
|
+
Return extra keyword arguments for get_last_k_interactions_grouped in precheck.
|
|
266
|
+
|
|
267
|
+
Override in subclasses that need additional query parameters
|
|
268
|
+
(e.g., agent_version for playbook services).
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
dict: Extra kwargs to pass to get_last_k_interactions_grouped
|
|
272
|
+
"""
|
|
273
|
+
return {}
|
|
274
|
+
|
|
275
|
+
def _resolve_should_run_model(self) -> str:
|
|
276
|
+
"""
|
|
277
|
+
Resolve the model name for should_run/should_generate LLM checks.
|
|
278
|
+
|
|
279
|
+
Uses LLM config override if available, falls back to site var setting.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
str: Model name for the should_run check
|
|
283
|
+
"""
|
|
284
|
+
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
285
|
+
from reflexio.server.site_var.site_var_manager import SiteVarManager
|
|
286
|
+
|
|
287
|
+
root_config = self.request_context.configurator.get_config()
|
|
288
|
+
llm_config = root_config.llm_config if root_config else None
|
|
289
|
+
api_key_config = root_config.api_key_config if root_config else None
|
|
290
|
+
|
|
291
|
+
model_setting = SiteVarManager().get_site_var("llm_model_setting")
|
|
292
|
+
site_var = model_setting if isinstance(model_setting, dict) else {}
|
|
293
|
+
|
|
294
|
+
return resolve_model_name(
|
|
295
|
+
ModelRole.SHOULD_RUN,
|
|
296
|
+
site_var_value=site_var.get("should_run_model_name"),
|
|
297
|
+
config_override=llm_config.should_run_model_name if llm_config else None,
|
|
298
|
+
api_key_config=api_key_config,
|
|
299
|
+
)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Upgrade/downgrade status-change orchestration for ``BaseGenerationService``.
|
|
2
|
+
|
|
3
|
+
``StatusChangeMixin`` holds the public ``run_upgrade``/``run_downgrade`` template
|
|
4
|
+
methods plus the six status-change hooks subclasses override. This bucket touches
|
|
5
|
+
NONE of the per-run ``self`` fields and no billing — the cleanest boundary of the
|
|
6
|
+
decomposition. Method bodies are moved verbatim from the former monolithic
|
|
7
|
+
``base_generation_service.py``; ``run_upgrade``/``run_downgrade`` are PUBLIC (called
|
|
8
|
+
from ``lib/_profiles.py`` and ``lib/_user_playbook.py``) so their signatures are
|
|
9
|
+
byte-identical.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
from typing import Any, Generic, TypeVar
|
|
14
|
+
|
|
15
|
+
from reflexio.models.api_schema.service_schemas import Status
|
|
16
|
+
|
|
17
|
+
TRequest = TypeVar("TRequest")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StatusChangeOperation(StrEnum):
|
|
21
|
+
"""Operation type for upgrade/downgrade responses."""
|
|
22
|
+
|
|
23
|
+
UPGRADE = "upgrade"
|
|
24
|
+
DOWNGRADE = "downgrade"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StatusChangeMixin(Generic[TRequest]): # noqa: UP046
|
|
28
|
+
"""Upgrade/downgrade template methods + overridable status-change hooks.
|
|
29
|
+
|
|
30
|
+
Mixed into ``BaseGenerationService`` ahead of ``ABC``; every ``self.`` call in
|
|
31
|
+
these methods resolves to a sibling hook on this same mixin, so the mixin needs
|
|
32
|
+
no cross-mixin attribute stubs.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# ===============================
|
|
36
|
+
# Upgrade/Downgrade methods (optional - override to enable)
|
|
37
|
+
# ===============================
|
|
38
|
+
|
|
39
|
+
def _has_items_with_status(self, status: Status | None, request: TRequest) -> bool:
|
|
40
|
+
"""Check if items exist with given status and filters from request.
|
|
41
|
+
|
|
42
|
+
Override this method to enable upgrade/downgrade functionality for the service.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
status: The status to check for (None for CURRENT)
|
|
46
|
+
request: The upgrade/downgrade request object with filters
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bool: True if any matching items exist
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError("Upgrade/downgrade not supported by this service")
|
|
52
|
+
|
|
53
|
+
def _delete_items_by_status(self, status: Status, request: TRequest) -> int:
|
|
54
|
+
"""Delete items with given status matching request filters.
|
|
55
|
+
|
|
56
|
+
Override this method to enable upgrade/downgrade functionality for the service.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
status: The status of items to delete
|
|
60
|
+
request: The upgrade/downgrade request object with filters
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
int: Number of items deleted
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError("Upgrade/downgrade not supported by this service")
|
|
66
|
+
|
|
67
|
+
def _update_items_status(
|
|
68
|
+
self,
|
|
69
|
+
old_status: Status | None,
|
|
70
|
+
new_status: Status | None,
|
|
71
|
+
request: TRequest,
|
|
72
|
+
user_ids: list[str] | None = None,
|
|
73
|
+
) -> int:
|
|
74
|
+
"""Update items from old_status to new_status with request filters.
|
|
75
|
+
|
|
76
|
+
Override this method to enable upgrade/downgrade functionality for the service.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
old_status: The current status to match (None for CURRENT)
|
|
80
|
+
new_status: The new status to set (None for CURRENT)
|
|
81
|
+
request: The upgrade/downgrade request object with filters
|
|
82
|
+
user_ids: Optional pre-computed list of user IDs to filter by
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
int: Number of items updated
|
|
86
|
+
"""
|
|
87
|
+
raise NotImplementedError("Upgrade/downgrade not supported by this service")
|
|
88
|
+
|
|
89
|
+
def _get_affected_user_ids_for_upgrade(self, request: TRequest) -> list[str] | None: # noqa: ARG002
|
|
90
|
+
"""Get user IDs to filter by for upgrade operations.
|
|
91
|
+
|
|
92
|
+
Override this method to support the only_affected_users flag.
|
|
93
|
+
By default returns None (no filtering).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
request: The upgrade request object
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Optional[list[str]]: List of user IDs to filter by, or None for no filtering
|
|
100
|
+
"""
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _get_affected_user_ids_for_downgrade(
|
|
104
|
+
self,
|
|
105
|
+
request: TRequest, # noqa: ARG002
|
|
106
|
+
) -> list[str] | None:
|
|
107
|
+
"""Get user IDs to filter by for downgrade operations.
|
|
108
|
+
|
|
109
|
+
Override this method to support the only_affected_users flag.
|
|
110
|
+
By default returns None (no filtering).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
request: The downgrade request object
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Optional[list[str]]: List of user IDs to filter by, or None for no filtering
|
|
117
|
+
"""
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def _create_status_change_response(
|
|
121
|
+
self,
|
|
122
|
+
operation: StatusChangeOperation,
|
|
123
|
+
success: bool,
|
|
124
|
+
counts: dict,
|
|
125
|
+
msg: str,
|
|
126
|
+
) -> Any:
|
|
127
|
+
"""Create upgrade or downgrade response object based on operation type.
|
|
128
|
+
|
|
129
|
+
Override this method to enable upgrade/downgrade functionality for the service.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
operation: The operation type (UPGRADE or DOWNGRADE)
|
|
133
|
+
success: Whether the operation succeeded
|
|
134
|
+
counts: Dictionary of counts (upgrade: deleted/archived/promoted, downgrade: demoted/restored)
|
|
135
|
+
msg: Status message
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
A response object (e.g., UpgradeProfilesResponse, DowngradeUserPlaybooksResponse)
|
|
139
|
+
"""
|
|
140
|
+
raise NotImplementedError("Upgrade/downgrade not supported by this service")
|
|
141
|
+
|
|
142
|
+
def run_upgrade(self, request: TRequest) -> Any:
|
|
143
|
+
"""Run the upgrade workflow for the service.
|
|
144
|
+
|
|
145
|
+
This template method orchestrates the upgrade process:
|
|
146
|
+
1. Validate that pending items exist
|
|
147
|
+
2. Delete old archived items
|
|
148
|
+
3. Archive current items (None → ARCHIVED)
|
|
149
|
+
4. Promote pending items (PENDING → None/CURRENT)
|
|
150
|
+
|
|
151
|
+
Child classes must implement the hook methods to enable upgrade functionality:
|
|
152
|
+
- _has_items_with_status()
|
|
153
|
+
- _delete_items_by_status()
|
|
154
|
+
- _update_items_status()
|
|
155
|
+
- _create_status_change_response()
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
request: The upgrade request object with optional filters
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A response object with success status, counts, and message
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# 1. Validate pending items exist
|
|
165
|
+
if not self._has_items_with_status(Status.PENDING, request):
|
|
166
|
+
return self._create_status_change_response(
|
|
167
|
+
StatusChangeOperation.UPGRADE,
|
|
168
|
+
False,
|
|
169
|
+
{"deleted": 0, "archived": 0, "promoted": 0},
|
|
170
|
+
"No pending items found to upgrade",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Get affected user IDs once (child class determines the logic)
|
|
174
|
+
affected_user_ids = self._get_affected_user_ids_for_upgrade(request)
|
|
175
|
+
|
|
176
|
+
# 2. Delete old archived items (skip if archive_current=False)
|
|
177
|
+
deleted = 0
|
|
178
|
+
archived = 0
|
|
179
|
+
if getattr(request, "archive_current", True):
|
|
180
|
+
deleted = self._delete_items_by_status(Status.ARCHIVED, request)
|
|
181
|
+
|
|
182
|
+
# 3. Archive current items (None → ARCHIVED)
|
|
183
|
+
archived = self._update_items_status(
|
|
184
|
+
None, Status.ARCHIVED, request, user_ids=affected_user_ids
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# 4. Promote pending items (PENDING → None)
|
|
188
|
+
promoted = self._update_items_status(
|
|
189
|
+
Status.PENDING, None, request, user_ids=affected_user_ids
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
msg = f"Upgraded: {promoted} promoted, {archived} archived, {deleted} old archived deleted"
|
|
193
|
+
return self._create_status_change_response(
|
|
194
|
+
StatusChangeOperation.UPGRADE,
|
|
195
|
+
True,
|
|
196
|
+
{"deleted": deleted, "archived": archived, "promoted": promoted},
|
|
197
|
+
msg,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return self._create_status_change_response(
|
|
202
|
+
StatusChangeOperation.UPGRADE,
|
|
203
|
+
False,
|
|
204
|
+
{"deleted": 0, "archived": 0, "promoted": 0},
|
|
205
|
+
f"Failed to upgrade: {str(e)}",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def run_downgrade(self, request: TRequest) -> Any:
|
|
209
|
+
"""Run the downgrade workflow for the service.
|
|
210
|
+
|
|
211
|
+
This template method orchestrates the downgrade process:
|
|
212
|
+
1. Validate that archived items exist
|
|
213
|
+
2. Demote current items (None → ARCHIVE_IN_PROGRESS)
|
|
214
|
+
3. Restore archived items (ARCHIVED → None/CURRENT)
|
|
215
|
+
4. Complete archiving (ARCHIVE_IN_PROGRESS → ARCHIVED)
|
|
216
|
+
|
|
217
|
+
Child classes must implement the hook methods to enable downgrade functionality:
|
|
218
|
+
- _has_items_with_status()
|
|
219
|
+
- _update_items_status()
|
|
220
|
+
- _create_status_change_response()
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
request: The downgrade request object with optional filters
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A response object with success status, counts, and message
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
# 1. Validate archived items exist
|
|
230
|
+
if not self._has_items_with_status(Status.ARCHIVED, request):
|
|
231
|
+
return self._create_status_change_response(
|
|
232
|
+
StatusChangeOperation.DOWNGRADE,
|
|
233
|
+
False,
|
|
234
|
+
{"demoted": 0, "restored": 0},
|
|
235
|
+
"No archived items found to restore",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Get affected user IDs once (child class determines the logic)
|
|
239
|
+
affected_user_ids = self._get_affected_user_ids_for_downgrade(request)
|
|
240
|
+
|
|
241
|
+
# 2. Demote current (None → ARCHIVE_IN_PROGRESS)
|
|
242
|
+
demoted = self._update_items_status(
|
|
243
|
+
None, Status.ARCHIVE_IN_PROGRESS, request, user_ids=affected_user_ids
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# 3. Restore archived (ARCHIVED → None)
|
|
247
|
+
restored = self._update_items_status(
|
|
248
|
+
Status.ARCHIVED, None, request, user_ids=affected_user_ids
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# 4. Complete archiving (ARCHIVE_IN_PROGRESS → ARCHIVED)
|
|
252
|
+
self._update_items_status(
|
|
253
|
+
Status.ARCHIVE_IN_PROGRESS,
|
|
254
|
+
Status.ARCHIVED,
|
|
255
|
+
request,
|
|
256
|
+
user_ids=affected_user_ids,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
msg = f"Downgraded: {demoted} archived, {restored} restored"
|
|
260
|
+
return self._create_status_change_response(
|
|
261
|
+
StatusChangeOperation.DOWNGRADE,
|
|
262
|
+
True,
|
|
263
|
+
{"demoted": demoted, "restored": restored},
|
|
264
|
+
msg,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
return self._create_status_change_response(
|
|
269
|
+
StatusChangeOperation.DOWNGRADE,
|
|
270
|
+
False,
|
|
271
|
+
{"demoted": 0, "restored": 0},
|
|
272
|
+
f"Failed to downgrade: {str(e)}",
|
|
273
|
+
)
|