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
package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Abstract user playbook CRUD + search declarations."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
from reflexio.models.api_schema.common import BlockingIssue
|
|
6
|
+
from reflexio.models.api_schema.domain import Status, UserPlaybook
|
|
7
|
+
from reflexio.models.api_schema.retriever_schema import SearchUserPlaybookRequest
|
|
8
|
+
from reflexio.models.config_schema import SearchOptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserPlaybookStoreMixin:
|
|
12
|
+
"""Abstract user playbook CRUD + search methods."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def save_user_playbooks(self, user_playbooks: list[UserPlaybook]) -> None:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_user_playbooks(
|
|
20
|
+
self,
|
|
21
|
+
limit: int = 100,
|
|
22
|
+
user_id: str | None = None,
|
|
23
|
+
playbook_name: str | None = None,
|
|
24
|
+
agent_version: str | None = None,
|
|
25
|
+
status_filter: list[Status | None] | None = None,
|
|
26
|
+
start_time: int | None = None,
|
|
27
|
+
end_time: int | None = None,
|
|
28
|
+
include_embedding: bool = False,
|
|
29
|
+
tags: list[str] | None = None,
|
|
30
|
+
offset: int = 0,
|
|
31
|
+
user_playbook_id: int | None = None,
|
|
32
|
+
request_id: str | None = None,
|
|
33
|
+
query: str | None = None,
|
|
34
|
+
) -> list[UserPlaybook]:
|
|
35
|
+
"""Get user playbooks from storage.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
limit (int): Maximum number of playbooks to return
|
|
39
|
+
user_playbook_id (int, optional): Exact user playbook ID to retrieve.
|
|
40
|
+
user_id (str, optional): The user ID to filter by. If None, returns playbooks for all users.
|
|
41
|
+
request_id (str, optional): Request ID that generated the playbook.
|
|
42
|
+
query (str, optional): Case-insensitive text filter across visible fields.
|
|
43
|
+
playbook_name (str, optional): The playbook name to filter by. If None, returns all user playbooks.
|
|
44
|
+
agent_version (str, optional): The agent version to filter by. If None, returns all agent versions.
|
|
45
|
+
status_filter (list[Optional[Status]], optional): List of status values to filter by.
|
|
46
|
+
Can include None (current), Status.PENDING (from rerun), Status.ARCHIVED (old).
|
|
47
|
+
If None, returns playbooks with all statuses.
|
|
48
|
+
start_time (int, optional): Unix timestamp. Only return playbooks created at or after this time.
|
|
49
|
+
end_time (int, optional): Unix timestamp. Only return playbooks created at or before this time.
|
|
50
|
+
include_embedding (bool): If True, fetch and parse embedding vectors. Defaults to False.
|
|
51
|
+
tags (list[str], optional): Match playbooks having any of these tags.
|
|
52
|
+
offset (int): Number of matching rows to skip. Defaults to 0.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
list[UserPlaybook]: List of user playbook objects
|
|
56
|
+
"""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def count_user_playbooks(
|
|
61
|
+
self,
|
|
62
|
+
user_id: str | None = None,
|
|
63
|
+
playbook_name: str | None = None,
|
|
64
|
+
min_user_playbook_id: int | None = None,
|
|
65
|
+
agent_version: str | None = None,
|
|
66
|
+
status_filter: list[Status | None] | None = None,
|
|
67
|
+
) -> int:
|
|
68
|
+
"""Count user playbooks in storage efficiently.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
user_id (str, optional): The user ID to filter by. If None, counts playbooks for all users.
|
|
72
|
+
playbook_name (str, optional): The playbook name to filter by. If None, counts all user playbooks.
|
|
73
|
+
min_user_playbook_id (int, optional): Only count playbooks with user_playbook_id greater than this value.
|
|
74
|
+
agent_version (str, optional): The agent version to filter by. If None, counts all agent versions.
|
|
75
|
+
status_filter (list[Optional[Status]], optional): List of status values to filter by.
|
|
76
|
+
Can include None (current), Status.PENDING (from rerun), Status.ARCHIVED (old).
|
|
77
|
+
If None, returns playbooks with all statuses.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
int: Count of user playbooks matching the filters
|
|
81
|
+
"""
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def count_user_playbooks_by_session(self, session_id: str) -> int:
|
|
86
|
+
"""Count user playbooks linked to a session via request_id -> requests.session_id.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session_id (str): The session ID to count user playbooks for
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
int: Count of user playbooks linked to the session
|
|
93
|
+
"""
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def delete_all_user_playbooks(self) -> None:
|
|
98
|
+
"""Delete all user playbooks from storage."""
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def delete_all_user_playbooks_by_playbook_name(
|
|
103
|
+
self, playbook_name: str, agent_version: str | None = None
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Delete all user playbooks by playbook name from storage.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
playbook_name (str): The playbook name to delete
|
|
109
|
+
agent_version (str, optional): The agent version to filter by. If None, deletes all agent versions.
|
|
110
|
+
"""
|
|
111
|
+
raise NotImplementedError
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def delete_user_playbook(self, user_playbook_id: int) -> None:
|
|
115
|
+
"""Delete a user playbook by ID.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
user_playbook_id (int): The ID of the user playbook to delete
|
|
119
|
+
"""
|
|
120
|
+
raise NotImplementedError
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def update_all_user_playbooks_status(
|
|
124
|
+
self,
|
|
125
|
+
old_status: Status | None,
|
|
126
|
+
new_status: Status | None,
|
|
127
|
+
agent_version: str | None = None,
|
|
128
|
+
playbook_name: str | None = None,
|
|
129
|
+
) -> int:
|
|
130
|
+
"""Update all user playbooks with old_status to new_status atomically.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
old_status: The current status to match (None for CURRENT)
|
|
134
|
+
new_status: The new status to set (None for CURRENT)
|
|
135
|
+
agent_version: Optional filter by agent version
|
|
136
|
+
playbook_name: Optional filter by playbook name
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
int: Number of user playbooks updated
|
|
140
|
+
"""
|
|
141
|
+
raise NotImplementedError
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def delete_all_user_playbooks_by_status(
|
|
145
|
+
self,
|
|
146
|
+
status: Status,
|
|
147
|
+
agent_version: str | None = None,
|
|
148
|
+
playbook_name: str | None = None,
|
|
149
|
+
) -> int:
|
|
150
|
+
"""Delete all user playbooks with the given status atomically.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
status: The status of user playbooks to delete
|
|
154
|
+
agent_version: Optional filter by agent version
|
|
155
|
+
playbook_name: Optional filter by playbook name
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
int: Number of user playbooks deleted
|
|
159
|
+
"""
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def delete_user_playbooks_by_ids(
|
|
164
|
+
self, user_playbook_ids: list[int], *, emit_hard_delete: bool = True
|
|
165
|
+
) -> int:
|
|
166
|
+
"""Delete user playbooks by their IDs.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
user_playbook_ids: List of user_playbook_id values to delete
|
|
170
|
+
emit_hard_delete: When True (default), append a ``hard_delete``
|
|
171
|
+
lineage event per id (genuine erasure). Set False for rollback
|
|
172
|
+
cleanup of a never-live row (e.g. a lost supersede CAS), so no
|
|
173
|
+
spurious audit event is recorded.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
int: Number of user playbooks deleted
|
|
177
|
+
"""
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def get_user_playbooks_by_ids(
|
|
182
|
+
self,
|
|
183
|
+
user_id: str,
|
|
184
|
+
user_playbook_ids: list[int],
|
|
185
|
+
status_filter: list[Status | None] | None = None,
|
|
186
|
+
) -> list[UserPlaybook]:
|
|
187
|
+
"""Fetch the subset of a user's playbooks whose ids are in the list.
|
|
188
|
+
|
|
189
|
+
Server-side filter on (``user_id``, ``user_playbook_id IN (...)``)
|
|
190
|
+
so callers (e.g. the reflection service resolving a small set of
|
|
191
|
+
cited playbook ids) avoid scanning every playbook for the user.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
user_id (str): Owning user id.
|
|
195
|
+
user_playbook_ids (list[int]): Playbook ids to fetch. Empty
|
|
196
|
+
list returns ``[]`` without hitting storage.
|
|
197
|
+
status_filter (list[Status | None] | None): Statuses to
|
|
198
|
+
include. ``None`` (default) means CURRENT only — same
|
|
199
|
+
default as ``get_user_playbooks`` for consistency.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
list[UserPlaybook]: Matching playbooks. Order is unspecified.
|
|
203
|
+
Ids that do not exist (or do not match the user / status
|
|
204
|
+
filter) are silently omitted.
|
|
205
|
+
"""
|
|
206
|
+
raise NotImplementedError
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def get_user_playbook_by_id(
|
|
210
|
+
self, user_playbook_id: int, *, include_tombstones: bool = False
|
|
211
|
+
) -> UserPlaybook | None:
|
|
212
|
+
"""Fetch one user playbook by primary key.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
user_playbook_id: The user_playbook_id to look up.
|
|
216
|
+
include_tombstones: When False (default), MERGED/SUPERSEDED rows
|
|
217
|
+
return None. Set to True for lineage resolution (resolve_current).
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
The UserPlaybook if found and not filtered, otherwise None.
|
|
221
|
+
"""
|
|
222
|
+
raise NotImplementedError
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
def get_user_playbooks_by_ids_any_user(
|
|
226
|
+
self,
|
|
227
|
+
user_playbook_ids: list[int],
|
|
228
|
+
status_filter: list[Status | None] | None = None,
|
|
229
|
+
) -> list[UserPlaybook]:
|
|
230
|
+
"""Fetch user playbooks by ids without requiring a single owner id."""
|
|
231
|
+
raise NotImplementedError
|
|
232
|
+
|
|
233
|
+
@abstractmethod
|
|
234
|
+
def archive_user_playbook_by_id(self, user_id: str, user_playbook_id: int) -> bool:
|
|
235
|
+
"""Atomically archive a single user playbook by id, only if CURRENT.
|
|
236
|
+
|
|
237
|
+
Flips the row's ``status`` from ``None`` (CURRENT) to
|
|
238
|
+
``Status.ARCHIVED``. No-op when the playbook does not exist, has
|
|
239
|
+
a different ``user_id``, or is already non-current.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
user_id (str): Owning user id; used as a guard so callers
|
|
243
|
+
cannot accidentally archive another user's playbook.
|
|
244
|
+
user_playbook_id (int): The user_playbook_id to archive.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
bool: True if a row was archived; False otherwise.
|
|
248
|
+
"""
|
|
249
|
+
raise NotImplementedError
|
|
250
|
+
|
|
251
|
+
@abstractmethod
|
|
252
|
+
def has_user_playbooks_with_status(
|
|
253
|
+
self,
|
|
254
|
+
status: Status | None,
|
|
255
|
+
agent_version: str | None = None,
|
|
256
|
+
playbook_name: str | None = None,
|
|
257
|
+
) -> bool:
|
|
258
|
+
"""Check if any user playbooks exist with given status and filters.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
status: The status to check for (None for CURRENT)
|
|
262
|
+
agent_version: Optional filter by agent version
|
|
263
|
+
playbook_name: Optional filter by playbook name
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
bool: True if any matching user playbooks exist
|
|
267
|
+
"""
|
|
268
|
+
raise NotImplementedError
|
|
269
|
+
|
|
270
|
+
@abstractmethod
|
|
271
|
+
def update_user_playbook(
|
|
272
|
+
self,
|
|
273
|
+
user_playbook_id: int,
|
|
274
|
+
playbook_name: str | None = None,
|
|
275
|
+
content: str | None = None,
|
|
276
|
+
trigger: str | None = None,
|
|
277
|
+
rationale: str | None = None,
|
|
278
|
+
blocking_issue: BlockingIssue | None = None,
|
|
279
|
+
tags: list[str] | None = None,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Update editable fields of a user playbook. Only non-None fields are updated.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
user_playbook_id (int): The ID of the user playbook to update
|
|
285
|
+
playbook_name (str, optional): New playbook name
|
|
286
|
+
content (str, optional): New content text
|
|
287
|
+
trigger (str, optional): New trigger text
|
|
288
|
+
rationale (str, optional): New rationale text
|
|
289
|
+
blocking_issue (BlockingIssue, optional): New blocking issue
|
|
290
|
+
tags (list[str], optional): Replacement tags
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: If user playbook with the given ID is not found
|
|
294
|
+
"""
|
|
295
|
+
raise NotImplementedError
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
def supersede_user_playbooks_by_ids(
|
|
299
|
+
self, user_playbook_ids: list[int], request_id: str
|
|
300
|
+
) -> int:
|
|
301
|
+
"""Soft-delete user playbooks by setting status to SUPERSEDED.
|
|
302
|
+
|
|
303
|
+
Eligible rows (CURRENT, PENDING, or ARCHIVED; not already MERGED /
|
|
304
|
+
SUPERSEDED) are transitioned to SUPERSEDED and emit one status_change
|
|
305
|
+
lineage event under the shared request id. This is the user-playbook
|
|
306
|
+
analogue of the existing agent/profile soft-supersede helpers and
|
|
307
|
+
preserves dead-source content for point-in-time attribution reads.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
user_playbook_ids (list[int]): User playbook ids to supersede.
|
|
311
|
+
request_id (str): Shared request id for all emitted lineage events.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
int: Number of user playbooks actually updated.
|
|
315
|
+
"""
|
|
316
|
+
raise NotImplementedError
|
|
317
|
+
|
|
318
|
+
@abstractmethod
|
|
319
|
+
def search_user_playbooks(
|
|
320
|
+
self,
|
|
321
|
+
request: SearchUserPlaybookRequest,
|
|
322
|
+
options: SearchOptions | None = None,
|
|
323
|
+
) -> list[UserPlaybook]:
|
|
324
|
+
"""Search user playbooks with advanced filtering including semantic search.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
request (SearchUserPlaybookRequest): Search request with query, filters, and pagination
|
|
328
|
+
options (SearchOptions, optional): Engine-level search parameters (e.g. pre-computed embedding)
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
list[UserPlaybook]: List of matching user playbook objects
|
|
332
|
+
"""
|
|
333
|
+
raise NotImplementedError
|
|
@@ -17,6 +17,7 @@ from collections import OrderedDict
|
|
|
17
17
|
from collections.abc import Callable
|
|
18
18
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
19
19
|
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
20
|
+
from datetime import UTC, datetime
|
|
20
21
|
from typing import TYPE_CHECKING, Any, cast
|
|
21
22
|
|
|
22
23
|
from reflexio.models.api_schema.retriever_schema import (
|
|
@@ -41,6 +42,13 @@ from reflexio.models.config_schema import (
|
|
|
41
42
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
42
43
|
from reflexio.server.prompt.prompt_manager import PromptManager
|
|
43
44
|
from reflexio.server.services.pre_retrieval import QueryReformulator
|
|
45
|
+
from reflexio.server.services.retrieval.recency import (
|
|
46
|
+
RecencyConfig,
|
|
47
|
+
ScoredItem,
|
|
48
|
+
additive_penalty,
|
|
49
|
+
decay_for_item,
|
|
50
|
+
multiplicative_factor,
|
|
51
|
+
)
|
|
44
52
|
from reflexio.server.services.retrieval.relevance_floor import apply_relevance_floors
|
|
45
53
|
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
46
54
|
from reflexio.server.tracing import profile_step, set_span_data
|
|
@@ -102,6 +110,7 @@ def run_unified_search(
|
|
|
102
110
|
prompt_manager: PromptManager,
|
|
103
111
|
pre_retrieval_model_name: str | None = None,
|
|
104
112
|
retrieval_floor: RetrievalFloorConfig | None = None,
|
|
113
|
+
recency: RecencyConfig | None = None,
|
|
105
114
|
) -> UnifiedSearchResponse:
|
|
106
115
|
"""
|
|
107
116
|
Search across all entity types (profiles, agent playbooks, user playbooks) in parallel.
|
|
@@ -129,7 +138,12 @@ def run_unified_search(
|
|
|
129
138
|
|
|
130
139
|
floor_cfg = retrieval_floor or RetrievalFloorConfig()
|
|
131
140
|
floor_on = floor_cfg.enabled
|
|
132
|
-
|
|
141
|
+
recency_on = bool(recency and recency.enabled)
|
|
142
|
+
fetch_k = max(
|
|
143
|
+
top_k,
|
|
144
|
+
floor_cfg.pool_size if floor_on else 0,
|
|
145
|
+
recency.pool_size if recency_on and recency is not None else 0,
|
|
146
|
+
)
|
|
133
147
|
|
|
134
148
|
# --- Phase A: query reformulation + embedding generation ---
|
|
135
149
|
reformulated_query, embedding = _run_phase_a(
|
|
@@ -153,6 +167,7 @@ def run_unified_search(
|
|
|
153
167
|
query=reformulated_query,
|
|
154
168
|
top_k=fetch_k,
|
|
155
169
|
threshold=threshold,
|
|
170
|
+
recency_on=recency_on,
|
|
156
171
|
)
|
|
157
172
|
|
|
158
173
|
if profiles is None:
|
|
@@ -166,7 +181,28 @@ def run_unified_search(
|
|
|
166
181
|
user_playbooks=user_playbooks, # type: ignore[arg-type]
|
|
167
182
|
top_k=top_k,
|
|
168
183
|
cfg=floor_cfg,
|
|
184
|
+
recency=recency if recency_on else None,
|
|
185
|
+
)
|
|
186
|
+
elif recency_on and recency is not None:
|
|
187
|
+
profiles = _apply_combined_score_recency(
|
|
188
|
+
profiles or [], entity_type="profiles", top_k=top_k, cfg=recency
|
|
189
|
+
)
|
|
190
|
+
agent_playbooks = _apply_combined_score_recency(
|
|
191
|
+
agent_playbooks or [],
|
|
192
|
+
entity_type="agent_playbooks",
|
|
193
|
+
top_k=top_k,
|
|
194
|
+
cfg=recency,
|
|
195
|
+
)
|
|
196
|
+
user_playbooks = _apply_combined_score_recency(
|
|
197
|
+
user_playbooks or [],
|
|
198
|
+
entity_type="user_playbooks",
|
|
199
|
+
top_k=top_k,
|
|
200
|
+
cfg=recency,
|
|
169
201
|
)
|
|
202
|
+
else:
|
|
203
|
+
profiles = _unwrap_items(profiles or [])[:top_k]
|
|
204
|
+
agent_playbooks = _unwrap_items(agent_playbooks or [])[:top_k]
|
|
205
|
+
user_playbooks = _unwrap_items(user_playbooks or [])[:top_k]
|
|
170
206
|
|
|
171
207
|
user_playbooks = _suppress_source_user_playbooks(
|
|
172
208
|
storage=storage,
|
|
@@ -284,10 +320,11 @@ def _run_phase_b(
|
|
|
284
320
|
query: str,
|
|
285
321
|
top_k: int,
|
|
286
322
|
threshold: float,
|
|
323
|
+
recency_on: bool = False,
|
|
287
324
|
) -> tuple[
|
|
288
|
-
list[
|
|
289
|
-
list[
|
|
290
|
-
list[
|
|
325
|
+
list[Any] | None,
|
|
326
|
+
list[Any] | None,
|
|
327
|
+
list[Any] | None,
|
|
291
328
|
]:
|
|
292
329
|
"""Run parallel searches across all entity types by delegating to storage methods.
|
|
293
330
|
|
|
@@ -314,9 +351,18 @@ def _run_phase_b(
|
|
|
314
351
|
entity_types=sorted(entity_types),
|
|
315
352
|
top_k=top_k,
|
|
316
353
|
) as span:
|
|
317
|
-
|
|
354
|
+
# Recency needs the per-row ``combined_score``, which only the scored
|
|
355
|
+
# single-RPC method threads back. Backends that don't advertise
|
|
356
|
+
# ``supports_unified_hybrid_search`` (e.g. native Postgres, which still
|
|
357
|
+
# inherits ``unified_hybrid_search_scored`` and runs it via the same
|
|
358
|
+
# ``_rpc`` it already uses for ``hybrid_match_*``) opt into the scored
|
|
359
|
+
# path only when recency is on, so non-recency routing is unchanged.
|
|
360
|
+
wants_scored_single_rpc = recency_on and callable(
|
|
361
|
+
getattr(storage, "unified_hybrid_search_scored", None)
|
|
362
|
+
)
|
|
363
|
+
if _unified_single_rpc_enabled() and (
|
|
318
364
|
getattr(storage, "supports_unified_hybrid_search", False)
|
|
319
|
-
|
|
365
|
+
or wants_scored_single_rpc
|
|
320
366
|
):
|
|
321
367
|
combined = _run_phase_b_single_rpc(
|
|
322
368
|
request=request,
|
|
@@ -327,6 +373,7 @@ def _run_phase_b(
|
|
|
327
373
|
threshold=threshold,
|
|
328
374
|
entity_types=entity_types,
|
|
329
375
|
allowed_agent_statuses=allowed_agent_statuses,
|
|
376
|
+
recency_on=recency_on,
|
|
330
377
|
)
|
|
331
378
|
if combined is not None:
|
|
332
379
|
profiles, agent_playbooks, user_playbooks = combined
|
|
@@ -437,7 +484,8 @@ def _run_phase_b_single_rpc(
|
|
|
437
484
|
threshold: float,
|
|
438
485
|
entity_types: set[str],
|
|
439
486
|
allowed_agent_statuses: list[PlaybookStatus] | None,
|
|
440
|
-
|
|
487
|
+
recency_on: bool = False,
|
|
488
|
+
) -> tuple[list[Any], list[Any], list[Any]] | None:
|
|
441
489
|
"""Run all Phase B arms through one combined storage round trip.
|
|
442
490
|
|
|
443
491
|
Trades the per-arm round-trip overhead for serialized execution of the
|
|
@@ -458,8 +506,16 @@ def _run_phase_b_single_rpc(
|
|
|
458
506
|
)
|
|
459
507
|
# Resolve storage.unified_hybrid_search before submit so missing or stale
|
|
460
508
|
# capability flags can fall back to the fan-out path.
|
|
461
|
-
|
|
509
|
+
method_name = (
|
|
510
|
+
"unified_hybrid_search_scored" if recency_on else "unified_hybrid_search"
|
|
511
|
+
)
|
|
512
|
+
unified_hybrid_search = getattr(storage, method_name, None)
|
|
462
513
|
if not callable(unified_hybrid_search):
|
|
514
|
+
if recency_on:
|
|
515
|
+
logger.warning(
|
|
516
|
+
"event=search_recency_missing_scores source=single_rpc method=%s",
|
|
517
|
+
method_name,
|
|
518
|
+
)
|
|
463
519
|
return None
|
|
464
520
|
|
|
465
521
|
future = _submit_with_current_context(
|
|
@@ -490,13 +546,14 @@ def _run_phase_b_single_rpc(
|
|
|
490
546
|
return None
|
|
491
547
|
|
|
492
548
|
# Mirror _search_agent_playbooks_via_storage: dedupe by id, cap at top_k.
|
|
493
|
-
deduped: list[
|
|
549
|
+
deduped: list[Any] = []
|
|
494
550
|
seen_ids: set[str] = set()
|
|
495
|
-
for
|
|
551
|
+
for candidate in agent_playbooks:
|
|
552
|
+
playbook = _unwrap_item(candidate)
|
|
496
553
|
playbook_id = str(getattr(playbook, "agent_playbook_id", ""))
|
|
497
554
|
if playbook_id and playbook_id not in seen_ids:
|
|
498
555
|
seen_ids.add(playbook_id)
|
|
499
|
-
deduped.append(
|
|
556
|
+
deduped.append(candidate)
|
|
500
557
|
if len(deduped) >= top_k:
|
|
501
558
|
break
|
|
502
559
|
return profiles, deduped, user_playbooks
|
|
@@ -509,6 +566,7 @@ def _apply_floors(
|
|
|
509
566
|
user_playbooks: list[UserPlaybook],
|
|
510
567
|
top_k: int,
|
|
511
568
|
cfg: RetrievalFloorConfig,
|
|
569
|
+
recency: RecencyConfig | None = None,
|
|
512
570
|
) -> tuple[list[UserProfile], list[AgentPlaybook], list[UserPlaybook]]:
|
|
513
571
|
"""Apply the per-arm relevance floor with one batched cross-encoder call."""
|
|
514
572
|
floored_profiles, floored_agent, floored_user = apply_relevance_floors(
|
|
@@ -519,8 +577,91 @@ def _apply_floors(
|
|
|
519
577
|
("user_playbooks", user_playbooks, cfg.user_playbook_floor),
|
|
520
578
|
],
|
|
521
579
|
top_k,
|
|
580
|
+
content_of=lambda item: _unwrap_item(item).content,
|
|
522
581
|
)
|
|
523
|
-
return
|
|
582
|
+
return (
|
|
583
|
+
_finalize_floor_arm(
|
|
584
|
+
floored_profiles, entity_type="profiles", top_k=top_k, recency=recency
|
|
585
|
+
),
|
|
586
|
+
_finalize_floor_arm(
|
|
587
|
+
floored_agent,
|
|
588
|
+
entity_type="agent_playbooks",
|
|
589
|
+
top_k=top_k,
|
|
590
|
+
recency=recency,
|
|
591
|
+
),
|
|
592
|
+
_finalize_floor_arm(
|
|
593
|
+
floored_user,
|
|
594
|
+
entity_type="user_playbooks",
|
|
595
|
+
top_k=top_k,
|
|
596
|
+
recency=recency,
|
|
597
|
+
),
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _finalize_floor_arm(
|
|
602
|
+
result: Any,
|
|
603
|
+
*,
|
|
604
|
+
entity_type: str,
|
|
605
|
+
top_k: int,
|
|
606
|
+
recency: RecencyConfig | None,
|
|
607
|
+
) -> list[Any]:
|
|
608
|
+
if not recency or not recency.enabled:
|
|
609
|
+
return _unwrap_items(result.items)[:top_k]
|
|
610
|
+
if result.scores is None:
|
|
611
|
+
return _apply_combined_score_recency(
|
|
612
|
+
result.items, entity_type=entity_type, top_k=top_k, cfg=recency
|
|
613
|
+
)
|
|
614
|
+
now = int(datetime.now(UTC).timestamp())
|
|
615
|
+
rescored = []
|
|
616
|
+
for item, score in zip(result.items, result.scores, strict=True):
|
|
617
|
+
unwrapped = _unwrap_item(item)
|
|
618
|
+
freshness = decay_for_item(unwrapped, entity_type=entity_type, now=now)
|
|
619
|
+
rescored.append(
|
|
620
|
+
(unwrapped, score - additive_penalty(freshness, recency.max_penalty_logit))
|
|
621
|
+
)
|
|
622
|
+
rescored.sort(key=lambda pair: pair[1], reverse=True)
|
|
623
|
+
return [item for item, _score in rescored[:top_k]]
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _apply_combined_score_recency(
|
|
627
|
+
items: list[Any],
|
|
628
|
+
*,
|
|
629
|
+
entity_type: str,
|
|
630
|
+
top_k: int,
|
|
631
|
+
cfg: RecencyConfig,
|
|
632
|
+
) -> list[Any]:
|
|
633
|
+
if not items:
|
|
634
|
+
return []
|
|
635
|
+
scored_items: list[tuple[Any, float]] = []
|
|
636
|
+
for item in items:
|
|
637
|
+
if not isinstance(item, ScoredItem) or item.score is None:
|
|
638
|
+
logger.warning(
|
|
639
|
+
"event=search_recency_missing_scores entity_type=%s items=%d",
|
|
640
|
+
entity_type,
|
|
641
|
+
len(items),
|
|
642
|
+
)
|
|
643
|
+
return _unwrap_items(items)[:top_k]
|
|
644
|
+
scored_items.append((item.item, item.score))
|
|
645
|
+
now = int(datetime.now(UTC).timestamp())
|
|
646
|
+
rescored = []
|
|
647
|
+
for item, score in scored_items:
|
|
648
|
+
freshness = decay_for_item(item, entity_type=entity_type, now=now)
|
|
649
|
+
rescored.append(
|
|
650
|
+
(
|
|
651
|
+
item,
|
|
652
|
+
score * multiplicative_factor(freshness, cfg.max_penalty_frac),
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
rescored.sort(key=lambda pair: pair[1], reverse=True)
|
|
656
|
+
return [item for item, _score in rescored[:top_k]]
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _unwrap_item(item: Any) -> Any:
|
|
660
|
+
return item.item if isinstance(item, ScoredItem) else item
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _unwrap_items(items: list[Any]) -> list[Any]:
|
|
664
|
+
return [_unwrap_item(item) for item in items]
|
|
524
665
|
|
|
525
666
|
|
|
526
667
|
def _suppress_source_user_playbooks(
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from typing import Protocol
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass(frozen=True)
|
|
9
|
-
class DetectedEntity:
|
|
10
|
-
start: int
|
|
11
|
-
end: int
|
|
12
|
-
entity_type: str
|
|
13
|
-
replacement: str
|
|
14
|
-
confidence: float
|
|
15
|
-
source: str
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass(frozen=True)
|
|
19
|
-
class StrippingResult:
|
|
20
|
-
text: str
|
|
21
|
-
detections: list[DetectedEntity] = field(default_factory=list)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class UserDetailDetector(Protocol):
|
|
25
|
-
def detect(self, text: str) -> list[DetectedEntity]: ...
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class UserDetailStripper(Protocol):
|
|
29
|
-
prompt_extra_instructions: str | None
|
|
30
|
-
|
|
31
|
-
def strip_user_details(
|
|
32
|
-
self,
|
|
33
|
-
text: str,
|
|
34
|
-
shared_mapping: dict[str, int] | None = None,
|
|
35
|
-
) -> StrippingResult: ...
|
|
36
|
-
|
|
37
|
-
def sanitize_aggregation_output_text(
|
|
38
|
-
self,
|
|
39
|
-
text: str | None,
|
|
40
|
-
) -> tuple[str | None, int]: ...
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class PassthroughStripper:
|
|
44
|
-
prompt_extra_instructions: str | None = None
|
|
45
|
-
|
|
46
|
-
def strip_user_details(
|
|
47
|
-
self,
|
|
48
|
-
text: str,
|
|
49
|
-
shared_mapping: dict[str, int] | None = None, # noqa: ARG002
|
|
50
|
-
) -> StrippingResult:
|
|
51
|
-
return StrippingResult(text=text, detections=[])
|
|
52
|
-
|
|
53
|
-
def sanitize_aggregation_output_text(
|
|
54
|
-
self,
|
|
55
|
-
text: str | None,
|
|
56
|
-
) -> tuple[str | None, int]:
|
|
57
|
-
return text, 0
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
UserDetailStripperFactory = Callable[[object], UserDetailStripper | None]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _default_user_detail_stripper_factory(
|
|
64
|
-
_configurator: object,
|
|
65
|
-
) -> UserDetailStripper | None:
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
_user_detail_stripper_factory: UserDetailStripperFactory = (
|
|
70
|
-
_default_user_detail_stripper_factory
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def set_user_detail_stripper_factory(factory: UserDetailStripperFactory) -> None:
|
|
75
|
-
"""Register the deployment-specific aggregation stripper factory."""
|
|
76
|
-
global _user_detail_stripper_factory # noqa: PLW0603
|
|
77
|
-
_user_detail_stripper_factory = factory
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def create_aggregation_user_detail_stripper(
|
|
81
|
-
configurator: object,
|
|
82
|
-
) -> UserDetailStripper | None:
|
|
83
|
-
"""Create the deployment-specific stripper for aggregation, if any."""
|
|
84
|
-
return _user_detail_stripper_factory(configurator)
|