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
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
"""Agent success evaluation result store methods for SQLite storage."""
|
|
2
2
|
|
|
3
3
|
import sqlite3
|
|
4
|
+
from datetime import UTC, datetime
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from reflexio.models.api_schema.service_schemas import (
|
|
7
8
|
AgentSuccessEvaluationResult,
|
|
9
|
+
RetrievedLearningEvaluationResult,
|
|
8
10
|
)
|
|
9
11
|
|
|
12
|
+
from ...storage_base.retrieved_learning_state import (
|
|
13
|
+
CANONICAL_RETRIEVED_KINDS,
|
|
14
|
+
DEFAULT_TRANSCRIPT_CHAR_LIMIT,
|
|
15
|
+
TERMINAL_RETRIEVED_STATUSES,
|
|
16
|
+
BoundedRetrievedLearningSnapshot,
|
|
17
|
+
RetrievedLearningCommitResult,
|
|
18
|
+
SessionFingerprintBuilder,
|
|
19
|
+
append_bounded_snapshot_interaction,
|
|
20
|
+
build_retrieved_learning_state_key,
|
|
21
|
+
)
|
|
10
22
|
from .._base import (
|
|
11
23
|
SQLiteStorageBase,
|
|
12
24
|
_epoch_to_iso,
|
|
25
|
+
_iso_to_epoch,
|
|
13
26
|
_json_dumps,
|
|
27
|
+
_json_loads,
|
|
14
28
|
_row_to_eval_result,
|
|
15
29
|
)
|
|
16
30
|
|
|
@@ -23,9 +37,12 @@ class AgentEvaluationResultStoreMixin:
|
|
|
23
37
|
conn: sqlite3.Connection
|
|
24
38
|
_execute: Any
|
|
25
39
|
_fetchall: Any
|
|
40
|
+
_fetchone: Any
|
|
26
41
|
_get_embedding: Any
|
|
27
42
|
_subject_ref_for_user_id: Any
|
|
28
43
|
_assert_subject_writable_locked: Any
|
|
44
|
+
_current_timestamp: Any
|
|
45
|
+
get_operation_state: Any
|
|
29
46
|
|
|
30
47
|
# ------------------------------------------------------------------
|
|
31
48
|
# Agent Success Evaluation methods
|
|
@@ -36,11 +53,12 @@ class AgentEvaluationResultStoreMixin:
|
|
|
36
53
|
self, results: list[AgentSuccessEvaluationResult]
|
|
37
54
|
) -> None:
|
|
38
55
|
for result in results:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
if not result.embedding:
|
|
57
|
+
embedding_text = f"{result.failure_type} {result.failure_reason}"
|
|
58
|
+
if embedding_text.strip():
|
|
59
|
+
result.embedding = self._get_embedding(embedding_text)
|
|
60
|
+
else:
|
|
61
|
+
result.embedding = []
|
|
44
62
|
|
|
45
63
|
created_at_iso = _epoch_to_iso(result.created_at)
|
|
46
64
|
subject_ref = self._subject_ref_for_user_id(result.user_id)
|
|
@@ -187,3 +205,410 @@ class AgentEvaluationResultStoreMixin:
|
|
|
187
205
|
list(result_ids),
|
|
188
206
|
)
|
|
189
207
|
return cur.rowcount
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# Retrieved-learning evaluation methods
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Generation fencing + session-fingerprint CAS; see
|
|
213
|
+
# storage_base/retrieved_learning_state.py for the concurrency contract.
|
|
214
|
+
# All state reads/writes inside a transaction use raw self.conn.execute
|
|
215
|
+
# (never self._execute or self-committing helpers) so replacement stays
|
|
216
|
+
# atomic on the shared connection.
|
|
217
|
+
|
|
218
|
+
def _rle_state_row(self, state_key: str) -> dict[str, Any]:
|
|
219
|
+
cur = self.conn.execute(
|
|
220
|
+
"SELECT operation_state FROM _operation_state WHERE service_name = ?",
|
|
221
|
+
(state_key,),
|
|
222
|
+
)
|
|
223
|
+
row = cur.fetchone()
|
|
224
|
+
if not row:
|
|
225
|
+
return {}
|
|
226
|
+
return _json_loads(row["operation_state"]) or {}
|
|
227
|
+
|
|
228
|
+
def _rle_upsert_state(self, state_key: str, state: dict[str, Any]) -> None:
|
|
229
|
+
self.conn.execute(
|
|
230
|
+
"""INSERT INTO _operation_state (service_name, operation_state, updated_at)
|
|
231
|
+
VALUES (?,?,?)
|
|
232
|
+
ON CONFLICT(service_name) DO UPDATE SET
|
|
233
|
+
operation_state = excluded.operation_state,
|
|
234
|
+
updated_at = excluded.updated_at""",
|
|
235
|
+
(state_key, _json_dumps(state), self._current_timestamp()),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _rle_fingerprint_now(self, user_id: str, session_id: str) -> str:
|
|
239
|
+
"""Recompute the session fingerprint from live rows (in-transaction)."""
|
|
240
|
+
cur = self.conn.execute(
|
|
241
|
+
"""SELECT i.interaction_id, i.role,
|
|
242
|
+
substr(i.content, 1, ?) AS content, i.retrieved_learnings
|
|
243
|
+
FROM interactions i JOIN requests r ON i.request_id = r.request_id
|
|
244
|
+
WHERE r.session_id = ? AND i.user_id = ?
|
|
245
|
+
ORDER BY i.created_at ASC, i.interaction_id ASC""",
|
|
246
|
+
(DEFAULT_TRANSCRIPT_CHAR_LIMIT, session_id, user_id),
|
|
247
|
+
)
|
|
248
|
+
builder = SessionFingerprintBuilder()
|
|
249
|
+
for row in cur:
|
|
250
|
+
builder.add(
|
|
251
|
+
int(row["interaction_id"]),
|
|
252
|
+
_parse_attachment_refs(row["retrieved_learnings"]),
|
|
253
|
+
row["role"] or "User",
|
|
254
|
+
row["content"] or "",
|
|
255
|
+
)
|
|
256
|
+
return builder.hexdigest()
|
|
257
|
+
|
|
258
|
+
def _rle_attached_refs(self, user_id: str, session_id: str) -> set[tuple[str, str]]:
|
|
259
|
+
"""Canonical ``(kind, learning_id)`` refs attached to the live session."""
|
|
260
|
+
cur = self.conn.execute(
|
|
261
|
+
"""SELECT i.retrieved_learnings
|
|
262
|
+
FROM interactions i JOIN requests r ON i.request_id = r.request_id
|
|
263
|
+
WHERE r.session_id = ? AND i.user_id = ?""",
|
|
264
|
+
(session_id, user_id),
|
|
265
|
+
)
|
|
266
|
+
attached: set[tuple[str, str]] = set()
|
|
267
|
+
for row in cur:
|
|
268
|
+
attached.update(_parse_attachment_refs(row["retrieved_learnings"]))
|
|
269
|
+
return attached
|
|
270
|
+
|
|
271
|
+
def _rle_resolvable_refs(
|
|
272
|
+
self, user_id: str, results: list[RetrievedLearningEvaluationResult]
|
|
273
|
+
) -> set[tuple[str, str]]:
|
|
274
|
+
"""Recheck original source rows for historical resolution (in-transaction)."""
|
|
275
|
+
by_kind: dict[str, list[str]] = {}
|
|
276
|
+
for r in results:
|
|
277
|
+
by_kind.setdefault(r.kind, []).append(r.learning_id)
|
|
278
|
+
resolvable: set[tuple[str, str]] = set()
|
|
279
|
+
for chunk in _chunked(by_kind.get("profile", [])):
|
|
280
|
+
ph = ",".join("?" for _ in chunk)
|
|
281
|
+
cur = self.conn.execute(
|
|
282
|
+
f"""SELECT profile_id FROM profiles
|
|
283
|
+
WHERE user_id = ? AND profile_id IN ({ph})""",
|
|
284
|
+
(user_id, *chunk),
|
|
285
|
+
)
|
|
286
|
+
resolvable.update(("profile", str(row[0])) for row in cur.fetchall())
|
|
287
|
+
user_playbook_ids = [
|
|
288
|
+
i for i in (_as_int(x) for x in by_kind.get("user_playbook", [])) if i
|
|
289
|
+
]
|
|
290
|
+
for chunk in _chunked(user_playbook_ids):
|
|
291
|
+
ph = ",".join("?" for _ in chunk)
|
|
292
|
+
cur = self.conn.execute(
|
|
293
|
+
f"""SELECT user_playbook_id FROM user_playbooks
|
|
294
|
+
WHERE user_id = ? AND user_playbook_id IN ({ph})""",
|
|
295
|
+
(user_id, *chunk),
|
|
296
|
+
)
|
|
297
|
+
resolvable.update(("user_playbook", str(row[0])) for row in cur.fetchall())
|
|
298
|
+
agent_playbook_ids = [
|
|
299
|
+
i for i in (_as_int(x) for x in by_kind.get("agent_playbook", [])) if i
|
|
300
|
+
]
|
|
301
|
+
for chunk in _chunked(agent_playbook_ids):
|
|
302
|
+
ph = ",".join("?" for _ in chunk)
|
|
303
|
+
cur = self.conn.execute(
|
|
304
|
+
f"""SELECT agent_playbook_id FROM agent_playbooks
|
|
305
|
+
WHERE agent_playbook_id IN ({ph})""",
|
|
306
|
+
chunk,
|
|
307
|
+
)
|
|
308
|
+
resolvable.update(("agent_playbook", str(row[0])) for row in cur.fetchall())
|
|
309
|
+
return resolvable
|
|
310
|
+
|
|
311
|
+
@SQLiteStorageBase.handle_exceptions
|
|
312
|
+
def begin_retrieved_learning_evaluation_run(
|
|
313
|
+
self, user_id: str, session_id: str
|
|
314
|
+
) -> int:
|
|
315
|
+
state_key = build_retrieved_learning_state_key(user_id, session_id)
|
|
316
|
+
subject_ref = self._subject_ref_for_user_id(user_id)
|
|
317
|
+
with self._lock:
|
|
318
|
+
try:
|
|
319
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
320
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
321
|
+
state = self._rle_state_row(state_key)
|
|
322
|
+
generation = int(state.get("generation") or 0) + 1
|
|
323
|
+
state.update(
|
|
324
|
+
{
|
|
325
|
+
"user_id": user_id,
|
|
326
|
+
"session_id": session_id,
|
|
327
|
+
"generation": generation,
|
|
328
|
+
"attempted_at": int(self._current_epoch()),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
state.setdefault("status", "pending")
|
|
332
|
+
state.setdefault("session_fingerprint", "")
|
|
333
|
+
self._rle_upsert_state(state_key, state)
|
|
334
|
+
self.conn.commit()
|
|
335
|
+
except Exception:
|
|
336
|
+
self.conn.rollback()
|
|
337
|
+
raise
|
|
338
|
+
return generation
|
|
339
|
+
|
|
340
|
+
@SQLiteStorageBase.handle_exceptions
|
|
341
|
+
def load_bounded_retrieved_learning_snapshot(
|
|
342
|
+
self,
|
|
343
|
+
user_id: str,
|
|
344
|
+
session_id: str,
|
|
345
|
+
raw_ref_limit: int = 5_000,
|
|
346
|
+
transcript_char_limit: int = DEFAULT_TRANSCRIPT_CHAR_LIMIT,
|
|
347
|
+
) -> BoundedRetrievedLearningSnapshot:
|
|
348
|
+
snapshot = BoundedRetrievedLearningSnapshot()
|
|
349
|
+
row = self._fetchone(
|
|
350
|
+
"""SELECT MIN(created_at) AS earliest FROM requests
|
|
351
|
+
WHERE session_id = ? AND user_id = ?""",
|
|
352
|
+
(session_id, user_id),
|
|
353
|
+
)
|
|
354
|
+
if row and row["earliest"]:
|
|
355
|
+
snapshot.earliest_request_created_at = _iso_to_epoch(row["earliest"])
|
|
356
|
+
row = self._fetchone(
|
|
357
|
+
"""SELECT agent_version FROM requests
|
|
358
|
+
WHERE session_id = ? AND user_id = ?
|
|
359
|
+
ORDER BY created_at DESC LIMIT 1""",
|
|
360
|
+
(session_id, user_id),
|
|
361
|
+
)
|
|
362
|
+
if row:
|
|
363
|
+
snapshot.agent_version = row["agent_version"] or ""
|
|
364
|
+
cursor = self.conn.execute(
|
|
365
|
+
# ``fp_content`` is truncated to the fixed fingerprint limit,
|
|
366
|
+
# independent of the caller's ``transcript_char_limit`` snapshot
|
|
367
|
+
# budget, so the commit-side recompute (which has no such budget)
|
|
368
|
+
# produces an identical digest.
|
|
369
|
+
"""SELECT i.interaction_id, i.role,
|
|
370
|
+
substr(i.content, 1, ?) AS content,
|
|
371
|
+
substr(i.content, 1, ?) AS fp_content,
|
|
372
|
+
i.created_at, i.retrieved_learnings
|
|
373
|
+
FROM interactions i JOIN requests r ON i.request_id = r.request_id
|
|
374
|
+
WHERE r.session_id = ? AND i.user_id = ?
|
|
375
|
+
ORDER BY i.created_at ASC, i.interaction_id ASC""",
|
|
376
|
+
(transcript_char_limit, DEFAULT_TRANSCRIPT_CHAR_LIMIT, session_id, user_id),
|
|
377
|
+
)
|
|
378
|
+
builder = SessionFingerprintBuilder()
|
|
379
|
+
transcript_chars_remaining = transcript_char_limit
|
|
380
|
+
for r in cursor:
|
|
381
|
+
refs = _parse_attachment_refs(r["retrieved_learnings"])
|
|
382
|
+
snapshot.raw_attachment_count += len(refs)
|
|
383
|
+
builder.add(
|
|
384
|
+
int(r["interaction_id"]),
|
|
385
|
+
refs,
|
|
386
|
+
r["role"] or "User",
|
|
387
|
+
r["fp_content"] or "",
|
|
388
|
+
)
|
|
389
|
+
if snapshot.raw_attachment_count > raw_ref_limit:
|
|
390
|
+
snapshot.attachment_limit_exceeded = True
|
|
391
|
+
snapshot.interactions.clear()
|
|
392
|
+
transcript_chars_remaining = 0
|
|
393
|
+
continue
|
|
394
|
+
transcript_chars_remaining = append_bounded_snapshot_interaction(
|
|
395
|
+
snapshot,
|
|
396
|
+
interaction_id=int(r["interaction_id"]),
|
|
397
|
+
role=r["role"] or "User",
|
|
398
|
+
content=r["content"] or "",
|
|
399
|
+
created_at=_iso_to_epoch(r["created_at"]),
|
|
400
|
+
refs=refs,
|
|
401
|
+
transcript_chars_remaining=transcript_chars_remaining,
|
|
402
|
+
)
|
|
403
|
+
snapshot.precomputed_fingerprint = builder.hexdigest()
|
|
404
|
+
return snapshot
|
|
405
|
+
|
|
406
|
+
@SQLiteStorageBase.handle_exceptions
|
|
407
|
+
def get_matching_retrieved_learning_terminal_state(
|
|
408
|
+
self, user_id: str, session_id: str, session_fingerprint: str
|
|
409
|
+
) -> dict[str, Any] | None:
|
|
410
|
+
state_key = build_retrieved_learning_state_key(user_id, session_id)
|
|
411
|
+
with self._lock:
|
|
412
|
+
try:
|
|
413
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
414
|
+
state = self._rle_state_row(state_key)
|
|
415
|
+
live_fingerprint = self._rle_fingerprint_now(user_id, session_id)
|
|
416
|
+
matched = (
|
|
417
|
+
state.get("status") in TERMINAL_RETRIEVED_STATUSES
|
|
418
|
+
and state.get("session_fingerprint") == session_fingerprint
|
|
419
|
+
and live_fingerprint == session_fingerprint
|
|
420
|
+
)
|
|
421
|
+
self.conn.commit()
|
|
422
|
+
except Exception:
|
|
423
|
+
self.conn.rollback()
|
|
424
|
+
raise
|
|
425
|
+
return state if matched else None
|
|
426
|
+
|
|
427
|
+
@SQLiteStorageBase.handle_exceptions
|
|
428
|
+
def replace_retrieved_learning_evaluation_results(
|
|
429
|
+
self,
|
|
430
|
+
user_id: str,
|
|
431
|
+
session_id: str,
|
|
432
|
+
generation: int,
|
|
433
|
+
session_fingerprint: str,
|
|
434
|
+
proposed_status: str,
|
|
435
|
+
diagnostics: dict[str, Any],
|
|
436
|
+
results: list[RetrievedLearningEvaluationResult],
|
|
437
|
+
) -> RetrievedLearningCommitResult:
|
|
438
|
+
if proposed_status not in ("complete", "degraded"):
|
|
439
|
+
raise ValueError(f"invalid proposed_status: {proposed_status}")
|
|
440
|
+
state_key = build_retrieved_learning_state_key(user_id, session_id)
|
|
441
|
+
subject_ref = self._subject_ref_for_user_id(user_id)
|
|
442
|
+
with self._lock:
|
|
443
|
+
try:
|
|
444
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
445
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
446
|
+
state = self._rle_state_row(state_key)
|
|
447
|
+
if int(state.get("generation") or 0) != generation:
|
|
448
|
+
self.conn.rollback()
|
|
449
|
+
return RetrievedLearningCommitResult(disposition="superseded")
|
|
450
|
+
if self._rle_fingerprint_now(user_id, session_id) != (
|
|
451
|
+
session_fingerprint
|
|
452
|
+
):
|
|
453
|
+
self.conn.rollback()
|
|
454
|
+
return RetrievedLearningCommitResult(disposition="stale")
|
|
455
|
+
# Persist only records that are still attached to the live
|
|
456
|
+
# session AND whose original row still exists. A duplicate identity is left
|
|
457
|
+
# to trip the UNIQUE index and roll back the commit — caller
|
|
458
|
+
# bugs fail loud rather than silently dropping data.
|
|
459
|
+
attached = self._rle_attached_refs(user_id, session_id)
|
|
460
|
+
resolvable = self._rle_resolvable_refs(user_id, results)
|
|
461
|
+
kept = [
|
|
462
|
+
r
|
|
463
|
+
for r in results
|
|
464
|
+
if (r.kind, r.learning_id) in attached
|
|
465
|
+
and (r.kind, r.learning_id) in resolvable
|
|
466
|
+
]
|
|
467
|
+
final_status = proposed_status if kept else "not_applicable"
|
|
468
|
+
self.conn.execute(
|
|
469
|
+
"""DELETE FROM retrieved_learning_evaluation
|
|
470
|
+
WHERE user_id = ? AND session_id = ?""",
|
|
471
|
+
(user_id, session_id),
|
|
472
|
+
)
|
|
473
|
+
for r in kept:
|
|
474
|
+
self.conn.execute(
|
|
475
|
+
"""INSERT INTO retrieved_learning_evaluation
|
|
476
|
+
(user_id, session_id, agent_version, kind,
|
|
477
|
+
learning_id, is_relevant, relevance_reason, impact,
|
|
478
|
+
impact_reason, created_at, governance_subject_ref)
|
|
479
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
|
480
|
+
(
|
|
481
|
+
user_id,
|
|
482
|
+
session_id,
|
|
483
|
+
r.agent_version,
|
|
484
|
+
r.kind,
|
|
485
|
+
r.learning_id,
|
|
486
|
+
None if r.is_relevant is None else int(r.is_relevant),
|
|
487
|
+
r.relevance_reason,
|
|
488
|
+
r.impact,
|
|
489
|
+
r.impact_reason,
|
|
490
|
+
r.created_at,
|
|
491
|
+
subject_ref,
|
|
492
|
+
),
|
|
493
|
+
)
|
|
494
|
+
state.update(diagnostics)
|
|
495
|
+
state.update(
|
|
496
|
+
{
|
|
497
|
+
"user_id": user_id,
|
|
498
|
+
"session_id": session_id,
|
|
499
|
+
"generation": generation,
|
|
500
|
+
"status": final_status,
|
|
501
|
+
"session_fingerprint": session_fingerprint,
|
|
502
|
+
"resolvable_count": len(resolvable),
|
|
503
|
+
"committed_count": len(kept),
|
|
504
|
+
"completed_at": int(self._current_epoch()),
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
self._rle_upsert_state(state_key, state)
|
|
508
|
+
self.conn.commit()
|
|
509
|
+
except Exception:
|
|
510
|
+
self.conn.rollback()
|
|
511
|
+
raise
|
|
512
|
+
return RetrievedLearningCommitResult(
|
|
513
|
+
disposition="applied", status=final_status, committed_count=len(kept)
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
@SQLiteStorageBase.handle_exceptions
|
|
517
|
+
def finish_retrieved_learning_evaluation_run(
|
|
518
|
+
self,
|
|
519
|
+
user_id: str,
|
|
520
|
+
session_id: str,
|
|
521
|
+
generation: int,
|
|
522
|
+
status: str,
|
|
523
|
+
diagnostics: dict[str, Any],
|
|
524
|
+
) -> None:
|
|
525
|
+
if status not in ("failed", "pending"):
|
|
526
|
+
raise ValueError(f"invalid finish status: {status}")
|
|
527
|
+
state_key = build_retrieved_learning_state_key(user_id, session_id)
|
|
528
|
+
with self._lock:
|
|
529
|
+
try:
|
|
530
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
531
|
+
state = self._rle_state_row(state_key)
|
|
532
|
+
if int(state.get("generation") or 0) != generation:
|
|
533
|
+
self.conn.rollback()
|
|
534
|
+
return
|
|
535
|
+
state.update(diagnostics)
|
|
536
|
+
state["status"] = status
|
|
537
|
+
self._rle_upsert_state(state_key, state)
|
|
538
|
+
self.conn.commit()
|
|
539
|
+
except Exception:
|
|
540
|
+
self.conn.rollback()
|
|
541
|
+
raise
|
|
542
|
+
|
|
543
|
+
@SQLiteStorageBase.handle_exceptions
|
|
544
|
+
def get_retrieved_learning_evaluation_results(
|
|
545
|
+
self,
|
|
546
|
+
user_id: str | None = None,
|
|
547
|
+
session_id: str | None = None,
|
|
548
|
+
limit: int = 100,
|
|
549
|
+
) -> list[RetrievedLearningEvaluationResult]:
|
|
550
|
+
sql = "SELECT * FROM retrieved_learning_evaluation"
|
|
551
|
+
clauses: list[str] = []
|
|
552
|
+
params: list[Any] = []
|
|
553
|
+
if user_id is not None:
|
|
554
|
+
clauses.append("user_id = ?")
|
|
555
|
+
params.append(user_id)
|
|
556
|
+
if session_id is not None:
|
|
557
|
+
clauses.append("session_id = ?")
|
|
558
|
+
params.append(session_id)
|
|
559
|
+
if clauses:
|
|
560
|
+
sql += " WHERE " + " AND ".join(clauses)
|
|
561
|
+
sql += " ORDER BY created_at DESC, result_id DESC LIMIT ?"
|
|
562
|
+
params.append(limit)
|
|
563
|
+
rows = self._fetchall(sql, params)
|
|
564
|
+
return [_row_to_retrieved_learning_result(r) for r in rows]
|
|
565
|
+
|
|
566
|
+
def _current_epoch(self) -> int:
|
|
567
|
+
return int(datetime.now(UTC).timestamp())
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _parse_attachment_refs(raw: Any) -> list[tuple[str, str]]:
|
|
571
|
+
"""Parse a stored retrieved_learnings JSON blob into (kind, learning_id) tuples."""
|
|
572
|
+
parsed = _json_loads(raw) if isinstance(raw, str) else raw
|
|
573
|
+
if not parsed or not isinstance(parsed, list):
|
|
574
|
+
return []
|
|
575
|
+
refs: list[tuple[str, str]] = []
|
|
576
|
+
for item in parsed:
|
|
577
|
+
if not isinstance(item, dict):
|
|
578
|
+
continue
|
|
579
|
+
kind = str(item.get("kind") or "")
|
|
580
|
+
learning_id = str(item.get("learning_id") or "")
|
|
581
|
+
if kind in CANONICAL_RETRIEVED_KINDS and learning_id:
|
|
582
|
+
refs.append((kind, learning_id))
|
|
583
|
+
return refs
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _row_to_retrieved_learning_result(
|
|
587
|
+
row: sqlite3.Row,
|
|
588
|
+
) -> RetrievedLearningEvaluationResult:
|
|
589
|
+
d = dict(row)
|
|
590
|
+
return RetrievedLearningEvaluationResult(
|
|
591
|
+
result_id=int(d["result_id"]),
|
|
592
|
+
user_id=d["user_id"],
|
|
593
|
+
session_id=d["session_id"],
|
|
594
|
+
agent_version=d.get("agent_version") or "",
|
|
595
|
+
kind=d["kind"],
|
|
596
|
+
learning_id=d["learning_id"],
|
|
597
|
+
is_relevant=None if d.get("is_relevant") is None else bool(d["is_relevant"]),
|
|
598
|
+
relevance_reason=d.get("relevance_reason") or "",
|
|
599
|
+
impact=d.get("impact"),
|
|
600
|
+
impact_reason=d.get("impact_reason") or "",
|
|
601
|
+
created_at=int(d["created_at"]),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _as_int(value: str) -> int | None:
|
|
606
|
+
try:
|
|
607
|
+
parsed = int(value)
|
|
608
|
+
except (TypeError, ValueError):
|
|
609
|
+
return None
|
|
610
|
+
return parsed if parsed > 0 else None
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _chunked(items: list, size: int = 500) -> list[list]:
|
|
614
|
+
return [items[i : i + size] for i in range(0, len(items), size)]
|
|
@@ -22,6 +22,7 @@ class PlaybookSourceLinkageMixin:
|
|
|
22
22
|
_fetchall: Any
|
|
23
23
|
_subject_ref_for_user_id: Any
|
|
24
24
|
_assert_subject_writable_locked: Any
|
|
25
|
+
_own_transaction: Any
|
|
25
26
|
|
|
26
27
|
@SQLiteStorageBase.handle_exceptions
|
|
27
28
|
def set_source_user_playbook_ids_for_agent_playbook(
|
|
@@ -87,8 +88,10 @@ class PlaybookSourceLinkageMixin:
|
|
|
87
88
|
ids.append(source_id)
|
|
88
89
|
seen.add(source_id)
|
|
89
90
|
with self._lock:
|
|
91
|
+
own_txn = self._own_transaction()
|
|
90
92
|
try:
|
|
91
|
-
|
|
93
|
+
if own_txn:
|
|
94
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
92
95
|
for user_playbook_id in by_id:
|
|
93
96
|
row = self.conn.execute(
|
|
94
97
|
"""SELECT user_id, governance_subject_ref FROM user_playbooks
|
|
@@ -120,9 +123,11 @@ class PlaybookSourceLinkageMixin:
|
|
|
120
123
|
for upid, source_interaction_ids in by_id.items()
|
|
121
124
|
],
|
|
122
125
|
)
|
|
123
|
-
|
|
126
|
+
if own_txn:
|
|
127
|
+
self.conn.commit()
|
|
124
128
|
except Exception:
|
|
125
|
-
|
|
129
|
+
if own_txn:
|
|
130
|
+
self.conn.rollback()
|
|
126
131
|
raise
|
|
127
132
|
|
|
128
133
|
@SQLiteStorageBase.handle_exceptions
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py
CHANGED
|
@@ -10,6 +10,9 @@ from reflexio.models.api_schema.common import BlockingIssue
|
|
|
10
10
|
from reflexio.models.api_schema.retriever_schema import SearchUserPlaybookRequest
|
|
11
11
|
from reflexio.models.api_schema.service_schemas import Status, UserPlaybook
|
|
12
12
|
from reflexio.models.config_schema import SearchMode, SearchOptions
|
|
13
|
+
from reflexio.server.services.storage.lifecycle_filters import (
|
|
14
|
+
validate_include_inactive,
|
|
15
|
+
)
|
|
13
16
|
|
|
14
17
|
from .._base import (
|
|
15
18
|
_TOMBSTONE_STATUS_VALUES,
|
|
@@ -71,6 +74,7 @@ class UserPlaybookStoreMixin:
|
|
|
71
74
|
_delete_playbook_search_rows: Any
|
|
72
75
|
_subject_ref_for_user_id: Any
|
|
73
76
|
_assert_subject_writable_locked: Any
|
|
77
|
+
_own_transaction: Any
|
|
74
78
|
|
|
75
79
|
def _subject_ref_from_user_playbook_row(self, row: sqlite3.Row) -> str:
|
|
76
80
|
subject_ref = row["governance_subject_ref"]
|
|
@@ -96,12 +100,17 @@ class UserPlaybookStoreMixin:
|
|
|
96
100
|
)
|
|
97
101
|
return row
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
def precompute_user_playbook_embeddings(
|
|
104
|
+
self, playbooks: list[UserPlaybook]
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Populate ``.embedding`` / ``.expanded_terms`` in place; no DB write.
|
|
107
|
+
|
|
108
|
+
Extracted verbatim from the former ``save_user_playbooks`` prelude
|
|
109
|
+
(including the ``if embedding_text:`` guard) so the durable
|
|
110
|
+
compute/persist split can embed outside the writer transaction and then
|
|
111
|
+
persist with ``skip_embedding=True``.
|
|
112
|
+
"""
|
|
113
|
+
for up in playbooks:
|
|
105
114
|
embedding_text = up.trigger or up.content
|
|
106
115
|
if embedding_text:
|
|
107
116
|
if self._should_expand_documents():
|
|
@@ -117,10 +126,30 @@ class UserPlaybookStoreMixin:
|
|
|
117
126
|
else:
|
|
118
127
|
up.embedding = self._get_embedding(embedding_text)
|
|
119
128
|
|
|
129
|
+
@SQLiteStorageBase.handle_exceptions
|
|
130
|
+
def save_user_playbooks(
|
|
131
|
+
self,
|
|
132
|
+
user_playbooks: list[UserPlaybook],
|
|
133
|
+
*,
|
|
134
|
+
skip_embedding: bool = False,
|
|
135
|
+
) -> None:
|
|
136
|
+
for up in user_playbooks:
|
|
137
|
+
subject_ref = self._subject_ref_for_user_id(up.user_id)
|
|
138
|
+
with self._lock:
|
|
139
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
140
|
+
# Default (skip_embedding=False) recomputes unconditionally, exactly
|
|
141
|
+
# as before — model_copy callers that change content while keeping
|
|
142
|
+
# the old embedding depend on this. The durable persist path opts
|
|
143
|
+
# out (embedding already set by precompute_user_playbook_embeddings).
|
|
144
|
+
if not skip_embedding:
|
|
145
|
+
self.precompute_user_playbook_embeddings([up])
|
|
146
|
+
|
|
120
147
|
created_at_iso = _epoch_to_iso(up.created_at)
|
|
121
148
|
with self._lock:
|
|
149
|
+
own_txn = self._own_transaction()
|
|
122
150
|
try:
|
|
123
|
-
|
|
151
|
+
if own_txn:
|
|
152
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
124
153
|
self._assert_subject_writable_locked(subject_ref)
|
|
125
154
|
cur = self.conn.execute(
|
|
126
155
|
"""INSERT INTO user_playbooks
|
|
@@ -159,9 +188,11 @@ class UserPlaybookStoreMixin:
|
|
|
159
188
|
)
|
|
160
189
|
upid = cur.lastrowid or 0
|
|
161
190
|
up.user_playbook_id = upid
|
|
162
|
-
|
|
191
|
+
if own_txn:
|
|
192
|
+
self.conn.commit()
|
|
163
193
|
except Exception:
|
|
164
|
-
|
|
194
|
+
if own_txn:
|
|
195
|
+
self.conn.rollback()
|
|
165
196
|
raise
|
|
166
197
|
|
|
167
198
|
fts_parts = [up.trigger or "", up.content or ""]
|
|
@@ -516,13 +547,25 @@ class UserPlaybookStoreMixin:
|
|
|
516
547
|
user_id: str,
|
|
517
548
|
user_playbook_ids: list[int],
|
|
518
549
|
status_filter: list[Status | None] | None = None,
|
|
550
|
+
*,
|
|
551
|
+
include_inactive: bool = False,
|
|
519
552
|
) -> list[UserPlaybook]:
|
|
553
|
+
validate_include_inactive(
|
|
554
|
+
include_inactive=include_inactive, status_filter=status_filter
|
|
555
|
+
)
|
|
520
556
|
if not user_playbook_ids:
|
|
521
557
|
return []
|
|
558
|
+
ph = ",".join("?" for _ in user_playbook_ids)
|
|
559
|
+
if include_inactive:
|
|
560
|
+
rows = self._fetchall(
|
|
561
|
+
"SELECT * FROM user_playbooks "
|
|
562
|
+
f"WHERE user_id = ? AND user_playbook_id IN ({ph})",
|
|
563
|
+
(user_id, *user_playbook_ids),
|
|
564
|
+
)
|
|
565
|
+
return [_row_to_user_playbook(r) for r in rows]
|
|
522
566
|
if status_filter is None:
|
|
523
567
|
status_filter = [None]
|
|
524
568
|
frag, sparams = _build_status_sql(status_filter)
|
|
525
|
-
ph = ",".join("?" for _ in user_playbook_ids)
|
|
526
569
|
sql = (
|
|
527
570
|
f"SELECT * FROM user_playbooks "
|
|
528
571
|
f"WHERE user_id = ? AND user_playbook_id IN ({ph}) AND {frag}"
|
|
@@ -789,7 +832,8 @@ class UserPlaybookStoreMixin:
|
|
|
789
832
|
to_status=None,
|
|
790
833
|
status_namespace=None,
|
|
791
834
|
)
|
|
792
|
-
self.
|
|
835
|
+
if self._own_transaction():
|
|
836
|
+
self.conn.commit()
|
|
793
837
|
|
|
794
838
|
@SQLiteStorageBase.handle_exceptions
|
|
795
839
|
def supersede_user_playbooks_by_ids(
|
|
@@ -840,5 +884,6 @@ class UserPlaybookStoreMixin:
|
|
|
840
884
|
request_id=request_id,
|
|
841
885
|
)
|
|
842
886
|
updated += 1
|
|
843
|
-
self.
|
|
887
|
+
if self._own_transaction():
|
|
888
|
+
self.conn.commit()
|
|
844
889
|
return updated
|