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
|
@@ -27,6 +27,16 @@ from .._base import (
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _embed_text_for_interaction(content: str | None, action_desc: str | None) -> str:
|
|
31
|
+
"""Derive the text embedded for an interaction.
|
|
32
|
+
|
|
33
|
+
Kept byte-for-byte identical to the ingest path
|
|
34
|
+
(``add_user_interactions_bulk`` / ``prepare_interaction_embeddings``) so a
|
|
35
|
+
backfilled vector matches a freshly-ingested one.
|
|
36
|
+
"""
|
|
37
|
+
return "\n".join([content or "", action_desc or ""])
|
|
38
|
+
|
|
39
|
+
|
|
30
40
|
class InteractionStoreMixin:
|
|
31
41
|
"""Mixin providing interaction-store CRUD for SQLite storage."""
|
|
32
42
|
|
|
@@ -45,6 +55,7 @@ class InteractionStoreMixin:
|
|
|
45
55
|
embedding_dimensions: int
|
|
46
56
|
_subject_ref_for_user_id: Any
|
|
47
57
|
_assert_subject_writable_locked: Any
|
|
58
|
+
_own_transaction: Any
|
|
48
59
|
|
|
49
60
|
# ------------------------------------------------------------------
|
|
50
61
|
# CRUD — Interactions
|
|
@@ -71,6 +82,12 @@ class InteractionStoreMixin:
|
|
|
71
82
|
|
|
72
83
|
@SQLiteStorageBase.handle_exceptions
|
|
73
84
|
def add_user_interaction(self, user_id: str, interaction: Interaction) -> None: # noqa: ARG002
|
|
85
|
+
# NOTE: distinct from the bulk/backfill derivation
|
|
86
|
+
# (_embed_text_for_interaction). This legacy single-insert path embeds via
|
|
87
|
+
# the purpose-prefixed _get_embedding ("search_document: ...") and its own
|
|
88
|
+
# f-string, so it is intentionally NOT routed through the shared helper —
|
|
89
|
+
# aligning it would silently change stored embedding values (e.g. null ->
|
|
90
|
+
# literal "None"). The real bulk ingest + backfill share the helper.
|
|
74
91
|
embedding = self._get_embedding(
|
|
75
92
|
f"{interaction.content}\n{interaction.user_action_description}"
|
|
76
93
|
)
|
|
@@ -81,8 +98,10 @@ class InteractionStoreMixin:
|
|
|
81
98
|
created_at_iso = _epoch_to_iso(interaction.created_at)
|
|
82
99
|
subject_ref = self._subject_ref_for_user_id(interaction.user_id)
|
|
83
100
|
with self._lock:
|
|
101
|
+
own_txn = self._own_transaction()
|
|
84
102
|
try:
|
|
85
|
-
|
|
103
|
+
if own_txn:
|
|
104
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
86
105
|
self._assert_subject_writable_locked(subject_ref)
|
|
87
106
|
if interaction.interaction_id:
|
|
88
107
|
self.conn.execute(
|
|
@@ -90,8 +109,9 @@ class InteractionStoreMixin:
|
|
|
90
109
|
(interaction_id, user_id, content, request_id, created_at,
|
|
91
110
|
role, user_action, user_action_description,
|
|
92
111
|
interacted_image_url, image_encoding, shadow_content,
|
|
93
|
-
expert_content, tools_used, citations,
|
|
94
|
-
|
|
112
|
+
expert_content, tools_used, citations, retrieved_learnings,
|
|
113
|
+
embedding, governance_subject_ref)
|
|
114
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
95
115
|
(
|
|
96
116
|
interaction.interaction_id,
|
|
97
117
|
interaction.user_id,
|
|
@@ -111,6 +131,12 @@ class InteractionStoreMixin:
|
|
|
111
131
|
_json_dumps(
|
|
112
132
|
[c.model_dump() for c in interaction.citations]
|
|
113
133
|
),
|
|
134
|
+
_json_dumps(
|
|
135
|
+
[
|
|
136
|
+
c.model_dump()
|
|
137
|
+
for c in interaction.retrieved_learnings
|
|
138
|
+
]
|
|
139
|
+
),
|
|
114
140
|
_json_dumps(interaction.embedding),
|
|
115
141
|
subject_ref,
|
|
116
142
|
),
|
|
@@ -122,8 +148,9 @@ class InteractionStoreMixin:
|
|
|
122
148
|
(user_id, content, request_id, created_at,
|
|
123
149
|
role, user_action, user_action_description,
|
|
124
150
|
interacted_image_url, image_encoding, shadow_content,
|
|
125
|
-
expert_content, tools_used, citations,
|
|
126
|
-
|
|
151
|
+
expert_content, tools_used, citations, retrieved_learnings,
|
|
152
|
+
embedding, governance_subject_ref)
|
|
153
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
127
154
|
(
|
|
128
155
|
interaction.user_id,
|
|
129
156
|
interaction.content,
|
|
@@ -142,15 +169,23 @@ class InteractionStoreMixin:
|
|
|
142
169
|
_json_dumps(
|
|
143
170
|
[c.model_dump() for c in interaction.citations]
|
|
144
171
|
),
|
|
172
|
+
_json_dumps(
|
|
173
|
+
[
|
|
174
|
+
c.model_dump()
|
|
175
|
+
for c in interaction.retrieved_learnings
|
|
176
|
+
]
|
|
177
|
+
),
|
|
145
178
|
_json_dumps(interaction.embedding),
|
|
146
179
|
subject_ref,
|
|
147
180
|
),
|
|
148
181
|
)
|
|
149
182
|
iid = cur.lastrowid or 0
|
|
150
183
|
interaction.interaction_id = iid
|
|
151
|
-
|
|
184
|
+
if own_txn:
|
|
185
|
+
self.conn.commit()
|
|
152
186
|
except Exception:
|
|
153
|
-
|
|
187
|
+
if own_txn:
|
|
188
|
+
self.conn.rollback()
|
|
154
189
|
raise
|
|
155
190
|
# Update FTS and vec
|
|
156
191
|
self._fts_upsert(
|
|
@@ -168,12 +203,53 @@ class InteractionStoreMixin:
|
|
|
168
203
|
self,
|
|
169
204
|
user_id: str, # noqa: ARG002
|
|
170
205
|
interactions: list[Interaction],
|
|
206
|
+
*,
|
|
207
|
+
embeddings_prepared: bool = False,
|
|
171
208
|
) -> None:
|
|
172
209
|
if not interactions:
|
|
173
210
|
return
|
|
211
|
+
if not embeddings_prepared:
|
|
212
|
+
# Only generate embeddings for interactions that do not already have them.
|
|
213
|
+
# This allows callers to pre-populate embeddings (e.g. via
|
|
214
|
+
# prepare_interaction_embeddings) before opening a commit_scope so that no
|
|
215
|
+
# network I/O occurs inside the transaction.
|
|
216
|
+
to_embed = [i for i in interactions if not i.embedding]
|
|
217
|
+
if to_embed:
|
|
218
|
+
texts = [
|
|
219
|
+
_embed_text_for_interaction(i.content, i.user_action_description)
|
|
220
|
+
for i in to_embed
|
|
221
|
+
]
|
|
222
|
+
try:
|
|
223
|
+
embeddings = self.llm_client.get_embeddings(
|
|
224
|
+
texts, self.embedding_model_name, self.embedding_dimensions
|
|
225
|
+
)
|
|
226
|
+
except EmbeddingUnavailableError as exc:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"Embedding unavailable for interaction bulk insert; "
|
|
229
|
+
"continuing without vectors: %s",
|
|
230
|
+
exc,
|
|
231
|
+
)
|
|
232
|
+
embeddings = [[] for _ in texts]
|
|
233
|
+
for interaction, embedding in zip(to_embed, embeddings, strict=False):
|
|
234
|
+
interaction.embedding = embedding
|
|
235
|
+
for interaction in interactions:
|
|
236
|
+
self._insert_interaction(interaction)
|
|
237
|
+
|
|
238
|
+
@SQLiteStorageBase.handle_exceptions
|
|
239
|
+
def prepare_interaction_embeddings(self, interactions: list[Interaction]) -> None:
|
|
240
|
+
"""Pre-populate interaction.embedding for each interaction (no DB write).
|
|
241
|
+
|
|
242
|
+
Generates embeddings in one batch call so the write path inside a
|
|
243
|
+
commit_scope can skip the network round-trip.
|
|
244
|
+
"""
|
|
245
|
+
if not interactions:
|
|
246
|
+
return
|
|
247
|
+
to_embed = [i for i in interactions if not i.embedding]
|
|
248
|
+
if not to_embed:
|
|
249
|
+
return
|
|
174
250
|
texts = [
|
|
175
|
-
|
|
176
|
-
for i in
|
|
251
|
+
_embed_text_for_interaction(i.content, i.user_action_description)
|
|
252
|
+
for i in to_embed
|
|
177
253
|
]
|
|
178
254
|
try:
|
|
179
255
|
embeddings = self.llm_client.get_embeddings(
|
|
@@ -181,14 +257,123 @@ class InteractionStoreMixin:
|
|
|
181
257
|
)
|
|
182
258
|
except EmbeddingUnavailableError as exc:
|
|
183
259
|
logger.warning(
|
|
184
|
-
"Embedding unavailable
|
|
260
|
+
"Embedding unavailable during prepare_interaction_embeddings; "
|
|
185
261
|
"continuing without vectors: %s",
|
|
186
262
|
exc,
|
|
187
263
|
)
|
|
188
264
|
embeddings = [[] for _ in texts]
|
|
189
|
-
for interaction, embedding in zip(
|
|
265
|
+
for interaction, embedding in zip(to_embed, embeddings, strict=False):
|
|
190
266
|
interaction.embedding = embedding
|
|
191
|
-
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
# Missing-vector backfill (durability sweep)
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
@SQLiteStorageBase.handle_exceptions
|
|
273
|
+
def iter_interactions_missing_vectors(self, limit: int) -> list[tuple[int, str]]:
|
|
274
|
+
"""Enumerate interactions whose embedding was never persisted.
|
|
275
|
+
|
|
276
|
+
A degraded/failed embedding leaves the ``embedding`` column empty
|
|
277
|
+
(``'[]'`` or NULL) and writes no ``interactions_vec`` row — equivalent
|
|
278
|
+
conditions on the write path. Detecting on the ``embedding`` column is
|
|
279
|
+
both the root-cause signal and portable across backends (the column
|
|
280
|
+
exists everywhere; ``interactions_vec`` is a SQLite-vec detail).
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
limit: Maximum number of interactions to return.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
list[tuple[int, str]]: ``(interaction_id, embed_text)`` pairs, at
|
|
287
|
+
most ``limit`` long, ordered by ``interaction_id`` for stable paging.
|
|
288
|
+
"""
|
|
289
|
+
if limit <= 0:
|
|
290
|
+
return []
|
|
291
|
+
rows = self._fetchall(
|
|
292
|
+
"""SELECT interaction_id, content, user_action_description
|
|
293
|
+
FROM interactions
|
|
294
|
+
WHERE embedding IS NULL OR embedding = '[]'
|
|
295
|
+
ORDER BY interaction_id
|
|
296
|
+
LIMIT ?""",
|
|
297
|
+
(limit,),
|
|
298
|
+
)
|
|
299
|
+
return [
|
|
300
|
+
(
|
|
301
|
+
r["interaction_id"],
|
|
302
|
+
_embed_text_for_interaction(r["content"], r["user_action_description"]),
|
|
303
|
+
)
|
|
304
|
+
for r in rows
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
@SQLiteStorageBase.handle_exceptions
|
|
308
|
+
def backfill_missing_interaction_vectors(self, limit: int) -> int:
|
|
309
|
+
"""Re-embed and persist vectors for interactions missing them.
|
|
310
|
+
|
|
311
|
+
Bounded by ``limit`` and idempotent: once the ``embedding`` column and
|
|
312
|
+
``interactions_vec`` row are written, the row no longer matches
|
|
313
|
+
detection and is skipped next time. Embeds via the same batch call and
|
|
314
|
+
text derivation the ingest path uses so backfilled vectors match
|
|
315
|
+
freshly-ingested ones.
|
|
316
|
+
|
|
317
|
+
Fail-safe: if the embedder is unavailable the batch call raises
|
|
318
|
+
``EmbeddingUnavailableError``; we log a single bounded WARN and return 0,
|
|
319
|
+
leaving the rows for the next tick rather than crashing the caller or
|
|
320
|
+
hot-looping a down embedder.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
limit: Maximum number of interactions to re-embed this call.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
int: Number of interactions whose vector was backfilled.
|
|
327
|
+
"""
|
|
328
|
+
if limit <= 0:
|
|
329
|
+
return 0
|
|
330
|
+
pairs = self.iter_interactions_missing_vectors(limit)
|
|
331
|
+
if not pairs:
|
|
332
|
+
return 0
|
|
333
|
+
texts = [text for _, text in pairs]
|
|
334
|
+
try:
|
|
335
|
+
embeddings = self.llm_client.get_embeddings(
|
|
336
|
+
texts, self.embedding_model_name, self.embedding_dimensions
|
|
337
|
+
)
|
|
338
|
+
except EmbeddingUnavailableError as exc:
|
|
339
|
+
logger.warning(
|
|
340
|
+
"Embedding unavailable during missing-vector backfill; leaving "
|
|
341
|
+
"%d interaction(s) for the next tick: %s",
|
|
342
|
+
len(pairs),
|
|
343
|
+
exc,
|
|
344
|
+
)
|
|
345
|
+
return 0
|
|
346
|
+
backfilled = 0
|
|
347
|
+
for (iid, _text), embedding in zip(pairs, embeddings, strict=False):
|
|
348
|
+
if not embedding:
|
|
349
|
+
# Embedder returned empty for this row — leave it for next tick.
|
|
350
|
+
continue
|
|
351
|
+
self._persist_backfilled_vector(iid, embedding)
|
|
352
|
+
backfilled += 1
|
|
353
|
+
return backfilled
|
|
354
|
+
|
|
355
|
+
def _persist_backfilled_vector(
|
|
356
|
+
self, interaction_id: int, embedding: list[float]
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Write a re-embedded vector for one interaction (column + vec row).
|
|
359
|
+
|
|
360
|
+
Updates the ``embedding`` TEXT column (read by the Python vector-rank
|
|
361
|
+
search path) and upserts the ``interactions_vec`` row via the same
|
|
362
|
+
``_vec_upsert`` helper the insert path uses.
|
|
363
|
+
|
|
364
|
+
Only commits when it owns the transaction (mirroring ``_insert_interaction``)
|
|
365
|
+
so it can never flush a partial outer transaction if ever called inside an
|
|
366
|
+
open ``commit_scope``; ``_vec_upsert`` likewise defers under an open scope.
|
|
367
|
+
"""
|
|
368
|
+
with self._lock:
|
|
369
|
+
own_txn = self._own_transaction()
|
|
370
|
+
self.conn.execute(
|
|
371
|
+
"UPDATE interactions SET embedding = ? WHERE interaction_id = ?",
|
|
372
|
+
(_json_dumps(embedding), interaction_id),
|
|
373
|
+
)
|
|
374
|
+
if own_txn:
|
|
375
|
+
self.conn.commit()
|
|
376
|
+
self._vec_upsert("interactions_vec", interaction_id, embedding)
|
|
192
377
|
|
|
193
378
|
@SQLiteStorageBase.handle_exceptions
|
|
194
379
|
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
@@ -16,6 +16,9 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
16
16
|
Status,
|
|
17
17
|
UserProfile,
|
|
18
18
|
)
|
|
19
|
+
from reflexio.server.services.storage.lifecycle_filters import (
|
|
20
|
+
validate_include_inactive,
|
|
21
|
+
)
|
|
19
22
|
|
|
20
23
|
from .._base import (
|
|
21
24
|
_PROFILE_TOMBSTONE_STATUS_VALUES,
|
|
@@ -77,6 +80,7 @@ class ProfileStoreMixin:
|
|
|
77
80
|
_has_sqlite_vec: bool
|
|
78
81
|
_subject_ref_for_user_id: Any
|
|
79
82
|
_assert_subject_writable_locked: Any
|
|
83
|
+
_own_transaction: Any
|
|
80
84
|
|
|
81
85
|
def _subject_ref_from_profile_row(self, row: sqlite3.Row) -> str:
|
|
82
86
|
subject_ref = row["governance_subject_ref"]
|
|
@@ -207,12 +211,14 @@ class ProfileStoreMixin:
|
|
|
207
211
|
sql = f"SELECT * FROM profiles WHERE {' AND '.join(conditions)}"
|
|
208
212
|
return [_row_to_profile(r) for r in self._fetchall(sql, all_params)]
|
|
209
213
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
def precompute_profile_embeddings(self, profiles: list[UserProfile]) -> None:
|
|
215
|
+
"""Populate ``.embedding`` / ``.expanded_terms`` in place; no DB write.
|
|
216
|
+
|
|
217
|
+
Extracted verbatim from the former ``add_user_profile`` prelude so the
|
|
218
|
+
durable compute/persist split can embed outside the writer transaction
|
|
219
|
+
and then persist with ``skip_embedding=True``.
|
|
220
|
+
"""
|
|
221
|
+
for profile in profiles:
|
|
216
222
|
embedding_text = "\n".join([profile.content, str(profile.custom_features)])
|
|
217
223
|
if self._should_expand_documents():
|
|
218
224
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
@@ -222,10 +228,31 @@ class ProfileStoreMixin:
|
|
|
222
228
|
profile.expanded_terms = exp_future.result(timeout=15)
|
|
223
229
|
else:
|
|
224
230
|
profile.embedding = self._get_embedding(embedding_text)
|
|
231
|
+
|
|
232
|
+
@SQLiteStorageBase.handle_exceptions
|
|
233
|
+
def add_user_profile(
|
|
234
|
+
self,
|
|
235
|
+
user_id: str, # noqa: ARG002
|
|
236
|
+
user_profiles: list[UserProfile],
|
|
237
|
+
*,
|
|
238
|
+
skip_embedding: bool = False,
|
|
239
|
+
) -> None:
|
|
240
|
+
for profile in user_profiles:
|
|
241
|
+
subject_ref = self._subject_ref_for_user_id(profile.user_id)
|
|
242
|
+
with self._lock:
|
|
243
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
244
|
+
# Default (skip_embedding=False) recomputes unconditionally, exactly
|
|
245
|
+
# as before — model_copy callers that change content while keeping
|
|
246
|
+
# the old embedding depend on this. The durable persist path opts
|
|
247
|
+
# out (embedding already set by precompute_profile_embeddings).
|
|
248
|
+
if not skip_embedding:
|
|
249
|
+
self.precompute_profile_embeddings([profile])
|
|
225
250
|
embedding = profile.embedding
|
|
226
251
|
with self._lock:
|
|
252
|
+
own_txn = self._own_transaction()
|
|
227
253
|
try:
|
|
228
|
-
|
|
254
|
+
if own_txn:
|
|
255
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
229
256
|
self._assert_subject_writable_locked(subject_ref)
|
|
230
257
|
self.conn.execute(
|
|
231
258
|
"""INSERT OR REPLACE INTO profiles
|
|
@@ -261,9 +288,11 @@ class ProfileStoreMixin:
|
|
|
261
288
|
subject_ref,
|
|
262
289
|
),
|
|
263
290
|
)
|
|
264
|
-
|
|
291
|
+
if own_txn:
|
|
292
|
+
self.conn.commit()
|
|
265
293
|
except Exception:
|
|
266
|
-
|
|
294
|
+
if own_txn:
|
|
295
|
+
self.conn.rollback()
|
|
267
296
|
raise
|
|
268
297
|
fts_parts = [profile.content or ""]
|
|
269
298
|
if profile.custom_features:
|
|
@@ -473,6 +502,24 @@ class ProfileStoreMixin:
|
|
|
473
502
|
row = self._fetchone("SELECT COUNT(*) as cnt FROM profiles")
|
|
474
503
|
return row["cnt"] if row else 0
|
|
475
504
|
|
|
505
|
+
@SQLiteStorageBase.handle_exceptions
|
|
506
|
+
def count_user_profiles_by_status(
|
|
507
|
+
self, user_ids: list[str], status: Status | None
|
|
508
|
+
) -> int:
|
|
509
|
+
if not user_ids:
|
|
510
|
+
return 0
|
|
511
|
+
|
|
512
|
+
current_ts = _epoch_now()
|
|
513
|
+
status_frag, status_params = _build_status_sql([status])
|
|
514
|
+
ph = ",".join("?" for _ in user_ids)
|
|
515
|
+
sql = (
|
|
516
|
+
f"SELECT COUNT(*) as cnt FROM profiles "
|
|
517
|
+
f"WHERE user_id IN ({ph}) "
|
|
518
|
+
f"AND expiration_timestamp >= ? AND {status_frag}"
|
|
519
|
+
)
|
|
520
|
+
row = self._fetchone(sql, [*user_ids, current_ts, *status_params])
|
|
521
|
+
return row["cnt"] if row else 0
|
|
522
|
+
|
|
476
523
|
@SQLiteStorageBase.handle_exceptions
|
|
477
524
|
def update_all_profiles_status(
|
|
478
525
|
self,
|
|
@@ -616,14 +663,25 @@ class ProfileStoreMixin:
|
|
|
616
663
|
user_id: str,
|
|
617
664
|
profile_ids: list[str],
|
|
618
665
|
status_filter: list[Status | None] | None = None,
|
|
666
|
+
*,
|
|
667
|
+
include_inactive: bool = False,
|
|
619
668
|
) -> list[UserProfile]:
|
|
669
|
+
validate_include_inactive(
|
|
670
|
+
include_inactive=include_inactive, status_filter=status_filter
|
|
671
|
+
)
|
|
620
672
|
if not profile_ids:
|
|
621
673
|
return []
|
|
674
|
+
ph = ",".join("?" for _ in profile_ids)
|
|
675
|
+
if include_inactive:
|
|
676
|
+
rows = self._fetchall(
|
|
677
|
+
f"SELECT * FROM profiles WHERE user_id = ? AND profile_id IN ({ph})",
|
|
678
|
+
(user_id, *profile_ids),
|
|
679
|
+
)
|
|
680
|
+
return [_row_to_profile(r) for r in rows]
|
|
622
681
|
if status_filter is None:
|
|
623
682
|
status_filter = [None]
|
|
624
683
|
current_ts = _epoch_now()
|
|
625
684
|
frag, sparams = _build_status_sql(status_filter)
|
|
626
|
-
ph = ",".join("?" for _ in profile_ids)
|
|
627
685
|
sql = (
|
|
628
686
|
f"SELECT * FROM profiles "
|
|
629
687
|
f"WHERE user_id = ? AND profile_id IN ({ph}) "
|
|
@@ -809,7 +867,8 @@ class ProfileStoreMixin:
|
|
|
809
867
|
status_namespace="lifecycle_status",
|
|
810
868
|
)
|
|
811
869
|
committed_ids.append(pid)
|
|
812
|
-
self.
|
|
870
|
+
if self._own_transaction():
|
|
871
|
+
self.conn.commit()
|
|
813
872
|
return committed_ids
|
|
814
873
|
|
|
815
874
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -22,7 +22,9 @@ from ._agent_run import (
|
|
|
22
22
|
not_applicable_tool_result,
|
|
23
23
|
)
|
|
24
24
|
from ._base import BaseStorageCore, matches_status_filter
|
|
25
|
+
from ._commit_scope import CommitScopeMixin
|
|
25
26
|
from ._extras import ExtrasMixin
|
|
27
|
+
from ._learning_jobs import LearningJob, LearningJobStatus, LearningJobStoreABC
|
|
26
28
|
from ._lineage import EntityType, LineageEventMixin
|
|
27
29
|
from ._operations import OperationMixin
|
|
28
30
|
from ._requests import RequestMixin
|
|
@@ -48,6 +50,8 @@ from .profiles import InteractionStoreMixin, ProfileSearchMixin, ProfileStoreMix
|
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class BaseStorage(
|
|
53
|
+
LearningJobStoreABC,
|
|
54
|
+
CommitScopeMixin,
|
|
51
55
|
AgentRunMixin,
|
|
52
56
|
ProfileStoreMixin,
|
|
53
57
|
InteractionStoreMixin,
|
|
@@ -260,8 +264,21 @@ class BaseStorage(
|
|
|
260
264
|
"purged_user_playbooks": len(purge_upb_ids),
|
|
261
265
|
}
|
|
262
266
|
|
|
267
|
+
def learning_jobs_columns(self) -> list[str]:
|
|
268
|
+
"""Return the column names of the learning_jobs table.
|
|
269
|
+
|
|
270
|
+
Each backend overrides this to query its own schema introspection
|
|
271
|
+
mechanism (SQLite: PRAGMA table_info; Postgres/Supabase:
|
|
272
|
+
information_schema.columns).
|
|
273
|
+
"""
|
|
274
|
+
raise NotImplementedError
|
|
275
|
+
|
|
263
276
|
|
|
264
277
|
__all__ = [
|
|
278
|
+
"LearningJob",
|
|
279
|
+
"LearningJobStatus",
|
|
280
|
+
"LearningJobStoreABC",
|
|
281
|
+
"CommitScopeMixin",
|
|
265
282
|
"AgentBinding",
|
|
266
283
|
"AgentRunMixin",
|
|
267
284
|
"EntityType",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from contextlib import AbstractContextManager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommitScopeMixin(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def commit_scope(self) -> AbstractContextManager[None]:
|
|
8
|
+
"""Atomic multi-write transaction; see Workstream A plan Task 1."""
|
|
9
|
+
raise NotImplementedError
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
+
from typing import cast
|
|
2
3
|
|
|
3
4
|
from reflexio.models.api_schema.braintrust_schema import (
|
|
4
5
|
BraintrustConnection,
|
|
5
6
|
ImportedScore,
|
|
6
7
|
)
|
|
7
8
|
from reflexio.models.api_schema.domain import (
|
|
9
|
+
CitationKind,
|
|
8
10
|
Interaction,
|
|
9
11
|
)
|
|
10
12
|
from reflexio.models.api_schema.internal_schema import SessionCitation
|
|
13
|
+
|
|
14
|
+
# Stored citation dicts are untyped JSON; only these kinds are valid.
|
|
15
|
+
CITATION_KINDS: frozenset[str] = frozenset(
|
|
16
|
+
{"playbook", "profile", "user_playbook", "agent_playbook"}
|
|
17
|
+
)
|
|
11
18
|
from reflexio.models.api_schema.retriever_schema import PlaybookApplicationStat
|
|
12
19
|
|
|
13
20
|
|
|
@@ -142,12 +149,12 @@ class ExtrasMixin:
|
|
|
142
149
|
kind = getattr(cite, "kind", None)
|
|
143
150
|
real_id = getattr(cite, "real_id", None)
|
|
144
151
|
title = getattr(cite, "title", "") or ""
|
|
145
|
-
if kind and real_id:
|
|
152
|
+
if str(kind) in CITATION_KINDS and real_id:
|
|
146
153
|
out.append(
|
|
147
154
|
SessionCitation(
|
|
148
155
|
user_id="",
|
|
149
156
|
session_id=session_id,
|
|
150
|
-
kind=str(kind),
|
|
157
|
+
kind=cast("CitationKind", str(kind)),
|
|
151
158
|
real_id=str(real_id),
|
|
152
159
|
title=str(title),
|
|
153
160
|
)
|