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
|
@@ -5,6 +5,7 @@ Base class for generation services
|
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
7
|
import time
|
|
8
|
+
import unicodedata
|
|
8
9
|
import uuid
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
from dataclasses import dataclass
|
|
@@ -25,6 +26,10 @@ from reflexio.server.services.base_generation import (
|
|
|
25
26
|
from reflexio.server.services.base_generation import (
|
|
26
27
|
StatusChangeOperation as StatusChangeOperation, # re-export for back-compat
|
|
27
28
|
)
|
|
29
|
+
from reflexio.server.services.deferred_learning_plan import (
|
|
30
|
+
ExtractorBookmarkAdvance,
|
|
31
|
+
GenerationComputePlan,
|
|
32
|
+
)
|
|
28
33
|
from reflexio.server.services.extractor_config_utils import (
|
|
29
34
|
get_extractor_name,
|
|
30
35
|
)
|
|
@@ -38,10 +43,11 @@ class ExtractorExecutionError(RuntimeError):
|
|
|
38
43
|
logger = logging.getLogger(__name__)
|
|
39
44
|
|
|
40
45
|
|
|
41
|
-
# Cheap-signal
|
|
46
|
+
# Cheap-signal threshold for the pre-LLM should_run filter. Tuned for
|
|
42
47
|
# coding-assistant traffic where most turns are either slash commands
|
|
43
48
|
# or tool scaffolding, and where the LLM should_run gate costs 5–7s
|
|
44
|
-
# even when it ultimately votes False.
|
|
49
|
+
# even when it ultimately votes False. Non-Latin letters and numbers
|
|
50
|
+
# count twice because many scripts carry more meaning per character.
|
|
45
51
|
_MIN_USER_CONTENT_LEN = 30
|
|
46
52
|
# Heuristic match for reflexio's own extractor system prompts that
|
|
47
53
|
# sometimes leak into the corpus via the claude-code LLM provider's
|
|
@@ -76,17 +82,47 @@ def _is_pure_slash_command(content: str) -> bool:
|
|
|
76
82
|
def _iter_user_contents(
|
|
77
83
|
session_data_models: list[RequestInteractionDataModel],
|
|
78
84
|
) -> list[str]:
|
|
79
|
-
"""Collect
|
|
85
|
+
"""Collect non-empty user-authored content, preserving interaction order.
|
|
86
|
+
|
|
87
|
+
Role matching is case-insensitive because API and CLI publishers use
|
|
88
|
+
different casing. These contents feed user-text-only heuristics; they do
|
|
89
|
+
not determine stride or window eligibility.
|
|
90
|
+
"""
|
|
80
91
|
out: list[str] = []
|
|
81
92
|
for model in session_data_models:
|
|
82
93
|
out.extend(
|
|
83
94
|
interaction.content
|
|
84
95
|
for interaction in model.interactions
|
|
85
|
-
if interaction.role == "
|
|
96
|
+
if interaction.role.casefold() == "user" and interaction.content
|
|
86
97
|
)
|
|
87
98
|
return out
|
|
88
99
|
|
|
89
100
|
|
|
101
|
+
def _weighted_content_length(content: str) -> int:
|
|
102
|
+
"""Count original non-Latin letters and numbers twice.
|
|
103
|
+
|
|
104
|
+
Normalize each code point only for script classification so compatibility
|
|
105
|
+
forms such as full-width Latin stay unweighted without changing the
|
|
106
|
+
original content length.
|
|
107
|
+
"""
|
|
108
|
+
stripped = content.strip()
|
|
109
|
+
|
|
110
|
+
def is_non_latin_letter_or_number(char: str) -> bool:
|
|
111
|
+
normalized_chars = [
|
|
112
|
+
normalized_char
|
|
113
|
+
for normalized_char in unicodedata.normalize("NFKC", char)
|
|
114
|
+
if unicodedata.category(normalized_char)[0] in {"L", "N"}
|
|
115
|
+
]
|
|
116
|
+
return bool(normalized_chars) and all(
|
|
117
|
+
not normalized_char.isascii()
|
|
118
|
+
and "LATIN" not in unicodedata.name(normalized_char, "")
|
|
119
|
+
for normalized_char in normalized_chars
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
non_latin_bonus = sum(1 for char in stripped if is_non_latin_letter_or_number(char))
|
|
123
|
+
return len(stripped) + non_latin_bonus
|
|
124
|
+
|
|
125
|
+
|
|
90
126
|
def _cheap_should_run_reject(
|
|
91
127
|
session_data_models: list[RequestInteractionDataModel],
|
|
92
128
|
) -> str | None:
|
|
@@ -97,9 +133,14 @@ def _cheap_should_run_reject(
|
|
|
97
133
|
LLM call. Returns None when we cannot decide cheaply and the LLM
|
|
98
134
|
should run.
|
|
99
135
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
136
|
+
When the scoped batch has no non-empty user-authored content, these
|
|
137
|
+
user-text-only heuristics are inapplicable and the batch falls through to
|
|
138
|
+
the LLM gate. Stride and window eligibility are evaluated independently.
|
|
139
|
+
|
|
140
|
+
Rejection rules when user-authored content is present:
|
|
141
|
+
- No user message reaches ``_MIN_USER_CONTENT_LEN`` weighted
|
|
142
|
+
characters (purely short commands / confirmations). Non-Latin
|
|
143
|
+
letters and numbers count twice.
|
|
103
144
|
- Every user message is a bare slash-command dispatch with no
|
|
104
145
|
substantive trailing text (e.g. ``/commit``, ``/review``,
|
|
105
146
|
``/claude-smart:tag``). Slash commands that carry user text
|
|
@@ -116,14 +157,18 @@ def _cheap_should_run_reject(
|
|
|
116
157
|
"""
|
|
117
158
|
user_contents = _iter_user_contents(session_data_models)
|
|
118
159
|
if not user_contents:
|
|
119
|
-
return
|
|
160
|
+
return None
|
|
120
161
|
|
|
121
162
|
for content in user_contents:
|
|
122
163
|
lowered = content.lstrip().lower()
|
|
123
164
|
if any(lowered.startswith(p) for p in _EXTRACTOR_PROMPT_PREFIXES):
|
|
124
165
|
return "extractor_prompt_echo"
|
|
125
166
|
|
|
126
|
-
if not any(
|
|
167
|
+
if not any(
|
|
168
|
+
len(content.strip()) >= _MIN_USER_CONTENT_LEN
|
|
169
|
+
or _weighted_content_length(content) >= _MIN_USER_CONTENT_LEN
|
|
170
|
+
for content in user_contents
|
|
171
|
+
):
|
|
127
172
|
return "all_user_turns_too_short"
|
|
128
173
|
|
|
129
174
|
if all(_is_pure_slash_command(c) for c in user_contents):
|
|
@@ -159,8 +204,9 @@ class BaseGenerationService(
|
|
|
159
204
|
ABC,
|
|
160
205
|
Generic[TExtractorConfig, TExtractor, TGenerationServiceConfig, TRequest], # noqa: UP046
|
|
161
206
|
):
|
|
162
|
-
# Only profile/playbook GENERATION services
|
|
163
|
-
#
|
|
207
|
+
# Only profile/playbook GENERATION services emit extraction-run billing here.
|
|
208
|
+
# Non-extraction learning mutation paths emit their value facet at their own
|
|
209
|
+
# durable-success point.
|
|
164
210
|
# Default is False so any future subclass is safe by default (opt-IN).
|
|
165
211
|
EMITS_LEARNING_BILLING: bool = False
|
|
166
212
|
"""
|
|
@@ -208,6 +254,9 @@ class BaseGenerationService(
|
|
|
208
254
|
}
|
|
209
255
|
self._last_extraction_run_ids: list[str] = []
|
|
210
256
|
self._last_token_totals: RunTokenTotals | None = None
|
|
257
|
+
# Stride-bookmark advance deferred off the extractor (F1); captured in
|
|
258
|
+
# ``_execute_extractor`` and applied in the persist half of the run.
|
|
259
|
+
self._last_bookmark_advance: ExtractorBookmarkAdvance | None = None
|
|
211
260
|
# Window fetched by the should-run gate (_collect_scoped_interactions_for_precheck),
|
|
212
261
|
# stashed so the billing path (_extraction_input_text) can reuse it instead of
|
|
213
262
|
# re-querying storage. None when the gate did not run (bypass paths).
|
|
@@ -428,7 +477,7 @@ class BaseGenerationService(
|
|
|
428
477
|
|
|
429
478
|
# Get scope ID and request ID for in-progress tracking
|
|
430
479
|
scope_id = self._get_lock_scope_id(request)
|
|
431
|
-
|
|
480
|
+
lock_request_id = getattr(request, "request_id", None) or str(uuid.uuid4())
|
|
432
481
|
|
|
433
482
|
state_manager = self._create_state_manager()
|
|
434
483
|
|
|
@@ -439,7 +488,7 @@ class BaseGenerationService(
|
|
|
439
488
|
# never get extracted.
|
|
440
489
|
my_payload = self._serialize_request_for_queue(request)
|
|
441
490
|
if not state_manager.acquire_lock(
|
|
442
|
-
|
|
491
|
+
lock_request_id, scope_id=scope_id, payload=my_payload
|
|
443
492
|
):
|
|
444
493
|
return # Another operation is running, we've enqueued ourselves
|
|
445
494
|
|
|
@@ -464,21 +513,21 @@ class BaseGenerationService(
|
|
|
464
513
|
# request's ID + payload so the rerun runs against THAT
|
|
465
514
|
# publish's data, not the original holder's.
|
|
466
515
|
next_entry = state_manager.release_lock_pop_queue(
|
|
467
|
-
|
|
516
|
+
lock_request_id, scope_id=scope_id
|
|
468
517
|
)
|
|
469
518
|
|
|
470
519
|
if next_entry is None:
|
|
471
520
|
break # Queue empty — we're done
|
|
472
521
|
|
|
473
|
-
|
|
522
|
+
next_lock_request_id = next_entry["request_id"]
|
|
474
523
|
next_payload = next_entry.get("payload")
|
|
475
524
|
|
|
476
525
|
logger.info(
|
|
477
|
-
"Draining queued %s request:
|
|
526
|
+
"Draining queued %s request: prev_lock_request_id=%s, next_lock_request_id=%s, "
|
|
478
527
|
"payload_present=%s",
|
|
479
528
|
self._get_service_name(),
|
|
480
|
-
|
|
481
|
-
|
|
529
|
+
lock_request_id,
|
|
530
|
+
next_lock_request_id,
|
|
482
531
|
next_payload is not None,
|
|
483
532
|
)
|
|
484
533
|
|
|
@@ -492,7 +541,7 @@ class BaseGenerationService(
|
|
|
492
541
|
else:
|
|
493
542
|
current_request = request
|
|
494
543
|
|
|
495
|
-
|
|
544
|
+
lock_request_id = next_lock_request_id
|
|
496
545
|
|
|
497
546
|
except Exception:
|
|
498
547
|
# Clear lock on error to prevent deadlock
|
|
@@ -500,69 +549,29 @@ class BaseGenerationService(
|
|
|
500
549
|
raise
|
|
501
550
|
|
|
502
551
|
def _run_generation(self, request: TRequest) -> None:
|
|
503
|
-
"""
|
|
504
|
-
|
|
552
|
+
"""Run one generation synchronously: compute -> persist -> emit.
|
|
553
|
+
|
|
554
|
+
Thin wrapper preserving the pre-split behavior for the synchronous
|
|
555
|
+
``.run()`` / manual / rerun callers: compute (extractor + dedup +
|
|
556
|
+
embeddings), persist (row writes + the extractor stride-bookmark
|
|
557
|
+
advance) and post-commit side-effects (telemetry + billing) run together
|
|
558
|
+
with **no** external ``commit_scope``. Only the durable worker (Task 8/9)
|
|
559
|
+
drives ``compute_generation`` / ``persist_generation`` /
|
|
560
|
+
``emit_generation_side_effects`` separately across its fenced scope.
|
|
505
561
|
|
|
506
|
-
|
|
507
|
-
|
|
562
|
+
The outer ``except`` records ``generation_failed`` and re-raises
|
|
563
|
+
``ExtractorExecutionError`` exactly as before.
|
|
508
564
|
|
|
509
565
|
Args:
|
|
510
566
|
request: The request object containing parameters
|
|
511
567
|
"""
|
|
512
|
-
if not request:
|
|
513
|
-
logger.error("Received None request for %s", self._get_service_name())
|
|
514
|
-
return
|
|
515
|
-
|
|
516
568
|
generation_start = time.perf_counter()
|
|
517
569
|
try:
|
|
518
|
-
|
|
519
|
-
if
|
|
570
|
+
plan = self.compute_generation(request)
|
|
571
|
+
if plan is None:
|
|
520
572
|
return
|
|
521
|
-
|
|
522
|
-
self.
|
|
523
|
-
event_name="generation_started",
|
|
524
|
-
outcome="started",
|
|
525
|
-
count_value=1,
|
|
526
|
-
metadata={
|
|
527
|
-
"identifier": prepared.identifier,
|
|
528
|
-
"extractor_name": prepared.extractor_name,
|
|
529
|
-
},
|
|
530
|
-
)
|
|
531
|
-
self._last_extraction_run_ids = []
|
|
532
|
-
self._last_token_totals = None
|
|
533
|
-
result = self._execute_extractor(
|
|
534
|
-
prepared.extractor_config, prepared.identifier
|
|
535
|
-
)
|
|
536
|
-
generated_count = self._count_generated_results(result)
|
|
537
|
-
|
|
538
|
-
try:
|
|
539
|
-
if result:
|
|
540
|
-
self._process_results([result])
|
|
541
|
-
self._finalize_extraction_runs()
|
|
542
|
-
except Exception as exc:
|
|
543
|
-
self._mark_extraction_runs_finalization_failed(exc)
|
|
544
|
-
raise
|
|
545
|
-
|
|
546
|
-
self._record_generation_event(
|
|
547
|
-
event_name="generation_succeeded",
|
|
548
|
-
outcome="success",
|
|
549
|
-
count_value=generated_count,
|
|
550
|
-
duration_ms=int((time.perf_counter() - generation_start) * 1000),
|
|
551
|
-
metadata={
|
|
552
|
-
"identifier": prepared.identifier,
|
|
553
|
-
"extractor_name": prepared.extractor_name,
|
|
554
|
-
"extractor_failed": bool(
|
|
555
|
-
self._last_extractor_run_stats.get("failed")
|
|
556
|
-
),
|
|
557
|
-
"extractor_timed_out": bool(
|
|
558
|
-
self._last_extractor_run_stats.get("timed_out")
|
|
559
|
-
),
|
|
560
|
-
},
|
|
561
|
-
)
|
|
562
|
-
self._record_billing_learning_events(
|
|
563
|
-
prepared=prepared, generated_count=generated_count
|
|
564
|
-
)
|
|
565
|
-
|
|
573
|
+
self.persist_generation(plan)
|
|
574
|
+
self.emit_generation_side_effects(plan)
|
|
566
575
|
except Exception as e:
|
|
567
576
|
self._record_generation_event(
|
|
568
577
|
event_name="generation_failed",
|
|
@@ -579,6 +588,178 @@ class BaseGenerationService(
|
|
|
579
588
|
if isinstance(e, ExtractorExecutionError):
|
|
580
589
|
raise
|
|
581
590
|
|
|
591
|
+
def compute_generation(self, request: TRequest) -> GenerationComputePlan | None:
|
|
592
|
+
"""Compute half of one generation run — NO learning DB write, NO fence.
|
|
593
|
+
|
|
594
|
+
Runs the prepare gate, the extractor (thread-pool LLM tool-loop), dedup
|
|
595
|
+
+ embedding resolution (``_resolve_write_plan``), and drives the
|
|
596
|
+
``agent_run`` rows to their terminal state (``_finalize_extraction_runs``
|
|
597
|
+
— agent_run only, §4.3). Snapshots the billing inputs onto the returned
|
|
598
|
+
plan so ``emit_generation_side_effects`` never depends on the reused
|
|
599
|
+
instance's mutable ``_last_*``.
|
|
600
|
+
|
|
601
|
+
Returns a resolved ``GenerationComputePlan`` for persist/emit, or
|
|
602
|
+
``None`` when the prepare gate closes (nothing to run). Raises
|
|
603
|
+
``ExtractorExecutionError`` on extractor failure — the caller
|
|
604
|
+
(``_run_generation`` / the durable worker) records ``generation_failed``.
|
|
605
|
+
"""
|
|
606
|
+
if not request:
|
|
607
|
+
logger.error("Received None request for %s", self._get_service_name())
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
generation_start = time.perf_counter()
|
|
611
|
+
prepared = self._prepare_generation_run(request)
|
|
612
|
+
if prepared is None:
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
self._record_generation_event(
|
|
616
|
+
event_name="generation_started",
|
|
617
|
+
outcome="started",
|
|
618
|
+
count_value=1,
|
|
619
|
+
metadata={
|
|
620
|
+
"identifier": prepared.identifier,
|
|
621
|
+
"extractor_name": prepared.extractor_name,
|
|
622
|
+
},
|
|
623
|
+
)
|
|
624
|
+
self._last_extraction_run_ids = []
|
|
625
|
+
self._last_token_totals = None
|
|
626
|
+
self._last_bookmark_advance = None
|
|
627
|
+
result = self._execute_extractor(prepared.extractor_config, prepared.identifier)
|
|
628
|
+
generated_count = self._count_generated_results(result)
|
|
629
|
+
|
|
630
|
+
try:
|
|
631
|
+
write_plan = self._resolve_write_plan([result]) if result else None
|
|
632
|
+
self._finalize_extraction_runs()
|
|
633
|
+
except Exception as exc:
|
|
634
|
+
self._mark_extraction_runs_finalization_failed(exc)
|
|
635
|
+
raise
|
|
636
|
+
|
|
637
|
+
return GenerationComputePlan(
|
|
638
|
+
prepared=prepared,
|
|
639
|
+
generated_count=generated_count,
|
|
640
|
+
write_plan=write_plan,
|
|
641
|
+
bookmark_advance=self._last_bookmark_advance,
|
|
642
|
+
generation_start=generation_start,
|
|
643
|
+
extraction_run_ids=list(self._last_extraction_run_ids),
|
|
644
|
+
token_totals=self._last_token_totals,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
def persist_generation(self, plan: GenerationComputePlan) -> None:
|
|
648
|
+
"""Persist half — apply the write-plan + the extractor bookmark advance.
|
|
649
|
+
|
|
650
|
+
This is the ONLY part that runs inside the durable worker's fenced
|
|
651
|
+
``commit_scope`` (and inline on the synchronous ``.run()`` path). It
|
|
652
|
+
issues only the resolved write-plan's row writes (embeddings already
|
|
653
|
+
precomputed in compute for Tasks 6-7) and the extractor stride-bookmark
|
|
654
|
+
advance — **no** events, **no** billing, **no** LLM / embedding compute.
|
|
655
|
+
|
|
656
|
+
The bookmark advance is applied HERE so that BOTH the synchronous
|
|
657
|
+
``.run()`` path and the durable persist fence advance the bookmark
|
|
658
|
+
atomically with the row writes it corresponds to (F1) — and it is
|
|
659
|
+
applied in exactly one place, so it is never double-applied.
|
|
660
|
+
"""
|
|
661
|
+
if plan.write_plan is not None:
|
|
662
|
+
self._persist_write_plan(plan.write_plan)
|
|
663
|
+
self._apply_bookmark_advance(plan.bookmark_advance)
|
|
664
|
+
|
|
665
|
+
def emit_generation_side_effects(self, plan: GenerationComputePlan) -> None:
|
|
666
|
+
"""Post-commit side-effects — telemetry + ② Learning billing.
|
|
667
|
+
|
|
668
|
+
Runs only for a fence-winning job (the durable worker calls it after the
|
|
669
|
+
scope commits; ``.run()`` calls it inline). Reads the plan's compute-time
|
|
670
|
+
snapshot (``generated_count`` / ``prepared`` / ``generation_start``) so a
|
|
671
|
+
fence-lost job never emits.
|
|
672
|
+
|
|
673
|
+
Billing purity note (round-2 finding): ``_record_billing_learning_events``
|
|
674
|
+
also reads ``self._last_token_totals`` / ``self._last_precheck_sessions``,
|
|
675
|
+
and the ``generation_succeeded`` metadata reads ``self._last_extractor_run_stats``.
|
|
676
|
+
These are NOT re-plumbed to the plan — the money helper lives in a
|
|
677
|
+
separate mixin behind a documented patch seam (``_usage_billing.py`` SINK-3)
|
|
678
|
+
and re-plumbing it would touch the money path for zero behavior change.
|
|
679
|
+
It is safe under the **single-use-instance invariant**: every generation
|
|
680
|
+
service instance runs exactly one compute→persist→emit for one job with
|
|
681
|
+
no interleaving (``.run()`` is synchronous; the durable orchestration in
|
|
682
|
+
Task 8 holds one ``(instance, plan)`` pair per profile/playbook service
|
|
683
|
+
and never re-runs compute on that instance before emit), so ``self._last_*``
|
|
684
|
+
is exactly as compute left it when emit runs. The plan still snapshots the
|
|
685
|
+
billing inputs so a future durable reuse can re-plumb locally.
|
|
686
|
+
"""
|
|
687
|
+
self._record_generation_event(
|
|
688
|
+
event_name="generation_succeeded",
|
|
689
|
+
outcome="success",
|
|
690
|
+
count_value=plan.generated_count,
|
|
691
|
+
duration_ms=int((time.perf_counter() - plan.generation_start) * 1000),
|
|
692
|
+
metadata={
|
|
693
|
+
"identifier": plan.prepared.identifier,
|
|
694
|
+
"extractor_name": plan.prepared.extractor_name,
|
|
695
|
+
"extractor_failed": bool(self._last_extractor_run_stats.get("failed")),
|
|
696
|
+
"extractor_timed_out": bool(
|
|
697
|
+
self._last_extractor_run_stats.get("timed_out")
|
|
698
|
+
),
|
|
699
|
+
},
|
|
700
|
+
)
|
|
701
|
+
self._record_billing_learning_events(
|
|
702
|
+
prepared=plan.prepared, generated_count=plan.generated_count
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
@abstractmethod
|
|
706
|
+
def _resolve_write_plan(self, results: list) -> Any | None:
|
|
707
|
+
"""Compute-half of item finalization — resolve a write-plan, NO DB write.
|
|
708
|
+
|
|
709
|
+
Runs the dedup + source/status assignment + embedding precompute and
|
|
710
|
+
returns a resolved write-plan (or ``None`` when there is nothing to
|
|
711
|
+
write). Issues NO learning DB write — the write is the persist half's
|
|
712
|
+
job (``_persist_write_plan``), so this stays inside the compute purity
|
|
713
|
+
contract (the compute-write-tripwire contract test).
|
|
714
|
+
|
|
715
|
+
The durable-split ``ProfileGenerationService`` /
|
|
716
|
+
``PlaybookGenerationService`` return a real ``ProfileWritePlan`` /
|
|
717
|
+
``PlaybookWritePlan`` (Tasks 6-7). ``AgentSuccessEvaluationService`` was
|
|
718
|
+
never split into a durable persist path — it keeps a concrete override
|
|
719
|
+
that writes in compute via its permanent ``_finalize_extracted_items``
|
|
720
|
+
wrapper (its results never flow through the durable worker fence).
|
|
721
|
+
"""
|
|
722
|
+
|
|
723
|
+
@abstractmethod
|
|
724
|
+
def _persist_write_plan(self, plan: Any) -> None:
|
|
725
|
+
"""Persist-half of item finalization — apply the resolved write-plan.
|
|
726
|
+
|
|
727
|
+
Issues only the fence-critical row writes for a plan produced by
|
|
728
|
+
``_resolve_write_plan`` (embeddings already precomputed in compute), with
|
|
729
|
+
NO LLM / embedding / dedup. This is the only item-finalization work that
|
|
730
|
+
runs inside the durable worker's fenced ``commit_scope``.
|
|
731
|
+
|
|
732
|
+
``AgentSuccessEvaluationService`` (never split) implements this as a
|
|
733
|
+
no-op — its ``_resolve_write_plan`` performs the write in compute and
|
|
734
|
+
returns ``None`` so ``persist_generation`` never invokes this.
|
|
735
|
+
"""
|
|
736
|
+
|
|
737
|
+
def _apply_bookmark_advance(self, advance: ExtractorBookmarkAdvance | None) -> None:
|
|
738
|
+
"""Apply the deferred extractor stride-bookmark advance (F1).
|
|
739
|
+
|
|
740
|
+
The extractor emits its bookmark advance on the ``ExtractionOutcome``
|
|
741
|
+
rather than self-advancing; ``_execute_extractor`` captures it and
|
|
742
|
+
``compute_generation`` snapshots it onto the ``GenerationComputePlan``.
|
|
743
|
+
This applies it via ``update_extractor_bookmark`` using the same
|
|
744
|
+
OperationStateManager service name the extractor used, so the bookmark
|
|
745
|
+
key is byte-identical. No-op when the extractor produced no output
|
|
746
|
+
(``advance`` is ``None``) or the service has no stride bookmark.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
advance: The bookmark advance from ``plan.bookmark_advance``.
|
|
750
|
+
"""
|
|
751
|
+
if advance is None or self.storage is None:
|
|
752
|
+
return
|
|
753
|
+
service_name = self._get_extractor_state_service_name()
|
|
754
|
+
if service_name is None:
|
|
755
|
+
return
|
|
756
|
+
manager = OperationStateManager(self.storage, self.org_id, service_name)
|
|
757
|
+
manager.update_extractor_bookmark(
|
|
758
|
+
extractor_name=advance.extractor_name,
|
|
759
|
+
processed_interactions=advance.processed_interactions,
|
|
760
|
+
user_id=advance.user_id,
|
|
761
|
+
)
|
|
762
|
+
|
|
582
763
|
def _prepare_generation_run(
|
|
583
764
|
self, request: TRequest
|
|
584
765
|
) -> PreparedGenerationRun[TExtractorConfig] | None:
|
|
@@ -8,14 +8,86 @@ ProfileConsolidator and PlaybookConsolidator.
|
|
|
8
8
|
import logging
|
|
9
9
|
from abc import ABC
|
|
10
10
|
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any, cast
|
|
11
12
|
|
|
12
13
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
13
14
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
14
15
|
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
16
|
+
from reflexio.server.services.embedding_text import embedding_input
|
|
15
17
|
from reflexio.server.site_var.site_var_manager import SiteVarManager
|
|
16
18
|
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
def resolve_dedup_query_embeddings(
|
|
23
|
+
storage: Any,
|
|
24
|
+
client: LiteLLMClient,
|
|
25
|
+
query_texts: list[str],
|
|
26
|
+
*,
|
|
27
|
+
entity_label: str,
|
|
28
|
+
) -> list[list[float] | None]:
|
|
29
|
+
"""Embed dedup-search queries with the model that indexed the store.
|
|
30
|
+
|
|
31
|
+
Prefers the storage's own embedding path (correct model + "query" prefix);
|
|
32
|
+
falls back to ``client`` pinned to the storage's model/dimensions. Letting
|
|
33
|
+
the client resolve its default embedding model would be wrong here: it
|
|
34
|
+
picks the OSS default, which the enterprise embedding daemon rejects
|
|
35
|
+
(409 model conflict) and which would not match the indexed vectors anyway.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
storage: Storage backend (duck-typed: ``_get_embedding`` preferred,
|
|
39
|
+
else ``embedding_model_name`` / ``embedding_dimensions``).
|
|
40
|
+
client: Shared LLM client, used only in the fallback path.
|
|
41
|
+
query_texts: Query strings to embed.
|
|
42
|
+
entity_label: Log prefix identifying the caller, e.g. "Profile".
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
One embedding (or None) per query text, in input order. On any
|
|
46
|
+
failure, all entries are None so the caller degrades to text-only
|
|
47
|
+
search. A backend that signals embedding-service unavailability with
|
|
48
|
+
an empty vector (e.g. the Supabase query path) is normalized to None
|
|
49
|
+
so search falls back to its own embedding/FTS path instead of sending
|
|
50
|
+
an empty vector to the database.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
get_storage_embedding = getattr(storage, "_get_embedding", None)
|
|
54
|
+
if callable(get_storage_embedding):
|
|
55
|
+
logger.info(
|
|
56
|
+
"%s dedup query embeddings: source=storage model=%s",
|
|
57
|
+
entity_label,
|
|
58
|
+
getattr(storage, "embedding_model_name", "unknown"),
|
|
59
|
+
)
|
|
60
|
+
embeddings = [
|
|
61
|
+
cast(
|
|
62
|
+
"list[float] | None",
|
|
63
|
+
get_storage_embedding(query_text, purpose="query"),
|
|
64
|
+
)
|
|
65
|
+
for query_text in query_texts
|
|
66
|
+
]
|
|
67
|
+
else:
|
|
68
|
+
embedding_model_name = storage.embedding_model_name
|
|
69
|
+
embedding_dimensions = storage.embedding_dimensions
|
|
70
|
+
logger.info(
|
|
71
|
+
"%s dedup query embeddings: source=llm_client model=%s",
|
|
72
|
+
entity_label,
|
|
73
|
+
embedding_model_name,
|
|
74
|
+
)
|
|
75
|
+
embeddings = list(
|
|
76
|
+
client.get_embeddings(
|
|
77
|
+
[
|
|
78
|
+
embedding_input(query_text, purpose="query")
|
|
79
|
+
for query_text in query_texts
|
|
80
|
+
],
|
|
81
|
+
model=embedding_model_name,
|
|
82
|
+
dimensions=embedding_dimensions,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.warning("Failed to generate embeddings for dedup search: %s", e)
|
|
87
|
+
return [None] * len(query_texts)
|
|
88
|
+
return [emb or None for emb in embeddings]
|
|
89
|
+
|
|
90
|
+
|
|
19
91
|
# Format used for "Last Modified" timestamps shown to deduplication LLMs.
|
|
20
92
|
# Includes hours and minutes so same-day contradictions (morning vs evening)
|
|
21
93
|
# can be distinguished.
|