claude-smart 0.2.46 → 0.2.47
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/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/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +1 -1
- package/plugin/vendor/reflexio/reflexio/README.md +3 -1
- 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/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -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/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 +16 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
- 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/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -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/services/README.md +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- 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 +45 -19
- 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 +129 -111
- 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/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 +14 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
- 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 +1965 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
- 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 +262 -107
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- 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 +952 -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 +838 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -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/unified_search_service.py +153 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
from reflexio.server.extensions import ServiceKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AggregationPromptProcessingContext:
|
|
11
|
+
"""Per-cluster state shared across aggregation prompt-processing hooks.
|
|
12
|
+
|
|
13
|
+
``changed`` controls whether contextual prompt guidance is requested.
|
|
14
|
+
``data`` is an opaque deployment-owned mapping. The aggregator initializes
|
|
15
|
+
``agent_version`` and ``org_id`` keys when those values are available.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
changed: bool = False
|
|
19
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class PromptPreprocessResult:
|
|
24
|
+
text: str
|
|
25
|
+
changed: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class PromptPostprocessResult:
|
|
30
|
+
value: object
|
|
31
|
+
artifacts_removed: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AggregationPromptProcessor(Protocol):
|
|
35
|
+
"""Optional deployment hook for the playbook aggregation boundary.
|
|
36
|
+
|
|
37
|
+
Implementations may transform prompt text before the aggregation LLM call,
|
|
38
|
+
provide contextual prompt instructions when preprocessing changed prompt
|
|
39
|
+
input, and post-process aggregation output before storage or response
|
|
40
|
+
logging. Deployments supply one by registering it via the
|
|
41
|
+
``AGGREGATION_PROMPT_PROCESSOR`` ServiceKey (``register_service``).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def preprocess_prompt_text(
|
|
45
|
+
self,
|
|
46
|
+
text: str,
|
|
47
|
+
*,
|
|
48
|
+
shared_state: dict[str, Any] | None = None,
|
|
49
|
+
context: AggregationPromptProcessingContext | None = None,
|
|
50
|
+
) -> PromptPreprocessResult:
|
|
51
|
+
"""Return text to send to the aggregation prompt."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def prompt_instructions(
|
|
55
|
+
self,
|
|
56
|
+
context: AggregationPromptProcessingContext,
|
|
57
|
+
) -> str | None:
|
|
58
|
+
"""Return extra prompt guidance for this aggregation context, if any."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def postprocess_aggregation_output(
|
|
62
|
+
self,
|
|
63
|
+
value: object,
|
|
64
|
+
*,
|
|
65
|
+
context: AggregationPromptProcessingContext | None = None,
|
|
66
|
+
) -> PromptPostprocessResult:
|
|
67
|
+
"""Return output to store or log after the aggregation LLM call."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PassthroughPromptProcessor:
|
|
72
|
+
def preprocess_prompt_text(
|
|
73
|
+
self,
|
|
74
|
+
text: str,
|
|
75
|
+
*,
|
|
76
|
+
shared_state: dict[str, Any] | None = None, # noqa: ARG002
|
|
77
|
+
context: AggregationPromptProcessingContext | None = None, # noqa: ARG002
|
|
78
|
+
) -> PromptPreprocessResult:
|
|
79
|
+
return PromptPreprocessResult(text=text)
|
|
80
|
+
|
|
81
|
+
def prompt_instructions(
|
|
82
|
+
self,
|
|
83
|
+
context: AggregationPromptProcessingContext, # noqa: ARG002
|
|
84
|
+
) -> str | None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def postprocess_aggregation_output(
|
|
88
|
+
self,
|
|
89
|
+
value: object,
|
|
90
|
+
*,
|
|
91
|
+
context: AggregationPromptProcessingContext | None = None, # noqa: ARG002
|
|
92
|
+
) -> PromptPostprocessResult:
|
|
93
|
+
return PromptPostprocessResult(value=value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Singleton runtime-service key (round-5 RD-NEC-002): one processor registered at
|
|
97
|
+
# the enterprise composition root; None in local OSS (passthrough, no redaction).
|
|
98
|
+
AGGREGATION_PROMPT_PROCESSOR: ServiceKey[AggregationPromptProcessor] = ServiceKey(
|
|
99
|
+
"aggregation_prompt_processor"
|
|
100
|
+
)
|
|
@@ -24,6 +24,10 @@ from reflexio.models.config_schema import (
|
|
|
24
24
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
25
25
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
26
26
|
from reflexio.server.services.operation_state_utils import OperationStateManager
|
|
27
|
+
from reflexio.server.services.playbook.aggregation_prompt_processing import (
|
|
28
|
+
AggregationPromptProcessingContext,
|
|
29
|
+
AggregationPromptProcessor,
|
|
30
|
+
)
|
|
27
31
|
from reflexio.server.services.playbook.playbook_service_constants import (
|
|
28
32
|
PlaybookServiceConstants,
|
|
29
33
|
)
|
|
@@ -33,7 +37,6 @@ from reflexio.server.services.playbook.playbook_service_utils import (
|
|
|
33
37
|
StructuredPlaybookContent,
|
|
34
38
|
ensure_playbook_content,
|
|
35
39
|
)
|
|
36
|
-
from reflexio.server.services.playbook.user_detail_stripping import UserDetailStripper
|
|
37
40
|
from reflexio.server.services.service_utils import log_model_response
|
|
38
41
|
from reflexio.server.tracing import capture_anomaly, sentry_tags
|
|
39
42
|
from reflexio.server.usage_metrics import record_usage_event
|
|
@@ -52,22 +55,14 @@ class PlaybookAggregator:
|
|
|
52
55
|
llm_client: LiteLLMClient,
|
|
53
56
|
request_context: RequestContext,
|
|
54
57
|
agent_version: str,
|
|
55
|
-
|
|
58
|
+
aggregation_prompt_processor: AggregationPromptProcessor | None = None,
|
|
56
59
|
) -> None:
|
|
57
60
|
self.client = llm_client
|
|
58
61
|
self.storage = request_context.storage
|
|
59
62
|
self.configurator = request_context.configurator
|
|
60
63
|
self.request_context = request_context
|
|
61
64
|
self.agent_version = agent_version
|
|
62
|
-
self.
|
|
63
|
-
prompt_extra_instructions = getattr(
|
|
64
|
-
user_detail_stripper, "prompt_extra_instructions", None
|
|
65
|
-
)
|
|
66
|
-
if not isinstance(prompt_extra_instructions, str):
|
|
67
|
-
prompt_extra_instructions = None
|
|
68
|
-
self.aggregation_prompt_extra_instructions = (
|
|
69
|
-
self._format_prompt_extra_instructions(prompt_extra_instructions)
|
|
70
|
-
)
|
|
65
|
+
self.aggregation_prompt_processor = aggregation_prompt_processor
|
|
71
66
|
|
|
72
67
|
# ===============================
|
|
73
68
|
# private methods - operation state
|
|
@@ -210,106 +205,96 @@ class PlaybookAggregator:
|
|
|
210
205
|
blocks.append("\n".join(lines))
|
|
211
206
|
return "\n\n".join(blocks) if blocks else "(No playbook items)"
|
|
212
207
|
|
|
213
|
-
def
|
|
208
|
+
def _preprocess_prompt_field(
|
|
214
209
|
self,
|
|
215
210
|
text: str | None,
|
|
216
|
-
|
|
211
|
+
shared_state: dict[str, object],
|
|
212
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
217
213
|
) -> str | None:
|
|
218
|
-
if text is None or self.
|
|
214
|
+
if text is None or self.aggregation_prompt_processor is None:
|
|
219
215
|
return text
|
|
220
|
-
|
|
221
|
-
text,
|
|
222
|
-
|
|
216
|
+
result = self.aggregation_prompt_processor.preprocess_prompt_text(
|
|
217
|
+
text,
|
|
218
|
+
shared_state=shared_state,
|
|
219
|
+
context=processing_context,
|
|
220
|
+
)
|
|
221
|
+
if processing_context is not None and (result.changed or result.text != text):
|
|
222
|
+
processing_context.changed = True
|
|
223
|
+
return result.text
|
|
223
224
|
|
|
224
|
-
def
|
|
225
|
+
def _preprocess_user_playbook_for_prompt(
|
|
225
226
|
self,
|
|
226
227
|
playbook: UserPlaybook,
|
|
227
|
-
|
|
228
|
+
shared_state: dict[str, object],
|
|
229
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
228
230
|
) -> UserPlaybook:
|
|
229
|
-
if self.
|
|
231
|
+
if self.aggregation_prompt_processor is None:
|
|
230
232
|
return playbook
|
|
231
233
|
return playbook.model_copy(
|
|
232
234
|
update={
|
|
233
|
-
"content": self.
|
|
235
|
+
"content": self._preprocess_prompt_field(
|
|
236
|
+
playbook.content, shared_state, processing_context
|
|
237
|
+
)
|
|
234
238
|
or "",
|
|
235
|
-
"trigger": self.
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
"trigger": self._preprocess_prompt_field(
|
|
240
|
+
playbook.trigger, shared_state, processing_context
|
|
241
|
+
),
|
|
242
|
+
"rationale": self._preprocess_prompt_field(
|
|
243
|
+
playbook.rationale, shared_state, processing_context
|
|
238
244
|
),
|
|
239
245
|
}
|
|
240
246
|
)
|
|
241
247
|
|
|
242
|
-
def
|
|
248
|
+
def _aggregation_prompt_extra_instructions_for_context(
|
|
243
249
|
self,
|
|
244
|
-
|
|
245
|
-
) ->
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
processing_context: AggregationPromptProcessingContext | None,
|
|
251
|
+
) -> str:
|
|
252
|
+
if (
|
|
253
|
+
processing_context is None
|
|
254
|
+
or not processing_context.changed
|
|
255
|
+
or self.aggregation_prompt_processor is None
|
|
256
|
+
):
|
|
257
|
+
return ""
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
context_instructions = self.aggregation_prompt_processor.prompt_instructions(
|
|
260
|
+
processing_context
|
|
261
|
+
)
|
|
262
|
+
if not isinstance(context_instructions, str):
|
|
263
|
+
context_instructions = None
|
|
264
|
+
return self._format_prompt_extra_instructions(context_instructions)
|
|
256
265
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
def _postprocess_aggregation_output(
|
|
267
|
+
self,
|
|
268
|
+
value: object,
|
|
269
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
270
|
+
) -> tuple[object, int]:
|
|
271
|
+
if self.aggregation_prompt_processor is None:
|
|
272
|
+
return value, 0
|
|
273
|
+
result = self.aggregation_prompt_processor.postprocess_aggregation_output(
|
|
274
|
+
value,
|
|
275
|
+
context=processing_context,
|
|
276
|
+
)
|
|
277
|
+
return result.value, result.artifacts_removed
|
|
266
278
|
|
|
267
|
-
|
|
279
|
+
def _postprocess_aggregation_response(
|
|
280
|
+
self,
|
|
281
|
+
response: PlaybookAggregationOutput,
|
|
282
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
283
|
+
) -> tuple[PlaybookAggregationOutput, int]:
|
|
284
|
+
processed, artifact_count = self._postprocess_aggregation_output(
|
|
285
|
+
response,
|
|
286
|
+
processing_context,
|
|
287
|
+
)
|
|
288
|
+
if not isinstance(processed, PlaybookAggregationOutput):
|
|
268
289
|
return response, 0
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def _sanitize_aggregation_log_value(self, value: object) -> tuple[object, int]:
|
|
274
|
-
if isinstance(value, str):
|
|
275
|
-
return self._sanitize_aggregation_output_text(value)
|
|
276
|
-
|
|
277
|
-
if isinstance(value, dict):
|
|
278
|
-
sanitized: dict[object, object] = {}
|
|
279
|
-
placeholder_count = 0
|
|
280
|
-
for key, item in value.items():
|
|
281
|
-
sanitized_key, key_count = self._sanitize_aggregation_log_value(key)
|
|
282
|
-
sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
|
|
283
|
-
sanitized[sanitized_key] = sanitized_item
|
|
284
|
-
placeholder_count += key_count + item_count
|
|
285
|
-
return sanitized, placeholder_count
|
|
286
|
-
|
|
287
|
-
if isinstance(value, list):
|
|
288
|
-
sanitized_items: list[object] = []
|
|
289
|
-
placeholder_count = 0
|
|
290
|
-
for item in value:
|
|
291
|
-
sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
|
|
292
|
-
sanitized_items.append(sanitized_item)
|
|
293
|
-
placeholder_count += item_count
|
|
294
|
-
return sanitized_items, placeholder_count
|
|
295
|
-
|
|
296
|
-
if isinstance(value, tuple):
|
|
297
|
-
sanitized_items: list[object] = []
|
|
298
|
-
placeholder_count = 0
|
|
299
|
-
for item in value:
|
|
300
|
-
sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
|
|
301
|
-
sanitized_items.append(sanitized_item)
|
|
302
|
-
placeholder_count += item_count
|
|
303
|
-
return tuple(sanitized_items), placeholder_count
|
|
304
|
-
|
|
305
|
-
return value, 0
|
|
306
|
-
|
|
307
|
-
def _record_placeholder_leakage(self, placeholder_count: int) -> None:
|
|
308
|
-
if placeholder_count <= 0:
|
|
290
|
+
return processed, artifact_count
|
|
291
|
+
|
|
292
|
+
def _record_postprocessing_artifacts(self, artifact_count: int) -> None:
|
|
293
|
+
if artifact_count <= 0:
|
|
309
294
|
return
|
|
310
295
|
logger.warning(
|
|
311
|
-
"
|
|
312
|
-
|
|
296
|
+
"Post-processed %d residual artifacts in aggregated playbook output",
|
|
297
|
+
artifact_count,
|
|
313
298
|
)
|
|
314
299
|
|
|
315
300
|
@staticmethod
|
|
@@ -692,9 +677,9 @@ class PlaybookAggregator:
|
|
|
692
677
|
self.agent_version,
|
|
693
678
|
)
|
|
694
679
|
logger.info(
|
|
695
|
-
"
|
|
696
|
-
type(self.
|
|
697
|
-
if self.
|
|
680
|
+
"Aggregation prompt processor: %s",
|
|
681
|
+
type(self.aggregation_prompt_processor).__name__
|
|
682
|
+
if self.aggregation_prompt_processor is not None
|
|
698
683
|
else "disabled",
|
|
699
684
|
)
|
|
700
685
|
|
|
@@ -979,6 +964,22 @@ class PlaybookAggregator:
|
|
|
979
964
|
# Remove archived playbooks after successful aggregation. ALWAYS soft-supersede
|
|
980
965
|
# (never hard-delete) so the removal is reconstructable from lineage — mirrors the
|
|
981
966
|
# profile dedup always-soft path (#206).
|
|
967
|
+
archived_ids_without_overlapping_changed_cluster: set[int] = set()
|
|
968
|
+
if new_playbooks and prev_fingerprints:
|
|
969
|
+
archived_id_set = set(archived_playbook_ids)
|
|
970
|
+
for prev_fp, fp_data in prev_fingerprints.items():
|
|
971
|
+
playbook_id = fp_data.get("agent_playbook_id")
|
|
972
|
+
if (
|
|
973
|
+
playbook_id in archived_id_set
|
|
974
|
+
and prev_fp not in changed_fps_by_previous_fp
|
|
975
|
+
):
|
|
976
|
+
archived_ids_without_overlapping_changed_cluster.add(
|
|
977
|
+
playbook_id
|
|
978
|
+
)
|
|
979
|
+
ids_to_supersede = {
|
|
980
|
+
*selective_supersede_playbook_ids,
|
|
981
|
+
*archived_ids_without_overlapping_changed_cluster,
|
|
982
|
+
}
|
|
982
983
|
if not _run_id:
|
|
983
984
|
# Empty request_id makes the removal unreconstructable (lineage events are keyed
|
|
984
985
|
# on it). Fail loud and skip removal — never silently hard-delete.
|
|
@@ -996,9 +997,9 @@ class PlaybookAggregator:
|
|
|
996
997
|
agent_version=self.agent_version,
|
|
997
998
|
request_id=_run_id,
|
|
998
999
|
)
|
|
999
|
-
elif
|
|
1000
|
+
elif ids_to_supersede:
|
|
1000
1001
|
self.storage.supersede_agent_playbooks_by_ids( # type: ignore[reportOptionalMemberAccess]
|
|
1001
|
-
sorted(
|
|
1002
|
+
sorted(ids_to_supersede),
|
|
1002
1003
|
request_id=_run_id,
|
|
1003
1004
|
)
|
|
1004
1005
|
elif archived_playbook_ids:
|
|
@@ -1309,12 +1310,20 @@ class PlaybookAggregator:
|
|
|
1309
1310
|
else "None"
|
|
1310
1311
|
)
|
|
1311
1312
|
for cluster_playbooks in clusters.values():
|
|
1312
|
-
|
|
1313
|
-
|
|
1313
|
+
shared_state: dict[str, object] = {}
|
|
1314
|
+
processing_context = AggregationPromptProcessingContext(
|
|
1315
|
+
data={
|
|
1316
|
+
"agent_version": self.agent_version,
|
|
1317
|
+
"org_id": self.request_context.org_id,
|
|
1318
|
+
}
|
|
1319
|
+
)
|
|
1320
|
+
if self.aggregation_prompt_processor is None:
|
|
1314
1321
|
prompt_cluster_playbooks = cluster_playbooks
|
|
1315
1322
|
else:
|
|
1316
1323
|
prompt_cluster_playbooks = [
|
|
1317
|
-
self.
|
|
1324
|
+
self._preprocess_user_playbook_for_prompt(
|
|
1325
|
+
playbook, shared_state, processing_context
|
|
1326
|
+
)
|
|
1318
1327
|
for playbook in cluster_playbooks
|
|
1319
1328
|
]
|
|
1320
1329
|
|
|
@@ -1322,6 +1331,7 @@ class PlaybookAggregator:
|
|
|
1322
1331
|
prompt_cluster_playbooks,
|
|
1323
1332
|
approved_playbooks_str,
|
|
1324
1333
|
direction_overlap_threshold=direction_overlap_threshold,
|
|
1334
|
+
processing_context=processing_context,
|
|
1325
1335
|
)
|
|
1326
1336
|
if playbook is not None:
|
|
1327
1337
|
new_playbooks.append((playbook, cluster_playbooks))
|
|
@@ -1371,6 +1381,7 @@ class PlaybookAggregator:
|
|
|
1371
1381
|
cluster_playbooks: list[UserPlaybook],
|
|
1372
1382
|
existing_approved_playbooks_str: str,
|
|
1373
1383
|
direction_overlap_threshold: float = 0.6,
|
|
1384
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
1374
1385
|
) -> AgentPlaybook | None:
|
|
1375
1386
|
"""
|
|
1376
1387
|
Generate a playbook from a cluster using structured JSON output.
|
|
@@ -1407,8 +1418,11 @@ class PlaybookAggregator:
|
|
|
1407
1418
|
trigger=trigger,
|
|
1408
1419
|
)
|
|
1409
1420
|
)
|
|
1410
|
-
response,
|
|
1411
|
-
|
|
1421
|
+
response, artifact_count = self._postprocess_aggregation_response(
|
|
1422
|
+
response,
|
|
1423
|
+
processing_context,
|
|
1424
|
+
)
|
|
1425
|
+
self._record_postprocessing_artifacts(artifact_count)
|
|
1412
1426
|
playbook = self._process_aggregation_response(response, cluster_playbooks)
|
|
1413
1427
|
if playbook is None:
|
|
1414
1428
|
return None
|
|
@@ -1428,7 +1442,9 @@ class PlaybookAggregator:
|
|
|
1428
1442
|
{
|
|
1429
1443
|
"user_playbooks": raw_playbooks_str,
|
|
1430
1444
|
"existing_approved_playbooks": existing_approved_playbooks_str,
|
|
1431
|
-
"aggregation_prompt_extra_instructions": self.
|
|
1445
|
+
"aggregation_prompt_extra_instructions": self._aggregation_prompt_extra_instructions_for_context(
|
|
1446
|
+
processing_context
|
|
1447
|
+
),
|
|
1432
1448
|
},
|
|
1433
1449
|
),
|
|
1434
1450
|
}
|
|
@@ -1442,15 +1458,17 @@ class PlaybookAggregator:
|
|
|
1442
1458
|
parse_structured_output=True,
|
|
1443
1459
|
)
|
|
1444
1460
|
if isinstance(response, PlaybookAggregationOutput):
|
|
1445
|
-
response,
|
|
1446
|
-
response
|
|
1461
|
+
response, artifact_count = self._postprocess_aggregation_response(
|
|
1462
|
+
response,
|
|
1463
|
+
processing_context,
|
|
1447
1464
|
)
|
|
1448
|
-
self.
|
|
1465
|
+
self._record_postprocessing_artifacts(artifact_count)
|
|
1449
1466
|
else:
|
|
1450
|
-
response,
|
|
1451
|
-
response
|
|
1467
|
+
response, artifact_count = self._postprocess_aggregation_output(
|
|
1468
|
+
response,
|
|
1469
|
+
processing_context,
|
|
1452
1470
|
)
|
|
1453
|
-
self.
|
|
1471
|
+
self._record_postprocessing_artifacts(artifact_count)
|
|
1454
1472
|
log_model_response(logger, "Aggregation structured response", response)
|
|
1455
1473
|
|
|
1456
1474
|
if not isinstance(response, PlaybookAggregationOutput):
|
|
@@ -1462,13 +1480,14 @@ class PlaybookAggregator:
|
|
|
1462
1480
|
|
|
1463
1481
|
return self._process_aggregation_response(response, cluster_playbooks)
|
|
1464
1482
|
except Exception as exc:
|
|
1465
|
-
|
|
1466
|
-
str(exc)
|
|
1483
|
+
processed_error, artifact_count = self._postprocess_aggregation_output(
|
|
1484
|
+
str(exc),
|
|
1485
|
+
processing_context,
|
|
1467
1486
|
)
|
|
1468
|
-
self.
|
|
1487
|
+
self._record_postprocessing_artifacts(artifact_count)
|
|
1469
1488
|
logger.error(
|
|
1470
1489
|
"AgentPlaybook aggregation failed due to %s, returning None.",
|
|
1471
|
-
|
|
1490
|
+
processed_error,
|
|
1472
1491
|
)
|
|
1473
1492
|
return None
|
|
1474
1493
|
|
|
@@ -1482,7 +1501,7 @@ class PlaybookAggregator:
|
|
|
1482
1501
|
response: Parsed PlaybookAggregationOutput from LLM
|
|
1483
1502
|
cluster_playbooks: Cluster playbooks used only for non-user metadata
|
|
1484
1503
|
such as playbook name and agent version. Callers may pass
|
|
1485
|
-
prompt-
|
|
1504
|
+
prompt-preprocessed copies here, so this method must not read
|
|
1486
1505
|
user-authored fields from them.
|
|
1487
1506
|
|
|
1488
1507
|
Returns:
|
|
@@ -1490,7 +1509,6 @@ class PlaybookAggregator:
|
|
|
1490
1509
|
"""
|
|
1491
1510
|
if not response:
|
|
1492
1511
|
return None
|
|
1493
|
-
response, _placeholder_count = self._sanitize_aggregation_response(response)
|
|
1494
1512
|
|
|
1495
1513
|
structured = response.playbook
|
|
1496
1514
|
if structured is None:
|
|
@@ -22,11 +22,15 @@ from reflexio.models.api_schema.service_schemas import (
|
|
|
22
22
|
UserPlaybook,
|
|
23
23
|
)
|
|
24
24
|
from reflexio.models.config_schema import PlaybookConfig
|
|
25
|
+
from reflexio.server.extensions import get_service
|
|
25
26
|
from reflexio.server.operation_limiter import run_with_operation_limit
|
|
26
27
|
from reflexio.server.services.base_generation_service import (
|
|
27
28
|
BaseGenerationService,
|
|
28
29
|
StatusChangeOperation,
|
|
29
30
|
)
|
|
31
|
+
from reflexio.server.services.playbook.aggregation_prompt_processing import (
|
|
32
|
+
AGGREGATION_PROMPT_PROCESSOR,
|
|
33
|
+
)
|
|
30
34
|
from reflexio.server.services.playbook.components.aggregator import PlaybookAggregator
|
|
31
35
|
from reflexio.server.services.playbook.components.extractor import PlaybookExtractor
|
|
32
36
|
from reflexio.server.services.playbook.playbook_service_constants import (
|
|
@@ -537,16 +541,12 @@ class PlaybookGenerationService(
|
|
|
537
541
|
)
|
|
538
542
|
|
|
539
543
|
# Initialize and run aggregator (synchronous)
|
|
540
|
-
|
|
541
|
-
create_aggregation_user_detail_stripper,
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
user_detail_stripper = create_aggregation_user_detail_stripper(
|
|
545
|
-
self.request_context.configurator
|
|
546
|
-
)
|
|
544
|
+
aggregation_prompt_processor = get_service(AGGREGATION_PROMPT_PROCESSOR)
|
|
547
545
|
aggregator_kwargs = {}
|
|
548
|
-
if
|
|
549
|
-
aggregator_kwargs["
|
|
546
|
+
if aggregation_prompt_processor is not None:
|
|
547
|
+
aggregator_kwargs["aggregation_prompt_processor"] = (
|
|
548
|
+
aggregation_prompt_processor
|
|
549
|
+
)
|
|
550
550
|
aggregator = PlaybookAggregator(
|
|
551
551
|
llm_client=self.client,
|
|
552
552
|
request_context=self.request_context,
|
|
@@ -279,7 +279,7 @@ class ProfileExtractor:
|
|
|
279
279
|
raw_profiles: list[dict],
|
|
280
280
|
user_id: str,
|
|
281
281
|
request_id: str,
|
|
282
|
-
source_interaction_ids: list[int],
|
|
282
|
+
source_interaction_ids: list[int] | None = None,
|
|
283
283
|
) -> list[UserProfile]:
|
|
284
284
|
"""
|
|
285
285
|
Convert raw profile dicts from LLM to UserProfile objects.
|
|
@@ -294,6 +294,7 @@ class ProfileExtractor:
|
|
|
294
294
|
List of UserProfile objects
|
|
295
295
|
"""
|
|
296
296
|
new_profiles = []
|
|
297
|
+
profile_source_interaction_ids = list(source_interaction_ids or [])
|
|
297
298
|
for profile_content in raw_profiles:
|
|
298
299
|
if (
|
|
299
300
|
not isinstance(profile_content, dict)
|
|
@@ -322,7 +323,7 @@ class ProfileExtractor:
|
|
|
322
323
|
expiration_timestamp=calculate_expiration_timestamp(now_ts, ttl),
|
|
323
324
|
custom_features=custom_features or None,
|
|
324
325
|
extractor_names=None,
|
|
325
|
-
source_interaction_ids=
|
|
326
|
+
source_interaction_ids=profile_source_interaction_ids,
|
|
326
327
|
)
|
|
327
328
|
|
|
328
329
|
new_profiles.append(added_profile)
|