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
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"""Abstract agent success evaluation result store declarations."""
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from reflexio.models.api_schema.domain import (
|
|
6
7
|
AgentSuccessEvaluationResult,
|
|
8
|
+
RetrievedLearningEvaluationResult,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ..retrieved_learning_state import (
|
|
12
|
+
DEFAULT_TRANSCRIPT_CHAR_LIMIT,
|
|
13
|
+
BoundedRetrievedLearningSnapshot,
|
|
14
|
+
RetrievedLearningCommitResult,
|
|
7
15
|
)
|
|
8
16
|
|
|
9
17
|
|
|
@@ -122,3 +130,166 @@ class AgentEvaluationResultStoreMixin:
|
|
|
122
130
|
int: Number of rows deleted.
|
|
123
131
|
"""
|
|
124
132
|
raise NotImplementedError
|
|
133
|
+
|
|
134
|
+
# ==============================
|
|
135
|
+
# Retrieved-learning evaluation methods
|
|
136
|
+
# ==============================
|
|
137
|
+
#
|
|
138
|
+
# Concurrency contract (see storage_base/retrieved_learning_state.py):
|
|
139
|
+
# generation fencing + session-fingerprint CAS recomputed under the
|
|
140
|
+
# replacement transaction's lock. No interaction write/delete path is
|
|
141
|
+
# instrumented.
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def begin_retrieved_learning_evaluation_run(
|
|
145
|
+
self, user_id: str, session_id: str
|
|
146
|
+
) -> int:
|
|
147
|
+
"""Allocate the next evaluation generation for one session.
|
|
148
|
+
|
|
149
|
+
Atomically increments (creating on first use) the ``generation``
|
|
150
|
+
counter in the session's ``_operation_state`` row after asserting the
|
|
151
|
+
governance subject is writable. Concurrent callers receive distinct
|
|
152
|
+
generations.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
user_id (str): Session owner.
|
|
156
|
+
session_id (str): Evaluated session.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
int: The allocated generation (monotonically increasing per
|
|
160
|
+
session).
|
|
161
|
+
"""
|
|
162
|
+
raise NotImplementedError
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def load_bounded_retrieved_learning_snapshot(
|
|
166
|
+
self,
|
|
167
|
+
user_id: str,
|
|
168
|
+
session_id: str,
|
|
169
|
+
raw_ref_limit: int = 5_000,
|
|
170
|
+
transcript_char_limit: int = DEFAULT_TRANSCRIPT_CHAR_LIMIT,
|
|
171
|
+
) -> BoundedRetrievedLearningSnapshot:
|
|
172
|
+
"""Load the bounded session projection for evaluation.
|
|
173
|
+
|
|
174
|
+
Streams interaction rows (no ``Interaction`` objects, no embeddings or
|
|
175
|
+
image encodings) and counts raw attachment occurrences before
|
|
176
|
+
appending; on occurrence ``raw_ref_limit + 1`` the scan aborts with
|
|
177
|
+
``attachment_limit_exceeded=True``.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
user_id (str): Session owner.
|
|
181
|
+
session_id (str): Evaluated session.
|
|
182
|
+
raw_ref_limit (int): Maximum raw attachment occurrences to parse.
|
|
183
|
+
transcript_char_limit (int): Maximum transcript characters retained.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
BoundedRetrievedLearningSnapshot: The bounded projection.
|
|
187
|
+
"""
|
|
188
|
+
raise NotImplementedError
|
|
189
|
+
|
|
190
|
+
@abstractmethod
|
|
191
|
+
def get_matching_retrieved_learning_terminal_state(
|
|
192
|
+
self, user_id: str, session_id: str, session_fingerprint: str
|
|
193
|
+
) -> dict[str, Any] | None:
|
|
194
|
+
"""Return the session's terminal evaluation state if still fresh.
|
|
195
|
+
|
|
196
|
+
Implementations must use a writer transaction and recompute the live
|
|
197
|
+
session fingerprint in that transaction. A match requires terminal
|
|
198
|
+
status and equality among the supplied, persisted, and live
|
|
199
|
+
fingerprints.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
user_id (str): Session owner.
|
|
203
|
+
session_id (str): Evaluated session.
|
|
204
|
+
session_fingerprint (str): Fingerprint of the current session.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
dict | None: The persisted state JSON on a match, else None.
|
|
208
|
+
"""
|
|
209
|
+
raise NotImplementedError
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def replace_retrieved_learning_evaluation_results(
|
|
213
|
+
self,
|
|
214
|
+
user_id: str,
|
|
215
|
+
session_id: str,
|
|
216
|
+
generation: int,
|
|
217
|
+
session_fingerprint: str,
|
|
218
|
+
proposed_status: str,
|
|
219
|
+
diagnostics: dict[str, Any],
|
|
220
|
+
results: list[RetrievedLearningEvaluationResult],
|
|
221
|
+
) -> RetrievedLearningCommitResult:
|
|
222
|
+
"""Atomically replace one session's evaluation result set.
|
|
223
|
+
|
|
224
|
+
In one transaction: locks the session's state row, requires the
|
|
225
|
+
current generation to equal ``generation`` (else ``superseded``),
|
|
226
|
+
recomputes the session fingerprint from live rows and requires it to
|
|
227
|
+
equal ``session_fingerprint`` (else ``stale``), rechecks every
|
|
228
|
+
result's source row for retrieval eligibility (ineligible rows are
|
|
229
|
+
dropped), then deletes the prior session set, inserts the filtered
|
|
230
|
+
set, and persists completion state — all or nothing.
|
|
231
|
+
|
|
232
|
+
When commit-time eligibility removes every candidate, prior rows are
|
|
233
|
+
cleared and the final status is ``not_applicable`` regardless of
|
|
234
|
+
``proposed_status``.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
user_id (str): Session owner.
|
|
238
|
+
session_id (str): Evaluated session.
|
|
239
|
+
generation (int): Generation allocated by
|
|
240
|
+
:meth:`begin_retrieved_learning_evaluation_run`.
|
|
241
|
+
session_fingerprint (str): Fingerprint of the judged snapshot.
|
|
242
|
+
proposed_status (str): ``"complete"`` or ``"degraded"``.
|
|
243
|
+
diagnostics (dict): Sanitized counters/timestamps persisted in
|
|
244
|
+
operation state (never prompt content or raw exception text).
|
|
245
|
+
results (list[RetrievedLearningEvaluationResult]): Proposed rows.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
RetrievedLearningCommitResult: Disposition plus authoritative
|
|
249
|
+
final status/count when applied.
|
|
250
|
+
"""
|
|
251
|
+
raise NotImplementedError
|
|
252
|
+
|
|
253
|
+
@abstractmethod
|
|
254
|
+
def finish_retrieved_learning_evaluation_run(
|
|
255
|
+
self,
|
|
256
|
+
user_id: str,
|
|
257
|
+
session_id: str,
|
|
258
|
+
generation: int,
|
|
259
|
+
status: str,
|
|
260
|
+
diagnostics: dict[str, Any],
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Record a non-applied run outcome (``failed`` or ``pending``).
|
|
263
|
+
|
|
264
|
+
Generation-guarded CAS: updates state only while ``generation`` is
|
|
265
|
+
still current, so an older failure cannot overwrite a newer run.
|
|
266
|
+
Result rows are never touched.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
user_id (str): Session owner.
|
|
270
|
+
session_id (str): Evaluated session.
|
|
271
|
+
generation (int): Generation of the finishing run.
|
|
272
|
+
status (str): ``"failed"`` or ``"pending"``.
|
|
273
|
+
diagnostics (dict): Sanitized counters and error type.
|
|
274
|
+
"""
|
|
275
|
+
raise NotImplementedError
|
|
276
|
+
|
|
277
|
+
@abstractmethod
|
|
278
|
+
def get_retrieved_learning_evaluation_results(
|
|
279
|
+
self,
|
|
280
|
+
user_id: str | None = None,
|
|
281
|
+
session_id: str | None = None,
|
|
282
|
+
limit: int = 100,
|
|
283
|
+
) -> list[RetrievedLearningEvaluationResult]:
|
|
284
|
+
"""Read persisted per-learning verdicts.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
user_id (str, optional): Filter by session owner.
|
|
288
|
+
session_id (str, optional): Filter by session.
|
|
289
|
+
limit (int): Maximum rows, ordered by
|
|
290
|
+
``created_at DESC, result_id DESC``.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
list[RetrievedLearningEvaluationResult]: Matching rows.
|
|
294
|
+
"""
|
|
295
|
+
raise NotImplementedError
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py
CHANGED
|
@@ -12,7 +12,44 @@ class UserPlaybookStoreMixin:
|
|
|
12
12
|
"""Abstract user playbook CRUD + search methods."""
|
|
13
13
|
|
|
14
14
|
@abstractmethod
|
|
15
|
-
def save_user_playbooks(
|
|
15
|
+
def save_user_playbooks(
|
|
16
|
+
self,
|
|
17
|
+
user_playbooks: list[UserPlaybook],
|
|
18
|
+
*,
|
|
19
|
+
skip_embedding: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Insert user playbooks, assigning survivor ids.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
user_playbooks: Playbooks to insert.
|
|
25
|
+
skip_embedding: When ``False`` (default — what every current caller
|
|
26
|
+
gets), the embedding (and, when document expansion is enabled,
|
|
27
|
+
``expanded_terms``) is recomputed unconditionally at write time,
|
|
28
|
+
exactly as before. Callers that ``model_copy`` a DB-loaded row
|
|
29
|
+
with changed content but the old embedding preserved rely on
|
|
30
|
+
this recompute, so it must stay the default. When ``True``, the
|
|
31
|
+
embedding step is skipped because ``.embedding`` was already
|
|
32
|
+
populated up front by
|
|
33
|
+
:meth:`precompute_user_playbook_embeddings` (the durable
|
|
34
|
+
compute/persist split — the only caller that opts in). A bare
|
|
35
|
+
``if not up.embedding`` guard is deliberately NOT used: it would
|
|
36
|
+
persist a stale vector for the ``model_copy`` callers' changed
|
|
37
|
+
content (silent search corruption).
|
|
38
|
+
"""
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def precompute_user_playbook_embeddings(
|
|
43
|
+
self, playbooks: list[UserPlaybook]
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Populate ``.embedding`` (and ``.expanded_terms`` when document
|
|
46
|
+
expansion is enabled) on each playbook in place, issuing NO DB write.
|
|
47
|
+
|
|
48
|
+
Lets the durable learning worker do the (slow, LLM/embedding) compute
|
|
49
|
+
outside the writer transaction, then pass ``skip_embedding=True`` to
|
|
50
|
+
:meth:`save_user_playbooks` so the fenced persist writes the
|
|
51
|
+
pre-computed vector without re-embedding.
|
|
52
|
+
"""
|
|
16
53
|
raise NotImplementedError
|
|
17
54
|
|
|
18
55
|
@abstractmethod
|
|
@@ -183,6 +220,8 @@ class UserPlaybookStoreMixin:
|
|
|
183
220
|
user_id: str,
|
|
184
221
|
user_playbook_ids: list[int],
|
|
185
222
|
status_filter: list[Status | None] | None = None,
|
|
223
|
+
*,
|
|
224
|
+
include_inactive: bool = False,
|
|
186
225
|
) -> list[UserPlaybook]:
|
|
187
226
|
"""Fetch the subset of a user's playbooks whose ids are in the list.
|
|
188
227
|
|
|
@@ -197,11 +236,23 @@ class UserPlaybookStoreMixin:
|
|
|
197
236
|
status_filter (list[Status | None] | None): Statuses to
|
|
198
237
|
include. ``None`` (default) means CURRENT only — same
|
|
199
238
|
default as ``get_user_playbooks`` for consistency.
|
|
239
|
+
include_inactive (bool): Return matching owned rows regardless of
|
|
240
|
+
lifecycle status. This is the *historical resolution* mode (see
|
|
241
|
+
``RetrievedLearningEvaluator``): it answers "what did this id
|
|
242
|
+
point at", not "what is retrievable now". It is a strict superset
|
|
243
|
+
of ``include_tombstones`` on ``get_user_playbook_by_id``, which
|
|
244
|
+
only unhides MERGED/SUPERSEDED for lineage walks —
|
|
245
|
+
``include_inactive`` also returns ARCHIVED rows. ``user_id``
|
|
246
|
+
scoping still applies. The default preserves retrieval behavior.
|
|
200
247
|
|
|
201
248
|
Returns:
|
|
202
249
|
list[UserPlaybook]: Matching playbooks. Order is unspecified.
|
|
203
250
|
Ids that do not exist (or do not match the user / status
|
|
204
251
|
filter) are silently omitted.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
StorageError: If ``include_inactive`` is combined with an explicit
|
|
255
|
+
``status_filter`` — the two are contradictory.
|
|
205
256
|
"""
|
|
206
257
|
raise NotImplementedError
|
|
207
258
|
|
|
@@ -60,6 +60,65 @@ class InteractionStoreMixin:
|
|
|
60
60
|
"""
|
|
61
61
|
return
|
|
62
62
|
|
|
63
|
+
def iter_interactions_missing_vectors(
|
|
64
|
+
self,
|
|
65
|
+
limit: int, # noqa: ARG002
|
|
66
|
+
) -> list[tuple[int, str]]:
|
|
67
|
+
"""Enumerate interactions whose embedding vector was never persisted.
|
|
68
|
+
|
|
69
|
+
When an embedding call fails or degrades, the ingest path stores an empty
|
|
70
|
+
embedding (and writes no vector row), returns success, and nothing ever
|
|
71
|
+
re-embeds the row — so that interaction is invisible to vector/hybrid
|
|
72
|
+
search forever. This method surfaces those rows so a background sweep can
|
|
73
|
+
re-embed them (see ``backfill_missing_interaction_vectors``).
|
|
74
|
+
|
|
75
|
+
Returns ``(interaction_id, embed_text)`` pairs where ``embed_text`` is
|
|
76
|
+
derived exactly as the ingest path derives it, so a backfilled vector is
|
|
77
|
+
byte-for-byte comparable to a freshly-ingested one.
|
|
78
|
+
|
|
79
|
+
Safe default: returns ``[]`` — "this backend does not support backfill
|
|
80
|
+
detection". Backends that store embeddings (SQLite here; Supabase and
|
|
81
|
+
Postgres in the enterprise repo) MUST override this so their rows are
|
|
82
|
+
actually recoverable. Bounded by ``limit`` per call.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
limit: Maximum number of interactions to return.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
list[tuple[int, str]]: ``(interaction_id, embed_text)`` pairs, at
|
|
89
|
+
most ``limit`` long. Empty when nothing needs backfilling or the
|
|
90
|
+
backend does not support detection.
|
|
91
|
+
"""
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
def backfill_missing_interaction_vectors(
|
|
95
|
+
self,
|
|
96
|
+
limit: int, # noqa: ARG002
|
|
97
|
+
) -> int:
|
|
98
|
+
"""Re-embed and persist vectors for interactions missing them.
|
|
99
|
+
|
|
100
|
+
Idempotent and bounded: enumerates up to ``limit`` interactions via
|
|
101
|
+
``iter_interactions_missing_vectors``, re-embeds their text using the
|
|
102
|
+
same embedder + derivation the ingest path uses, and writes the vector
|
|
103
|
+
back through the same code path a fresh insert uses. Once a vector
|
|
104
|
+
exists the row is skipped on the next call.
|
|
105
|
+
|
|
106
|
+
Fail-safe: if the embedder is unavailable the implementation must skip
|
|
107
|
+
gracefully and leave the work for a later call (never raise, never
|
|
108
|
+
hot-loop a down embedder).
|
|
109
|
+
|
|
110
|
+
Safe default: returns ``0`` — "this backend does not support backfill".
|
|
111
|
+
SQLite overrides this; the enterprise Supabase/Postgres backends must
|
|
112
|
+
override it (and ``iter_interactions_missing_vectors``) for production.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
limit: Maximum number of interactions to re-embed this call.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
int: Number of interactions whose vector was backfilled.
|
|
119
|
+
"""
|
|
120
|
+
return 0
|
|
121
|
+
|
|
63
122
|
@abstractmethod
|
|
64
123
|
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
65
124
|
raise NotImplementedError
|
|
@@ -66,8 +66,43 @@ class ProfileStoreMixin:
|
|
|
66
66
|
raise NotImplementedError
|
|
67
67
|
|
|
68
68
|
@abstractmethod
|
|
69
|
-
def add_user_profile(
|
|
70
|
-
|
|
69
|
+
def add_user_profile(
|
|
70
|
+
self,
|
|
71
|
+
user_id: str,
|
|
72
|
+
user_profiles: list[UserProfile],
|
|
73
|
+
*,
|
|
74
|
+
skip_embedding: bool = False,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Add the user profile for a given user id.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
user_id: The owning user id (positional, unused by some backends).
|
|
80
|
+
user_profiles: Profiles to insert.
|
|
81
|
+
skip_embedding: When ``False`` (default — what every current caller
|
|
82
|
+
gets), the embedding (and, when document expansion is enabled,
|
|
83
|
+
``expanded_terms``) is recomputed unconditionally at write time,
|
|
84
|
+
exactly as before. Callers that ``model_copy`` a DB-loaded row
|
|
85
|
+
with changed content but the old embedding preserved rely on
|
|
86
|
+
this recompute, so it must stay the default. When ``True``, the
|
|
87
|
+
embedding step is skipped because ``.embedding`` was already
|
|
88
|
+
populated up front by :meth:`precompute_profile_embeddings`
|
|
89
|
+
(the durable compute/persist split — the only caller that opts
|
|
90
|
+
in). A bare ``if not profile.embedding`` guard is deliberately
|
|
91
|
+
NOT used: it would persist a stale vector for the ``model_copy``
|
|
92
|
+
callers' changed content (silent search corruption).
|
|
93
|
+
"""
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def precompute_profile_embeddings(self, profiles: list[UserProfile]) -> None:
|
|
98
|
+
"""Populate ``.embedding`` (and ``.expanded_terms`` when document
|
|
99
|
+
expansion is enabled) on each profile in place, issuing NO DB write.
|
|
100
|
+
|
|
101
|
+
Lets the durable learning worker do the (slow, LLM/embedding) compute
|
|
102
|
+
outside the writer transaction, then pass ``skip_embedding=True`` to
|
|
103
|
+
:meth:`add_user_profile` so the fenced persist writes the pre-computed
|
|
104
|
+
vector without re-embedding.
|
|
105
|
+
"""
|
|
71
106
|
raise NotImplementedError
|
|
72
107
|
|
|
73
108
|
@abstractmethod
|
|
@@ -175,6 +210,8 @@ class ProfileStoreMixin:
|
|
|
175
210
|
user_id: str,
|
|
176
211
|
profile_ids: list[str],
|
|
177
212
|
status_filter: list[Status | None] | None = None,
|
|
213
|
+
*,
|
|
214
|
+
include_inactive: bool = False,
|
|
178
215
|
) -> list[UserProfile]:
|
|
179
216
|
"""Fetch the subset of a user's profiles whose ids are in the list.
|
|
180
217
|
|
|
@@ -189,11 +226,24 @@ class ProfileStoreMixin:
|
|
|
189
226
|
status_filter (list[Status | None] | None): Statuses to
|
|
190
227
|
include. ``None`` (default) means CURRENT only — same
|
|
191
228
|
default as ``get_user_profile`` for consistency.
|
|
229
|
+
include_inactive (bool): Return matching owned rows regardless of
|
|
230
|
+
lifecycle status or expiry. This is the *historical resolution*
|
|
231
|
+
mode (see ``RetrievedLearningEvaluator``): it answers "what did
|
|
232
|
+
this id point at", not "what is retrievable now". It is a strict
|
|
233
|
+
superset of ``include_tombstones`` on ``get_profile_by_id``,
|
|
234
|
+
which only unhides MERGED/SUPERSEDED for lineage walks —
|
|
235
|
+
``include_inactive`` also returns ARCHIVED and EXPIRED rows.
|
|
236
|
+
``user_id`` scoping still applies. The default preserves
|
|
237
|
+
retrieval behavior.
|
|
192
238
|
|
|
193
239
|
Returns:
|
|
194
240
|
list[UserProfile]: Matching profiles. Order is unspecified.
|
|
195
241
|
Ids that do not exist (or do not match the user / status
|
|
196
242
|
filter) are silently omitted.
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
StorageError: If ``include_inactive`` is combined with an explicit
|
|
246
|
+
``status_filter`` — the two are contradictory.
|
|
197
247
|
"""
|
|
198
248
|
raise NotImplementedError
|
|
199
249
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Shared pure helpers for retrieved-learning evaluation state.
|
|
2
|
+
|
|
3
|
+
Used by every storage backend (SQLite, Supabase, native Postgres) and by the
|
|
4
|
+
group-evaluation runner, so this module must stay free of storage imports.
|
|
5
|
+
|
|
6
|
+
Concurrency model (no mutation-site instrumentation):
|
|
7
|
+
|
|
8
|
+
- The **session fingerprint** is a SHA-256 digest over every interaction ID in
|
|
9
|
+
the session plus each interaction's canonical attachment refs. Any
|
|
10
|
+
interaction publish or delete changes it (autoincrement interaction IDs make
|
|
11
|
+
ABA collisions unrealizable), so it covers transcript-only changes as well
|
|
12
|
+
as attachment changes.
|
|
13
|
+
- Each evaluation run receives a monotonically increasing **generation**
|
|
14
|
+
allocated in ``_operation_state``. Replacement CASes generation and a
|
|
15
|
+
fingerprint recomputed under the replacement transaction's lock, so an
|
|
16
|
+
older or stale snapshot cannot overwrite newer session state.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Literal
|
|
25
|
+
|
|
26
|
+
RETRIEVED_LEARNING_STATE_PREFIX = "retrieved_learning_eval"
|
|
27
|
+
|
|
28
|
+
# Statuses persisted in _operation_state. "complete" and "not_applicable" are
|
|
29
|
+
# terminal (the fast path may short-circuit on them); "degraded" and "failed"
|
|
30
|
+
# are retried by the next scheduled or forced run.
|
|
31
|
+
type RetrievedLearningPersistedStatus = Literal[
|
|
32
|
+
"pending", "in_progress", "complete", "degraded", "failed", "not_applicable"
|
|
33
|
+
]
|
|
34
|
+
TERMINAL_RETRIEVED_STATUSES: frozenset[str] = frozenset({"complete", "not_applicable"})
|
|
35
|
+
|
|
36
|
+
# Outcomes of one runner invocation. "stale"/"superseded"/"skipped" are
|
|
37
|
+
# invocation outcomes only and are never stored as persisted status.
|
|
38
|
+
type RetrievedLearningInvocationStatus = Literal[
|
|
39
|
+
"pending",
|
|
40
|
+
"complete",
|
|
41
|
+
"degraded",
|
|
42
|
+
"failed",
|
|
43
|
+
"not_applicable",
|
|
44
|
+
"stale",
|
|
45
|
+
"superseded",
|
|
46
|
+
"skipped",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Canonical kinds accepted and persisted by retrieved-learning evaluation.
|
|
50
|
+
# ``RetrievedLearning.kind`` validates these at the API boundary; storage
|
|
51
|
+
# parsers still filter defensively against this set.
|
|
52
|
+
CANONICAL_RETRIEVED_KINDS: frozenset[str] = frozenset(
|
|
53
|
+
{"profile", "user_playbook", "agent_playbook"}
|
|
54
|
+
)
|
|
55
|
+
DEFAULT_TRANSCRIPT_CHAR_LIMIT = 64_000
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SessionFingerprintBuilder:
|
|
59
|
+
"""Incrementally build the canonical session fingerprint."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._digest = hashlib.sha256()
|
|
63
|
+
self._digest.update(b"[")
|
|
64
|
+
self._has_entries = False
|
|
65
|
+
|
|
66
|
+
def add(
|
|
67
|
+
self,
|
|
68
|
+
interaction_id: int,
|
|
69
|
+
refs: list[tuple[str, str]],
|
|
70
|
+
role: str = "",
|
|
71
|
+
content: str = "",
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Fold one interaction into the running digest.
|
|
74
|
+
|
|
75
|
+
``role``/``content`` are the transcript the judges actually saw, so a
|
|
76
|
+
content-only edit (e.g. ``INSERT OR REPLACE`` on an existing
|
|
77
|
+
interaction that keeps its id and attachments) still invalidates the
|
|
78
|
+
fingerprint. Callers on the precompute and commit-recompute sides MUST
|
|
79
|
+
pass content truncated identically (``DEFAULT_TRANSCRIPT_CHAR_LIMIT``);
|
|
80
|
+
otherwise a session would compare unequal and never commit.
|
|
81
|
+
"""
|
|
82
|
+
if self._has_entries:
|
|
83
|
+
self._digest.update(b",")
|
|
84
|
+
self._digest.update(
|
|
85
|
+
json.dumps(
|
|
86
|
+
[interaction_id, sorted(refs), role, content],
|
|
87
|
+
separators=(",", ":"),
|
|
88
|
+
).encode("utf-8")
|
|
89
|
+
)
|
|
90
|
+
self._has_entries = True
|
|
91
|
+
|
|
92
|
+
def hexdigest(self) -> str:
|
|
93
|
+
digest = self._digest.copy()
|
|
94
|
+
digest.update(b"]")
|
|
95
|
+
return digest.hexdigest()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_retrieved_learning_state_key(user_id: str, session_id: str) -> str:
|
|
99
|
+
"""Build the ``_operation_state`` key for one session's evaluation state.
|
|
100
|
+
|
|
101
|
+
Length-prefixed (following the grade_on_demand cache-key precedent) so a
|
|
102
|
+
crafted user/session pair cannot collide with another pair. Org scoping is
|
|
103
|
+
unnecessary: ``_operation_state`` is already org-scoped on every backend.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
user_id (str): Session owner.
|
|
107
|
+
session_id (str): Evaluated session.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
str: The state key.
|
|
111
|
+
"""
|
|
112
|
+
return (
|
|
113
|
+
f"{RETRIEVED_LEARNING_STATE_PREFIX}"
|
|
114
|
+
f"::{len(user_id)}:{user_id}::{len(session_id)}:{session_id}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class SnapshotInteraction:
|
|
120
|
+
"""One interaction row in the bounded session projection.
|
|
121
|
+
|
|
122
|
+
``refs`` holds ``(kind, learning_id)`` attachment tuples; interaction
|
|
123
|
+
content is carried only for transcript construction and is never
|
|
124
|
+
persisted in operation state.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
interaction_id: int
|
|
128
|
+
role: str
|
|
129
|
+
content: str
|
|
130
|
+
created_at: int
|
|
131
|
+
refs: list[tuple[str, str]] = field(default_factory=list)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class BoundedRetrievedLearningSnapshot:
|
|
136
|
+
"""Bounded projection of one session for retrieved-learning evaluation.
|
|
137
|
+
|
|
138
|
+
Loaded without constructing full ``Interaction`` objects (no embeddings,
|
|
139
|
+
no image encodings). ``attachment_limit_exceeded`` is set when the raw
|
|
140
|
+
attachment-occurrence cap was hit; the evaluator must then make zero LLM
|
|
141
|
+
calls.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
interactions: list[SnapshotInteraction] = field(default_factory=list)
|
|
145
|
+
earliest_request_created_at: int | None = None
|
|
146
|
+
agent_version: str = ""
|
|
147
|
+
raw_attachment_count: int = 0
|
|
148
|
+
attachment_limit_exceeded: bool = False
|
|
149
|
+
precomputed_fingerprint: str | None = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def append_bounded_snapshot_interaction(
|
|
153
|
+
snapshot: BoundedRetrievedLearningSnapshot,
|
|
154
|
+
*,
|
|
155
|
+
interaction_id: int,
|
|
156
|
+
role: str,
|
|
157
|
+
content: str,
|
|
158
|
+
created_at: int,
|
|
159
|
+
refs: list[tuple[str, str]],
|
|
160
|
+
transcript_chars_remaining: int,
|
|
161
|
+
) -> int:
|
|
162
|
+
"""Retain refs and only the transcript prefix that fits the char budget."""
|
|
163
|
+
retained_role = ""
|
|
164
|
+
retained_content = ""
|
|
165
|
+
if transcript_chars_remaining > 0 and content:
|
|
166
|
+
prefix_size = len(role) + 3
|
|
167
|
+
content_budget = max(0, transcript_chars_remaining - prefix_size)
|
|
168
|
+
if content_budget:
|
|
169
|
+
retained_role = role
|
|
170
|
+
retained_content = content[:content_budget]
|
|
171
|
+
transcript_chars_remaining -= prefix_size + len(retained_content)
|
|
172
|
+
if refs or retained_content:
|
|
173
|
+
snapshot.interactions.append(
|
|
174
|
+
SnapshotInteraction(
|
|
175
|
+
interaction_id=interaction_id,
|
|
176
|
+
role=retained_role,
|
|
177
|
+
content=retained_content,
|
|
178
|
+
created_at=created_at,
|
|
179
|
+
refs=refs,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return max(0, transcript_chars_remaining)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def session_fingerprint(snapshot: BoundedRetrievedLearningSnapshot) -> str:
|
|
186
|
+
"""Compute the canonical session fingerprint for a bounded snapshot.
|
|
187
|
+
|
|
188
|
+
Covers every interaction ID, each interaction's ``(kind, learning_id)``
|
|
189
|
+
refs, and its transcript role/content, so any publish, delete, or in-place
|
|
190
|
+
content edit in the session invalidates it. Only this digest is ever
|
|
191
|
+
persisted.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
snapshot (BoundedRetrievedLearningSnapshot): The session projection.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
str: Hex SHA-256 digest.
|
|
198
|
+
"""
|
|
199
|
+
if snapshot.precomputed_fingerprint is not None:
|
|
200
|
+
return snapshot.precomputed_fingerprint
|
|
201
|
+
builder = SessionFingerprintBuilder()
|
|
202
|
+
for interaction in sorted(
|
|
203
|
+
snapshot.interactions,
|
|
204
|
+
key=lambda item: (item.created_at, item.interaction_id),
|
|
205
|
+
):
|
|
206
|
+
builder.add(
|
|
207
|
+
interaction.interaction_id,
|
|
208
|
+
interaction.refs,
|
|
209
|
+
interaction.role,
|
|
210
|
+
interaction.content,
|
|
211
|
+
)
|
|
212
|
+
return builder.hexdigest()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class RetrievedLearningCommitResult:
|
|
217
|
+
"""Outcome of one atomic result-set replacement.
|
|
218
|
+
|
|
219
|
+
``status``/``committed_count`` are authoritative only when ``disposition``
|
|
220
|
+
is ``"applied"`` (commit-time eligibility can turn a proposed status into
|
|
221
|
+
``"not_applicable"``).
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
disposition: Literal["applied", "stale", "superseded"]
|
|
225
|
+
status: RetrievedLearningPersistedStatus | None = None
|
|
226
|
+
committed_count: int = 0
|
|
@@ -20,8 +20,10 @@ import os
|
|
|
20
20
|
import threading
|
|
21
21
|
import time
|
|
22
22
|
from collections.abc import Callable
|
|
23
|
+
from functools import partial
|
|
23
24
|
|
|
24
25
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
26
|
+
from reflexio.server.callback_executor import submit_callback
|
|
25
27
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
26
28
|
from reflexio.server.services.tagging.service import TaggingService
|
|
27
29
|
|
|
@@ -101,13 +103,10 @@ class TaggingScheduler:
|
|
|
101
103
|
continue
|
|
102
104
|
|
|
103
105
|
del self._scheduled[key]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
daemon=True,
|
|
108
|
-
name=f"tagging-{key[1][:20]}",
|
|
106
|
+
submit_callback(
|
|
107
|
+
f"tagging-{key[1][:20]}",
|
|
108
|
+
partial(self._run_callback, key, callback),
|
|
109
109
|
)
|
|
110
|
-
t.start()
|
|
111
110
|
except Exception:
|
|
112
111
|
logger.exception("Error in tagging scheduler loop")
|
|
113
112
|
time.sleep(1)
|