claude-smart 0.2.49 → 0.2.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.md +10 -43
- package/bin/claude-smart.js +105 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +197 -1
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +757 -119
- package/plugin/scripts/codex-hook.js +63 -225
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +117 -136
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/smart-install.sh +15 -1
- package/plugin/src/claude_smart/cli.py +14 -0
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +4 -1
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -166,6 +166,7 @@ class SubjectBarrierMixin:
|
|
|
166
166
|
"profiles",
|
|
167
167
|
"user_playbooks",
|
|
168
168
|
"agent_success_evaluation_result",
|
|
169
|
+
"retrieved_learning_evaluation",
|
|
169
170
|
):
|
|
170
171
|
row = self.conn.execute(
|
|
171
172
|
f"""SELECT 1 FROM {table}
|
|
@@ -198,9 +199,14 @@ class SubjectBarrierMixin:
|
|
|
198
199
|
request_id_column="request_id",
|
|
199
200
|
):
|
|
200
201
|
return True
|
|
201
|
-
|
|
202
|
+
if self._legacy_user_id_rows_remain_locked(
|
|
202
203
|
table="agent_success_evaluation_result",
|
|
203
204
|
subject_ref=subject_ref,
|
|
205
|
+
):
|
|
206
|
+
return True
|
|
207
|
+
return self._legacy_user_id_rows_remain_locked(
|
|
208
|
+
table="retrieved_learning_evaluation",
|
|
209
|
+
subject_ref=subject_ref,
|
|
204
210
|
)
|
|
205
211
|
|
|
206
212
|
def begin_subject_erasure_barrier(
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py
CHANGED
|
@@ -19,6 +19,9 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
19
19
|
Status,
|
|
20
20
|
)
|
|
21
21
|
from reflexio.models.config_schema import SearchMode, SearchOptions
|
|
22
|
+
from reflexio.server.services.storage.lifecycle_filters import (
|
|
23
|
+
validate_include_inactive,
|
|
24
|
+
)
|
|
22
25
|
from reflexio.server.services.storage.storage_base._playbook import (
|
|
23
26
|
AGGREGATE_REASON_PREFIX,
|
|
24
27
|
)
|
|
@@ -344,6 +347,46 @@ class AgentPlaybookStoreMixin:
|
|
|
344
347
|
row = self._fetchone(sql, (agent_playbook_id,))
|
|
345
348
|
return _row_to_agent_playbook(row) if row else None
|
|
346
349
|
|
|
350
|
+
@SQLiteStorageBase.handle_exceptions
|
|
351
|
+
def get_agent_playbooks_by_ids(
|
|
352
|
+
self,
|
|
353
|
+
agent_playbook_ids: list[int],
|
|
354
|
+
*,
|
|
355
|
+
status_filter: list[Status | None] | None = None,
|
|
356
|
+
playbook_status_filter: list[PlaybookStatus] | None = None,
|
|
357
|
+
include_inactive: bool = False,
|
|
358
|
+
) -> list[AgentPlaybook]:
|
|
359
|
+
validate_include_inactive(
|
|
360
|
+
include_inactive=include_inactive,
|
|
361
|
+
status_filter=status_filter,
|
|
362
|
+
playbook_status_filter=playbook_status_filter,
|
|
363
|
+
)
|
|
364
|
+
if not agent_playbook_ids:
|
|
365
|
+
return []
|
|
366
|
+
unique_ids = list(dict.fromkeys(agent_playbook_ids))
|
|
367
|
+
rows: list[sqlite3.Row] = []
|
|
368
|
+
for start in range(0, len(unique_ids), 900):
|
|
369
|
+
chunk = unique_ids[start : start + 900]
|
|
370
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
371
|
+
query = (
|
|
372
|
+
"SELECT * FROM agent_playbooks "
|
|
373
|
+
f"WHERE agent_playbook_id IN ({placeholders})"
|
|
374
|
+
)
|
|
375
|
+
params: list[Any] = list(chunk)
|
|
376
|
+
if not include_inactive:
|
|
377
|
+
if status_filter is not None:
|
|
378
|
+
fragment, status_params = _build_status_sql(status_filter)
|
|
379
|
+
query += f" AND {fragment}"
|
|
380
|
+
params.extend(status_params)
|
|
381
|
+
else:
|
|
382
|
+
query += " AND status IS NULL"
|
|
383
|
+
if playbook_status_filter:
|
|
384
|
+
status_placeholders = ",".join("?" for _ in playbook_status_filter)
|
|
385
|
+
query += f" AND playbook_status IN ({status_placeholders})"
|
|
386
|
+
params.extend(status.value for status in playbook_status_filter)
|
|
387
|
+
rows.extend(self._fetchall(query, params))
|
|
388
|
+
return [_row_to_agent_playbook(row) for row in rows]
|
|
389
|
+
|
|
347
390
|
@SQLiteStorageBase.handle_exceptions
|
|
348
391
|
def delete_all_agent_playbooks(self) -> None:
|
|
349
392
|
batch_request_id = uuid.uuid4().hex
|
|
@@ -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)]
|
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,
|
|
@@ -97,12 +100,17 @@ class UserPlaybookStoreMixin:
|
|
|
97
100
|
)
|
|
98
101
|
return row
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
106
114
|
embedding_text = up.trigger or up.content
|
|
107
115
|
if embedding_text:
|
|
108
116
|
if self._should_expand_documents():
|
|
@@ -118,6 +126,24 @@ class UserPlaybookStoreMixin:
|
|
|
118
126
|
else:
|
|
119
127
|
up.embedding = self._get_embedding(embedding_text)
|
|
120
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
|
+
|
|
121
147
|
created_at_iso = _epoch_to_iso(up.created_at)
|
|
122
148
|
with self._lock:
|
|
123
149
|
own_txn = self._own_transaction()
|
|
@@ -521,13 +547,25 @@ class UserPlaybookStoreMixin:
|
|
|
521
547
|
user_id: str,
|
|
522
548
|
user_playbook_ids: list[int],
|
|
523
549
|
status_filter: list[Status | None] | None = None,
|
|
550
|
+
*,
|
|
551
|
+
include_inactive: bool = False,
|
|
524
552
|
) -> list[UserPlaybook]:
|
|
553
|
+
validate_include_inactive(
|
|
554
|
+
include_inactive=include_inactive, status_filter=status_filter
|
|
555
|
+
)
|
|
525
556
|
if not user_playbook_ids:
|
|
526
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]
|
|
527
566
|
if status_filter is None:
|
|
528
567
|
status_filter = [None]
|
|
529
568
|
frag, sparams = _build_status_sql(status_filter)
|
|
530
|
-
ph = ",".join("?" for _ in user_playbook_ids)
|
|
531
569
|
sql = (
|
|
532
570
|
f"SELECT * FROM user_playbooks "
|
|
533
571
|
f"WHERE user_id = ? AND user_playbook_id IN ({ph}) AND {frag}"
|