claude-smart 0.2.48 → 0.2.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.md +11 -40
- package/bin/claude-smart.js +393 -55
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +255 -7
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +766 -123
- package/plugin/scripts/codex-hook.js +79 -229
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +118 -137
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +116 -21
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +93 -29
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +56 -11
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
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
|
|
|
@@ -28,16 +28,97 @@ class InteractionStoreMixin:
|
|
|
28
28
|
|
|
29
29
|
@abstractmethod
|
|
30
30
|
def add_user_interactions_bulk(
|
|
31
|
-
self,
|
|
31
|
+
self,
|
|
32
|
+
user_id: str,
|
|
33
|
+
interactions: list[Interaction],
|
|
34
|
+
*,
|
|
35
|
+
embeddings_prepared: bool = False,
|
|
32
36
|
) -> None:
|
|
33
37
|
"""Add multiple user interactions with batched embedding generation.
|
|
34
38
|
|
|
35
39
|
Args:
|
|
36
40
|
user_id: The user ID
|
|
37
41
|
interactions: List of interactions to add
|
|
42
|
+
embeddings_prepared: When True, skip all embedding generation and write
|
|
43
|
+
whatever embedding is already on each interaction (real or ``[]``).
|
|
44
|
+
Use on the durable path after ``prepare_interaction_embeddings`` to
|
|
45
|
+
ensure no network I/O occurs inside an open ``commit_scope``.
|
|
38
46
|
"""
|
|
39
47
|
raise NotImplementedError
|
|
40
48
|
|
|
49
|
+
def prepare_interaction_embeddings(self, interactions: list[Interaction]) -> None: # noqa: ARG002
|
|
50
|
+
"""Pre-populate interaction.embedding for each interaction without writing to storage.
|
|
51
|
+
|
|
52
|
+
Call this before opening a commit_scope to avoid a network round-trip inside
|
|
53
|
+
the transaction. Subclasses that generate embeddings client-side MUST override
|
|
54
|
+
this to call get_embeddings and populate interaction.embedding before the scope
|
|
55
|
+
is opened; backends where the embedding is generated server-side can leave this
|
|
56
|
+
as a no-op.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
interactions: Interactions whose embedding fields will be populated in-place.
|
|
60
|
+
"""
|
|
61
|
+
return
|
|
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
|
+
|
|
41
122
|
@abstractmethod
|
|
42
123
|
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
43
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
|
|
@@ -110,6 +145,28 @@ class ProfileStoreMixin:
|
|
|
110
145
|
"""
|
|
111
146
|
raise NotImplementedError
|
|
112
147
|
|
|
148
|
+
def count_user_profiles_by_status(
|
|
149
|
+
self, user_ids: list[str], status: Status | None
|
|
150
|
+
) -> int:
|
|
151
|
+
"""Count non-expired profiles for concrete users with the given status.
|
|
152
|
+
|
|
153
|
+
Storage backends may override this with a single query. The default keeps
|
|
154
|
+
the storage contract backward-compatible for out-of-tree backends.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
user_ids: Concrete user IDs to include. Empty list returns 0.
|
|
158
|
+
status: Profile status to match; None means CURRENT.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
int: Number of matching profiles
|
|
162
|
+
"""
|
|
163
|
+
if not user_ids:
|
|
164
|
+
return 0
|
|
165
|
+
return sum(
|
|
166
|
+
len(self.get_user_profile(user_id=user_id, status_filter=[status]))
|
|
167
|
+
for user_id in user_ids
|
|
168
|
+
)
|
|
169
|
+
|
|
113
170
|
@abstractmethod
|
|
114
171
|
def update_all_profiles_status(
|
|
115
172
|
self,
|
|
@@ -153,6 +210,8 @@ class ProfileStoreMixin:
|
|
|
153
210
|
user_id: str,
|
|
154
211
|
profile_ids: list[str],
|
|
155
212
|
status_filter: list[Status | None] | None = None,
|
|
213
|
+
*,
|
|
214
|
+
include_inactive: bool = False,
|
|
156
215
|
) -> list[UserProfile]:
|
|
157
216
|
"""Fetch the subset of a user's profiles whose ids are in the list.
|
|
158
217
|
|
|
@@ -167,11 +226,24 @@ class ProfileStoreMixin:
|
|
|
167
226
|
status_filter (list[Status | None] | None): Statuses to
|
|
168
227
|
include. ``None`` (default) means CURRENT only — same
|
|
169
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.
|
|
170
238
|
|
|
171
239
|
Returns:
|
|
172
240
|
list[UserProfile]: Matching profiles. Order is unspecified.
|
|
173
241
|
Ids that do not exist (or do not match the user / status
|
|
174
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.
|
|
175
247
|
"""
|
|
176
248
|
raise NotImplementedError
|
|
177
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)
|