claude-smart 0.2.47 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
|
@@ -10,6 +10,7 @@ import uuid
|
|
|
10
10
|
from collections.abc import Callable
|
|
11
11
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
12
|
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
13
|
+
from contextlib import nullcontext
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from typing import TYPE_CHECKING
|
|
15
16
|
|
|
@@ -21,7 +22,9 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
21
22
|
)
|
|
22
23
|
from reflexio.models.config_schema import Config
|
|
23
24
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
25
|
+
from reflexio.server.env_utils import env_str, env_truthy
|
|
24
26
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
27
|
+
from reflexio.server.operation_limiter import operation_limit
|
|
25
28
|
from reflexio.server.services.agent_success_evaluation.runner import (
|
|
26
29
|
run_group_evaluation,
|
|
27
30
|
)
|
|
@@ -140,7 +143,12 @@ class GenerationService:
|
|
|
140
143
|
# ===============================
|
|
141
144
|
|
|
142
145
|
def run(
|
|
143
|
-
self,
|
|
146
|
+
self,
|
|
147
|
+
publish_user_interaction_request: PublishUserInteractionRequest,
|
|
148
|
+
*,
|
|
149
|
+
use_publish_limiter: bool = True,
|
|
150
|
+
publish_limiter_wait_forever: bool = True,
|
|
151
|
+
defer_learning: bool = False,
|
|
144
152
|
) -> GenerationServiceResult:
|
|
145
153
|
"""
|
|
146
154
|
Process a user interaction request by storing interactions and triggering generation services.
|
|
@@ -156,6 +164,14 @@ class GenerationService:
|
|
|
156
164
|
|
|
157
165
|
Args:
|
|
158
166
|
publish_user_interaction_request: The incoming user interaction request
|
|
167
|
+
use_publish_limiter: Whether this service should throttle the post-write
|
|
168
|
+
learning pipeline. Server sync calls acquire the bounded API
|
|
169
|
+
limiter before invoking this service, so they disable the inner
|
|
170
|
+
limiter to avoid waiting after storage side effects.
|
|
171
|
+
publish_limiter_wait_forever: Whether to queue indefinitely for the
|
|
172
|
+
post-write learning limiter when ``use_publish_limiter`` is true.
|
|
173
|
+
defer_learning: Whether to persist the publish and enqueue learning
|
|
174
|
+
for a process-local worker instead of running it inline.
|
|
159
175
|
|
|
160
176
|
Returns:
|
|
161
177
|
GenerationServiceResult: The request_id assigned to this publish call
|
|
@@ -234,17 +250,32 @@ class GenerationService:
|
|
|
234
250
|
session_id=publish_user_interaction_request.session_id,
|
|
235
251
|
evaluation_only=publish_user_interaction_request.evaluation_only,
|
|
236
252
|
)
|
|
237
|
-
self.storage.add_request(new_request) # type: ignore[reportOptionalMemberAccess]
|
|
238
253
|
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
# When the durable queue is enabled and this is a deferred publish, the
|
|
255
|
+
# request + interactions + job row are written together inside a single
|
|
256
|
+
# commit_scope (below). Every other path persists here unconditionally.
|
|
257
|
+
use_durable_queue = env_truthy(
|
|
258
|
+
env_str("REFLEXIO_DURABLE_LEARNING_QUEUE", "false")
|
|
242
259
|
)
|
|
260
|
+
_durable_defer = defer_learning and use_durable_queue
|
|
261
|
+
|
|
262
|
+
if not _durable_defer:
|
|
263
|
+
self.storage.add_request(new_request) # type: ignore[reportOptionalMemberAccess]
|
|
264
|
+
# Add interactions to storage (bulk insert with batched embedding generation)
|
|
265
|
+
self.storage.add_user_interactions_bulk( # type: ignore[reportOptionalMemberAccess]
|
|
266
|
+
user_id=user_id, interactions=new_interactions
|
|
267
|
+
)
|
|
243
268
|
|
|
244
269
|
# Extract source (empty string treated as None)
|
|
245
270
|
source = publish_user_interaction_request.source or None
|
|
246
271
|
|
|
247
272
|
if publish_user_interaction_request.evaluation_only:
|
|
273
|
+
if _durable_defer:
|
|
274
|
+
# evaluation_only returns early; ensure data lands even on durable path.
|
|
275
|
+
self.storage.add_request(new_request) # type: ignore[reportOptionalMemberAccess]
|
|
276
|
+
self.storage.add_user_interactions_bulk( # type: ignore[reportOptionalMemberAccess]
|
|
277
|
+
user_id=user_id, interactions=new_interactions
|
|
278
|
+
)
|
|
248
279
|
self._schedule_group_evaluation_if_needed(
|
|
249
280
|
new_request=new_request,
|
|
250
281
|
user_id=user_id,
|
|
@@ -275,6 +306,12 @@ class GenerationService:
|
|
|
275
306
|
not publish_user_interaction_request.override_learning_stall
|
|
276
307
|
and (stall_warning := self._active_learning_stall_warning()) is not None
|
|
277
308
|
):
|
|
309
|
+
if _durable_defer:
|
|
310
|
+
# Stall skips learning but data must still land.
|
|
311
|
+
self.storage.add_request(new_request) # type: ignore[reportOptionalMemberAccess]
|
|
312
|
+
self.storage.add_user_interactions_bulk( # type: ignore[reportOptionalMemberAccess]
|
|
313
|
+
user_id=user_id, interactions=new_interactions
|
|
314
|
+
)
|
|
278
315
|
result.warnings.append(stall_warning)
|
|
279
316
|
logger.warning("%s; skipping automatic extraction", stall_warning)
|
|
280
317
|
record_usage_event(
|
|
@@ -293,51 +330,295 @@ class GenerationService:
|
|
|
293
330
|
)
|
|
294
331
|
return result
|
|
295
332
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
333
|
+
if defer_learning:
|
|
334
|
+
if _durable_defer:
|
|
335
|
+
# Durable atomic path: pre-generate embeddings OUTSIDE the
|
|
336
|
+
# transaction (network call), then persist request + interactions
|
|
337
|
+
# + job in one commit_scope.
|
|
338
|
+
self.storage.prepare_interaction_embeddings(new_interactions) # type: ignore[reportOptionalMemberAccess]
|
|
339
|
+
covers_through = max(i.created_at for i in new_interactions)
|
|
340
|
+
with self.storage.commit_scope(): # type: ignore[reportOptionalMemberAccess]
|
|
341
|
+
self.storage.add_request(new_request) # type: ignore[reportOptionalMemberAccess]
|
|
342
|
+
self.storage.add_user_interactions_bulk( # type: ignore[reportOptionalMemberAccess]
|
|
343
|
+
user_id=user_id,
|
|
344
|
+
interactions=new_interactions,
|
|
345
|
+
embeddings_prepared=True,
|
|
346
|
+
)
|
|
347
|
+
self.storage.enqueue_learning_job( # type: ignore[reportOptionalMemberAccess]
|
|
348
|
+
org_id=self.org_id,
|
|
349
|
+
user_id=user_id,
|
|
350
|
+
request_id=request_id,
|
|
351
|
+
covers_through=covers_through,
|
|
352
|
+
force_extraction=publish_user_interaction_request.force_extraction,
|
|
353
|
+
skip_aggregation=publish_user_interaction_request.skip_aggregation,
|
|
354
|
+
)
|
|
355
|
+
self._schedule_group_evaluation_if_needed(
|
|
356
|
+
new_request=new_request,
|
|
357
|
+
user_id=user_id,
|
|
358
|
+
agent_version=agent_version,
|
|
359
|
+
source=source,
|
|
360
|
+
)
|
|
361
|
+
record_usage_event(
|
|
362
|
+
org_id=self.org_id,
|
|
363
|
+
user_id=user_id,
|
|
364
|
+
request_id=request_id,
|
|
365
|
+
session_id=new_request.session_id,
|
|
366
|
+
source=source,
|
|
367
|
+
agent_version=agent_version,
|
|
368
|
+
backend="durable",
|
|
369
|
+
event_name="publish_request_succeeded",
|
|
370
|
+
event_category="publish",
|
|
371
|
+
outcome="success",
|
|
372
|
+
count_value=len(new_interactions),
|
|
373
|
+
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
374
|
+
metadata={
|
|
375
|
+
"defer_learning": True,
|
|
376
|
+
"durable_queue": True,
|
|
377
|
+
"warning_count": len(result.warnings),
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
return result
|
|
381
|
+
|
|
382
|
+
from reflexio.server.services.publish_learning_worker import (
|
|
383
|
+
PublishLearningJob,
|
|
384
|
+
enqueue_publish_learning,
|
|
385
|
+
)
|
|
307
386
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
387
|
+
self._schedule_group_evaluation_if_needed(
|
|
388
|
+
new_request=new_request,
|
|
389
|
+
user_id=user_id,
|
|
390
|
+
agent_version=agent_version,
|
|
391
|
+
source=source,
|
|
392
|
+
)
|
|
393
|
+
learning_queued = enqueue_publish_learning(
|
|
394
|
+
PublishLearningJob(
|
|
395
|
+
org_id=self.org_id,
|
|
396
|
+
user_id=user_id,
|
|
397
|
+
request_id=request_id,
|
|
398
|
+
session_id=new_request.session_id,
|
|
399
|
+
source=source,
|
|
400
|
+
agent_version=agent_version,
|
|
401
|
+
force_extraction=publish_user_interaction_request.force_extraction,
|
|
402
|
+
skip_aggregation=publish_user_interaction_request.skip_aggregation,
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
record_usage_event(
|
|
406
|
+
org_id=self.org_id,
|
|
407
|
+
user_id=user_id,
|
|
408
|
+
request_id=request_id,
|
|
409
|
+
session_id=new_request.session_id,
|
|
410
|
+
source=source,
|
|
411
|
+
agent_version=agent_version,
|
|
412
|
+
backend="classic",
|
|
413
|
+
event_name="publish_request_succeeded",
|
|
414
|
+
event_category="publish",
|
|
415
|
+
outcome="success",
|
|
416
|
+
count_value=len(new_interactions),
|
|
417
|
+
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
418
|
+
metadata={
|
|
419
|
+
"defer_learning": True,
|
|
420
|
+
"learning_queued": learning_queued,
|
|
421
|
+
"warning_count": len(result.warnings),
|
|
422
|
+
},
|
|
423
|
+
)
|
|
424
|
+
return result
|
|
319
425
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
426
|
+
# The durable write above must not sit behind the publish limiter:
|
|
427
|
+
# async API callers have already received success=True. Throttle only
|
|
428
|
+
# the post-write learning pipeline so a hung LLM cannot silently drop
|
|
429
|
+
# later acknowledged publishes before they reach storage.
|
|
430
|
+
learning_limit = (
|
|
431
|
+
operation_limit(
|
|
432
|
+
self.org_id,
|
|
433
|
+
"publish",
|
|
434
|
+
wait_forever=publish_limiter_wait_forever,
|
|
435
|
+
)
|
|
436
|
+
if use_publish_limiter
|
|
437
|
+
else nullcontext()
|
|
324
438
|
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
439
|
+
with learning_limit:
|
|
440
|
+
self._run_learning_steps(
|
|
441
|
+
user_id=user_id,
|
|
442
|
+
request_id=request_id,
|
|
443
|
+
agent_version=agent_version,
|
|
444
|
+
force_extraction=publish_user_interaction_request.force_extraction,
|
|
445
|
+
skip_aggregation=publish_user_interaction_request.skip_aggregation,
|
|
446
|
+
source=source,
|
|
447
|
+
result=result,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Schedule delayed group evaluation for the required session.
|
|
451
|
+
self._schedule_group_evaluation_if_needed(
|
|
452
|
+
new_request=new_request,
|
|
453
|
+
user_id=user_id,
|
|
454
|
+
agent_version=agent_version,
|
|
455
|
+
source=source,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
record_usage_event(
|
|
459
|
+
org_id=self.org_id,
|
|
460
|
+
user_id=user_id,
|
|
461
|
+
request_id=request_id,
|
|
462
|
+
session_id=new_request.session_id,
|
|
463
|
+
source=source,
|
|
464
|
+
agent_version=agent_version,
|
|
465
|
+
backend="classic",
|
|
466
|
+
event_name="publish_request_succeeded",
|
|
467
|
+
event_category="publish",
|
|
468
|
+
outcome="success",
|
|
469
|
+
count_value=len(new_interactions),
|
|
470
|
+
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
471
|
+
metadata={"warning_count": len(result.warnings)},
|
|
472
|
+
)
|
|
473
|
+
return result
|
|
474
|
+
|
|
475
|
+
except Exception as e:
|
|
476
|
+
record_usage_event(
|
|
477
|
+
org_id=self.org_id,
|
|
328
478
|
user_id=user_id,
|
|
329
|
-
|
|
330
|
-
|
|
479
|
+
request_id=result.request_id,
|
|
480
|
+
session_id=publish_user_interaction_request.session_id,
|
|
481
|
+
source=publish_user_interaction_request.source,
|
|
482
|
+
agent_version=agent_version,
|
|
483
|
+
event_name="publish_request_failed",
|
|
484
|
+
event_category="publish",
|
|
485
|
+
outcome="failed",
|
|
486
|
+
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
487
|
+
error_kind=type(e).__name__,
|
|
331
488
|
)
|
|
489
|
+
with sentry_tags(
|
|
490
|
+
subsystem="generation",
|
|
491
|
+
op="refresh_profile",
|
|
492
|
+
org_id=self.org_id,
|
|
493
|
+
user_id=user_id,
|
|
494
|
+
request_id=result.request_id,
|
|
495
|
+
error_type=type(e).__name__,
|
|
496
|
+
):
|
|
497
|
+
logger.exception(
|
|
498
|
+
"Failed to refresh user profile for user id: %s",
|
|
499
|
+
user_id,
|
|
500
|
+
)
|
|
501
|
+
raise
|
|
502
|
+
|
|
503
|
+
# ===============================
|
|
504
|
+
# deferred learning
|
|
505
|
+
# ===============================
|
|
506
|
+
|
|
507
|
+
def run_deferred_learning(
|
|
508
|
+
self,
|
|
509
|
+
*,
|
|
510
|
+
user_id: str,
|
|
511
|
+
request_id: str,
|
|
512
|
+
session_id: str | None, # noqa: ARG002 - kept for worker telemetry symmetry
|
|
513
|
+
source: str | None,
|
|
514
|
+
agent_version: str,
|
|
515
|
+
force_extraction: bool,
|
|
516
|
+
skip_aggregation: bool,
|
|
517
|
+
sequential: bool = False,
|
|
518
|
+
) -> GenerationServiceResult:
|
|
519
|
+
"""Run the learning steps for a deferred job.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
sequential: When True, run profile and playbook generation on the
|
|
523
|
+
calling thread (no ThreadPoolExecutor). Required when the
|
|
524
|
+
caller already holds the storage commit_scope lock — spawning
|
|
525
|
+
worker threads inside that scope would deadlock on the RLock.
|
|
526
|
+
"""
|
|
527
|
+
result = GenerationServiceResult(request_id=request_id)
|
|
528
|
+
self._run_learning_steps(
|
|
529
|
+
user_id=user_id,
|
|
530
|
+
request_id=request_id,
|
|
531
|
+
agent_version=agent_version,
|
|
532
|
+
force_extraction=force_extraction,
|
|
533
|
+
skip_aggregation=skip_aggregation,
|
|
534
|
+
source=source,
|
|
535
|
+
result=result,
|
|
536
|
+
parallel=not sequential,
|
|
537
|
+
)
|
|
538
|
+
return result
|
|
539
|
+
|
|
540
|
+
# ===============================
|
|
541
|
+
# private methods
|
|
542
|
+
# ===============================
|
|
332
543
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
544
|
+
def _run_learning_steps(
|
|
545
|
+
self,
|
|
546
|
+
*,
|
|
547
|
+
user_id: str,
|
|
548
|
+
request_id: str,
|
|
549
|
+
agent_version: str,
|
|
550
|
+
force_extraction: bool,
|
|
551
|
+
skip_aggregation: bool,
|
|
552
|
+
source: str | None,
|
|
553
|
+
result: GenerationServiceResult,
|
|
554
|
+
parallel: bool = True,
|
|
555
|
+
) -> None:
|
|
556
|
+
# Reflection runs as its own sliding-window step BEFORE the extractor
|
|
557
|
+
# pool spins up, so replacements are visible to extractors.
|
|
558
|
+
self._maybe_run_reflection(
|
|
559
|
+
user_id=user_id,
|
|
560
|
+
request_id=request_id,
|
|
561
|
+
agent_version=agent_version,
|
|
562
|
+
source=source,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
profile_generation_service = ProfileGenerationService(
|
|
566
|
+
llm_client=self.client, request_context=self.request_context
|
|
567
|
+
)
|
|
568
|
+
profile_generation_request = ProfileGenerationRequest(
|
|
569
|
+
user_id=user_id,
|
|
570
|
+
request_id=request_id,
|
|
571
|
+
source=source,
|
|
572
|
+
force_extraction=force_extraction,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
playbook_generation_service = PlaybookGenerationService(
|
|
576
|
+
llm_client=self.client,
|
|
577
|
+
request_context=self.request_context,
|
|
578
|
+
skip_aggregation=skip_aggregation,
|
|
579
|
+
)
|
|
580
|
+
playbook_generation_request = PlaybookGenerationRequest(
|
|
581
|
+
request_id=request_id,
|
|
582
|
+
agent_version=agent_version,
|
|
583
|
+
user_id=user_id,
|
|
584
|
+
source=source,
|
|
585
|
+
force_extraction=force_extraction,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
if not parallel:
|
|
589
|
+
# Sequential mode: run both services on the calling thread.
|
|
590
|
+
# Required when the caller holds a commit_scope lock — spawning
|
|
591
|
+
# child threads inside that scope would deadlock the SQLite RLock.
|
|
592
|
+
for svc_name, _run in (
|
|
593
|
+
(
|
|
594
|
+
"profile_generation",
|
|
595
|
+
lambda: profile_generation_service.run(profile_generation_request),
|
|
596
|
+
),
|
|
597
|
+
(
|
|
598
|
+
"playbook_generation",
|
|
599
|
+
lambda: playbook_generation_service.run(
|
|
600
|
+
playbook_generation_request
|
|
601
|
+
),
|
|
602
|
+
),
|
|
603
|
+
):
|
|
604
|
+
try:
|
|
605
|
+
_run()
|
|
606
|
+
except Exception as e:
|
|
607
|
+
msg = f"{svc_name} failed: {e}"
|
|
608
|
+
with sentry_tags(
|
|
609
|
+
subsystem="generation",
|
|
610
|
+
service=svc_name,
|
|
611
|
+
request_id=request_id,
|
|
612
|
+
error_type=type(e).__name__,
|
|
613
|
+
):
|
|
614
|
+
logger.exception(
|
|
615
|
+
"Generation service failed for request %s",
|
|
616
|
+
request_id,
|
|
617
|
+
)
|
|
618
|
+
result.warnings.append(msg)
|
|
619
|
+
else:
|
|
338
620
|
executor = ThreadPoolExecutor(max_workers=2)
|
|
339
621
|
try:
|
|
340
|
-
# Each thread needs its own context copy — Context.run() is non-reentrant
|
|
341
622
|
futures = [
|
|
342
623
|
executor.submit(
|
|
343
624
|
contextvars.copy_context().run,
|
|
@@ -351,14 +632,15 @@ class GenerationService:
|
|
|
351
632
|
),
|
|
352
633
|
]
|
|
353
634
|
|
|
354
|
-
# Collect results and handle any exceptions
|
|
355
|
-
# Each service failure is logged but doesn't block others
|
|
356
635
|
service_names = ["profile_generation", "playbook_generation"]
|
|
357
636
|
for future, service_name in zip(futures, service_names, strict=True):
|
|
358
637
|
try:
|
|
359
638
|
future.result(timeout=GENERATION_SERVICE_TIMEOUT_SECONDS)
|
|
360
639
|
except FuturesTimeoutError: # noqa: PERF203
|
|
361
|
-
msg =
|
|
640
|
+
msg = (
|
|
641
|
+
f"{service_name} timed out after "
|
|
642
|
+
f"{GENERATION_SERVICE_TIMEOUT_SECONDS}s"
|
|
643
|
+
)
|
|
362
644
|
with sentry_tags(
|
|
363
645
|
subsystem="generation",
|
|
364
646
|
service=service_name,
|
|
@@ -383,79 +665,19 @@ class GenerationService:
|
|
|
383
665
|
finally:
|
|
384
666
|
executor.shutdown(wait=False, cancel_futures=True)
|
|
385
667
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
# (already-tagged entities are skipped).
|
|
389
|
-
try:
|
|
390
|
-
schedule_tagging(
|
|
391
|
-
org_id=self.org_id,
|
|
392
|
-
user_id=user_id,
|
|
393
|
-
agent_version=agent_version,
|
|
394
|
-
request_context=self.request_context,
|
|
395
|
-
llm_client=self.client,
|
|
396
|
-
)
|
|
397
|
-
except Exception:
|
|
398
|
-
logger.exception(
|
|
399
|
-
"Failed to schedule tagging for publish request %s",
|
|
400
|
-
request_id,
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
# Schedule delayed group evaluation for the required session.
|
|
404
|
-
self._schedule_group_evaluation_if_needed(
|
|
405
|
-
new_request=new_request,
|
|
406
|
-
user_id=user_id,
|
|
407
|
-
agent_version=agent_version,
|
|
408
|
-
source=source,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
record_usage_event(
|
|
668
|
+
try:
|
|
669
|
+
schedule_tagging(
|
|
412
670
|
org_id=self.org_id,
|
|
413
671
|
user_id=user_id,
|
|
414
|
-
request_id=request_id,
|
|
415
|
-
session_id=new_request.session_id,
|
|
416
|
-
source=source,
|
|
417
672
|
agent_version=agent_version,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
event_category="publish",
|
|
421
|
-
outcome="success",
|
|
422
|
-
count_value=len(new_interactions),
|
|
423
|
-
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
424
|
-
metadata={"warning_count": len(result.warnings)},
|
|
673
|
+
request_context=self.request_context,
|
|
674
|
+
llm_client=self.client,
|
|
425
675
|
)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
org_id=self.org_id,
|
|
431
|
-
user_id=user_id,
|
|
432
|
-
request_id=result.request_id,
|
|
433
|
-
session_id=publish_user_interaction_request.session_id,
|
|
434
|
-
source=publish_user_interaction_request.source,
|
|
435
|
-
agent_version=agent_version,
|
|
436
|
-
event_name="publish_request_failed",
|
|
437
|
-
event_category="publish",
|
|
438
|
-
outcome="failed",
|
|
439
|
-
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
440
|
-
error_kind=type(e).__name__,
|
|
676
|
+
except Exception:
|
|
677
|
+
logger.exception(
|
|
678
|
+
"Failed to schedule tagging for publish request %s",
|
|
679
|
+
request_id,
|
|
441
680
|
)
|
|
442
|
-
with sentry_tags(
|
|
443
|
-
subsystem="generation",
|
|
444
|
-
op="refresh_profile",
|
|
445
|
-
org_id=self.org_id,
|
|
446
|
-
user_id=user_id,
|
|
447
|
-
request_id=result.request_id,
|
|
448
|
-
error_type=type(e).__name__,
|
|
449
|
-
):
|
|
450
|
-
logger.exception(
|
|
451
|
-
"Failed to refresh user profile for user id: %s",
|
|
452
|
-
user_id,
|
|
453
|
-
)
|
|
454
|
-
raise
|
|
455
|
-
|
|
456
|
-
# ===============================
|
|
457
|
-
# private methods
|
|
458
|
-
# ===============================
|
|
459
681
|
|
|
460
682
|
def _schedule_group_evaluation_if_needed(
|
|
461
683
|
self,
|