claude-smart 0.2.47 → 0.2.49
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 +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- 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 +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- 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 +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
- 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 +25 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- 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/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py
CHANGED
|
@@ -71,6 +71,7 @@ class UserPlaybookStoreMixin:
|
|
|
71
71
|
_delete_playbook_search_rows: Any
|
|
72
72
|
_subject_ref_for_user_id: Any
|
|
73
73
|
_assert_subject_writable_locked: Any
|
|
74
|
+
_own_transaction: Any
|
|
74
75
|
|
|
75
76
|
def _subject_ref_from_user_playbook_row(self, row: sqlite3.Row) -> str:
|
|
76
77
|
subject_ref = row["governance_subject_ref"]
|
|
@@ -119,8 +120,10 @@ class UserPlaybookStoreMixin:
|
|
|
119
120
|
|
|
120
121
|
created_at_iso = _epoch_to_iso(up.created_at)
|
|
121
122
|
with self._lock:
|
|
123
|
+
own_txn = self._own_transaction()
|
|
122
124
|
try:
|
|
123
|
-
|
|
125
|
+
if own_txn:
|
|
126
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
124
127
|
self._assert_subject_writable_locked(subject_ref)
|
|
125
128
|
cur = self.conn.execute(
|
|
126
129
|
"""INSERT INTO user_playbooks
|
|
@@ -159,9 +162,11 @@ class UserPlaybookStoreMixin:
|
|
|
159
162
|
)
|
|
160
163
|
upid = cur.lastrowid or 0
|
|
161
164
|
up.user_playbook_id = upid
|
|
162
|
-
|
|
165
|
+
if own_txn:
|
|
166
|
+
self.conn.commit()
|
|
163
167
|
except Exception:
|
|
164
|
-
|
|
168
|
+
if own_txn:
|
|
169
|
+
self.conn.rollback()
|
|
165
170
|
raise
|
|
166
171
|
|
|
167
172
|
fts_parts = [up.trigger or "", up.content or ""]
|
|
@@ -230,7 +235,8 @@ class UserPlaybookStoreMixin:
|
|
|
230
235
|
params.extend(sparams)
|
|
231
236
|
else:
|
|
232
237
|
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
233
|
-
|
|
238
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
239
|
+
sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
|
|
234
240
|
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
235
241
|
tag_frag, tag_params = _build_tags_sql("user_playbooks", tags)
|
|
236
242
|
if tag_frag:
|
|
@@ -274,7 +280,8 @@ class UserPlaybookStoreMixin:
|
|
|
274
280
|
params.extend(sparams)
|
|
275
281
|
else:
|
|
276
282
|
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
277
|
-
|
|
283
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
284
|
+
sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
|
|
278
285
|
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
279
286
|
|
|
280
287
|
row = self._fetchone(sql, params)
|
|
@@ -282,11 +289,12 @@ class UserPlaybookStoreMixin:
|
|
|
282
289
|
|
|
283
290
|
@SQLiteStorageBase.handle_exceptions
|
|
284
291
|
def count_user_playbooks_by_session(self, session_id: str) -> int:
|
|
292
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
285
293
|
row = self._fetchone(
|
|
286
|
-
"""SELECT COUNT(*) as cnt FROM user_playbooks up
|
|
294
|
+
f"""SELECT COUNT(*) as cnt FROM user_playbooks up
|
|
287
295
|
JOIN requests r ON up.request_id = r.request_id
|
|
288
296
|
WHERE r.session_id = ?
|
|
289
|
-
AND (up.status IS NULL OR up.status NOT IN (
|
|
297
|
+
AND (up.status IS NULL OR up.status NOT IN ({_ph}))""", # noqa: S608
|
|
290
298
|
(session_id, *_TOMBSTONE_STATUS_VALUES),
|
|
291
299
|
)
|
|
292
300
|
return row["cnt"] if row else 0
|
|
@@ -620,7 +628,8 @@ class UserPlaybookStoreMixin:
|
|
|
620
628
|
params.extend(sparams)
|
|
621
629
|
else:
|
|
622
630
|
# Default: exclude tombstone statuses (MERGED/SUPERSEDED)
|
|
623
|
-
|
|
631
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
632
|
+
conditions.append(f"(up.status IS NULL OR up.status NOT IN ({_ph}))")
|
|
624
633
|
params.extend(_TOMBSTONE_STATUS_VALUES)
|
|
625
634
|
tag_frag, tag_params = _build_tags_sql("up", request.tags)
|
|
626
635
|
if tag_frag:
|
|
@@ -695,7 +704,8 @@ class UserPlaybookStoreMixin:
|
|
|
695
704
|
) -> UserPlaybook | None:
|
|
696
705
|
sql = "SELECT * FROM user_playbooks WHERE user_playbook_id = ?"
|
|
697
706
|
if not include_tombstones:
|
|
698
|
-
|
|
707
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
708
|
+
sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
|
|
699
709
|
row = self._fetchone(sql, (user_playbook_id, *_TOMBSTONE_STATUS_VALUES))
|
|
700
710
|
else:
|
|
701
711
|
row = self._fetchone(sql, (user_playbook_id,))
|
|
@@ -784,7 +794,8 @@ class UserPlaybookStoreMixin:
|
|
|
784
794
|
to_status=None,
|
|
785
795
|
status_namespace=None,
|
|
786
796
|
)
|
|
787
|
-
self.
|
|
797
|
+
if self._own_transaction():
|
|
798
|
+
self.conn.commit()
|
|
788
799
|
|
|
789
800
|
@SQLiteStorageBase.handle_exceptions
|
|
790
801
|
def supersede_user_playbooks_by_ids(
|
|
@@ -814,10 +825,11 @@ class UserPlaybookStoreMixin:
|
|
|
814
825
|
self._subject_ref_from_user_playbook_row(row)
|
|
815
826
|
)
|
|
816
827
|
old_status = row["status"]
|
|
828
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
817
829
|
cur = self.conn.execute(
|
|
818
830
|
"UPDATE user_playbooks SET status = ?, retired_at = ?"
|
|
819
831
|
" WHERE user_playbook_id = ?"
|
|
820
|
-
" AND (status IS NULL OR status NOT IN (
|
|
832
|
+
f" AND (status IS NULL OR status NOT IN ({_ph}))",
|
|
821
833
|
(
|
|
822
834
|
Status.SUPERSEDED.value,
|
|
823
835
|
now_ts,
|
|
@@ -834,5 +846,6 @@ class UserPlaybookStoreMixin:
|
|
|
834
846
|
request_id=request_id,
|
|
835
847
|
)
|
|
836
848
|
updated += 1
|
|
837
|
-
self.
|
|
849
|
+
if self._own_transaction():
|
|
850
|
+
self.conn.commit()
|
|
838
851
|
return updated
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Interaction store CRUD methods for SQLite storage.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_profiles.py`` (the InteractionStore bucket). Profile
|
|
4
|
+
CRUD lives in ``profiles._profile_store`` (``ProfileStoreMixin``); search methods
|
|
5
|
+
live in ``profiles._search`` (``ProfileSearchMixin``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import sqlite3
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from reflexio.models.api_schema.service_schemas import (
|
|
13
|
+
DeleteUserInteractionRequest,
|
|
14
|
+
Interaction,
|
|
15
|
+
)
|
|
16
|
+
from reflexio.server.llm.providers.embedding_service_provider import (
|
|
17
|
+
EmbeddingUnavailableError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .._base import (
|
|
21
|
+
SQLiteStorageBase,
|
|
22
|
+
_epoch_to_iso,
|
|
23
|
+
_json_dumps,
|
|
24
|
+
_row_to_interaction,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InteractionStoreMixin:
|
|
31
|
+
"""Mixin providing interaction-store CRUD for SQLite storage."""
|
|
32
|
+
|
|
33
|
+
# Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
|
|
34
|
+
_lock: Any
|
|
35
|
+
conn: sqlite3.Connection
|
|
36
|
+
_fetchone: Any
|
|
37
|
+
_fetchall: Any
|
|
38
|
+
_get_embedding: Any
|
|
39
|
+
_fts_upsert: Any
|
|
40
|
+
_vec_upsert: Any
|
|
41
|
+
_delete_in_chunks: Any
|
|
42
|
+
_has_sqlite_vec: bool
|
|
43
|
+
llm_client: Any
|
|
44
|
+
embedding_model_name: str
|
|
45
|
+
embedding_dimensions: int
|
|
46
|
+
_subject_ref_for_user_id: Any
|
|
47
|
+
_assert_subject_writable_locked: Any
|
|
48
|
+
_own_transaction: Any
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
# CRUD — Interactions
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
@SQLiteStorageBase.handle_exceptions
|
|
55
|
+
def get_all_interactions(self, limit: int = 100) -> list[Interaction]:
|
|
56
|
+
rows = self._fetchall(
|
|
57
|
+
"SELECT * FROM interactions ORDER BY created_at DESC LIMIT ?", (limit,)
|
|
58
|
+
)
|
|
59
|
+
return [_row_to_interaction(r) for r in rows]
|
|
60
|
+
|
|
61
|
+
@SQLiteStorageBase.handle_exceptions
|
|
62
|
+
def get_user_interaction(self, user_id: str) -> list[Interaction]:
|
|
63
|
+
rows = self._fetchall(
|
|
64
|
+
"SELECT * FROM interactions WHERE user_id = ?", (user_id,)
|
|
65
|
+
)
|
|
66
|
+
return [_row_to_interaction(r) for r in rows]
|
|
67
|
+
|
|
68
|
+
@SQLiteStorageBase.handle_exceptions
|
|
69
|
+
def get_all_user_ids(self) -> list[str]:
|
|
70
|
+
rows = self._fetchall("SELECT DISTINCT user_id FROM interactions")
|
|
71
|
+
return sorted(r["user_id"] for r in rows)
|
|
72
|
+
|
|
73
|
+
@SQLiteStorageBase.handle_exceptions
|
|
74
|
+
def add_user_interaction(self, user_id: str, interaction: Interaction) -> None: # noqa: ARG002
|
|
75
|
+
embedding = self._get_embedding(
|
|
76
|
+
f"{interaction.content}\n{interaction.user_action_description}"
|
|
77
|
+
)
|
|
78
|
+
interaction.embedding = embedding
|
|
79
|
+
self._insert_interaction(interaction)
|
|
80
|
+
|
|
81
|
+
def _insert_interaction(self, interaction: Interaction) -> int:
|
|
82
|
+
created_at_iso = _epoch_to_iso(interaction.created_at)
|
|
83
|
+
subject_ref = self._subject_ref_for_user_id(interaction.user_id)
|
|
84
|
+
with self._lock:
|
|
85
|
+
own_txn = self._own_transaction()
|
|
86
|
+
try:
|
|
87
|
+
if own_txn:
|
|
88
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
89
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
90
|
+
if interaction.interaction_id:
|
|
91
|
+
self.conn.execute(
|
|
92
|
+
"""INSERT OR REPLACE INTO interactions
|
|
93
|
+
(interaction_id, user_id, content, request_id, created_at,
|
|
94
|
+
role, user_action, user_action_description,
|
|
95
|
+
interacted_image_url, image_encoding, shadow_content,
|
|
96
|
+
expert_content, tools_used, citations, embedding, governance_subject_ref)
|
|
97
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
98
|
+
(
|
|
99
|
+
interaction.interaction_id,
|
|
100
|
+
interaction.user_id,
|
|
101
|
+
interaction.content,
|
|
102
|
+
interaction.request_id,
|
|
103
|
+
created_at_iso,
|
|
104
|
+
interaction.role,
|
|
105
|
+
interaction.user_action.value,
|
|
106
|
+
interaction.user_action_description,
|
|
107
|
+
interaction.interacted_image_url,
|
|
108
|
+
interaction.image_encoding,
|
|
109
|
+
interaction.shadow_content,
|
|
110
|
+
interaction.expert_content,
|
|
111
|
+
_json_dumps(
|
|
112
|
+
[t.model_dump() for t in interaction.tools_used]
|
|
113
|
+
),
|
|
114
|
+
_json_dumps(
|
|
115
|
+
[c.model_dump() for c in interaction.citations]
|
|
116
|
+
),
|
|
117
|
+
_json_dumps(interaction.embedding),
|
|
118
|
+
subject_ref,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
iid = interaction.interaction_id
|
|
122
|
+
else:
|
|
123
|
+
cur = self.conn.execute(
|
|
124
|
+
"""INSERT INTO interactions
|
|
125
|
+
(user_id, content, request_id, created_at,
|
|
126
|
+
role, user_action, user_action_description,
|
|
127
|
+
interacted_image_url, image_encoding, shadow_content,
|
|
128
|
+
expert_content, tools_used, citations, embedding, governance_subject_ref)
|
|
129
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
130
|
+
(
|
|
131
|
+
interaction.user_id,
|
|
132
|
+
interaction.content,
|
|
133
|
+
interaction.request_id,
|
|
134
|
+
created_at_iso,
|
|
135
|
+
interaction.role,
|
|
136
|
+
interaction.user_action.value,
|
|
137
|
+
interaction.user_action_description,
|
|
138
|
+
interaction.interacted_image_url,
|
|
139
|
+
interaction.image_encoding,
|
|
140
|
+
interaction.shadow_content,
|
|
141
|
+
interaction.expert_content,
|
|
142
|
+
_json_dumps(
|
|
143
|
+
[t.model_dump() for t in interaction.tools_used]
|
|
144
|
+
),
|
|
145
|
+
_json_dumps(
|
|
146
|
+
[c.model_dump() for c in interaction.citations]
|
|
147
|
+
),
|
|
148
|
+
_json_dumps(interaction.embedding),
|
|
149
|
+
subject_ref,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
iid = cur.lastrowid or 0
|
|
153
|
+
interaction.interaction_id = iid
|
|
154
|
+
if own_txn:
|
|
155
|
+
self.conn.commit()
|
|
156
|
+
except Exception:
|
|
157
|
+
if own_txn:
|
|
158
|
+
self.conn.rollback()
|
|
159
|
+
raise
|
|
160
|
+
# Update FTS and vec
|
|
161
|
+
self._fts_upsert(
|
|
162
|
+
"interactions_fts",
|
|
163
|
+
iid,
|
|
164
|
+
content=interaction.content,
|
|
165
|
+
user_action_description=interaction.user_action_description,
|
|
166
|
+
)
|
|
167
|
+
if interaction.embedding:
|
|
168
|
+
self._vec_upsert("interactions_vec", iid, interaction.embedding)
|
|
169
|
+
return iid
|
|
170
|
+
|
|
171
|
+
@SQLiteStorageBase.handle_exceptions
|
|
172
|
+
def add_user_interactions_bulk(
|
|
173
|
+
self,
|
|
174
|
+
user_id: str, # noqa: ARG002
|
|
175
|
+
interactions: list[Interaction],
|
|
176
|
+
*,
|
|
177
|
+
embeddings_prepared: bool = False,
|
|
178
|
+
) -> None:
|
|
179
|
+
if not interactions:
|
|
180
|
+
return
|
|
181
|
+
if not embeddings_prepared:
|
|
182
|
+
# Only generate embeddings for interactions that do not already have them.
|
|
183
|
+
# This allows callers to pre-populate embeddings (e.g. via
|
|
184
|
+
# prepare_interaction_embeddings) before opening a commit_scope so that no
|
|
185
|
+
# network I/O occurs inside the transaction.
|
|
186
|
+
to_embed = [i for i in interactions if not i.embedding]
|
|
187
|
+
if to_embed:
|
|
188
|
+
texts = [
|
|
189
|
+
"\n".join([i.content or "", i.user_action_description or ""])
|
|
190
|
+
for i in to_embed
|
|
191
|
+
]
|
|
192
|
+
try:
|
|
193
|
+
embeddings = self.llm_client.get_embeddings(
|
|
194
|
+
texts, self.embedding_model_name, self.embedding_dimensions
|
|
195
|
+
)
|
|
196
|
+
except EmbeddingUnavailableError as exc:
|
|
197
|
+
logger.warning(
|
|
198
|
+
"Embedding unavailable for interaction bulk insert; "
|
|
199
|
+
"continuing without vectors: %s",
|
|
200
|
+
exc,
|
|
201
|
+
)
|
|
202
|
+
embeddings = [[] for _ in texts]
|
|
203
|
+
for interaction, embedding in zip(to_embed, embeddings, strict=False):
|
|
204
|
+
interaction.embedding = embedding
|
|
205
|
+
for interaction in interactions:
|
|
206
|
+
self._insert_interaction(interaction)
|
|
207
|
+
|
|
208
|
+
@SQLiteStorageBase.handle_exceptions
|
|
209
|
+
def prepare_interaction_embeddings(self, interactions: list[Interaction]) -> None:
|
|
210
|
+
"""Pre-populate interaction.embedding for each interaction (no DB write).
|
|
211
|
+
|
|
212
|
+
Generates embeddings in one batch call so the write path inside a
|
|
213
|
+
commit_scope can skip the network round-trip.
|
|
214
|
+
"""
|
|
215
|
+
if not interactions:
|
|
216
|
+
return
|
|
217
|
+
to_embed = [i for i in interactions if not i.embedding]
|
|
218
|
+
if not to_embed:
|
|
219
|
+
return
|
|
220
|
+
texts = [
|
|
221
|
+
"\n".join([i.content or "", i.user_action_description or ""])
|
|
222
|
+
for i in to_embed
|
|
223
|
+
]
|
|
224
|
+
try:
|
|
225
|
+
embeddings = self.llm_client.get_embeddings(
|
|
226
|
+
texts, self.embedding_model_name, self.embedding_dimensions
|
|
227
|
+
)
|
|
228
|
+
except EmbeddingUnavailableError as exc:
|
|
229
|
+
logger.warning(
|
|
230
|
+
"Embedding unavailable during prepare_interaction_embeddings; "
|
|
231
|
+
"continuing without vectors: %s",
|
|
232
|
+
exc,
|
|
233
|
+
)
|
|
234
|
+
embeddings = [[] for _ in texts]
|
|
235
|
+
for interaction, embedding in zip(to_embed, embeddings, strict=False):
|
|
236
|
+
interaction.embedding = embedding
|
|
237
|
+
|
|
238
|
+
@SQLiteStorageBase.handle_exceptions
|
|
239
|
+
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
240
|
+
with self._lock:
|
|
241
|
+
row = self.conn.execute(
|
|
242
|
+
"SELECT interaction_id FROM interactions WHERE user_id = ? AND interaction_id = ?",
|
|
243
|
+
(request.user_id, request.interaction_id),
|
|
244
|
+
).fetchone()
|
|
245
|
+
if row is None:
|
|
246
|
+
return
|
|
247
|
+
self.conn.execute(
|
|
248
|
+
"DELETE FROM interactions_fts WHERE rowid = ?",
|
|
249
|
+
(request.interaction_id,),
|
|
250
|
+
)
|
|
251
|
+
if self._has_sqlite_vec:
|
|
252
|
+
self.conn.execute(
|
|
253
|
+
"DELETE FROM interactions_vec WHERE rowid = ?",
|
|
254
|
+
(request.interaction_id,),
|
|
255
|
+
)
|
|
256
|
+
self.conn.execute(
|
|
257
|
+
"DELETE FROM interactions WHERE user_id = ? AND interaction_id = ?",
|
|
258
|
+
(request.user_id, request.interaction_id),
|
|
259
|
+
)
|
|
260
|
+
self.conn.commit()
|
|
261
|
+
|
|
262
|
+
@SQLiteStorageBase.handle_exceptions
|
|
263
|
+
def delete_all_interactions_for_user(self, user_id: str) -> None:
|
|
264
|
+
with self._lock:
|
|
265
|
+
rows = self.conn.execute(
|
|
266
|
+
"SELECT interaction_id FROM interactions WHERE user_id = ?", (user_id,)
|
|
267
|
+
).fetchall()
|
|
268
|
+
if not rows:
|
|
269
|
+
return
|
|
270
|
+
ids = [r["interaction_id"] for r in rows]
|
|
271
|
+
self._delete_in_chunks("interactions_fts", "rowid", ids)
|
|
272
|
+
if self._has_sqlite_vec:
|
|
273
|
+
self._delete_in_chunks("interactions_vec", "rowid", ids)
|
|
274
|
+
self.conn.execute("DELETE FROM interactions WHERE user_id = ?", (user_id,))
|
|
275
|
+
self.conn.commit()
|
|
276
|
+
|
|
277
|
+
@SQLiteStorageBase.handle_exceptions
|
|
278
|
+
def delete_all_interactions(self) -> None:
|
|
279
|
+
with self._lock:
|
|
280
|
+
self.conn.execute("DELETE FROM interactions_fts")
|
|
281
|
+
if self._has_sqlite_vec:
|
|
282
|
+
self.conn.execute("DELETE FROM interactions_vec")
|
|
283
|
+
self.conn.execute("DELETE FROM interactions")
|
|
284
|
+
self.conn.commit()
|
|
285
|
+
|
|
286
|
+
@SQLiteStorageBase.handle_exceptions
|
|
287
|
+
def count_all_interactions(self) -> int:
|
|
288
|
+
row = self._fetchone("SELECT COUNT(*) as cnt FROM interactions")
|
|
289
|
+
return row["cnt"] if row else 0
|
|
290
|
+
|
|
291
|
+
@SQLiteStorageBase.handle_exceptions
|
|
292
|
+
def delete_oldest_interactions(self, count: int) -> int:
|
|
293
|
+
if count <= 0:
|
|
294
|
+
return 0
|
|
295
|
+
with self._lock:
|
|
296
|
+
rows = self.conn.execute(
|
|
297
|
+
"SELECT interaction_id FROM interactions ORDER BY created_at ASC LIMIT ?",
|
|
298
|
+
(count,),
|
|
299
|
+
).fetchall()
|
|
300
|
+
if not rows:
|
|
301
|
+
return 0
|
|
302
|
+
ids = [r["interaction_id"] for r in rows]
|
|
303
|
+
self._delete_in_chunks("interactions_fts", "rowid", ids)
|
|
304
|
+
if self._has_sqlite_vec:
|
|
305
|
+
self._delete_in_chunks("interactions_vec", "rowid", ids)
|
|
306
|
+
self._delete_in_chunks("interactions", "interaction_id", ids)
|
|
307
|
+
self.conn.commit()
|
|
308
|
+
return len(ids)
|