claude-smart 0.2.47 → 0.2.48
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 +1 -1
- 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/pyproject.toml +1 -1
- package/plugin/uv.lock +1 -1
- 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/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/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 +133 -3273
- 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 +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- 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 +259 -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/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- 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/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -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 +56 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- 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 +132 -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 +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
- 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 +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -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 +33 -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/_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 +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
- 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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Learning-provenance route handler (extracted from api.py, Tier3 A2)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
from fastapi import (
|
|
10
|
+
APIRouter,
|
|
11
|
+
Depends,
|
|
12
|
+
HTTPException,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from reflexio.models.api_schema.retriever_schema import (
|
|
16
|
+
GetLearningProvenanceRequest,
|
|
17
|
+
LearningProvenanceViewResponse,
|
|
18
|
+
SourceUserPlaybookProvenanceView,
|
|
19
|
+
)
|
|
20
|
+
from reflexio.models.api_schema.ui.converters import (
|
|
21
|
+
to_interaction_view,
|
|
22
|
+
to_user_playbook_view,
|
|
23
|
+
)
|
|
24
|
+
from reflexio.models.config_schema import (
|
|
25
|
+
DEFAULT_WINDOW_SIZE,
|
|
26
|
+
)
|
|
27
|
+
from reflexio.server.auth import (
|
|
28
|
+
default_get_org_id,
|
|
29
|
+
)
|
|
30
|
+
from reflexio.server.cache import reflexio_cache
|
|
31
|
+
from reflexio.server.services.extractor_interaction_utils import (
|
|
32
|
+
get_effective_source_filter,
|
|
33
|
+
get_extractor_window_params,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
router = APIRouter()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _learning_provenance_error(
|
|
41
|
+
payload: GetLearningProvenanceRequest,
|
|
42
|
+
msg: str,
|
|
43
|
+
) -> LearningProvenanceViewResponse:
|
|
44
|
+
return LearningProvenanceViewResponse(
|
|
45
|
+
success=False,
|
|
46
|
+
target_kind=payload.kind,
|
|
47
|
+
target_id=payload.id,
|
|
48
|
+
provenance_status="unavailable",
|
|
49
|
+
msg=msg,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_learning_target_int(
|
|
54
|
+
payload: GetLearningProvenanceRequest,
|
|
55
|
+
) -> int | LearningProvenanceViewResponse:
|
|
56
|
+
try:
|
|
57
|
+
return int(payload.id)
|
|
58
|
+
except ValueError:
|
|
59
|
+
return _learning_provenance_error(
|
|
60
|
+
payload,
|
|
61
|
+
f"{payload.kind} id must be an integer",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sort_interactions_by_time(interactions: list[Any]) -> list[Any]:
|
|
66
|
+
return sorted(
|
|
67
|
+
interactions,
|
|
68
|
+
key=lambda i: (
|
|
69
|
+
getattr(i, "created_at", 0) or 0,
|
|
70
|
+
getattr(i, "interaction_id", 0),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _interaction_views(interactions: list[Any]) -> list[Any]:
|
|
76
|
+
return [to_interaction_view(i) for i in _sort_interactions_by_time(interactions)]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _effective_profile_provenance_window(
|
|
80
|
+
reflexio: Any, trigger_source: str | None
|
|
81
|
+
) -> tuple[int, list[str] | None]:
|
|
82
|
+
config = reflexio.request_context.configurator.get_config()
|
|
83
|
+
profile_config = getattr(config, "profile_extractor_config", None)
|
|
84
|
+
if profile_config is None:
|
|
85
|
+
return getattr(config, "window_size", DEFAULT_WINDOW_SIZE), None
|
|
86
|
+
|
|
87
|
+
window_size, _ = get_extractor_window_params(
|
|
88
|
+
profile_config,
|
|
89
|
+
getattr(config, "window_size", None),
|
|
90
|
+
getattr(config, "stride_size", None),
|
|
91
|
+
)
|
|
92
|
+
should_skip, source_filter = get_effective_source_filter(
|
|
93
|
+
profile_config,
|
|
94
|
+
trigger_source,
|
|
95
|
+
)
|
|
96
|
+
if should_skip:
|
|
97
|
+
return window_size, None
|
|
98
|
+
return window_size, source_filter
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_profile_learning_provenance(
|
|
102
|
+
payload: GetLearningProvenanceRequest,
|
|
103
|
+
reflexio: Any,
|
|
104
|
+
) -> LearningProvenanceViewResponse:
|
|
105
|
+
storage = reflexio.request_context.storage
|
|
106
|
+
profile = storage.get_profile_by_id(payload.id)
|
|
107
|
+
if profile is None:
|
|
108
|
+
return _learning_provenance_error(payload, "Profile not found")
|
|
109
|
+
|
|
110
|
+
if profile.source_interaction_ids:
|
|
111
|
+
interactions = storage.get_interactions_by_ids(profile.source_interaction_ids)
|
|
112
|
+
return LearningProvenanceViewResponse(
|
|
113
|
+
success=True,
|
|
114
|
+
target_kind=payload.kind,
|
|
115
|
+
target_id=payload.id,
|
|
116
|
+
provenance_status="exact",
|
|
117
|
+
trigger_request_id=profile.generated_from_request_id,
|
|
118
|
+
interactions=_interaction_views(interactions),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not profile.generated_from_request_id:
|
|
122
|
+
return _learning_provenance_error(
|
|
123
|
+
payload,
|
|
124
|
+
"Profile has no generation request for provenance reconstruction",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
trigger_request = storage.get_request(profile.generated_from_request_id)
|
|
128
|
+
if trigger_request is None:
|
|
129
|
+
return _learning_provenance_error(
|
|
130
|
+
payload,
|
|
131
|
+
"Profile generation request was not found",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
trigger_interactions = storage.get_interactions_by_request_ids(
|
|
135
|
+
[profile.generated_from_request_id]
|
|
136
|
+
)
|
|
137
|
+
anchor_time = (
|
|
138
|
+
max(i.created_at for i in trigger_interactions)
|
|
139
|
+
if trigger_interactions
|
|
140
|
+
else trigger_request.created_at
|
|
141
|
+
)
|
|
142
|
+
window_size, source_filter = _effective_profile_provenance_window(
|
|
143
|
+
reflexio,
|
|
144
|
+
trigger_request.source,
|
|
145
|
+
)
|
|
146
|
+
_, interactions = storage.get_last_k_interactions_grouped(
|
|
147
|
+
user_id=profile.user_id,
|
|
148
|
+
k=window_size,
|
|
149
|
+
sources=source_filter,
|
|
150
|
+
end_time=anchor_time,
|
|
151
|
+
)
|
|
152
|
+
return LearningProvenanceViewResponse(
|
|
153
|
+
success=True,
|
|
154
|
+
target_kind=payload.kind,
|
|
155
|
+
target_id=payload.id,
|
|
156
|
+
provenance_status="best_effort" if interactions else "unavailable",
|
|
157
|
+
trigger_request_id=profile.generated_from_request_id,
|
|
158
|
+
interactions=_interaction_views(interactions),
|
|
159
|
+
msg=None if interactions else "No interactions found for reconstructed window",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_user_playbook_learning_provenance(
|
|
164
|
+
payload: GetLearningProvenanceRequest,
|
|
165
|
+
reflexio: Any,
|
|
166
|
+
) -> LearningProvenanceViewResponse:
|
|
167
|
+
parsed_id = _parse_learning_target_int(payload)
|
|
168
|
+
if isinstance(parsed_id, LearningProvenanceViewResponse):
|
|
169
|
+
return parsed_id
|
|
170
|
+
|
|
171
|
+
storage = reflexio.request_context.storage
|
|
172
|
+
playbook = storage.get_user_playbook_by_id(parsed_id)
|
|
173
|
+
if playbook is None:
|
|
174
|
+
return _learning_provenance_error(payload, "User playbook not found")
|
|
175
|
+
|
|
176
|
+
status = "exact"
|
|
177
|
+
if playbook.source_interaction_ids:
|
|
178
|
+
interactions = storage.get_interactions_by_ids(playbook.source_interaction_ids)
|
|
179
|
+
elif playbook.request_id:
|
|
180
|
+
status = "best_effort"
|
|
181
|
+
interactions = storage.get_interactions_by_request_ids([playbook.request_id])
|
|
182
|
+
else:
|
|
183
|
+
status = "unavailable"
|
|
184
|
+
interactions = []
|
|
185
|
+
|
|
186
|
+
return LearningProvenanceViewResponse(
|
|
187
|
+
success=True,
|
|
188
|
+
target_kind=payload.kind,
|
|
189
|
+
target_id=payload.id,
|
|
190
|
+
provenance_status=status if interactions else "unavailable",
|
|
191
|
+
trigger_request_id=playbook.request_id,
|
|
192
|
+
interactions=_interaction_views(interactions),
|
|
193
|
+
msg=None if interactions else "No interactions found for this user playbook",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _get_agent_playbook_learning_provenance(
|
|
198
|
+
payload: GetLearningProvenanceRequest,
|
|
199
|
+
reflexio: Any,
|
|
200
|
+
) -> LearningProvenanceViewResponse:
|
|
201
|
+
parsed_id = _parse_learning_target_int(payload)
|
|
202
|
+
if isinstance(parsed_id, LearningProvenanceViewResponse):
|
|
203
|
+
return parsed_id
|
|
204
|
+
|
|
205
|
+
storage = reflexio.request_context.storage
|
|
206
|
+
agent_playbook = storage.get_agent_playbook_by_id(parsed_id)
|
|
207
|
+
if agent_playbook is None:
|
|
208
|
+
return _learning_provenance_error(payload, "Agent playbook not found")
|
|
209
|
+
|
|
210
|
+
windows = storage.get_source_windows_for_agent_playbook(parsed_id)
|
|
211
|
+
if not windows:
|
|
212
|
+
return LearningProvenanceViewResponse(
|
|
213
|
+
success=True,
|
|
214
|
+
target_kind=payload.kind,
|
|
215
|
+
target_id=payload.id,
|
|
216
|
+
provenance_status="unavailable",
|
|
217
|
+
msg="No source user playbooks are recorded for this agent playbook",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
user_playbook_ids = [window.user_playbook_id for window in windows]
|
|
221
|
+
user_playbooks = storage.get_user_playbooks_by_ids_any_user(
|
|
222
|
+
user_playbook_ids,
|
|
223
|
+
status_filter=None,
|
|
224
|
+
)
|
|
225
|
+
user_playbooks_by_id = {
|
|
226
|
+
playbook.user_playbook_id: playbook for playbook in user_playbooks
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interaction_ids_to_fetch: set[int] = set()
|
|
230
|
+
request_ids_to_fetch: set[str] = set()
|
|
231
|
+
group_sources: dict[int, tuple[list[int], str | None, bool]] = {}
|
|
232
|
+
for window in windows:
|
|
233
|
+
source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
|
|
234
|
+
if source_user_playbook is None:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
source_ids = list(window.source_interaction_ids)
|
|
238
|
+
request_id: str | None = None
|
|
239
|
+
uses_fallback = False
|
|
240
|
+
if not source_ids and source_user_playbook.source_interaction_ids:
|
|
241
|
+
source_ids = list(source_user_playbook.source_interaction_ids)
|
|
242
|
+
elif not source_ids and source_user_playbook.request_id:
|
|
243
|
+
request_id = source_user_playbook.request_id
|
|
244
|
+
uses_fallback = True
|
|
245
|
+
elif not source_ids:
|
|
246
|
+
uses_fallback = True
|
|
247
|
+
|
|
248
|
+
interaction_ids_to_fetch.update(source_ids)
|
|
249
|
+
if request_id:
|
|
250
|
+
request_ids_to_fetch.add(request_id)
|
|
251
|
+
group_sources[window.user_playbook_id] = (source_ids, request_id, uses_fallback)
|
|
252
|
+
|
|
253
|
+
interactions_by_id = (
|
|
254
|
+
{
|
|
255
|
+
interaction.interaction_id: interaction
|
|
256
|
+
for interaction in storage.get_interactions_by_ids(
|
|
257
|
+
sorted(interaction_ids_to_fetch)
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
if interaction_ids_to_fetch
|
|
261
|
+
else {}
|
|
262
|
+
)
|
|
263
|
+
request_interactions: dict[str, list[Any]] = {}
|
|
264
|
+
if request_ids_to_fetch:
|
|
265
|
+
for interaction in storage.get_interactions_by_request_ids(
|
|
266
|
+
sorted(request_ids_to_fetch)
|
|
267
|
+
):
|
|
268
|
+
request_interactions.setdefault(interaction.request_id, []).append(
|
|
269
|
+
interaction
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
groups: list[SourceUserPlaybookProvenanceView] = []
|
|
273
|
+
used_fallback = False
|
|
274
|
+
found_interactions = False
|
|
275
|
+
for window in windows:
|
|
276
|
+
source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
|
|
277
|
+
if source_user_playbook is None:
|
|
278
|
+
used_fallback = True
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
source_ids, request_id, uses_fallback = group_sources.get(
|
|
282
|
+
window.user_playbook_id,
|
|
283
|
+
([], None, True),
|
|
284
|
+
)
|
|
285
|
+
used_fallback = used_fallback or uses_fallback
|
|
286
|
+
interactions = (
|
|
287
|
+
[interactions_by_id[i] for i in source_ids if i in interactions_by_id]
|
|
288
|
+
if source_ids
|
|
289
|
+
else request_interactions.get(request_id or "", [])
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
found_interactions = found_interactions or bool(interactions)
|
|
293
|
+
groups.append(
|
|
294
|
+
SourceUserPlaybookProvenanceView(
|
|
295
|
+
user_playbook=to_user_playbook_view(source_user_playbook),
|
|
296
|
+
interactions=_interaction_views(interactions),
|
|
297
|
+
source_interaction_ids=source_ids,
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if not groups:
|
|
302
|
+
return _learning_provenance_error(
|
|
303
|
+
payload,
|
|
304
|
+
"Recorded source user playbooks were not found",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return LearningProvenanceViewResponse(
|
|
308
|
+
success=True,
|
|
309
|
+
target_kind=payload.kind,
|
|
310
|
+
target_id=payload.id,
|
|
311
|
+
provenance_status=(
|
|
312
|
+
"exact"
|
|
313
|
+
if found_interactions and not used_fallback
|
|
314
|
+
else "best_effort"
|
|
315
|
+
if found_interactions
|
|
316
|
+
else "unavailable"
|
|
317
|
+
),
|
|
318
|
+
source_user_playbooks=groups,
|
|
319
|
+
msg=None if found_interactions else "No source interactions were found",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@router.post(
|
|
324
|
+
"/api/get_learning_provenance",
|
|
325
|
+
response_model=LearningProvenanceViewResponse,
|
|
326
|
+
response_model_exclude_none=True,
|
|
327
|
+
)
|
|
328
|
+
def get_learning_provenance(
|
|
329
|
+
payload: GetLearningProvenanceRequest,
|
|
330
|
+
org_id: str = Depends(default_get_org_id),
|
|
331
|
+
) -> LearningProvenanceViewResponse:
|
|
332
|
+
"""Return read-only learning provenance for a generated learning row.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
HTTPException: 503 when storage is not configured (matches the sibling
|
|
336
|
+
route convention, e.g. evaluation.py / stall_state_api.py).
|
|
337
|
+
"""
|
|
338
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
339
|
+
if reflexio.request_context.storage is None:
|
|
340
|
+
raise HTTPException(status_code=503, detail="Storage not configured")
|
|
341
|
+
if payload.kind == "profile":
|
|
342
|
+
return _get_profile_learning_provenance(payload, reflexio)
|
|
343
|
+
if payload.kind == "user_playbook":
|
|
344
|
+
return _get_user_playbook_learning_provenance(payload, reflexio)
|
|
345
|
+
return _get_agent_playbook_learning_provenance(payload, reflexio)
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Search/retrieval route handlers (extracted from api.py, Tier3 A2)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
from fastapi import (
|
|
11
|
+
APIRouter,
|
|
12
|
+
BackgroundTasks,
|
|
13
|
+
Depends,
|
|
14
|
+
Request,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from reflexio.models.api_schema.retriever_schema import (
|
|
18
|
+
RerankUserProfilesRequest,
|
|
19
|
+
SearchAgentPlaybookRequest,
|
|
20
|
+
SearchAgentPlaybooksViewResponse,
|
|
21
|
+
SearchInteractionRequest,
|
|
22
|
+
SearchInteractionsViewResponse,
|
|
23
|
+
SearchProfilesViewResponse,
|
|
24
|
+
SearchUserPlaybookRequest,
|
|
25
|
+
SearchUserPlaybooksViewResponse,
|
|
26
|
+
SearchUserProfileRequest,
|
|
27
|
+
UnifiedSearchRequest,
|
|
28
|
+
UnifiedSearchViewResponse,
|
|
29
|
+
)
|
|
30
|
+
from reflexio.models.api_schema.ui.converters import (
|
|
31
|
+
to_agent_playbook_view,
|
|
32
|
+
to_interaction_view,
|
|
33
|
+
to_profile_view,
|
|
34
|
+
to_user_playbook_view,
|
|
35
|
+
)
|
|
36
|
+
from reflexio.server.auth import (
|
|
37
|
+
default_billing_gate,
|
|
38
|
+
default_get_caller_type,
|
|
39
|
+
default_get_org_id,
|
|
40
|
+
)
|
|
41
|
+
from reflexio.server.cache import reflexio_cache
|
|
42
|
+
from reflexio.server.rate_limit import limiter
|
|
43
|
+
from reflexio.server.routes._common import _run_limited_api
|
|
44
|
+
from reflexio.server.routes._metering import (
|
|
45
|
+
_meter_applied_learnings,
|
|
46
|
+
_meter_search_request,
|
|
47
|
+
_stamp_search_dependencies_done,
|
|
48
|
+
)
|
|
49
|
+
from reflexio.server.tracing import profile_step
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
router = APIRouter()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post(
|
|
56
|
+
"/api/search_profiles",
|
|
57
|
+
response_model=SearchProfilesViewResponse,
|
|
58
|
+
response_model_exclude_none=True,
|
|
59
|
+
)
|
|
60
|
+
@limiter.limit("120/minute") # Rate limit for read operations
|
|
61
|
+
def search_user_profiles(
|
|
62
|
+
request: Request,
|
|
63
|
+
payload: SearchUserProfileRequest,
|
|
64
|
+
org_id: str = Depends(default_get_org_id),
|
|
65
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
66
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
67
|
+
) -> SearchProfilesViewResponse:
|
|
68
|
+
response = _run_limited_api(
|
|
69
|
+
org_id,
|
|
70
|
+
"search",
|
|
71
|
+
lambda: reflexio_cache.get_reflexio(org_id=org_id).search_user_profiles(
|
|
72
|
+
payload
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
resp = SearchProfilesViewResponse(
|
|
76
|
+
success=response.success,
|
|
77
|
+
user_profiles=[to_profile_view(p) for p in response.user_profiles],
|
|
78
|
+
msg=response.msg,
|
|
79
|
+
)
|
|
80
|
+
_meter_search_request(
|
|
81
|
+
org_id=org_id,
|
|
82
|
+
caller_type=caller_type,
|
|
83
|
+
request_id=getattr(payload, "request_id", None),
|
|
84
|
+
session_id=getattr(payload, "session_id", None),
|
|
85
|
+
)
|
|
86
|
+
_meter_applied_learnings(
|
|
87
|
+
org_id=org_id,
|
|
88
|
+
caller_type=caller_type,
|
|
89
|
+
surfaced_count=len(resp.user_profiles),
|
|
90
|
+
request_id=getattr(payload, "request_id", None),
|
|
91
|
+
session_id=getattr(payload, "session_id", None),
|
|
92
|
+
)
|
|
93
|
+
return resp
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.post(
|
|
97
|
+
"/api/rerank_user_profiles",
|
|
98
|
+
response_model=SearchProfilesViewResponse,
|
|
99
|
+
response_model_exclude_none=True,
|
|
100
|
+
)
|
|
101
|
+
@limiter.limit("120/minute") # Rate limit for read operations
|
|
102
|
+
def rerank_user_profiles(
|
|
103
|
+
request: Request,
|
|
104
|
+
payload: RerankUserProfilesRequest,
|
|
105
|
+
org_id: str = Depends(default_get_org_id),
|
|
106
|
+
) -> SearchProfilesViewResponse:
|
|
107
|
+
"""Rerank a list of profile ids by query relevance using a cross-encoder.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
request (Request): The HTTP request object (for rate limiting)
|
|
111
|
+
payload (RerankUserProfilesRequest): The rerank request
|
|
112
|
+
org_id (str): Organization ID
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
SearchProfilesViewResponse: Reranked profiles, top_k entries.
|
|
116
|
+
"""
|
|
117
|
+
response = _run_limited_api(
|
|
118
|
+
org_id,
|
|
119
|
+
"search",
|
|
120
|
+
lambda: reflexio_cache.get_reflexio(org_id=org_id).rerank_user_profiles(
|
|
121
|
+
payload
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
return SearchProfilesViewResponse(
|
|
125
|
+
success=response.success,
|
|
126
|
+
user_profiles=[to_profile_view(p) for p in response.user_profiles],
|
|
127
|
+
msg=response.msg,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@router.post(
|
|
132
|
+
"/api/search_interactions",
|
|
133
|
+
response_model=SearchInteractionsViewResponse,
|
|
134
|
+
response_model_exclude_none=True,
|
|
135
|
+
)
|
|
136
|
+
@limiter.limit("120/minute") # Rate limit for read operations
|
|
137
|
+
def search_interactions(
|
|
138
|
+
request: Request,
|
|
139
|
+
payload: SearchInteractionRequest,
|
|
140
|
+
org_id: str = Depends(default_get_org_id),
|
|
141
|
+
) -> SearchInteractionsViewResponse:
|
|
142
|
+
response = _run_limited_api(
|
|
143
|
+
org_id,
|
|
144
|
+
"search",
|
|
145
|
+
lambda: reflexio_cache.get_reflexio(org_id=org_id).search_interactions(payload),
|
|
146
|
+
)
|
|
147
|
+
return SearchInteractionsViewResponse(
|
|
148
|
+
success=response.success,
|
|
149
|
+
interactions=[to_interaction_view(i) for i in response.interactions],
|
|
150
|
+
msg=response.msg,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.post(
|
|
155
|
+
"/api/search_user_playbooks",
|
|
156
|
+
response_model=SearchUserPlaybooksViewResponse,
|
|
157
|
+
response_model_exclude_none=True,
|
|
158
|
+
)
|
|
159
|
+
@limiter.limit("120/minute") # Rate limit for read operations
|
|
160
|
+
def search_user_playbooks_endpoint(
|
|
161
|
+
request: Request,
|
|
162
|
+
payload: SearchUserPlaybookRequest,
|
|
163
|
+
org_id: str = Depends(default_get_org_id),
|
|
164
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
165
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
166
|
+
) -> SearchUserPlaybooksViewResponse:
|
|
167
|
+
"""Search user playbooks with semantic search and advanced filtering.
|
|
168
|
+
|
|
169
|
+
Supports filtering by user_id (via request_id linkage), agent_version,
|
|
170
|
+
playbook_name, datetime range, and status.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
request (Request): The HTTP request object (for rate limiting)
|
|
174
|
+
payload (SearchUserPlaybookRequest): The search request
|
|
175
|
+
org_id (str): Organization ID
|
|
176
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
SearchUserPlaybooksViewResponse: Response containing matching user playbooks
|
|
180
|
+
"""
|
|
181
|
+
response = _run_limited_api(
|
|
182
|
+
org_id,
|
|
183
|
+
"search",
|
|
184
|
+
lambda: reflexio_cache.get_reflexio(org_id=org_id).search_user_playbooks(
|
|
185
|
+
payload
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
resp = SearchUserPlaybooksViewResponse(
|
|
189
|
+
success=response.success,
|
|
190
|
+
user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
|
|
191
|
+
msg=response.msg,
|
|
192
|
+
)
|
|
193
|
+
_meter_search_request(
|
|
194
|
+
org_id=org_id,
|
|
195
|
+
caller_type=caller_type,
|
|
196
|
+
request_id=getattr(payload, "request_id", None),
|
|
197
|
+
session_id=getattr(payload, "session_id", None),
|
|
198
|
+
)
|
|
199
|
+
_meter_applied_learnings(
|
|
200
|
+
org_id=org_id,
|
|
201
|
+
caller_type=caller_type,
|
|
202
|
+
surfaced_count=len(resp.user_playbooks),
|
|
203
|
+
request_id=getattr(payload, "request_id", None),
|
|
204
|
+
session_id=getattr(payload, "session_id", None),
|
|
205
|
+
)
|
|
206
|
+
return resp
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@router.post(
|
|
210
|
+
"/api/search_agent_playbooks",
|
|
211
|
+
response_model=SearchAgentPlaybooksViewResponse,
|
|
212
|
+
response_model_exclude_none=True,
|
|
213
|
+
)
|
|
214
|
+
@limiter.limit("120/minute") # Rate limit for read operations
|
|
215
|
+
def search_agent_playbooks_endpoint(
|
|
216
|
+
request: Request,
|
|
217
|
+
payload: SearchAgentPlaybookRequest,
|
|
218
|
+
org_id: str = Depends(default_get_org_id),
|
|
219
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
220
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
221
|
+
) -> SearchAgentPlaybooksViewResponse:
|
|
222
|
+
"""Search agent playbooks with semantic search and advanced filtering.
|
|
223
|
+
|
|
224
|
+
Supports filtering by agent_version, playbook_name, datetime range,
|
|
225
|
+
status_filter, and playbook_status_filter.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
request (Request): The HTTP request object (for rate limiting)
|
|
229
|
+
payload (SearchAgentPlaybookRequest): The search request
|
|
230
|
+
org_id (str): Organization ID
|
|
231
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
SearchAgentPlaybooksViewResponse: Response containing matching agent playbooks
|
|
235
|
+
"""
|
|
236
|
+
response = _run_limited_api(
|
|
237
|
+
org_id,
|
|
238
|
+
"search",
|
|
239
|
+
lambda: reflexio_cache.get_reflexio(org_id=org_id).search_agent_playbooks(
|
|
240
|
+
payload
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
resp = SearchAgentPlaybooksViewResponse(
|
|
244
|
+
success=response.success,
|
|
245
|
+
agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
|
|
246
|
+
msg=response.msg,
|
|
247
|
+
)
|
|
248
|
+
_meter_search_request(
|
|
249
|
+
org_id=org_id,
|
|
250
|
+
caller_type=caller_type,
|
|
251
|
+
request_id=getattr(payload, "request_id", None),
|
|
252
|
+
session_id=getattr(payload, "session_id", None),
|
|
253
|
+
)
|
|
254
|
+
_meter_applied_learnings(
|
|
255
|
+
org_id=org_id,
|
|
256
|
+
caller_type=caller_type,
|
|
257
|
+
surfaced_count=len(resp.agent_playbooks),
|
|
258
|
+
request_id=getattr(payload, "request_id", None),
|
|
259
|
+
session_id=getattr(payload, "session_id", None),
|
|
260
|
+
)
|
|
261
|
+
return resp
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@router.post(
|
|
265
|
+
"/api/search",
|
|
266
|
+
response_model=UnifiedSearchViewResponse,
|
|
267
|
+
response_model_exclude_none=True,
|
|
268
|
+
)
|
|
269
|
+
@limiter.limit("120/minute")
|
|
270
|
+
def unified_search_endpoint(
|
|
271
|
+
request: Request,
|
|
272
|
+
payload: UnifiedSearchRequest,
|
|
273
|
+
background_tasks: BackgroundTasks,
|
|
274
|
+
org_id: str = Depends(default_get_org_id),
|
|
275
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
276
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
277
|
+
_deps_done: None = Depends(_stamp_search_dependencies_done),
|
|
278
|
+
) -> UnifiedSearchViewResponse:
|
|
279
|
+
"""Search across all entity types (profiles, agent playbooks, user playbooks).
|
|
280
|
+
|
|
281
|
+
Runs query rewriting and embedding generation in parallel, then searches
|
|
282
|
+
all entity types in parallel. Query rewriting is gated behind the
|
|
283
|
+
enable_reformulation request param.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
request (Request): The HTTP request object (for rate limiting)
|
|
287
|
+
payload (UnifiedSearchRequest): The unified search request
|
|
288
|
+
org_id (str): Organization ID
|
|
289
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
UnifiedSearchViewResponse: Combined search results
|
|
293
|
+
"""
|
|
294
|
+
deps_done = getattr(request.state, "search_deps_done_monotonic", None)
|
|
295
|
+
deps_to_body_ms = (
|
|
296
|
+
int((time.monotonic() - deps_done) * 1000) if deps_done is not None else None
|
|
297
|
+
)
|
|
298
|
+
with profile_step(
|
|
299
|
+
"search.endpoint",
|
|
300
|
+
enabled=bool(payload.enable_reformulation),
|
|
301
|
+
has_conversation_history=bool(payload.conversation_history),
|
|
302
|
+
search_mode=payload.search_mode,
|
|
303
|
+
) as endpoint_span:
|
|
304
|
+
endpoint_span.set_data("deps_to_body_ms", deps_to_body_ms)
|
|
305
|
+
endpoint_span.set_data(
|
|
306
|
+
"tp_borrowed", getattr(request.state, "tp_borrowed", None)
|
|
307
|
+
)
|
|
308
|
+
endpoint_span.set_data("tp_total", getattr(request.state, "tp_total", None))
|
|
309
|
+
endpoint_span.set_data("tp_waiting", getattr(request.state, "tp_waiting", None))
|
|
310
|
+
|
|
311
|
+
def run_search() -> Any:
|
|
312
|
+
with profile_step("search.reflexio_cache"):
|
|
313
|
+
reflexio = reflexio_cache.get_reflexio(org_id=org_id)
|
|
314
|
+
return reflexio.unified_search(payload, org_id=org_id)
|
|
315
|
+
|
|
316
|
+
response = _run_limited_api(org_id, "search", run_search)
|
|
317
|
+
with profile_step("search.response_view"):
|
|
318
|
+
resp = UnifiedSearchViewResponse(
|
|
319
|
+
success=response.success,
|
|
320
|
+
profiles=[to_profile_view(p) for p in response.profiles],
|
|
321
|
+
agent_playbooks=[
|
|
322
|
+
to_agent_playbook_view(fb) for fb in response.agent_playbooks
|
|
323
|
+
],
|
|
324
|
+
user_playbooks=[
|
|
325
|
+
to_user_playbook_view(rf) for rf in response.user_playbooks
|
|
326
|
+
],
|
|
327
|
+
reformulated_query=response.reformulated_query,
|
|
328
|
+
msg=response.msg,
|
|
329
|
+
agent_trace=response.agent_trace,
|
|
330
|
+
rehydrated_text=response.rehydrated_text,
|
|
331
|
+
)
|
|
332
|
+
background_tasks.add_task(
|
|
333
|
+
_meter_search_request,
|
|
334
|
+
org_id=org_id,
|
|
335
|
+
caller_type=caller_type,
|
|
336
|
+
request_id=getattr(payload, "request_id", None),
|
|
337
|
+
session_id=getattr(payload, "session_id", None),
|
|
338
|
+
)
|
|
339
|
+
background_tasks.add_task(
|
|
340
|
+
_meter_applied_learnings,
|
|
341
|
+
org_id=org_id,
|
|
342
|
+
caller_type=caller_type,
|
|
343
|
+
surfaced_count=len(resp.profiles)
|
|
344
|
+
+ len(resp.agent_playbooks)
|
|
345
|
+
+ len(resp.user_playbooks),
|
|
346
|
+
request_id=getattr(payload, "request_id", None),
|
|
347
|
+
session_id=getattr(payload, "session_id", None),
|
|
348
|
+
)
|
|
349
|
+
return resp
|