claude-smart 0.2.46 → 0.2.47
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 +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/opencode/dist/server.mjs +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +1 -1
- package/plugin/vendor/reflexio/reflexio/README.md +3 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +16 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +45 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +129 -111
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +14 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +262 -107
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
|
@@ -75,6 +75,10 @@ def _build_tags_sql(alias: str, tags: list[str] | None) -> tuple[str, list[Any]]
|
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _escape_like_pattern(value: str) -> str:
|
|
79
|
+
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
80
|
+
|
|
81
|
+
|
|
78
82
|
class ProfileMixin:
|
|
79
83
|
"""Mixin providing profile and interaction CRUD + search."""
|
|
80
84
|
|
|
@@ -100,21 +104,87 @@ class ProfileMixin:
|
|
|
100
104
|
llm_client: Any
|
|
101
105
|
embedding_model_name: str
|
|
102
106
|
embedding_dimensions: int
|
|
107
|
+
_subject_ref_for_user_id: Any
|
|
108
|
+
_assert_subject_writable_locked: Any
|
|
103
109
|
|
|
104
110
|
# ------------------------------------------------------------------
|
|
105
111
|
# CRUD — Profiles
|
|
106
112
|
# ------------------------------------------------------------------
|
|
107
113
|
|
|
114
|
+
def _subject_ref_from_profile_row(self, row: sqlite3.Row) -> str:
|
|
115
|
+
subject_ref = row["governance_subject_ref"]
|
|
116
|
+
return (
|
|
117
|
+
str(subject_ref)
|
|
118
|
+
if subject_ref
|
|
119
|
+
else self._subject_ref_for_user_id(row["user_id"])
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _assert_profile_writable_locked(
|
|
123
|
+
self,
|
|
124
|
+
profile_id: str,
|
|
125
|
+
*,
|
|
126
|
+
user_id: str | None = None,
|
|
127
|
+
) -> sqlite3.Row | None:
|
|
128
|
+
if user_id is None:
|
|
129
|
+
row = self.conn.execute(
|
|
130
|
+
"SELECT user_id, governance_subject_ref FROM profiles WHERE profile_id = ?",
|
|
131
|
+
(profile_id,),
|
|
132
|
+
).fetchone()
|
|
133
|
+
else:
|
|
134
|
+
row = self.conn.execute(
|
|
135
|
+
"SELECT user_id, governance_subject_ref FROM profiles WHERE profile_id = ? AND user_id = ?",
|
|
136
|
+
(profile_id, user_id),
|
|
137
|
+
).fetchone()
|
|
138
|
+
if row is None:
|
|
139
|
+
return None
|
|
140
|
+
self._assert_subject_writable_locked(self._subject_ref_from_profile_row(row))
|
|
141
|
+
return row
|
|
142
|
+
|
|
108
143
|
@SQLiteStorageBase.handle_exceptions
|
|
109
144
|
def get_all_profiles(
|
|
110
145
|
self,
|
|
111
146
|
limit: int = 100,
|
|
112
147
|
status_filter: list[Status | None] | None = None,
|
|
148
|
+
user_id: str | None = None,
|
|
149
|
+
profile_id: str | None = None,
|
|
150
|
+
query: str | None = None,
|
|
151
|
+
source: str | None = None,
|
|
152
|
+
profile_time_to_live: str | None = None,
|
|
153
|
+
start_time: int | None = None,
|
|
154
|
+
end_time: int | None = None,
|
|
113
155
|
) -> list[UserProfile]:
|
|
114
156
|
if status_filter is None:
|
|
115
157
|
status_filter = [None]
|
|
116
158
|
frag, params = _build_status_sql(status_filter)
|
|
117
|
-
|
|
159
|
+
conditions = [frag]
|
|
160
|
+
if user_id:
|
|
161
|
+
conditions.append("user_id = ?")
|
|
162
|
+
params.append(user_id)
|
|
163
|
+
if profile_id:
|
|
164
|
+
conditions.append("LOWER(profile_id) = LOWER(?)")
|
|
165
|
+
params.append(profile_id)
|
|
166
|
+
if query:
|
|
167
|
+
like = f"%{_escape_like_pattern(query.lower())}%"
|
|
168
|
+
conditions.append(
|
|
169
|
+
"(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(profile_id) LIKE ? ESCAPE '\\' OR LOWER(user_id) LIKE ? ESCAPE '\\')"
|
|
170
|
+
)
|
|
171
|
+
params.extend([like, like, like])
|
|
172
|
+
if source is not None:
|
|
173
|
+
conditions.append("source = ?")
|
|
174
|
+
params.append(source)
|
|
175
|
+
if profile_time_to_live:
|
|
176
|
+
conditions.append("profile_time_to_live = ?")
|
|
177
|
+
params.append(profile_time_to_live)
|
|
178
|
+
if start_time is not None:
|
|
179
|
+
conditions.append("last_modified_timestamp >= ?")
|
|
180
|
+
params.append(start_time)
|
|
181
|
+
if end_time is not None:
|
|
182
|
+
conditions.append("last_modified_timestamp <= ?")
|
|
183
|
+
params.append(end_time)
|
|
184
|
+
sql = (
|
|
185
|
+
f"SELECT * FROM profiles WHERE {' AND '.join(conditions)} "
|
|
186
|
+
"ORDER BY last_modified_timestamp DESC LIMIT ?"
|
|
187
|
+
)
|
|
118
188
|
params.append(limit)
|
|
119
189
|
return [_row_to_profile(r) for r in self._fetchall(sql, params)]
|
|
120
190
|
|
|
@@ -124,6 +194,12 @@ class ProfileMixin:
|
|
|
124
194
|
user_id: str,
|
|
125
195
|
status_filter: list[Status | None] | None = None,
|
|
126
196
|
tags: list[str] | None = None,
|
|
197
|
+
profile_id: str | None = None,
|
|
198
|
+
query: str | None = None,
|
|
199
|
+
source: str | None = None,
|
|
200
|
+
profile_time_to_live: str | None = None,
|
|
201
|
+
start_time: int | None = None,
|
|
202
|
+
end_time: int | None = None,
|
|
127
203
|
) -> list[UserProfile]:
|
|
128
204
|
if status_filter is None:
|
|
129
205
|
status_filter = [None]
|
|
@@ -131,6 +207,27 @@ class ProfileMixin:
|
|
|
131
207
|
frag, params = _build_status_sql(status_filter)
|
|
132
208
|
conditions = ["user_id = ?", "expiration_timestamp >= ?", frag]
|
|
133
209
|
all_params: list[Any] = [user_id, current_ts, *params]
|
|
210
|
+
if profile_id:
|
|
211
|
+
conditions.append("LOWER(profile_id) = LOWER(?)")
|
|
212
|
+
all_params.append(profile_id)
|
|
213
|
+
if query:
|
|
214
|
+
like = f"%{_escape_like_pattern(query.lower())}%"
|
|
215
|
+
conditions.append(
|
|
216
|
+
"(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(profile_id) LIKE ? ESCAPE '\\' OR LOWER(user_id) LIKE ? ESCAPE '\\')"
|
|
217
|
+
)
|
|
218
|
+
all_params.extend([like, like, like])
|
|
219
|
+
if source is not None:
|
|
220
|
+
conditions.append("source = ?")
|
|
221
|
+
all_params.append(source)
|
|
222
|
+
if profile_time_to_live:
|
|
223
|
+
conditions.append("profile_time_to_live = ?")
|
|
224
|
+
all_params.append(profile_time_to_live)
|
|
225
|
+
if start_time is not None:
|
|
226
|
+
conditions.append("last_modified_timestamp >= ?")
|
|
227
|
+
all_params.append(start_time)
|
|
228
|
+
if end_time is not None:
|
|
229
|
+
conditions.append("last_modified_timestamp <= ?")
|
|
230
|
+
all_params.append(end_time)
|
|
134
231
|
tag_frag, tag_params = _build_tags_sql("profiles", tags)
|
|
135
232
|
if tag_frag:
|
|
136
233
|
conditions.append(tag_frag)
|
|
@@ -141,6 +238,9 @@ class ProfileMixin:
|
|
|
141
238
|
@SQLiteStorageBase.handle_exceptions
|
|
142
239
|
def add_user_profile(self, user_id: str, user_profiles: list[UserProfile]) -> None: # noqa: ARG002
|
|
143
240
|
for profile in user_profiles:
|
|
241
|
+
subject_ref = self._subject_ref_for_user_id(profile.user_id)
|
|
242
|
+
with self._lock:
|
|
243
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
144
244
|
embedding_text = "\n".join([profile.content, str(profile.custom_features)])
|
|
145
245
|
if self._should_expand_documents():
|
|
146
246
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
@@ -151,39 +251,48 @@ class ProfileMixin:
|
|
|
151
251
|
else:
|
|
152
252
|
profile.embedding = self._get_embedding(embedding_text)
|
|
153
253
|
embedding = profile.embedding
|
|
154
|
-
self.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
254
|
+
with self._lock:
|
|
255
|
+
try:
|
|
256
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
257
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
258
|
+
self.conn.execute(
|
|
259
|
+
"""INSERT OR REPLACE INTO profiles
|
|
260
|
+
(profile_id, user_id, content, last_modified_timestamp,
|
|
261
|
+
generated_from_request_id, profile_time_to_live,
|
|
262
|
+
expiration_timestamp, custom_features, embedding, source,
|
|
263
|
+
status, extractor_names, expanded_terms,
|
|
264
|
+
source_span, notes, reader_angle, tags, source_interaction_ids, created_at,
|
|
265
|
+
merged_into, superseded_by, governance_subject_ref)
|
|
266
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
267
|
+
(
|
|
268
|
+
profile.profile_id,
|
|
269
|
+
profile.user_id,
|
|
270
|
+
profile.content,
|
|
271
|
+
profile.last_modified_timestamp,
|
|
272
|
+
profile.generated_from_request_id,
|
|
273
|
+
profile.profile_time_to_live.value,
|
|
274
|
+
profile.expiration_timestamp,
|
|
275
|
+
_json_dumps(profile.custom_features),
|
|
276
|
+
_json_dumps(profile.embedding),
|
|
277
|
+
profile.source,
|
|
278
|
+
profile.status.value if profile.status else None,
|
|
279
|
+
_json_dumps(profile.extractor_names),
|
|
280
|
+
profile.expanded_terms,
|
|
281
|
+
profile.source_span,
|
|
282
|
+
profile.notes,
|
|
283
|
+
profile.reader_angle,
|
|
284
|
+
_json_dumps(profile.tags),
|
|
285
|
+
_json_dumps(profile.source_interaction_ids),
|
|
286
|
+
_iso_now(),
|
|
287
|
+
profile.merged_into,
|
|
288
|
+
profile.superseded_by,
|
|
289
|
+
subject_ref,
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
self.conn.commit()
|
|
293
|
+
except Exception:
|
|
294
|
+
self.conn.rollback()
|
|
295
|
+
raise
|
|
187
296
|
fts_parts = [profile.content or ""]
|
|
188
297
|
if profile.custom_features:
|
|
189
298
|
fts_parts.extend(str(v) for v in profile.custom_features.values() if v)
|
|
@@ -211,18 +320,28 @@ class ProfileMixin:
|
|
|
211
320
|
that re-acquire it are safe.
|
|
212
321
|
"""
|
|
213
322
|
current_ts = _epoch_now()
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
323
|
+
with self._lock:
|
|
324
|
+
row = self.conn.execute(
|
|
325
|
+
"SELECT user_id, governance_subject_ref FROM profiles WHERE user_id = ? AND profile_id = ? AND expiration_timestamp >= ?",
|
|
326
|
+
(user_id, profile_id, current_ts),
|
|
327
|
+
).fetchone()
|
|
328
|
+
if not row:
|
|
329
|
+
logger.warning("User profile not found for user id: %s", user_id)
|
|
330
|
+
return
|
|
331
|
+
self._assert_subject_writable_locked(
|
|
332
|
+
self._subject_ref_from_profile_row(row)
|
|
333
|
+
)
|
|
221
334
|
embedding = self._get_embedding(
|
|
222
335
|
"\n".join([new_profile.content, str(new_profile.custom_features)])
|
|
223
336
|
)
|
|
224
337
|
new_profile.embedding = embedding
|
|
225
338
|
with self._lock:
|
|
339
|
+
if (
|
|
340
|
+
self._assert_profile_writable_locked(profile_id, user_id=user_id)
|
|
341
|
+
is None
|
|
342
|
+
):
|
|
343
|
+
logger.warning("User profile not found for user id: %s", user_id)
|
|
344
|
+
return
|
|
226
345
|
cur = self.conn.execute(
|
|
227
346
|
"""UPDATE profiles SET content=?, last_modified_timestamp=?,
|
|
228
347
|
generated_from_request_id=?, profile_time_to_live=?,
|
|
@@ -282,10 +401,17 @@ class ProfileMixin:
|
|
|
282
401
|
def update_user_profile_tags(
|
|
283
402
|
self, user_id: str, profile_id: str, tags: list[str]
|
|
284
403
|
) -> None:
|
|
285
|
-
self.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
404
|
+
with self._lock:
|
|
405
|
+
if (
|
|
406
|
+
self._assert_profile_writable_locked(profile_id, user_id=user_id)
|
|
407
|
+
is None
|
|
408
|
+
):
|
|
409
|
+
return
|
|
410
|
+
self.conn.execute(
|
|
411
|
+
"UPDATE profiles SET tags=? WHERE user_id=? AND profile_id=?",
|
|
412
|
+
(_json_dumps(tags), user_id, profile_id),
|
|
413
|
+
)
|
|
414
|
+
self.conn.commit()
|
|
289
415
|
|
|
290
416
|
@SQLiteStorageBase.handle_exceptions
|
|
291
417
|
def delete_user_profile(self, request: DeleteUserProfileRequest) -> None:
|
|
@@ -408,20 +534,24 @@ class ProfileMixin:
|
|
|
408
534
|
|
|
409
535
|
batch_request_id = uuid.uuid4().hex
|
|
410
536
|
with self._lock:
|
|
411
|
-
affected =
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
f"SELECT profile_id FROM profiles WHERE {where}",
|
|
537
|
+
affected = list(
|
|
538
|
+
self.conn.execute(
|
|
539
|
+
f"SELECT profile_id, user_id, governance_subject_ref FROM profiles WHERE {where}",
|
|
415
540
|
select_params + extra_params,
|
|
416
541
|
).fetchall()
|
|
417
|
-
|
|
542
|
+
)
|
|
543
|
+
for row in affected:
|
|
544
|
+
self._assert_subject_writable_locked(
|
|
545
|
+
self._subject_ref_from_profile_row(row)
|
|
546
|
+
)
|
|
418
547
|
cur = self.conn.execute(
|
|
419
548
|
f"UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? WHERE {where}",
|
|
420
549
|
[new_val, now_ts, retired_at_val] + select_params + extra_params,
|
|
421
550
|
)
|
|
422
551
|
from_val = old_status.value if old_status else None
|
|
423
552
|
to_val = new_status.value if new_status else None
|
|
424
|
-
for
|
|
553
|
+
for row in affected:
|
|
554
|
+
pid = row["profile_id"]
|
|
425
555
|
_append_event_stmt(
|
|
426
556
|
self.conn,
|
|
427
557
|
org_id=self.org_id,
|
|
@@ -530,6 +660,11 @@ class ProfileMixin:
|
|
|
530
660
|
@SQLiteStorageBase.handle_exceptions
|
|
531
661
|
def archive_profile_by_id(self, user_id: str, profile_id: str) -> bool:
|
|
532
662
|
with self._lock:
|
|
663
|
+
if (
|
|
664
|
+
self._assert_profile_writable_locked(profile_id, user_id=user_id)
|
|
665
|
+
is None
|
|
666
|
+
):
|
|
667
|
+
return False
|
|
533
668
|
now_ts = _epoch_now()
|
|
534
669
|
cur = self.conn.execute(
|
|
535
670
|
"UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? "
|
|
@@ -590,11 +725,14 @@ class ProfileMixin:
|
|
|
590
725
|
for pid in profile_ids:
|
|
591
726
|
# Read current status for from_status derivation (user_id scoped)
|
|
592
727
|
row = self.conn.execute(
|
|
593
|
-
"SELECT status FROM profiles WHERE profile_id = ? AND user_id = ?",
|
|
728
|
+
"SELECT status, user_id, governance_subject_ref FROM profiles WHERE profile_id = ? AND user_id = ?",
|
|
594
729
|
(pid, user_id),
|
|
595
730
|
).fetchone()
|
|
596
731
|
if row is None:
|
|
597
732
|
continue
|
|
733
|
+
self._assert_subject_writable_locked(
|
|
734
|
+
self._subject_ref_from_profile_row(row)
|
|
735
|
+
)
|
|
598
736
|
old_status_val = (
|
|
599
737
|
row[0] if isinstance(row, (tuple, list)) else row["status"]
|
|
600
738
|
)
|
|
@@ -744,62 +882,79 @@ class ProfileMixin:
|
|
|
744
882
|
|
|
745
883
|
def _insert_interaction(self, interaction: Interaction) -> int:
|
|
746
884
|
created_at_iso = _epoch_to_iso(interaction.created_at)
|
|
885
|
+
subject_ref = self._subject_ref_for_user_id(interaction.user_id)
|
|
747
886
|
with self._lock:
|
|
748
|
-
|
|
749
|
-
self.conn.execute(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
887
|
+
try:
|
|
888
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
889
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
890
|
+
if interaction.interaction_id:
|
|
891
|
+
self.conn.execute(
|
|
892
|
+
"""INSERT OR REPLACE INTO interactions
|
|
893
|
+
(interaction_id, user_id, content, request_id, created_at,
|
|
894
|
+
role, user_action, user_action_description,
|
|
895
|
+
interacted_image_url, image_encoding, shadow_content,
|
|
896
|
+
expert_content, tools_used, citations, embedding, governance_subject_ref)
|
|
897
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
898
|
+
(
|
|
899
|
+
interaction.interaction_id,
|
|
900
|
+
interaction.user_id,
|
|
901
|
+
interaction.content,
|
|
902
|
+
interaction.request_id,
|
|
903
|
+
created_at_iso,
|
|
904
|
+
interaction.role,
|
|
905
|
+
interaction.user_action.value,
|
|
906
|
+
interaction.user_action_description,
|
|
907
|
+
interaction.interacted_image_url,
|
|
908
|
+
interaction.image_encoding,
|
|
909
|
+
interaction.shadow_content,
|
|
910
|
+
interaction.expert_content,
|
|
911
|
+
_json_dumps(
|
|
912
|
+
[t.model_dump() for t in interaction.tools_used]
|
|
913
|
+
),
|
|
914
|
+
_json_dumps(
|
|
915
|
+
[c.model_dump() for c in interaction.citations]
|
|
916
|
+
),
|
|
917
|
+
_json_dumps(interaction.embedding),
|
|
918
|
+
subject_ref,
|
|
919
|
+
),
|
|
920
|
+
)
|
|
921
|
+
iid = interaction.interaction_id
|
|
922
|
+
else:
|
|
923
|
+
cur = self.conn.execute(
|
|
924
|
+
"""INSERT INTO interactions
|
|
925
|
+
(user_id, content, request_id, created_at,
|
|
926
|
+
role, user_action, user_action_description,
|
|
927
|
+
interacted_image_url, image_encoding, shadow_content,
|
|
928
|
+
expert_content, tools_used, citations, embedding, governance_subject_ref)
|
|
929
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
930
|
+
(
|
|
931
|
+
interaction.user_id,
|
|
932
|
+
interaction.content,
|
|
933
|
+
interaction.request_id,
|
|
934
|
+
created_at_iso,
|
|
935
|
+
interaction.role,
|
|
936
|
+
interaction.user_action.value,
|
|
937
|
+
interaction.user_action_description,
|
|
938
|
+
interaction.interacted_image_url,
|
|
939
|
+
interaction.image_encoding,
|
|
940
|
+
interaction.shadow_content,
|
|
941
|
+
interaction.expert_content,
|
|
942
|
+
_json_dumps(
|
|
943
|
+
[t.model_dump() for t in interaction.tools_used]
|
|
944
|
+
),
|
|
945
|
+
_json_dumps(
|
|
946
|
+
[c.model_dump() for c in interaction.citations]
|
|
947
|
+
),
|
|
948
|
+
_json_dumps(interaction.embedding),
|
|
949
|
+
subject_ref,
|
|
950
|
+
),
|
|
951
|
+
)
|
|
952
|
+
iid = cur.lastrowid or 0
|
|
953
|
+
interaction.interaction_id = iid
|
|
954
|
+
self.conn.commit()
|
|
955
|
+
except Exception:
|
|
956
|
+
self.conn.rollback()
|
|
957
|
+
raise
|
|
803
958
|
# Update FTS and vec
|
|
804
959
|
self._fts_upsert(
|
|
805
960
|
"interactions_fts",
|
|
@@ -30,6 +30,8 @@ class RequestMixin:
|
|
|
30
30
|
_execute: Any
|
|
31
31
|
_fetchone: Any
|
|
32
32
|
_fetchall: Any
|
|
33
|
+
_subject_ref_for_user_id: Any
|
|
34
|
+
_assert_subject_writable_locked: Any
|
|
33
35
|
|
|
34
36
|
# ------------------------------------------------------------------
|
|
35
37
|
# Request methods
|
|
@@ -38,20 +40,31 @@ class RequestMixin:
|
|
|
38
40
|
@SQLiteStorageBase.handle_exceptions
|
|
39
41
|
def add_request(self, request: Request) -> None:
|
|
40
42
|
created_at_iso = _epoch_to_iso(request.created_at)
|
|
41
|
-
self.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
subject_ref = self._subject_ref_for_user_id(request.user_id)
|
|
44
|
+
with self._lock:
|
|
45
|
+
try:
|
|
46
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
47
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
48
|
+
self.conn.execute(
|
|
49
|
+
"""INSERT OR REPLACE INTO requests
|
|
50
|
+
(request_id, user_id, created_at, source, agent_version, session_id,
|
|
51
|
+
evaluation_only, governance_subject_ref)
|
|
52
|
+
VALUES (?,?,?,?,?,?,?,?)""",
|
|
53
|
+
(
|
|
54
|
+
request.request_id,
|
|
55
|
+
request.user_id,
|
|
56
|
+
created_at_iso,
|
|
57
|
+
request.source,
|
|
58
|
+
request.agent_version,
|
|
59
|
+
request.session_id,
|
|
60
|
+
1 if request.evaluation_only else 0,
|
|
61
|
+
subject_ref,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
self.conn.commit()
|
|
65
|
+
except Exception:
|
|
66
|
+
self.conn.rollback()
|
|
67
|
+
raise
|
|
55
68
|
|
|
56
69
|
@SQLiteStorageBase.handle_exceptions
|
|
57
70
|
def get_request(self, request_id: str) -> Request | None:
|
|
@@ -155,35 +168,62 @@ class RequestMixin:
|
|
|
155
168
|
end_time: int | None = None,
|
|
156
169
|
top_k: int | None = 30,
|
|
157
170
|
offset: int = 0,
|
|
171
|
+
source: str | None = None,
|
|
158
172
|
) -> dict[str, list[RequestInteractionDataModel]]:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
# Request-level filters shared by both the session-page query and the
|
|
174
|
+
# request-fetch query. Pagination is applied at the SESSION level
|
|
175
|
+
# (top_k/offset count sessions, not request rows) so a session with many
|
|
176
|
+
# requests is never truncated to a subset of its rows.
|
|
177
|
+
filter_sql = ""
|
|
178
|
+
filter_params: list[Any] = []
|
|
162
179
|
if user_id:
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
filter_sql += " AND user_id = ?"
|
|
181
|
+
filter_params.append(user_id)
|
|
165
182
|
if request_id:
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
filter_sql += " AND request_id = ?"
|
|
184
|
+
filter_params.append(request_id)
|
|
168
185
|
if session_id:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
filter_sql += " AND session_id = ?"
|
|
187
|
+
filter_params.append(session_id)
|
|
188
|
+
if source is not None:
|
|
189
|
+
filter_sql += " AND source = ?"
|
|
190
|
+
filter_params.append(source)
|
|
191
|
+
if start_time is not None:
|
|
192
|
+
filter_sql += " AND created_at >= ?"
|
|
193
|
+
filter_params.append(_epoch_to_iso(start_time))
|
|
194
|
+
if end_time is not None:
|
|
195
|
+
filter_sql += " AND created_at <= ?"
|
|
196
|
+
filter_params.append(_epoch_to_iso(end_time))
|
|
177
197
|
|
|
198
|
+
# Step 1: select the page of session_ids, ordered by each session's most
|
|
199
|
+
# recent matching request (latest-first), with session_id as a stable
|
|
200
|
+
# tiebreak so pages don't overlap or skip.
|
|
178
201
|
effective_limit = top_k or 100
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
page_rows = self._fetchall(
|
|
203
|
+
f"SELECT session_id, MAX(created_at) AS latest FROM requests "
|
|
204
|
+
f"WHERE 1=1{filter_sql} "
|
|
205
|
+
f"GROUP BY session_id ORDER BY latest DESC, session_id DESC "
|
|
206
|
+
f"LIMIT ? OFFSET ?",
|
|
207
|
+
[*filter_params, effective_limit, offset],
|
|
208
|
+
)
|
|
209
|
+
if not page_rows:
|
|
210
|
+
return {}
|
|
211
|
+
page_session_ids = [r["session_id"] for r in page_rows]
|
|
181
212
|
|
|
182
|
-
|
|
213
|
+
# Step 2: fetch ALL matching requests for the sessions in this page.
|
|
214
|
+
placeholders = ",".join("?" for _ in page_session_ids)
|
|
215
|
+
req_rows = self._fetchall(
|
|
216
|
+
f"SELECT * FROM requests WHERE 1=1{filter_sql} "
|
|
217
|
+
f"AND session_id IN ({placeholders}) ORDER BY created_at DESC",
|
|
218
|
+
[*filter_params, *page_session_ids],
|
|
219
|
+
)
|
|
183
220
|
if not req_rows:
|
|
184
221
|
return {}
|
|
185
222
|
|
|
186
|
-
|
|
223
|
+
# Preserve the latest-first session ordering from step 1.
|
|
224
|
+
grouped: dict[str, list[RequestInteractionDataModel]] = {
|
|
225
|
+
(sid or ""): [] for sid in page_session_ids
|
|
226
|
+
}
|
|
187
227
|
for rr in req_rows:
|
|
188
228
|
req = _row_to_request(rr)
|
|
189
229
|
group_name = req.session_id or ""
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ._agent import AgentPlaybookStoreMixin
|
|
2
|
+
from ._eval_results import AgentEvaluationResultStoreMixin
|
|
3
|
+
from ._optimization import OptimizationJobStoreMixin
|
|
4
|
+
from ._source_linkage import PlaybookSourceLinkageMixin
|
|
5
|
+
from ._user import UserPlaybookStoreMixin
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentEvaluationResultStoreMixin",
|
|
9
|
+
"AgentPlaybookStoreMixin",
|
|
10
|
+
"OptimizationJobStoreMixin",
|
|
11
|
+
"PlaybookSourceLinkageMixin",
|
|
12
|
+
"UserPlaybookStoreMixin",
|
|
13
|
+
]
|