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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Process-local queue for learning work after async publish persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from reflexio.server.cache.reflexio_cache import get_reflexio
|
|
12
|
+
from reflexio.server.env_utils import env_str
|
|
13
|
+
from reflexio.server.operation_limiter import operation_limit, operation_limit_value
|
|
14
|
+
from reflexio.server.services.generation_service import GenerationService
|
|
15
|
+
from reflexio.server.usage_metrics import record_usage_event
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_DEFAULT_QUEUE_WARN_SIZE = 1000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _env_int(name: str, default: int, *, minimum: int = 1) -> int:
|
|
23
|
+
raw = env_str(name, str(default))
|
|
24
|
+
try:
|
|
25
|
+
return max(minimum, int(raw))
|
|
26
|
+
except ValueError:
|
|
27
|
+
logger.warning("Invalid %s=%r; using %s", name, raw, default)
|
|
28
|
+
return max(minimum, default)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PublishLearningJob:
|
|
33
|
+
"""Metadata needed to run post-persist publish learning."""
|
|
34
|
+
|
|
35
|
+
org_id: str
|
|
36
|
+
user_id: str
|
|
37
|
+
request_id: str
|
|
38
|
+
session_id: str | None
|
|
39
|
+
source: str | None
|
|
40
|
+
agent_version: str
|
|
41
|
+
force_extraction: bool
|
|
42
|
+
skip_aggregation: bool
|
|
43
|
+
enqueued_at: float = field(default_factory=time.monotonic)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PublishLearningWorker:
|
|
47
|
+
"""Process-local worker pool for deferred publish learning."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
queue_warn_size: int | None = None,
|
|
53
|
+
worker_count: int | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
default_workers = operation_limit_value("publish")
|
|
56
|
+
self.queue_warn_size = (
|
|
57
|
+
queue_warn_size
|
|
58
|
+
if queue_warn_size is not None
|
|
59
|
+
else _env_int(
|
|
60
|
+
"REFLEXIO_PUBLISH_LEARNING_QUEUE_WARN_SIZE",
|
|
61
|
+
_DEFAULT_QUEUE_WARN_SIZE,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
self.worker_count = (
|
|
65
|
+
worker_count
|
|
66
|
+
if worker_count is not None
|
|
67
|
+
else _env_int("REFLEXIO_PUBLISH_LEARNING_WORKERS", default_workers)
|
|
68
|
+
)
|
|
69
|
+
self._queue: queue.Queue[PublishLearningJob] = queue.Queue()
|
|
70
|
+
self._stop_event = threading.Event()
|
|
71
|
+
self._threads: list[threading.Thread] = []
|
|
72
|
+
self._started = False
|
|
73
|
+
self._start_lock = threading.Lock()
|
|
74
|
+
|
|
75
|
+
def start(self) -> None:
|
|
76
|
+
with self._start_lock:
|
|
77
|
+
if self._started:
|
|
78
|
+
return
|
|
79
|
+
self._stop_event.clear()
|
|
80
|
+
self._threads = [
|
|
81
|
+
threading.Thread(
|
|
82
|
+
target=self._worker_loop,
|
|
83
|
+
name=f"publish-learning-worker-{idx}",
|
|
84
|
+
daemon=True,
|
|
85
|
+
)
|
|
86
|
+
for idx in range(self.worker_count)
|
|
87
|
+
]
|
|
88
|
+
for thread in self._threads:
|
|
89
|
+
thread.start()
|
|
90
|
+
self._started = True
|
|
91
|
+
logger.info(
|
|
92
|
+
"event=publish_learning_worker_started workers=%d queue_warn_size=%d",
|
|
93
|
+
self.worker_count,
|
|
94
|
+
self.queue_warn_size,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def enqueue(self, job: PublishLearningJob) -> bool:
|
|
98
|
+
self.start()
|
|
99
|
+
self._queue.put_nowait(job)
|
|
100
|
+
queue_depth = self._queue.qsize()
|
|
101
|
+
if queue_depth >= self.queue_warn_size:
|
|
102
|
+
record_usage_event(
|
|
103
|
+
org_id=job.org_id,
|
|
104
|
+
user_id=job.user_id,
|
|
105
|
+
request_id=job.request_id,
|
|
106
|
+
session_id=job.session_id,
|
|
107
|
+
source=job.source,
|
|
108
|
+
agent_version=job.agent_version,
|
|
109
|
+
event_name="learning_queue_pressure",
|
|
110
|
+
event_category="publish_learning",
|
|
111
|
+
outcome="queued",
|
|
112
|
+
metadata={
|
|
113
|
+
"queue_depth": queue_depth,
|
|
114
|
+
"queue_warn_size": self.queue_warn_size,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
logger.warning(
|
|
118
|
+
"event=publish_learning_queue_pressure org_id=%s user_id=%s "
|
|
119
|
+
"request_id=%s queue_depth=%d queue_warn_size=%d",
|
|
120
|
+
job.org_id,
|
|
121
|
+
job.user_id,
|
|
122
|
+
job.request_id,
|
|
123
|
+
queue_depth,
|
|
124
|
+
self.queue_warn_size,
|
|
125
|
+
)
|
|
126
|
+
logger.info(
|
|
127
|
+
"event=publish_learning_enqueued org_id=%s user_id=%s request_id=%s "
|
|
128
|
+
"queue_depth=%d queue_warn_size=%d",
|
|
129
|
+
job.org_id,
|
|
130
|
+
job.user_id,
|
|
131
|
+
job.request_id,
|
|
132
|
+
queue_depth,
|
|
133
|
+
self.queue_warn_size,
|
|
134
|
+
)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def stop(self, timeout: float | None = 5.0) -> None:
|
|
138
|
+
self._stop_event.set()
|
|
139
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
140
|
+
for thread in list(self._threads):
|
|
141
|
+
remaining = (
|
|
142
|
+
None if deadline is None else max(0.0, deadline - time.monotonic())
|
|
143
|
+
)
|
|
144
|
+
thread.join(timeout=remaining)
|
|
145
|
+
if all(not thread.is_alive() for thread in self._threads):
|
|
146
|
+
self._threads = []
|
|
147
|
+
self._started = False
|
|
148
|
+
logger.info("event=publish_learning_worker_stopped")
|
|
149
|
+
|
|
150
|
+
def _worker_loop(self) -> None:
|
|
151
|
+
while not self._stop_event.is_set() or not self._queue.empty():
|
|
152
|
+
try:
|
|
153
|
+
job = self._queue.get(timeout=0.25)
|
|
154
|
+
except queue.Empty:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
self._process_job(job)
|
|
158
|
+
finally:
|
|
159
|
+
self._queue.task_done()
|
|
160
|
+
|
|
161
|
+
def _process_job(self, job: PublishLearningJob) -> None:
|
|
162
|
+
try:
|
|
163
|
+
with operation_limit(
|
|
164
|
+
job.org_id,
|
|
165
|
+
"publish",
|
|
166
|
+
wait_forever=False,
|
|
167
|
+
log_timeout=False,
|
|
168
|
+
):
|
|
169
|
+
reflexio = get_reflexio(org_id=job.org_id)
|
|
170
|
+
GenerationService(
|
|
171
|
+
llm_client=reflexio.llm_client,
|
|
172
|
+
request_context=reflexio.request_context,
|
|
173
|
+
).run_deferred_learning(
|
|
174
|
+
user_id=job.user_id,
|
|
175
|
+
request_id=job.request_id,
|
|
176
|
+
session_id=job.session_id,
|
|
177
|
+
source=job.source,
|
|
178
|
+
agent_version=job.agent_version,
|
|
179
|
+
force_extraction=job.force_extraction,
|
|
180
|
+
skip_aggregation=job.skip_aggregation,
|
|
181
|
+
)
|
|
182
|
+
except TimeoutError:
|
|
183
|
+
self._requeue_after_limiter_timeout(job)
|
|
184
|
+
return
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
record_usage_event(
|
|
187
|
+
org_id=job.org_id,
|
|
188
|
+
user_id=job.user_id,
|
|
189
|
+
request_id=job.request_id,
|
|
190
|
+
session_id=job.session_id,
|
|
191
|
+
source=job.source,
|
|
192
|
+
agent_version=job.agent_version,
|
|
193
|
+
event_name="learning_failed",
|
|
194
|
+
event_category="publish_learning",
|
|
195
|
+
outcome="failed",
|
|
196
|
+
error_kind=type(exc).__name__,
|
|
197
|
+
)
|
|
198
|
+
logger.exception(
|
|
199
|
+
"event=publish_learning_failed org_id=%s user_id=%s request_id=%s",
|
|
200
|
+
job.org_id,
|
|
201
|
+
job.user_id,
|
|
202
|
+
job.request_id,
|
|
203
|
+
)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
record_usage_event(
|
|
207
|
+
org_id=job.org_id,
|
|
208
|
+
user_id=job.user_id,
|
|
209
|
+
request_id=job.request_id,
|
|
210
|
+
session_id=job.session_id,
|
|
211
|
+
source=job.source,
|
|
212
|
+
agent_version=job.agent_version,
|
|
213
|
+
event_name="learning_succeeded",
|
|
214
|
+
event_category="publish_learning",
|
|
215
|
+
outcome="success",
|
|
216
|
+
)
|
|
217
|
+
logger.info(
|
|
218
|
+
"event=publish_learning_done org_id=%s user_id=%s request_id=%s "
|
|
219
|
+
"queue_depth=%d",
|
|
220
|
+
job.org_id,
|
|
221
|
+
job.user_id,
|
|
222
|
+
job.request_id,
|
|
223
|
+
self._queue.qsize(),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _requeue_after_limiter_timeout(self, job: PublishLearningJob) -> None:
|
|
227
|
+
self._queue.put_nowait(job)
|
|
228
|
+
|
|
229
|
+
record_usage_event(
|
|
230
|
+
org_id=job.org_id,
|
|
231
|
+
user_id=job.user_id,
|
|
232
|
+
request_id=job.request_id,
|
|
233
|
+
session_id=job.session_id,
|
|
234
|
+
source=job.source,
|
|
235
|
+
agent_version=job.agent_version,
|
|
236
|
+
event_name="learning_requeued_limiter_busy",
|
|
237
|
+
event_category="publish_learning",
|
|
238
|
+
outcome="requeued",
|
|
239
|
+
metadata={
|
|
240
|
+
"age_seconds": self._job_age_seconds(job),
|
|
241
|
+
"queue_depth": self._queue.qsize(),
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
logger.info(
|
|
245
|
+
"event=publish_learning_requeued_limiter_busy org_id=%s user_id=%s "
|
|
246
|
+
"request_id=%s queue_depth=%d age_seconds=%.3f",
|
|
247
|
+
job.org_id,
|
|
248
|
+
job.user_id,
|
|
249
|
+
job.request_id,
|
|
250
|
+
self._queue.qsize(),
|
|
251
|
+
self._job_age_seconds(job),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _job_age_seconds(job: PublishLearningJob) -> float:
|
|
256
|
+
return time.monotonic() - job.enqueued_at
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
_worker: PublishLearningWorker | None = None
|
|
260
|
+
_worker_lock = threading.Lock()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_publish_learning_worker() -> PublishLearningWorker:
|
|
264
|
+
global _worker # noqa: PLW0603
|
|
265
|
+
if _worker is None:
|
|
266
|
+
with _worker_lock:
|
|
267
|
+
if _worker is None:
|
|
268
|
+
_worker = PublishLearningWorker()
|
|
269
|
+
return _worker
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def enqueue_publish_learning(job: PublishLearningJob) -> bool:
|
|
273
|
+
return get_publish_learning_worker().enqueue(job)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def stop_publish_learning_worker(timeout: float | None = 5.0) -> None:
|
|
277
|
+
global _worker # noqa: PLW0603
|
|
278
|
+
worker = _worker
|
|
279
|
+
if worker is not None:
|
|
280
|
+
worker.stop(timeout=timeout)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def reset_publish_learning_worker_for_tests() -> None:
|
|
284
|
+
global _worker # noqa: PLW0603
|
|
285
|
+
worker = _worker
|
|
286
|
+
if worker is not None:
|
|
287
|
+
worker.stop(timeout=0.1)
|
|
288
|
+
_worker = None
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Recency adjustment helpers for unified search ranking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, replace
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
|
|
11
|
+
from reflexio.server.env_utils import env_str, env_truthy
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
SECONDS_PER_DAY = 24 * 60 * 60
|
|
16
|
+
PLAYBOOK_HALF_LIFE_SECONDS = 90 * SECONDS_PER_DAY
|
|
17
|
+
_PROFILE_TTL_HALF_LIFE_FRACTION = 0.5
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ScoredItem[T]:
|
|
22
|
+
item: T
|
|
23
|
+
score: float | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class RecencyConfig:
|
|
28
|
+
enabled: bool = True
|
|
29
|
+
max_penalty_frac: float = 0.15
|
|
30
|
+
max_penalty_logit: float = 0.2
|
|
31
|
+
pool_size: int = 20
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_env(cls, *, env: Mapping[str, str] | None = None) -> RecencyConfig:
|
|
35
|
+
return cls(
|
|
36
|
+
enabled=env_truthy(
|
|
37
|
+
env_str("REFLEXIO_SEARCH_RECENCY_ENABLED", "true", env=env)
|
|
38
|
+
),
|
|
39
|
+
max_penalty_frac=_float_env(
|
|
40
|
+
"REFLEXIO_SEARCH_RECENCY_MAX_PENALTY_FRAC",
|
|
41
|
+
cls.max_penalty_frac,
|
|
42
|
+
env=env,
|
|
43
|
+
minimum=0.0,
|
|
44
|
+
maximum=1.0,
|
|
45
|
+
),
|
|
46
|
+
max_penalty_logit=_float_env(
|
|
47
|
+
"REFLEXIO_SEARCH_RECENCY_MAX_PENALTY_LOGIT",
|
|
48
|
+
cls.max_penalty_logit,
|
|
49
|
+
env=env,
|
|
50
|
+
minimum=0.0,
|
|
51
|
+
),
|
|
52
|
+
pool_size=_int_env(
|
|
53
|
+
"REFLEXIO_SEARCH_RECENCY_POOL_SIZE",
|
|
54
|
+
cls.pool_size,
|
|
55
|
+
env=env,
|
|
56
|
+
minimum=1,
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def with_overrides(self, values: Mapping[str, object] | None) -> RecencyConfig:
|
|
61
|
+
if not values:
|
|
62
|
+
return self
|
|
63
|
+
updates: dict[str, object] = {}
|
|
64
|
+
if "recency_enabled" in values:
|
|
65
|
+
updates["enabled"] = _bool_value(values["recency_enabled"], self.enabled)
|
|
66
|
+
if "recency_max_penalty_frac" in values:
|
|
67
|
+
updates["max_penalty_frac"] = _float_value(
|
|
68
|
+
values["recency_max_penalty_frac"],
|
|
69
|
+
self.max_penalty_frac,
|
|
70
|
+
minimum=0.0,
|
|
71
|
+
maximum=1.0,
|
|
72
|
+
)
|
|
73
|
+
if "recency_max_penalty_logit" in values:
|
|
74
|
+
updates["max_penalty_logit"] = _float_value(
|
|
75
|
+
values["recency_max_penalty_logit"],
|
|
76
|
+
self.max_penalty_logit,
|
|
77
|
+
minimum=0.0,
|
|
78
|
+
)
|
|
79
|
+
if "recency_pool_size" in values:
|
|
80
|
+
updates["pool_size"] = _int_value(
|
|
81
|
+
values["recency_pool_size"], self.pool_size, minimum=1
|
|
82
|
+
)
|
|
83
|
+
return replace(self, **updates)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def decay_for_item(
|
|
87
|
+
item: object,
|
|
88
|
+
*,
|
|
89
|
+
entity_type: str,
|
|
90
|
+
now: int | None = None,
|
|
91
|
+
) -> float:
|
|
92
|
+
"""Return a freshness decay in [0, 1], where 1 means no penalty."""
|
|
93
|
+
now = now if now is not None else int(datetime.now(UTC).timestamp())
|
|
94
|
+
if entity_type == "profiles":
|
|
95
|
+
return _profile_decay(item, now=now)
|
|
96
|
+
return _timestamp_decay(
|
|
97
|
+
getattr(item, "created_at", None),
|
|
98
|
+
half_life_seconds=PLAYBOOK_HALF_LIFE_SECONDS,
|
|
99
|
+
now=now,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decay(age_seconds: float, half_life_seconds: float) -> float:
|
|
104
|
+
if half_life_seconds <= 0:
|
|
105
|
+
return 1.0
|
|
106
|
+
age_seconds = max(0.0, age_seconds)
|
|
107
|
+
return math.exp(-age_seconds / (half_life_seconds / math.log(2)))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def multiplicative_factor(decay_value: float, max_penalty_frac: float) -> float:
|
|
111
|
+
return 1.0 - _clamp(max_penalty_frac, 0.0, 1.0) * (1.0 - _decay(decay_value))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def additive_penalty(decay_value: float, max_penalty_logit: float) -> float:
|
|
115
|
+
return max(0.0, max_penalty_logit) * (1.0 - _decay(decay_value))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _profile_decay(item: object, *, now: int) -> float:
|
|
119
|
+
modified_at = _int_or_none(getattr(item, "last_modified_timestamp", None))
|
|
120
|
+
expires_at = _int_or_none(getattr(item, "expiration_timestamp", None))
|
|
121
|
+
if modified_at is None or expires_at is None or expires_at <= modified_at:
|
|
122
|
+
return 1.0
|
|
123
|
+
half_life = (expires_at - modified_at) * _PROFILE_TTL_HALF_LIFE_FRACTION
|
|
124
|
+
return decay(now - modified_at, half_life)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _timestamp_decay(value: object, *, half_life_seconds: int, now: int) -> float:
|
|
128
|
+
timestamp = _int_or_none(value)
|
|
129
|
+
if timestamp is None or timestamp <= 0:
|
|
130
|
+
return 1.0
|
|
131
|
+
return decay(now - timestamp, half_life_seconds)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _float_env(
|
|
135
|
+
name: str,
|
|
136
|
+
default: float,
|
|
137
|
+
*,
|
|
138
|
+
env: Mapping[str, str] | None,
|
|
139
|
+
minimum: float,
|
|
140
|
+
maximum: float | None = None,
|
|
141
|
+
) -> float:
|
|
142
|
+
return _float_value(
|
|
143
|
+
env_str(name, str(default), env=env),
|
|
144
|
+
default,
|
|
145
|
+
minimum=minimum,
|
|
146
|
+
maximum=maximum,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _int_env(
|
|
151
|
+
name: str,
|
|
152
|
+
default: int,
|
|
153
|
+
*,
|
|
154
|
+
env: Mapping[str, str] | None,
|
|
155
|
+
minimum: int,
|
|
156
|
+
) -> int:
|
|
157
|
+
return _int_value(env_str(name, str(default), env=env), default, minimum=minimum)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _bool_value(value: object, default: bool) -> bool:
|
|
161
|
+
if isinstance(value, bool):
|
|
162
|
+
return value
|
|
163
|
+
if isinstance(value, str) and value.strip():
|
|
164
|
+
return env_truthy(value)
|
|
165
|
+
return default
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _float_value(
|
|
169
|
+
value: object,
|
|
170
|
+
default: float,
|
|
171
|
+
*,
|
|
172
|
+
minimum: float,
|
|
173
|
+
maximum: float | None = None,
|
|
174
|
+
) -> float:
|
|
175
|
+
try:
|
|
176
|
+
if not isinstance(value, str | int | float):
|
|
177
|
+
raise TypeError
|
|
178
|
+
parsed = float(value)
|
|
179
|
+
except (TypeError, ValueError):
|
|
180
|
+
logger.warning("Invalid recency float override %r; using %.3f", value, default)
|
|
181
|
+
return default
|
|
182
|
+
parsed = max(minimum, parsed)
|
|
183
|
+
if maximum is not None:
|
|
184
|
+
parsed = min(maximum, parsed)
|
|
185
|
+
return parsed
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _int_value(value: object, default: int, *, minimum: int) -> int:
|
|
189
|
+
try:
|
|
190
|
+
if not isinstance(value, str | int):
|
|
191
|
+
raise TypeError
|
|
192
|
+
parsed = int(value)
|
|
193
|
+
except (TypeError, ValueError):
|
|
194
|
+
logger.warning("Invalid recency integer override %r; using %d", value, default)
|
|
195
|
+
return default
|
|
196
|
+
return max(minimum, parsed)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _int_or_none(value: object) -> int | None:
|
|
200
|
+
try:
|
|
201
|
+
return int(value) # type: ignore[arg-type]
|
|
202
|
+
except (TypeError, ValueError):
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _decay(value: float) -> float:
|
|
207
|
+
return _clamp(value, 0.0, 1.0)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _clamp(value: float, low: float, high: float) -> float:
|
|
211
|
+
return min(high, max(low, value))
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
from collections.abc import Callable, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
from reflexio.server.llm.rerank import score_pairs
|
|
@@ -21,12 +22,20 @@ from reflexio.server.tracing import profile_step
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class RelevanceFloorResult:
|
|
27
|
+
items: list[Any]
|
|
28
|
+
scores: list[float] | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _floor_and_sort[T](
|
|
32
|
+
items: list[T], scores: list[float], floor: float
|
|
33
|
+
) -> list[tuple[T, float]]:
|
|
34
|
+
"""Return ``(item, score)`` pairs scoring >= ``floor``, sorted descending."""
|
|
26
35
|
ranked = sorted(
|
|
27
36
|
zip(items, scores, strict=True), key=lambda pair: pair[1], reverse=True
|
|
28
37
|
)
|
|
29
|
-
return [item for item, score in ranked if score >= floor]
|
|
38
|
+
return [(item, score) for item, score in ranked if score >= floor]
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
def apply_relevance_floor[T](
|
|
@@ -71,7 +80,7 @@ def apply_relevance_floor[T](
|
|
|
71
80
|
return items[:top_k]
|
|
72
81
|
span.set_data("available", True)
|
|
73
82
|
|
|
74
|
-
survivors = _floor_and_sort(items, scores, floor)
|
|
83
|
+
survivors = [item for item, _score in _floor_and_sort(items, scores, floor)]
|
|
75
84
|
dropped = len(items) - len(survivors)
|
|
76
85
|
span.set_data("kept", len(survivors))
|
|
77
86
|
span.set_data("dropped", dropped)
|
|
@@ -92,7 +101,7 @@ def apply_relevance_floors(
|
|
|
92
101
|
top_k: int,
|
|
93
102
|
*,
|
|
94
103
|
content_of: Callable[[Any], str] = lambda item: item.content,
|
|
95
|
-
) -> list[
|
|
104
|
+
) -> list[RelevanceFloorResult]:
|
|
96
105
|
"""Floor every arm with a single cross-encoder batch.
|
|
97
106
|
|
|
98
107
|
CPU cross-encoder inference does not parallelize across threads, so
|
|
@@ -108,12 +117,13 @@ def apply_relevance_floors(
|
|
|
108
117
|
content_of: Extracts the text to score for an item.
|
|
109
118
|
|
|
110
119
|
Returns:
|
|
111
|
-
One
|
|
112
|
-
descending and
|
|
113
|
-
every arm returns
|
|
120
|
+
One result per arm, in input order. Available reranker results are
|
|
121
|
+
sorted by score descending and left uncapped with the paired raw logits.
|
|
122
|
+
On reranker unavailability, every arm returns the original full item
|
|
123
|
+
pool with a ``None`` score sentinel (logged).
|
|
114
124
|
"""
|
|
115
125
|
if not any(items for _, items, _ in arms):
|
|
116
|
-
return [[] for _ in arms]
|
|
126
|
+
return [RelevanceFloorResult([], []) for _ in arms]
|
|
117
127
|
arm_names = [name for name, _, _ in arms]
|
|
118
128
|
contents: list[str] = []
|
|
119
129
|
for _, items, _ in arms:
|
|
@@ -123,20 +133,21 @@ def apply_relevance_floors(
|
|
|
123
133
|
arm="all",
|
|
124
134
|
arms=arm_names,
|
|
125
135
|
items=len(contents),
|
|
136
|
+
top_k=top_k,
|
|
126
137
|
) as span:
|
|
127
138
|
try:
|
|
128
139
|
scores = score_pairs(query, contents)
|
|
129
140
|
except CrossEncoderUnavailableError:
|
|
130
141
|
span.set_data("available", False)
|
|
131
142
|
logger.warning(
|
|
132
|
-
"event=relevance_floor_unavailable arms=%s items=%d (returning unfiltered
|
|
143
|
+
"event=relevance_floor_unavailable arms=%s items=%d (returning unfiltered pool)",
|
|
133
144
|
arm_names,
|
|
134
145
|
len(contents),
|
|
135
146
|
)
|
|
136
|
-
return [items
|
|
147
|
+
return [RelevanceFloorResult(list(items), None) for _, items, _ in arms]
|
|
137
148
|
span.set_data("available", True)
|
|
138
149
|
|
|
139
|
-
results: list[
|
|
150
|
+
results: list[RelevanceFloorResult] = []
|
|
140
151
|
offset = 0
|
|
141
152
|
for name, items, floor in arms:
|
|
142
153
|
arm_scores = scores[offset : offset + len(items)]
|
|
@@ -153,5 +164,10 @@ def apply_relevance_floors(
|
|
|
153
164
|
dropped,
|
|
154
165
|
floor,
|
|
155
166
|
)
|
|
156
|
-
results.append(
|
|
167
|
+
results.append(
|
|
168
|
+
RelevanceFloorResult(
|
|
169
|
+
[item for item, _score in survivors],
|
|
170
|
+
[score for _item, score in survivors],
|
|
171
|
+
)
|
|
172
|
+
)
|
|
157
173
|
return results
|
|
@@ -14,6 +14,10 @@ class StorageError(Exception):
|
|
|
14
14
|
return f"StorageError: {self.message}"
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class SubjectWriteBarrierError(StorageError):
|
|
18
|
+
"""Raised when a write targets a subject with an active erasure barrier."""
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
def require_non_empty_session_id(value: Any) -> str:
|
|
18
22
|
"""Return a stripped, non-empty request ``session_id`` or raise ``StorageError``.
|
|
19
23
|
|