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.
Files changed (113) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  7. package/plugin/dashboard/package-lock.json +61 -390
  8. package/plugin/dashboard/package.json +2 -2
  9. package/plugin/pyproject.toml +1 -1
  10. package/plugin/uv.lock +1 -1
  11. package/plugin/vendor/reflexio/.env.example +7 -0
  12. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  13. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  14. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  15. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  16. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  17. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  18. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  19. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  20. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  21. package/plugin/vendor/reflexio/reflexio/server/api.py +133 -3273
  22. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  23. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  24. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  25. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  26. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  27. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  28. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  29. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  30. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  31. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  32. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  33. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  34. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  35. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  36. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  37. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  38. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  39. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  40. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  41. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  42. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  43. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  44. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  45. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  46. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  47. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  48. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  49. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  50. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  51. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  52. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  53. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  54. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  56. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  59. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  60. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  61. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  62. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  63. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  64. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  65. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  66. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  68. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -4
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +56 -351
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  88. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  89. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +6 -3
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +33 -8
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  113. 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