claude-smart 0.2.46 → 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 +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/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 +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +11 -6
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
- 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/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -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/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -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/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- 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 +5 -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/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
- 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/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
- 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 +58 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- 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/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
- 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 +50 -9
- 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/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -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/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/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} +57 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
|
@@ -48,6 +48,10 @@ def parse_item_id(item_id: str) -> tuple[str, int] | None:
|
|
|
48
48
|
"""
|
|
49
49
|
Parse a prompt-format item ID like 'NEW-0' or 'EXISTING-1' into its prefix and index.
|
|
50
50
|
|
|
51
|
+
Weak models sometimes echo the rendered display label with its brackets
|
|
52
|
+
(``[NEW-0]``); strip a single surrounding pair so those outputs parse
|
|
53
|
+
instead of being silently dropped.
|
|
54
|
+
|
|
51
55
|
Args:
|
|
52
56
|
item_id (str): Item ID string in the format 'PREFIX-N' (e.g., 'NEW-0', 'EXISTING-1')
|
|
53
57
|
|
|
@@ -55,7 +59,10 @@ def parse_item_id(item_id: str) -> tuple[str, int] | None:
|
|
|
55
59
|
Optional[tuple[str, int]]: A tuple of (prefix, index) where prefix is 'NEW' or 'EXISTING',
|
|
56
60
|
or None if the item ID is invalid
|
|
57
61
|
"""
|
|
58
|
-
|
|
62
|
+
stripped = item_id.strip()
|
|
63
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
64
|
+
stripped = stripped[1:-1].strip()
|
|
65
|
+
parts = stripped.rsplit("-", 1)
|
|
59
66
|
if len(parts) != 2:
|
|
60
67
|
logger.warning("Invalid item ID format: %s", item_id)
|
|
61
68
|
return None
|
|
@@ -11,11 +11,11 @@ with — never another tenant's runs.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
import threading
|
|
15
14
|
from collections.abc import Callable
|
|
16
15
|
from datetime import UTC, datetime
|
|
17
16
|
|
|
18
17
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
18
|
+
from reflexio.server.scheduling import ThreadedScheduler
|
|
19
19
|
from reflexio.server.services.extraction.resumable_agent import (
|
|
20
20
|
pending_tool_calls_enabled,
|
|
21
21
|
)
|
|
@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
|
|
27
27
|
_DEFAULT_POLL_INTERVAL_SECONDS = 5.0
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class ExtractionResumeScheduler:
|
|
30
|
+
class ExtractionResumeScheduler(ThreadedScheduler):
|
|
31
31
|
"""Small polling wrapper that drives :class:`ExtractionResumeWorker` per org."""
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
@@ -37,29 +37,15 @@ class ExtractionResumeScheduler:
|
|
|
37
37
|
bootstrap_org_id: str,
|
|
38
38
|
max_runs_per_tick: int = 10,
|
|
39
39
|
) -> None:
|
|
40
|
+
super().__init__(thread_name="reflexio-extraction-resume-scheduler")
|
|
40
41
|
self.request_context_factory = request_context_factory
|
|
41
42
|
self.bootstrap_org_id = bootstrap_org_id
|
|
42
43
|
self.max_runs_per_tick = max_runs_per_tick
|
|
43
|
-
self._stop_event = threading.Event()
|
|
44
|
-
self._thread: threading.Thread | None = None
|
|
45
44
|
|
|
46
|
-
def
|
|
47
|
-
if self._thread is not None and self._thread.is_alive():
|
|
48
|
-
return
|
|
49
|
-
self._stop_event.clear()
|
|
50
|
-
self._thread = threading.Thread(
|
|
51
|
-
target=self._run_loop,
|
|
52
|
-
name="reflexio-extraction-resume-scheduler",
|
|
53
|
-
daemon=True,
|
|
54
|
-
)
|
|
55
|
-
self._thread.start()
|
|
45
|
+
def _on_started(self) -> None:
|
|
56
46
|
logger.info("event=extraction_resume_scheduler_started")
|
|
57
47
|
|
|
58
|
-
def
|
|
59
|
-
self._stop_event.set()
|
|
60
|
-
if self._thread is not None:
|
|
61
|
-
self._thread.join(timeout=timeout_seconds)
|
|
62
|
-
self._thread = None
|
|
48
|
+
def _on_stopped(self) -> None:
|
|
63
49
|
logger.info("event=extraction_resume_scheduler_stopped")
|
|
64
50
|
|
|
65
51
|
def _discover_org_ids(self, bootstrap_ctx: RequestContext) -> list[str]:
|
|
@@ -116,28 +102,27 @@ class ExtractionResumeScheduler:
|
|
|
116
102
|
org_id,
|
|
117
103
|
)
|
|
118
104
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
self._stop_event.wait(poll_interval)
|
|
105
|
+
def _run_once(self) -> float:
|
|
106
|
+
poll_interval = _DEFAULT_POLL_INTERVAL_SECONDS
|
|
107
|
+
try:
|
|
108
|
+
bootstrap_ctx = self.request_context_factory(self.bootstrap_org_id)
|
|
109
|
+
config = bootstrap_ctx.configurator.get_config()
|
|
110
|
+
poll_interval = (
|
|
111
|
+
config.pending_tool_call_config.resume_poll_interval_seconds
|
|
112
|
+
)
|
|
113
|
+
self._expire_pending_tool_calls(bootstrap_ctx)
|
|
114
|
+
for org_id in self._discover_org_ids(bootstrap_ctx):
|
|
115
|
+
if self._stop_event.is_set():
|
|
116
|
+
break
|
|
117
|
+
self._drain_org(org_id)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
with sentry_tags(
|
|
120
|
+
subsystem="extraction",
|
|
121
|
+
op="scheduler_tick",
|
|
122
|
+
error_type=type(exc).__name__,
|
|
123
|
+
):
|
|
124
|
+
logger.exception("event=extraction_resume_scheduler_tick_failed")
|
|
125
|
+
return poll_interval
|
|
141
126
|
|
|
142
127
|
|
|
143
128
|
def maybe_start_resume_scheduler(
|
|
@@ -484,11 +484,18 @@ class ExtractionResumeWorker:
|
|
|
484
484
|
raw_profiles = [
|
|
485
485
|
profile.model_dump() for profile in result.output.profiles or []
|
|
486
486
|
]
|
|
487
|
+
source_interaction_ids = [
|
|
488
|
+
interaction.interaction_id
|
|
489
|
+
for request_model in request_interaction_data_models
|
|
490
|
+
for interaction in request_model.interactions
|
|
491
|
+
if interaction.interaction_id
|
|
492
|
+
]
|
|
487
493
|
return (
|
|
488
494
|
extractor._convert_raw_to_user_profiles(
|
|
489
495
|
raw_profiles=raw_profiles,
|
|
490
496
|
user_id=run.binding.user_id,
|
|
491
497
|
request_id=run.binding.request_id,
|
|
498
|
+
source_interaction_ids=source_interaction_ids,
|
|
492
499
|
),
|
|
493
500
|
result.pending_tool_call_ids,
|
|
494
501
|
)
|
|
@@ -698,6 +705,7 @@ class ExtractionResumeWorker:
|
|
|
698
705
|
raw_profiles=raw_profiles,
|
|
699
706
|
user_id=run.binding.user_id,
|
|
700
707
|
request_id=run.binding.request_id,
|
|
708
|
+
source_interaction_ids=list(run.binding.source_interaction_ids),
|
|
701
709
|
),
|
|
702
710
|
run.pending_tool_call_ids,
|
|
703
711
|
)
|
|
@@ -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
|
|
|
@@ -22,6 +23,7 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
22
23
|
from reflexio.models.config_schema import Config
|
|
23
24
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
24
25
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
26
|
+
from reflexio.server.operation_limiter import operation_limit
|
|
25
27
|
from reflexio.server.services.agent_success_evaluation.runner import (
|
|
26
28
|
run_group_evaluation,
|
|
27
29
|
)
|
|
@@ -140,7 +142,12 @@ class GenerationService:
|
|
|
140
142
|
# ===============================
|
|
141
143
|
|
|
142
144
|
def run(
|
|
143
|
-
self,
|
|
145
|
+
self,
|
|
146
|
+
publish_user_interaction_request: PublishUserInteractionRequest,
|
|
147
|
+
*,
|
|
148
|
+
use_publish_limiter: bool = True,
|
|
149
|
+
publish_limiter_wait_forever: bool = True,
|
|
150
|
+
defer_learning: bool = False,
|
|
144
151
|
) -> GenerationServiceResult:
|
|
145
152
|
"""
|
|
146
153
|
Process a user interaction request by storing interactions and triggering generation services.
|
|
@@ -156,6 +163,14 @@ class GenerationService:
|
|
|
156
163
|
|
|
157
164
|
Args:
|
|
158
165
|
publish_user_interaction_request: The incoming user interaction request
|
|
166
|
+
use_publish_limiter: Whether this service should throttle the post-write
|
|
167
|
+
learning pipeline. Server sync calls acquire the bounded API
|
|
168
|
+
limiter before invoking this service, so they disable the inner
|
|
169
|
+
limiter to avoid waiting after storage side effects.
|
|
170
|
+
publish_limiter_wait_forever: Whether to queue indefinitely for the
|
|
171
|
+
post-write learning limiter when ``use_publish_limiter`` is true.
|
|
172
|
+
defer_learning: Whether to persist the publish and enqueue learning
|
|
173
|
+
for a process-local worker instead of running it inline.
|
|
159
174
|
|
|
160
175
|
Returns:
|
|
161
176
|
GenerationServiceResult: The request_id assigned to this publish call
|
|
@@ -293,137 +308,99 @@ class GenerationService:
|
|
|
293
308
|
)
|
|
294
309
|
return result
|
|
295
310
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
self._maybe_run_reflection(
|
|
302
|
-
user_id=user_id,
|
|
303
|
-
request_id=request_id,
|
|
304
|
-
agent_version=agent_version,
|
|
305
|
-
source=source,
|
|
306
|
-
)
|
|
311
|
+
if defer_learning:
|
|
312
|
+
from reflexio.server.services.publish_learning_worker import (
|
|
313
|
+
PublishLearningJob,
|
|
314
|
+
enqueue_publish_learning,
|
|
315
|
+
)
|
|
307
316
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
self._schedule_group_evaluation_if_needed(
|
|
318
|
+
new_request=new_request,
|
|
319
|
+
user_id=user_id,
|
|
320
|
+
agent_version=agent_version,
|
|
321
|
+
source=source,
|
|
322
|
+
)
|
|
323
|
+
learning_queued = enqueue_publish_learning(
|
|
324
|
+
PublishLearningJob(
|
|
325
|
+
org_id=self.org_id,
|
|
326
|
+
user_id=user_id,
|
|
327
|
+
request_id=request_id,
|
|
328
|
+
session_id=new_request.session_id,
|
|
329
|
+
source=source,
|
|
330
|
+
agent_version=agent_version,
|
|
331
|
+
force_extraction=publish_user_interaction_request.force_extraction,
|
|
332
|
+
skip_aggregation=publish_user_interaction_request.skip_aggregation,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
record_usage_event(
|
|
336
|
+
org_id=self.org_id,
|
|
337
|
+
user_id=user_id,
|
|
338
|
+
request_id=request_id,
|
|
339
|
+
session_id=new_request.session_id,
|
|
340
|
+
source=source,
|
|
341
|
+
agent_version=agent_version,
|
|
342
|
+
backend="classic",
|
|
343
|
+
event_name="publish_request_succeeded",
|
|
344
|
+
event_category="publish",
|
|
345
|
+
outcome="success",
|
|
346
|
+
count_value=len(new_interactions),
|
|
347
|
+
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
348
|
+
metadata={
|
|
349
|
+
"defer_learning": True,
|
|
350
|
+
"learning_queued": learning_queued,
|
|
351
|
+
"warning_count": len(result.warnings),
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
return result
|
|
319
355
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
356
|
+
# The durable write above must not sit behind the publish limiter:
|
|
357
|
+
# async API callers have already received success=True. Throttle only
|
|
358
|
+
# the post-write learning pipeline so a hung LLM cannot silently drop
|
|
359
|
+
# later acknowledged publishes before they reach storage.
|
|
360
|
+
learning_limit = (
|
|
361
|
+
operation_limit(
|
|
362
|
+
self.org_id,
|
|
363
|
+
"publish",
|
|
364
|
+
wait_forever=publish_limiter_wait_forever,
|
|
365
|
+
)
|
|
366
|
+
if use_publish_limiter
|
|
367
|
+
else nullcontext()
|
|
331
368
|
)
|
|
369
|
+
with learning_limit:
|
|
370
|
+
self._run_learning_steps(
|
|
371
|
+
user_id=user_id,
|
|
372
|
+
request_id=request_id,
|
|
373
|
+
agent_version=agent_version,
|
|
374
|
+
force_extraction=publish_user_interaction_request.force_extraction,
|
|
375
|
+
skip_aggregation=publish_user_interaction_request.skip_aggregation,
|
|
376
|
+
source=source,
|
|
377
|
+
result=result,
|
|
378
|
+
)
|
|
332
379
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
# Each thread needs its own context copy — Context.run() is non-reentrant
|
|
341
|
-
futures = [
|
|
342
|
-
executor.submit(
|
|
343
|
-
contextvars.copy_context().run,
|
|
344
|
-
profile_generation_service.run,
|
|
345
|
-
profile_generation_request,
|
|
346
|
-
),
|
|
347
|
-
executor.submit(
|
|
348
|
-
contextvars.copy_context().run,
|
|
349
|
-
playbook_generation_service.run,
|
|
350
|
-
playbook_generation_request,
|
|
351
|
-
),
|
|
352
|
-
]
|
|
353
|
-
|
|
354
|
-
# Collect results and handle any exceptions
|
|
355
|
-
# Each service failure is logged but doesn't block others
|
|
356
|
-
service_names = ["profile_generation", "playbook_generation"]
|
|
357
|
-
for future, service_name in zip(futures, service_names, strict=True):
|
|
358
|
-
try:
|
|
359
|
-
future.result(timeout=GENERATION_SERVICE_TIMEOUT_SECONDS)
|
|
360
|
-
except FuturesTimeoutError: # noqa: PERF203
|
|
361
|
-
msg = f"{service_name} timed out after {GENERATION_SERVICE_TIMEOUT_SECONDS}s"
|
|
362
|
-
with sentry_tags(
|
|
363
|
-
subsystem="generation",
|
|
364
|
-
service=service_name,
|
|
365
|
-
request_id=request_id,
|
|
366
|
-
error_type="timeout",
|
|
367
|
-
):
|
|
368
|
-
logger.error("%s for request %s", msg, request_id)
|
|
369
|
-
result.warnings.append(msg)
|
|
370
|
-
except Exception as e:
|
|
371
|
-
msg = f"{service_name} failed: {e}"
|
|
372
|
-
with sentry_tags(
|
|
373
|
-
subsystem="generation",
|
|
374
|
-
service=service_name,
|
|
375
|
-
request_id=request_id,
|
|
376
|
-
error_type=type(e).__name__,
|
|
377
|
-
):
|
|
378
|
-
logger.exception(
|
|
379
|
-
"Generation service failed for request %s",
|
|
380
|
-
request_id,
|
|
381
|
-
)
|
|
382
|
-
result.warnings.append(msg)
|
|
383
|
-
finally:
|
|
384
|
-
executor.shutdown(wait=False, cancel_futures=True)
|
|
380
|
+
# Schedule delayed group evaluation for the required session.
|
|
381
|
+
self._schedule_group_evaluation_if_needed(
|
|
382
|
+
new_request=new_request,
|
|
383
|
+
user_id=user_id,
|
|
384
|
+
agent_version=agent_version,
|
|
385
|
+
source=source,
|
|
386
|
+
)
|
|
385
387
|
|
|
386
|
-
|
|
387
|
-
# the publish path. The scheduler debounces bursts and is idempotent
|
|
388
|
-
# (already-tagged entities are skipped).
|
|
389
|
-
try:
|
|
390
|
-
schedule_tagging(
|
|
388
|
+
record_usage_event(
|
|
391
389
|
org_id=self.org_id,
|
|
392
390
|
user_id=user_id,
|
|
391
|
+
request_id=request_id,
|
|
392
|
+
session_id=new_request.session_id,
|
|
393
|
+
source=source,
|
|
393
394
|
agent_version=agent_version,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
395
|
+
backend="classic",
|
|
396
|
+
event_name="publish_request_succeeded",
|
|
397
|
+
event_category="publish",
|
|
398
|
+
outcome="success",
|
|
399
|
+
count_value=len(new_interactions),
|
|
400
|
+
duration_ms=int((time.perf_counter() - publish_start) * 1000),
|
|
401
|
+
metadata={"warning_count": len(result.warnings)},
|
|
401
402
|
)
|
|
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(
|
|
412
|
-
org_id=self.org_id,
|
|
413
|
-
user_id=user_id,
|
|
414
|
-
request_id=request_id,
|
|
415
|
-
session_id=new_request.session_id,
|
|
416
|
-
source=source,
|
|
417
|
-
agent_version=agent_version,
|
|
418
|
-
backend="classic",
|
|
419
|
-
event_name="publish_request_succeeded",
|
|
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)},
|
|
425
|
-
)
|
|
426
|
-
return result
|
|
403
|
+
return result
|
|
427
404
|
|
|
428
405
|
except Exception as e:
|
|
429
406
|
record_usage_event(
|
|
@@ -453,10 +430,142 @@ class GenerationService:
|
|
|
453
430
|
)
|
|
454
431
|
raise
|
|
455
432
|
|
|
433
|
+
# ===============================
|
|
434
|
+
# deferred learning
|
|
435
|
+
# ===============================
|
|
436
|
+
|
|
437
|
+
def run_deferred_learning(
|
|
438
|
+
self,
|
|
439
|
+
*,
|
|
440
|
+
user_id: str,
|
|
441
|
+
request_id: str,
|
|
442
|
+
session_id: str | None, # noqa: ARG002 - kept for worker telemetry symmetry
|
|
443
|
+
source: str | None,
|
|
444
|
+
agent_version: str,
|
|
445
|
+
force_extraction: bool,
|
|
446
|
+
skip_aggregation: bool,
|
|
447
|
+
) -> GenerationServiceResult:
|
|
448
|
+
result = GenerationServiceResult(request_id=request_id)
|
|
449
|
+
self._run_learning_steps(
|
|
450
|
+
user_id=user_id,
|
|
451
|
+
request_id=request_id,
|
|
452
|
+
agent_version=agent_version,
|
|
453
|
+
force_extraction=force_extraction,
|
|
454
|
+
skip_aggregation=skip_aggregation,
|
|
455
|
+
source=source,
|
|
456
|
+
result=result,
|
|
457
|
+
)
|
|
458
|
+
return result
|
|
459
|
+
|
|
456
460
|
# ===============================
|
|
457
461
|
# private methods
|
|
458
462
|
# ===============================
|
|
459
463
|
|
|
464
|
+
def _run_learning_steps(
|
|
465
|
+
self,
|
|
466
|
+
*,
|
|
467
|
+
user_id: str,
|
|
468
|
+
request_id: str,
|
|
469
|
+
agent_version: str,
|
|
470
|
+
force_extraction: bool,
|
|
471
|
+
skip_aggregation: bool,
|
|
472
|
+
source: str | None,
|
|
473
|
+
result: GenerationServiceResult,
|
|
474
|
+
) -> None:
|
|
475
|
+
# Reflection runs as its own sliding-window step BEFORE the extractor
|
|
476
|
+
# pool spins up, so replacements are visible to extractors.
|
|
477
|
+
self._maybe_run_reflection(
|
|
478
|
+
user_id=user_id,
|
|
479
|
+
request_id=request_id,
|
|
480
|
+
agent_version=agent_version,
|
|
481
|
+
source=source,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
profile_generation_service = ProfileGenerationService(
|
|
485
|
+
llm_client=self.client, request_context=self.request_context
|
|
486
|
+
)
|
|
487
|
+
profile_generation_request = ProfileGenerationRequest(
|
|
488
|
+
user_id=user_id,
|
|
489
|
+
request_id=request_id,
|
|
490
|
+
source=source,
|
|
491
|
+
force_extraction=force_extraction,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
playbook_generation_service = PlaybookGenerationService(
|
|
495
|
+
llm_client=self.client,
|
|
496
|
+
request_context=self.request_context,
|
|
497
|
+
skip_aggregation=skip_aggregation,
|
|
498
|
+
)
|
|
499
|
+
playbook_generation_request = PlaybookGenerationRequest(
|
|
500
|
+
request_id=request_id,
|
|
501
|
+
agent_version=agent_version,
|
|
502
|
+
user_id=user_id,
|
|
503
|
+
source=source,
|
|
504
|
+
force_extraction=force_extraction,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
executor = ThreadPoolExecutor(max_workers=2)
|
|
508
|
+
try:
|
|
509
|
+
futures = [
|
|
510
|
+
executor.submit(
|
|
511
|
+
contextvars.copy_context().run,
|
|
512
|
+
profile_generation_service.run,
|
|
513
|
+
profile_generation_request,
|
|
514
|
+
),
|
|
515
|
+
executor.submit(
|
|
516
|
+
contextvars.copy_context().run,
|
|
517
|
+
playbook_generation_service.run,
|
|
518
|
+
playbook_generation_request,
|
|
519
|
+
),
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
service_names = ["profile_generation", "playbook_generation"]
|
|
523
|
+
for future, service_name in zip(futures, service_names, strict=True):
|
|
524
|
+
try:
|
|
525
|
+
future.result(timeout=GENERATION_SERVICE_TIMEOUT_SECONDS)
|
|
526
|
+
except FuturesTimeoutError: # noqa: PERF203
|
|
527
|
+
msg = (
|
|
528
|
+
f"{service_name} timed out after "
|
|
529
|
+
f"{GENERATION_SERVICE_TIMEOUT_SECONDS}s"
|
|
530
|
+
)
|
|
531
|
+
with sentry_tags(
|
|
532
|
+
subsystem="generation",
|
|
533
|
+
service=service_name,
|
|
534
|
+
request_id=request_id,
|
|
535
|
+
error_type="timeout",
|
|
536
|
+
):
|
|
537
|
+
logger.error("%s for request %s", msg, request_id)
|
|
538
|
+
result.warnings.append(msg)
|
|
539
|
+
except Exception as e:
|
|
540
|
+
msg = f"{service_name} failed: {e}"
|
|
541
|
+
with sentry_tags(
|
|
542
|
+
subsystem="generation",
|
|
543
|
+
service=service_name,
|
|
544
|
+
request_id=request_id,
|
|
545
|
+
error_type=type(e).__name__,
|
|
546
|
+
):
|
|
547
|
+
logger.exception(
|
|
548
|
+
"Generation service failed for request %s",
|
|
549
|
+
request_id,
|
|
550
|
+
)
|
|
551
|
+
result.warnings.append(msg)
|
|
552
|
+
finally:
|
|
553
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
schedule_tagging(
|
|
557
|
+
org_id=self.org_id,
|
|
558
|
+
user_id=user_id,
|
|
559
|
+
agent_version=agent_version,
|
|
560
|
+
request_context=self.request_context,
|
|
561
|
+
llm_client=self.client,
|
|
562
|
+
)
|
|
563
|
+
except Exception:
|
|
564
|
+
logger.exception(
|
|
565
|
+
"Failed to schedule tagging for publish request %s",
|
|
566
|
+
request_id,
|
|
567
|
+
)
|
|
568
|
+
|
|
460
569
|
def _schedule_group_evaluation_if_needed(
|
|
461
570
|
self,
|
|
462
571
|
*,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from reflexio.server.env_utils import env_str, env_truthy
|
|
4
|
+
from reflexio.server.services.governance.subject_refs import (
|
|
5
|
+
actor_ref,
|
|
6
|
+
request_ref,
|
|
7
|
+
subject_ref,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
LOCAL_GOVERNANCE_REF_SECRET = "reflexio-local-governance-ref-secret" # noqa: S105
|
|
11
|
+
_PRODUCTION_DEPLOYMENT_MODES = {"platform", "self_host"}
|
|
12
|
+
_PRODUCTION_ENVS = {"production", "staging"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_local_dev_or_test() -> bool:
|
|
16
|
+
deployment_mode = env_str("DEPLOYMENT_MODE").lower()
|
|
17
|
+
reflexio_env = env_str("REFLEXIO_ENV", "development").lower()
|
|
18
|
+
if env_truthy(env_str("REFLEXIO_TEST_MODE")):
|
|
19
|
+
return True
|
|
20
|
+
return (
|
|
21
|
+
deployment_mode not in _PRODUCTION_DEPLOYMENT_MODES
|
|
22
|
+
and reflexio_env not in _PRODUCTION_ENVS
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_governance_ref_secret() -> str:
|
|
27
|
+
secret = env_str("REFLEXIO_GOVERNANCE_REF_SECRET")
|
|
28
|
+
if secret:
|
|
29
|
+
return secret
|
|
30
|
+
if _is_local_dev_or_test():
|
|
31
|
+
return LOCAL_GOVERNANCE_REF_SECRET
|
|
32
|
+
raise RuntimeError("REFLEXIO_GOVERNANCE_REF_SECRET is required")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def governance_subject_ref(org_id: str, user_id: str, secret: str) -> str:
|
|
36
|
+
return subject_ref(f"{org_id}:subject:{user_id}", secret)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def governance_request_ref(org_id: str, request_id: str, secret: str) -> str:
|
|
40
|
+
return request_ref(f"{org_id}:request:{request_id}", secret)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def governance_actor_ref(
|
|
44
|
+
org_id: str,
|
|
45
|
+
credential_kind: str,
|
|
46
|
+
principal_or_fingerprint: str,
|
|
47
|
+
secret: str,
|
|
48
|
+
) -> str:
|
|
49
|
+
return actor_ref(
|
|
50
|
+
f"{org_id}:actor:{credential_kind}:{principal_or_fingerprint}",
|
|
51
|
+
secret,
|
|
52
|
+
)
|