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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import hashlib
|
|
4
3
|
import logging
|
|
5
4
|
import os
|
|
6
5
|
import time
|
|
@@ -24,6 +23,20 @@ from reflexio.models.config_schema import (
|
|
|
24
23
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
25
24
|
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
26
25
|
from reflexio.server.services.operation_state_utils import OperationStateManager
|
|
26
|
+
from reflexio.server.services.playbook.aggregation_prompt_processing import (
|
|
27
|
+
AggregationPromptProcessingContext,
|
|
28
|
+
AggregationPromptProcessor,
|
|
29
|
+
)
|
|
30
|
+
from reflexio.server.services.playbook.components import (
|
|
31
|
+
aggregator_clustering,
|
|
32
|
+
aggregator_prompt_formatting,
|
|
33
|
+
)
|
|
34
|
+
from reflexio.server.services.playbook.components.aggregator_clustering import (
|
|
35
|
+
CLUSTERING_ALGORITHM_THRESHOLD,
|
|
36
|
+
)
|
|
37
|
+
from reflexio.server.services.playbook.components.aggregator_postprocessing import (
|
|
38
|
+
AggregationPostProcessing,
|
|
39
|
+
)
|
|
27
40
|
from reflexio.server.services.playbook.playbook_service_constants import (
|
|
28
41
|
PlaybookServiceConstants,
|
|
29
42
|
)
|
|
@@ -33,18 +46,12 @@ from reflexio.server.services.playbook.playbook_service_utils import (
|
|
|
33
46
|
StructuredPlaybookContent,
|
|
34
47
|
ensure_playbook_content,
|
|
35
48
|
)
|
|
36
|
-
from reflexio.server.services.playbook.user_detail_stripping import UserDetailStripper
|
|
37
49
|
from reflexio.server.services.service_utils import log_model_response
|
|
38
50
|
from reflexio.server.tracing import capture_anomaly, sentry_tags
|
|
39
51
|
from reflexio.server.usage_metrics import record_usage_event
|
|
40
52
|
|
|
41
53
|
logger = logging.getLogger(__name__)
|
|
42
54
|
|
|
43
|
-
# Threshold for switching between clustering algorithms
|
|
44
|
-
# Below this, use Agglomerative (works better with small datasets)
|
|
45
|
-
# Above this, use HDBSCAN (scales better, handles noise)
|
|
46
|
-
CLUSTERING_ALGORITHM_THRESHOLD = 50
|
|
47
|
-
|
|
48
55
|
|
|
49
56
|
class PlaybookAggregator:
|
|
50
57
|
def __init__(
|
|
@@ -52,33 +59,23 @@ class PlaybookAggregator:
|
|
|
52
59
|
llm_client: LiteLLMClient,
|
|
53
60
|
request_context: RequestContext,
|
|
54
61
|
agent_version: str,
|
|
55
|
-
|
|
62
|
+
aggregation_prompt_processor: AggregationPromptProcessor | None = None,
|
|
56
63
|
) -> None:
|
|
57
64
|
self.client = llm_client
|
|
58
65
|
self.storage = request_context.storage
|
|
59
66
|
self.configurator = request_context.configurator
|
|
60
67
|
self.request_context = request_context
|
|
61
68
|
self.agent_version = agent_version
|
|
62
|
-
self.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
prompt_extra_instructions = None
|
|
68
|
-
self.aggregation_prompt_extra_instructions = (
|
|
69
|
-
self._format_prompt_extra_instructions(prompt_extra_instructions)
|
|
70
|
-
)
|
|
69
|
+
self.aggregation_prompt_processor = aggregation_prompt_processor
|
|
70
|
+
# Cohesive pre/post-processing component (the enterprise redaction
|
|
71
|
+
# Protocol seam). Constructed from the SAME injected instance stored
|
|
72
|
+
# above — do NOT re-resolve the AGGREGATION_PROMPT_PROCESSOR ServiceKey.
|
|
73
|
+
self._postproc = AggregationPostProcessing(aggregation_prompt_processor)
|
|
71
74
|
|
|
72
75
|
# ===============================
|
|
73
76
|
# private methods - operation state
|
|
74
77
|
# ===============================
|
|
75
78
|
|
|
76
|
-
@staticmethod
|
|
77
|
-
def _format_prompt_extra_instructions(instructions: str | None) -> str:
|
|
78
|
-
if not instructions or not instructions.strip():
|
|
79
|
-
return ""
|
|
80
|
-
return f"{instructions.strip()}\n"
|
|
81
|
-
|
|
82
79
|
def _create_state_manager(self) -> OperationStateManager:
|
|
83
80
|
"""
|
|
84
81
|
Create an OperationStateManager for the playbook aggregator.
|
|
@@ -185,403 +182,74 @@ class PlaybookAggregator:
|
|
|
185
182
|
last_processed_id=max_id,
|
|
186
183
|
)
|
|
187
184
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
Args:
|
|
196
|
-
cluster_playbooks: List of raw playbooks in this cluster
|
|
185
|
+
# ===============================
|
|
186
|
+
# private methods - aggregation pre/post-processing
|
|
187
|
+
# ===============================
|
|
188
|
+
# Bodies live on the AggregationPostProcessing component (self._postproc).
|
|
189
|
+
# These thin delegators are kept for OSS test call-sites that invoke them by
|
|
190
|
+
# name (test_playbook_aggregator.py). Internal callers use self._postproc.
|
|
197
191
|
|
|
198
|
-
|
|
199
|
-
str: Formatted input for the aggregation prompt
|
|
200
|
-
"""
|
|
201
|
-
blocks = []
|
|
202
|
-
for idx, fb in enumerate(cluster_playbooks, 1):
|
|
203
|
-
lines = [f"[{idx}]"]
|
|
204
|
-
if fb.content:
|
|
205
|
-
lines.append(f'Content: "{fb.content}"')
|
|
206
|
-
if fb.trigger:
|
|
207
|
-
lines.append(f'Trigger: "{fb.trigger}"')
|
|
208
|
-
if fb.rationale:
|
|
209
|
-
lines.append(f'Rationale: "{fb.rationale}"')
|
|
210
|
-
blocks.append("\n".join(lines))
|
|
211
|
-
return "\n\n".join(blocks) if blocks else "(No playbook items)"
|
|
212
|
-
|
|
213
|
-
def _strip_prompt_field(
|
|
192
|
+
def _postprocess_aggregation_output(
|
|
214
193
|
self,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
) ->
|
|
218
|
-
|
|
219
|
-
return text
|
|
220
|
-
return self.user_detail_stripper.strip_user_details(
|
|
221
|
-
text, shared_mapping=shared_mapping
|
|
222
|
-
).text
|
|
223
|
-
|
|
224
|
-
def _strip_user_playbook_for_prompt(
|
|
225
|
-
self,
|
|
226
|
-
playbook: UserPlaybook,
|
|
227
|
-
shared_mapping: dict[str, int],
|
|
228
|
-
) -> UserPlaybook:
|
|
229
|
-
if self.user_detail_stripper is None:
|
|
230
|
-
return playbook
|
|
231
|
-
return playbook.model_copy(
|
|
232
|
-
update={
|
|
233
|
-
"content": self._strip_prompt_field(playbook.content, shared_mapping)
|
|
234
|
-
or "",
|
|
235
|
-
"trigger": self._strip_prompt_field(playbook.trigger, shared_mapping),
|
|
236
|
-
"rationale": self._strip_prompt_field(
|
|
237
|
-
playbook.rationale, shared_mapping
|
|
238
|
-
),
|
|
239
|
-
}
|
|
240
|
-
)
|
|
194
|
+
value: object,
|
|
195
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
196
|
+
) -> tuple[object, int]:
|
|
197
|
+
return self._postproc._postprocess_aggregation_output(value, processing_context)
|
|
241
198
|
|
|
242
|
-
def
|
|
199
|
+
def _aggregation_prompt_extra_instructions_for_context(
|
|
243
200
|
self,
|
|
244
|
-
|
|
245
|
-
) ->
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return self.user_detail_stripper.sanitize_aggregation_output_text(text)
|
|
249
|
-
|
|
250
|
-
def _sanitize_aggregation_response(
|
|
251
|
-
self, response: PlaybookAggregationOutput
|
|
252
|
-
) -> tuple[PlaybookAggregationOutput, int]:
|
|
253
|
-
structured = response.playbook
|
|
254
|
-
if structured is None:
|
|
255
|
-
return response, 0
|
|
256
|
-
|
|
257
|
-
updates: dict[str, str | None] = {}
|
|
258
|
-
placeholder_count = 0
|
|
259
|
-
for field_name, value in structured.model_dump().items():
|
|
260
|
-
if not isinstance(value, str):
|
|
261
|
-
continue
|
|
262
|
-
sanitized, field_count = self._sanitize_aggregation_output_text(value)
|
|
263
|
-
placeholder_count += field_count
|
|
264
|
-
if sanitized != value:
|
|
265
|
-
updates[field_name] = sanitized
|
|
266
|
-
|
|
267
|
-
if not updates:
|
|
268
|
-
return response, 0
|
|
269
|
-
return response.model_copy(
|
|
270
|
-
update={"playbook": structured.model_copy(update=updates)}
|
|
271
|
-
), placeholder_count
|
|
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:
|
|
309
|
-
return
|
|
310
|
-
logger.warning(
|
|
311
|
-
"Replaced %d residual user-detail placeholders in aggregated playbook output",
|
|
312
|
-
placeholder_count,
|
|
201
|
+
processing_context: AggregationPromptProcessingContext | None,
|
|
202
|
+
) -> str:
|
|
203
|
+
return self._postproc._aggregation_prompt_extra_instructions_for_context(
|
|
204
|
+
processing_context
|
|
313
205
|
)
|
|
314
206
|
|
|
207
|
+
def _record_postprocessing_artifacts(self, artifact_count: int) -> None:
|
|
208
|
+
self._postproc._record_postprocessing_artifacts(artifact_count)
|
|
209
|
+
|
|
315
210
|
@staticmethod
|
|
316
211
|
def _get_direction_key(fb: UserPlaybook) -> str:
|
|
317
|
-
|
|
318
|
-
Extract a similarity key from a user playbook for grouping.
|
|
319
|
-
|
|
320
|
-
Returns the raw content used for token-overlap comparison. Grouping is
|
|
321
|
-
purely content-similarity based: under Option B a skill may legitimately
|
|
322
|
-
hold mixed-orientation rules (do-rules and avoid-rules for different
|
|
323
|
-
sub-aspects of one task), so whole-content polarity is NOT derived or
|
|
324
|
-
gated here. Preserving distinct do/avoid rules when similar items are
|
|
325
|
-
merged is the aggregation prompt's responsibility.
|
|
326
|
-
|
|
327
|
-
Args:
|
|
328
|
-
fb: A user playbook item
|
|
329
|
-
|
|
330
|
-
Returns:
|
|
331
|
-
str: Content used as the similarity key for grouping
|
|
332
|
-
"""
|
|
333
|
-
return fb.content or ""
|
|
212
|
+
return aggregator_prompt_formatting.get_direction_key(fb)
|
|
334
213
|
|
|
335
214
|
@staticmethod
|
|
336
215
|
def _token_overlap(str1: str, str2: str, threshold: float = 0.6) -> bool:
|
|
337
|
-
|
|
338
|
-
Check if two strings have significant token overlap using asymmetric containment.
|
|
339
|
-
|
|
340
|
-
Computes the ratio of shared tokens to the smaller set, so a short string
|
|
341
|
-
contained in a longer one still counts as a match.
|
|
342
|
-
|
|
343
|
-
Args:
|
|
344
|
-
str1: First string
|
|
345
|
-
str2: Second string
|
|
346
|
-
threshold: Minimum overlap ratio
|
|
347
|
-
|
|
348
|
-
Returns:
|
|
349
|
-
bool: True if overlap ratio >= threshold
|
|
350
|
-
"""
|
|
351
|
-
tokens1 = set(str1.lower().split())
|
|
352
|
-
tokens2 = set(str2.lower().split())
|
|
353
|
-
if not tokens1 or not tokens2:
|
|
354
|
-
return False
|
|
355
|
-
intersection = len(tokens1 & tokens2)
|
|
356
|
-
overlap_ratio = max(intersection / len(tokens1), intersection / len(tokens2))
|
|
357
|
-
return overlap_ratio >= threshold
|
|
216
|
+
return aggregator_prompt_formatting.token_overlap(str1, str2, threshold)
|
|
358
217
|
|
|
359
218
|
@staticmethod
|
|
360
219
|
def _group_playbooks_by_direction(
|
|
361
220
|
cluster_playbooks: list[UserPlaybook],
|
|
362
221
|
threshold: float = 0.6,
|
|
363
222
|
) -> list[list[UserPlaybook]]:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
Uses greedy single-linkage: each playbook is assigned to the first
|
|
368
|
-
existing group that has any member with sufficient token overlap.
|
|
369
|
-
Groups are returned sorted by size descending (largest first).
|
|
370
|
-
|
|
371
|
-
Grouping is purely content-similarity based and does NOT gate on a
|
|
372
|
-
derived whole-content polarity. Under Option B a skill may hold
|
|
373
|
-
mixed-orientation rules (do-rules and avoid-rules for different
|
|
374
|
-
sub-aspects), and whole-content polarity is undefined for such a skill.
|
|
375
|
-
Keeping a do-rule and an avoid-rule as distinct rules when similar items
|
|
376
|
-
are merged into one skill is the aggregation prompt's responsibility,
|
|
377
|
-
not a mechanical split here.
|
|
378
|
-
|
|
379
|
-
Args:
|
|
380
|
-
cluster_playbooks: List of raw playbooks to group
|
|
381
|
-
threshold: Token overlap threshold for grouping
|
|
382
|
-
|
|
383
|
-
Returns:
|
|
384
|
-
list[list[UserPlaybook]]: Groups sorted by size descending
|
|
385
|
-
"""
|
|
386
|
-
groups: list[list[UserPlaybook]] = []
|
|
387
|
-
|
|
388
|
-
for fb in cluster_playbooks:
|
|
389
|
-
key = PlaybookAggregator._get_direction_key(fb)
|
|
390
|
-
matched = False
|
|
391
|
-
for group in groups:
|
|
392
|
-
if any(
|
|
393
|
-
PlaybookAggregator._token_overlap(
|
|
394
|
-
key,
|
|
395
|
-
PlaybookAggregator._get_direction_key(group_fb),
|
|
396
|
-
threshold,
|
|
397
|
-
)
|
|
398
|
-
for group_fb in group
|
|
399
|
-
):
|
|
400
|
-
group.append(fb)
|
|
401
|
-
matched = True
|
|
402
|
-
break
|
|
403
|
-
if not matched:
|
|
404
|
-
groups.append([fb])
|
|
405
|
-
|
|
406
|
-
# Sort by group size descending (largest first)
|
|
407
|
-
groups.sort(key=len, reverse=True)
|
|
408
|
-
return groups
|
|
223
|
+
return aggregator_prompt_formatting.group_playbooks_by_direction(
|
|
224
|
+
cluster_playbooks, threshold
|
|
225
|
+
)
|
|
409
226
|
|
|
410
227
|
def _format_structured_cluster_input(
|
|
411
228
|
self,
|
|
412
229
|
cluster_playbooks: list[UserPlaybook],
|
|
413
230
|
direction_overlap_threshold: float = 0.6,
|
|
414
231
|
) -> str:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
When the cluster forms a single similarity group, uses the flat-list
|
|
419
|
-
format. When distinct similarity groups are detected (multiple groups),
|
|
420
|
-
uses a grouped format so the LLM can see which items are similar and
|
|
421
|
-
preserve distinct rules (e.g. a do-rule and an avoid-rule) as separate
|
|
422
|
-
rules in the merged skill rather than collapsing them.
|
|
423
|
-
|
|
424
|
-
Args:
|
|
425
|
-
cluster_playbooks: List of raw playbooks in this cluster
|
|
426
|
-
direction_overlap_threshold: Token overlap threshold for grouping by direction
|
|
427
|
-
|
|
428
|
-
Returns:
|
|
429
|
-
str: Formatted input for the aggregation prompt
|
|
430
|
-
"""
|
|
431
|
-
groups = self._group_playbooks_by_direction(
|
|
432
|
-
cluster_playbooks, threshold=direction_overlap_threshold
|
|
232
|
+
return aggregator_prompt_formatting.format_structured_cluster_input(
|
|
233
|
+
cluster_playbooks,
|
|
234
|
+
direction_overlap_threshold=direction_overlap_threshold,
|
|
433
235
|
)
|
|
434
236
|
|
|
435
|
-
if len(groups) <= 1:
|
|
436
|
-
return self._format_flat(cluster_playbooks)
|
|
437
|
-
return self._format_grouped(groups)
|
|
438
|
-
|
|
439
|
-
def _format_flat(self, cluster_playbooks: list[UserPlaybook]) -> str:
|
|
440
|
-
"""
|
|
441
|
-
Format playbooks as flat bullet lists (original format, used when no conflict).
|
|
442
|
-
|
|
443
|
-
Args:
|
|
444
|
-
cluster_playbooks: List of raw playbooks in this cluster
|
|
445
|
-
|
|
446
|
-
Returns:
|
|
447
|
-
str: Formatted input with separate field lists
|
|
448
|
-
"""
|
|
449
|
-
triggers = []
|
|
450
|
-
rationales = []
|
|
451
|
-
|
|
452
|
-
for fb in cluster_playbooks:
|
|
453
|
-
if fb.trigger:
|
|
454
|
-
triggers.append(fb.trigger)
|
|
455
|
-
if fb.rationale:
|
|
456
|
-
rationales.append(fb.rationale)
|
|
457
|
-
|
|
458
|
-
lines: list[str] = []
|
|
459
|
-
|
|
460
|
-
if triggers:
|
|
461
|
-
lines.append("TRIGGER conditions (to be consolidated):")
|
|
462
|
-
lines.extend(f"- {trigger}" for trigger in triggers)
|
|
463
|
-
else:
|
|
464
|
-
lines.append("TRIGGER conditions: (none specified)")
|
|
465
|
-
|
|
466
|
-
if rationales:
|
|
467
|
-
lines.append("RATIONALE summaries:")
|
|
468
|
-
lines.extend(f"- {r}" for r in rationales)
|
|
469
|
-
|
|
470
|
-
self._append_freeform_observations(lines, cluster_playbooks)
|
|
471
|
-
|
|
472
|
-
return "\n".join(lines)
|
|
473
|
-
|
|
474
|
-
def _format_grouped(
|
|
475
|
-
self,
|
|
476
|
-
groups: list[list[UserPlaybook]],
|
|
477
|
-
) -> str:
|
|
478
|
-
"""
|
|
479
|
-
Format playbooks in grouped layout (used when conflicting directions are detected).
|
|
480
|
-
|
|
481
|
-
Args:
|
|
482
|
-
groups: AgentPlaybook groups sorted by size descending
|
|
483
|
-
|
|
484
|
-
Returns:
|
|
485
|
-
str: Formatted input with group headers and per-playbook fields
|
|
486
|
-
"""
|
|
487
|
-
lines: list[str] = [
|
|
488
|
-
"The following playbook items are grouped by similarity. "
|
|
489
|
-
"Groups are ordered by size (largest first).",
|
|
490
|
-
"",
|
|
491
|
-
]
|
|
492
|
-
|
|
493
|
-
for idx, group in enumerate(groups, start=1):
|
|
494
|
-
count_label = "playbook" if len(group) == 1 else "playbooks"
|
|
495
|
-
lines.append(f"Group {idx} ({len(group)} {count_label}):")
|
|
496
|
-
for fb in group:
|
|
497
|
-
parts: list[str] = []
|
|
498
|
-
if fb.trigger:
|
|
499
|
-
parts.append(f'Trigger: "{fb.trigger}"')
|
|
500
|
-
if fb.rationale:
|
|
501
|
-
parts.append(f'Rationale: "{fb.rationale}"')
|
|
502
|
-
if not parts and fb.content:
|
|
503
|
-
parts.append(f'AgentPlaybook: "{fb.content}"')
|
|
504
|
-
if parts:
|
|
505
|
-
lines.append(f" - {parts[0]}")
|
|
506
|
-
lines.extend(f" {p}" for p in parts[1:])
|
|
507
|
-
lines.append("")
|
|
508
|
-
|
|
509
|
-
return "\n".join(lines)
|
|
510
|
-
|
|
511
|
-
@staticmethod
|
|
512
|
-
def _append_freeform_observations(
|
|
513
|
-
lines: list[str], cluster_playbooks: list[UserPlaybook]
|
|
514
|
-
) -> None:
|
|
515
|
-
"""Append freeform observations from cluster playbooks to output lines."""
|
|
516
|
-
freeform_observations = [
|
|
517
|
-
fb.content for fb in cluster_playbooks if not fb.trigger and fb.content
|
|
518
|
-
]
|
|
519
|
-
if freeform_observations:
|
|
520
|
-
lines.append("Freeform observations (from freeform cluster members):")
|
|
521
|
-
lines.extend(f"- {obs}" for obs in freeform_observations)
|
|
522
|
-
|
|
523
237
|
# ===============================
|
|
524
238
|
# private methods - cluster change detection
|
|
525
239
|
# ===============================
|
|
526
240
|
|
|
527
241
|
@staticmethod
|
|
528
242
|
def _compute_cluster_fingerprint(cluster_playbooks: list[UserPlaybook]) -> str:
|
|
529
|
-
|
|
530
|
-
Compute a fingerprint for a cluster based on its user_playbook_ids.
|
|
531
|
-
The fingerprint is deterministic and order-independent.
|
|
532
|
-
|
|
533
|
-
Args:
|
|
534
|
-
cluster_playbooks: List of raw playbooks in this cluster
|
|
535
|
-
|
|
536
|
-
Returns:
|
|
537
|
-
str: SHA-256 hash (truncated to 16 hex chars) of sorted user_playbook_ids
|
|
538
|
-
"""
|
|
539
|
-
sorted_ids = sorted(fb.user_playbook_id for fb in cluster_playbooks)
|
|
540
|
-
id_str = ",".join(str(id) for id in sorted_ids)
|
|
541
|
-
return hashlib.sha256(id_str.encode()).hexdigest()[:16]
|
|
243
|
+
return aggregator_clustering.compute_cluster_fingerprint(cluster_playbooks)
|
|
542
244
|
|
|
543
245
|
def _determine_cluster_changes(
|
|
544
246
|
self,
|
|
545
247
|
clusters: dict[int, list[UserPlaybook]],
|
|
546
248
|
prev_fingerprints: dict,
|
|
547
249
|
) -> tuple[dict[int, list[UserPlaybook]], list[int]]:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
Args:
|
|
552
|
-
clusters: Current clusters (cluster_id -> list of UserPlaybook)
|
|
553
|
-
prev_fingerprints: Previous fingerprint state
|
|
554
|
-
(fingerprint_hash -> {"agent_playbook_id": int, "user_playbook_ids": list})
|
|
555
|
-
|
|
556
|
-
Returns:
|
|
557
|
-
tuple of:
|
|
558
|
-
- changed_clusters: Only clusters needing new LLM calls
|
|
559
|
-
- playbook_ids_to_archive: Old playbook_ids from changed/disappeared clusters
|
|
560
|
-
"""
|
|
561
|
-
# Compute fingerprints for current clusters
|
|
562
|
-
current_fingerprints = {}
|
|
563
|
-
for cluster_id, cluster_playbooks in clusters.items():
|
|
564
|
-
fp = self._compute_cluster_fingerprint(cluster_playbooks)
|
|
565
|
-
current_fingerprints[cluster_id] = fp
|
|
566
|
-
|
|
567
|
-
current_fp_set = set(current_fingerprints.values())
|
|
568
|
-
prev_fp_set = set(prev_fingerprints.keys())
|
|
569
|
-
|
|
570
|
-
# Changed clusters: fingerprints that are new (not in previous state)
|
|
571
|
-
changed_clusters = {}
|
|
572
|
-
for cluster_id, fp in current_fingerprints.items():
|
|
573
|
-
if fp not in prev_fp_set:
|
|
574
|
-
changed_clusters[cluster_id] = clusters[cluster_id]
|
|
575
|
-
|
|
576
|
-
# Playbook IDs to archive: from fingerprints that disappeared or changed
|
|
577
|
-
playbook_ids_to_archive = []
|
|
578
|
-
for fp, fp_data in prev_fingerprints.items():
|
|
579
|
-
if fp not in current_fp_set:
|
|
580
|
-
playbook_id = fp_data.get("agent_playbook_id")
|
|
581
|
-
if playbook_id is not None:
|
|
582
|
-
playbook_ids_to_archive.append(playbook_id)
|
|
583
|
-
|
|
584
|
-
return changed_clusters, playbook_ids_to_archive
|
|
250
|
+
return aggregator_clustering.determine_cluster_changes(
|
|
251
|
+
clusters, prev_fingerprints
|
|
252
|
+
)
|
|
585
253
|
|
|
586
254
|
# ===============================
|
|
587
255
|
# public methods
|
|
@@ -692,9 +360,9 @@ class PlaybookAggregator:
|
|
|
692
360
|
self.agent_version,
|
|
693
361
|
)
|
|
694
362
|
logger.info(
|
|
695
|
-
"
|
|
696
|
-
type(self.
|
|
697
|
-
if self.
|
|
363
|
+
"Aggregation prompt processor: %s",
|
|
364
|
+
type(self.aggregation_prompt_processor).__name__
|
|
365
|
+
if self.aggregation_prompt_processor is not None
|
|
698
366
|
else "disabled",
|
|
699
367
|
)
|
|
700
368
|
|
|
@@ -979,6 +647,22 @@ class PlaybookAggregator:
|
|
|
979
647
|
# Remove archived playbooks after successful aggregation. ALWAYS soft-supersede
|
|
980
648
|
# (never hard-delete) so the removal is reconstructable from lineage — mirrors the
|
|
981
649
|
# profile dedup always-soft path (#206).
|
|
650
|
+
archived_ids_without_overlapping_changed_cluster: set[int] = set()
|
|
651
|
+
if new_playbooks and prev_fingerprints:
|
|
652
|
+
archived_id_set = set(archived_playbook_ids)
|
|
653
|
+
for prev_fp, fp_data in prev_fingerprints.items():
|
|
654
|
+
playbook_id = fp_data.get("agent_playbook_id")
|
|
655
|
+
if (
|
|
656
|
+
playbook_id in archived_id_set
|
|
657
|
+
and prev_fp not in changed_fps_by_previous_fp
|
|
658
|
+
):
|
|
659
|
+
archived_ids_without_overlapping_changed_cluster.add(
|
|
660
|
+
playbook_id
|
|
661
|
+
)
|
|
662
|
+
ids_to_supersede = {
|
|
663
|
+
*selective_supersede_playbook_ids,
|
|
664
|
+
*archived_ids_without_overlapping_changed_cluster,
|
|
665
|
+
}
|
|
982
666
|
if not _run_id:
|
|
983
667
|
# Empty request_id makes the removal unreconstructable (lineage events are keyed
|
|
984
668
|
# on it). Fail loud and skip removal — never silently hard-delete.
|
|
@@ -996,9 +680,9 @@ class PlaybookAggregator:
|
|
|
996
680
|
agent_version=self.agent_version,
|
|
997
681
|
request_id=_run_id,
|
|
998
682
|
)
|
|
999
|
-
elif
|
|
683
|
+
elif ids_to_supersede:
|
|
1000
684
|
self.storage.supersede_agent_playbooks_by_ids( # type: ignore[reportOptionalMemberAccess]
|
|
1001
|
-
sorted(
|
|
685
|
+
sorted(ids_to_supersede),
|
|
1002
686
|
request_id=_run_id,
|
|
1003
687
|
)
|
|
1004
688
|
elif archived_playbook_ids:
|
|
@@ -1107,7 +791,9 @@ class PlaybookAggregator:
|
|
|
1107
791
|
# Mock mode: cluster by trigger
|
|
1108
792
|
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
|
|
1109
793
|
logger.info("Mock mode: clustering by trigger")
|
|
1110
|
-
return
|
|
794
|
+
return aggregator_clustering.cluster_by_trigger_mock(
|
|
795
|
+
user_playbooks, min_cluster_size
|
|
796
|
+
)
|
|
1111
797
|
|
|
1112
798
|
# Extract embeddings from user playbooks
|
|
1113
799
|
import numpy as np
|
|
@@ -1162,139 +848,26 @@ class PlaybookAggregator:
|
|
|
1162
848
|
|
|
1163
849
|
return clusters
|
|
1164
850
|
|
|
1165
|
-
def _cluster_by_trigger_mock(
|
|
1166
|
-
self, user_playbooks: list[UserPlaybook], min_cluster_size: int
|
|
1167
|
-
) -> dict[int, list[UserPlaybook]]:
|
|
1168
|
-
"""
|
|
1169
|
-
Simple mock clustering by exact trigger match.
|
|
1170
|
-
|
|
1171
|
-
Args:
|
|
1172
|
-
user_playbooks: List of user playbooks with trigger field
|
|
1173
|
-
min_cluster_size: Minimum number of playbooks per cluster
|
|
1174
|
-
|
|
1175
|
-
Returns:
|
|
1176
|
-
dict[int, list[UserPlaybook]]: Clusters grouped by trigger
|
|
1177
|
-
"""
|
|
1178
|
-
# Group by trigger
|
|
1179
|
-
condition_groups: dict[str, list[UserPlaybook]] = {}
|
|
1180
|
-
for fb in user_playbooks:
|
|
1181
|
-
condition = fb.trigger or ""
|
|
1182
|
-
if condition not in condition_groups:
|
|
1183
|
-
condition_groups[condition] = []
|
|
1184
|
-
condition_groups[condition].append(fb)
|
|
1185
|
-
|
|
1186
|
-
# Convert to cluster format, filtering by min_cluster_size
|
|
1187
|
-
clusters: dict[int, list[UserPlaybook]] = {}
|
|
1188
|
-
cluster_id = 0
|
|
1189
|
-
for playbooks_group in condition_groups.values():
|
|
1190
|
-
if len(playbooks_group) >= min_cluster_size:
|
|
1191
|
-
clusters[cluster_id] = playbooks_group
|
|
1192
|
-
cluster_id += 1
|
|
1193
|
-
|
|
1194
|
-
logger.info(
|
|
1195
|
-
"Mock mode: created %d trigger clusters from %d playbooks",
|
|
1196
|
-
len(clusters),
|
|
1197
|
-
len(user_playbooks),
|
|
1198
|
-
)
|
|
1199
|
-
return clusters
|
|
1200
|
-
|
|
1201
851
|
def _cluster_with_agglomerative(
|
|
1202
852
|
self,
|
|
1203
853
|
distance_matrix: np.ndarray,
|
|
1204
|
-
min_cluster_size: int,
|
|
854
|
+
min_cluster_size: int,
|
|
1205
855
|
distance_threshold: float,
|
|
1206
856
|
) -> np.ndarray:
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
Args:
|
|
1211
|
-
distance_matrix: Precomputed cosine distance matrix
|
|
1212
|
-
min_cluster_size: Minimum cluster size (used for logging only,
|
|
1213
|
-
filtering happens in get_clusters)
|
|
1214
|
-
distance_threshold: Maximum cosine distance to merge clusters (1 - similarity_threshold)
|
|
1215
|
-
|
|
1216
|
-
Returns:
|
|
1217
|
-
np.ndarray: Cluster labels for each point
|
|
1218
|
-
"""
|
|
1219
|
-
from sklearn.cluster import AgglomerativeClustering
|
|
1220
|
-
|
|
1221
|
-
logger.info(
|
|
1222
|
-
"Using Agglomerative Clustering for %d playbooks (< %d threshold), distance_threshold=%.2f",
|
|
1223
|
-
len(distance_matrix),
|
|
1224
|
-
CLUSTERING_ALGORITHM_THRESHOLD,
|
|
1225
|
-
distance_threshold,
|
|
1226
|
-
)
|
|
1227
|
-
|
|
1228
|
-
clusterer = AgglomerativeClustering(
|
|
1229
|
-
n_clusters=None, # type: ignore[reportArgumentType]
|
|
1230
|
-
distance_threshold=distance_threshold,
|
|
1231
|
-
metric="precomputed",
|
|
1232
|
-
linkage="average",
|
|
857
|
+
return aggregator_clustering.cluster_with_agglomerative(
|
|
858
|
+
distance_matrix, min_cluster_size, distance_threshold
|
|
1233
859
|
)
|
|
1234
860
|
|
|
1235
|
-
return clusterer.fit_predict(distance_matrix)
|
|
1236
|
-
|
|
1237
861
|
def _cluster_with_hdbscan(
|
|
1238
862
|
self,
|
|
1239
863
|
distance_matrix: np.ndarray,
|
|
1240
864
|
min_cluster_size: int,
|
|
1241
865
|
distance_threshold: float,
|
|
1242
866
|
) -> np.ndarray:
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
Args:
|
|
1247
|
-
distance_matrix: Precomputed cosine distance matrix
|
|
1248
|
-
min_cluster_size: Minimum number of points to form a cluster
|
|
1249
|
-
distance_threshold: Maximum cosine distance for cluster merging (1 - similarity_threshold)
|
|
1250
|
-
|
|
1251
|
-
Returns:
|
|
1252
|
-
np.ndarray: Cluster labels for each point (-1 indicates noise)
|
|
1253
|
-
"""
|
|
1254
|
-
import hdbscan
|
|
1255
|
-
|
|
1256
|
-
logger.info(
|
|
1257
|
-
"Using HDBSCAN for %d playbooks (>= %d threshold), distance_threshold=%.2f",
|
|
1258
|
-
len(distance_matrix),
|
|
1259
|
-
CLUSTERING_ALGORITHM_THRESHOLD,
|
|
1260
|
-
distance_threshold,
|
|
1261
|
-
)
|
|
1262
|
-
|
|
1263
|
-
clusterer = hdbscan.HDBSCAN(
|
|
1264
|
-
min_cluster_size=min_cluster_size,
|
|
1265
|
-
min_samples=1,
|
|
1266
|
-
metric="precomputed",
|
|
1267
|
-
cluster_selection_epsilon=distance_threshold,
|
|
867
|
+
return aggregator_clustering.cluster_with_hdbscan(
|
|
868
|
+
distance_matrix, min_cluster_size, distance_threshold
|
|
1268
869
|
)
|
|
1269
870
|
|
|
1270
|
-
return clusterer.fit_predict(distance_matrix)
|
|
1271
|
-
|
|
1272
|
-
def _generate_playbooks_from_clusters(
|
|
1273
|
-
self,
|
|
1274
|
-
clusters: dict[int, list[UserPlaybook]],
|
|
1275
|
-
existing_approved_playbooks: list[AgentPlaybook],
|
|
1276
|
-
direction_overlap_threshold: float = 0.6,
|
|
1277
|
-
) -> list[AgentPlaybook]:
|
|
1278
|
-
"""
|
|
1279
|
-
Generate playbooks from clusters, considering existing approved playbooks.
|
|
1280
|
-
|
|
1281
|
-
Args:
|
|
1282
|
-
clusters: Dictionary mapping cluster IDs to lists of raw playbooks
|
|
1283
|
-
existing_approved_playbooks: List of existing approved playbooks to avoid duplication
|
|
1284
|
-
direction_overlap_threshold: Token overlap threshold for grouping by direction
|
|
1285
|
-
|
|
1286
|
-
Returns:
|
|
1287
|
-
list[AgentPlaybook]: List of newly generated playbooks (excludes duplicates)
|
|
1288
|
-
"""
|
|
1289
|
-
return [
|
|
1290
|
-
playbook
|
|
1291
|
-
for playbook, _ in self._generate_playbooks_with_source_clusters(
|
|
1292
|
-
clusters,
|
|
1293
|
-
existing_approved_playbooks,
|
|
1294
|
-
direction_overlap_threshold=direction_overlap_threshold,
|
|
1295
|
-
)
|
|
1296
|
-
]
|
|
1297
|
-
|
|
1298
871
|
def _generate_playbooks_with_source_clusters(
|
|
1299
872
|
self,
|
|
1300
873
|
clusters: dict[int, list[UserPlaybook]],
|
|
@@ -1309,12 +882,20 @@ class PlaybookAggregator:
|
|
|
1309
882
|
else "None"
|
|
1310
883
|
)
|
|
1311
884
|
for cluster_playbooks in clusters.values():
|
|
1312
|
-
|
|
1313
|
-
|
|
885
|
+
shared_state: dict[str, object] = {}
|
|
886
|
+
processing_context = AggregationPromptProcessingContext(
|
|
887
|
+
data={
|
|
888
|
+
"agent_version": self.agent_version,
|
|
889
|
+
"org_id": self.request_context.org_id,
|
|
890
|
+
}
|
|
891
|
+
)
|
|
892
|
+
if self.aggregation_prompt_processor is None:
|
|
1314
893
|
prompt_cluster_playbooks = cluster_playbooks
|
|
1315
894
|
else:
|
|
1316
895
|
prompt_cluster_playbooks = [
|
|
1317
|
-
self.
|
|
896
|
+
self._postproc._preprocess_user_playbook_for_prompt(
|
|
897
|
+
playbook, shared_state, processing_context
|
|
898
|
+
)
|
|
1318
899
|
for playbook in cluster_playbooks
|
|
1319
900
|
]
|
|
1320
901
|
|
|
@@ -1322,6 +903,7 @@ class PlaybookAggregator:
|
|
|
1322
903
|
prompt_cluster_playbooks,
|
|
1323
904
|
approved_playbooks_str,
|
|
1324
905
|
direction_overlap_threshold=direction_overlap_threshold,
|
|
906
|
+
processing_context=processing_context,
|
|
1325
907
|
)
|
|
1326
908
|
if playbook is not None:
|
|
1327
909
|
new_playbooks.append((playbook, cluster_playbooks))
|
|
@@ -1371,6 +953,7 @@ class PlaybookAggregator:
|
|
|
1371
953
|
cluster_playbooks: list[UserPlaybook],
|
|
1372
954
|
existing_approved_playbooks_str: str,
|
|
1373
955
|
direction_overlap_threshold: float = 0.6,
|
|
956
|
+
processing_context: AggregationPromptProcessingContext | None = None,
|
|
1374
957
|
) -> AgentPlaybook | None:
|
|
1375
958
|
"""
|
|
1376
959
|
Generate a playbook from a cluster using structured JSON output.
|
|
@@ -1407,8 +990,11 @@ class PlaybookAggregator:
|
|
|
1407
990
|
trigger=trigger,
|
|
1408
991
|
)
|
|
1409
992
|
)
|
|
1410
|
-
response,
|
|
1411
|
-
|
|
993
|
+
response, artifact_count = self._postproc._postprocess_aggregation_response(
|
|
994
|
+
response,
|
|
995
|
+
processing_context,
|
|
996
|
+
)
|
|
997
|
+
self._postproc._record_postprocessing_artifacts(artifact_count)
|
|
1412
998
|
playbook = self._process_aggregation_response(response, cluster_playbooks)
|
|
1413
999
|
if playbook is None:
|
|
1414
1000
|
return None
|
|
@@ -1428,7 +1014,9 @@ class PlaybookAggregator:
|
|
|
1428
1014
|
{
|
|
1429
1015
|
"user_playbooks": raw_playbooks_str,
|
|
1430
1016
|
"existing_approved_playbooks": existing_approved_playbooks_str,
|
|
1431
|
-
"aggregation_prompt_extra_instructions": self.
|
|
1017
|
+
"aggregation_prompt_extra_instructions": self._postproc._aggregation_prompt_extra_instructions_for_context(
|
|
1018
|
+
processing_context
|
|
1019
|
+
),
|
|
1432
1020
|
},
|
|
1433
1021
|
),
|
|
1434
1022
|
}
|
|
@@ -1442,15 +1030,21 @@ class PlaybookAggregator:
|
|
|
1442
1030
|
parse_structured_output=True,
|
|
1443
1031
|
)
|
|
1444
1032
|
if isinstance(response, PlaybookAggregationOutput):
|
|
1445
|
-
response,
|
|
1446
|
-
|
|
1033
|
+
response, artifact_count = (
|
|
1034
|
+
self._postproc._postprocess_aggregation_response(
|
|
1035
|
+
response,
|
|
1036
|
+
processing_context,
|
|
1037
|
+
)
|
|
1447
1038
|
)
|
|
1448
|
-
self.
|
|
1039
|
+
self._postproc._record_postprocessing_artifacts(artifact_count)
|
|
1449
1040
|
else:
|
|
1450
|
-
response,
|
|
1451
|
-
|
|
1041
|
+
response, artifact_count = (
|
|
1042
|
+
self._postproc._postprocess_aggregation_output(
|
|
1043
|
+
response,
|
|
1044
|
+
processing_context,
|
|
1045
|
+
)
|
|
1452
1046
|
)
|
|
1453
|
-
self.
|
|
1047
|
+
self._postproc._record_postprocessing_artifacts(artifact_count)
|
|
1454
1048
|
log_model_response(logger, "Aggregation structured response", response)
|
|
1455
1049
|
|
|
1456
1050
|
if not isinstance(response, PlaybookAggregationOutput):
|
|
@@ -1462,13 +1056,16 @@ class PlaybookAggregator:
|
|
|
1462
1056
|
|
|
1463
1057
|
return self._process_aggregation_response(response, cluster_playbooks)
|
|
1464
1058
|
except Exception as exc:
|
|
1465
|
-
|
|
1466
|
-
|
|
1059
|
+
processed_error, artifact_count = (
|
|
1060
|
+
self._postproc._postprocess_aggregation_output(
|
|
1061
|
+
str(exc),
|
|
1062
|
+
processing_context,
|
|
1063
|
+
)
|
|
1467
1064
|
)
|
|
1468
|
-
self.
|
|
1065
|
+
self._postproc._record_postprocessing_artifacts(artifact_count)
|
|
1469
1066
|
logger.error(
|
|
1470
1067
|
"AgentPlaybook aggregation failed due to %s, returning None.",
|
|
1471
|
-
|
|
1068
|
+
processed_error,
|
|
1472
1069
|
)
|
|
1473
1070
|
return None
|
|
1474
1071
|
|
|
@@ -1482,7 +1079,7 @@ class PlaybookAggregator:
|
|
|
1482
1079
|
response: Parsed PlaybookAggregationOutput from LLM
|
|
1483
1080
|
cluster_playbooks: Cluster playbooks used only for non-user metadata
|
|
1484
1081
|
such as playbook name and agent version. Callers may pass
|
|
1485
|
-
prompt-
|
|
1082
|
+
prompt-preprocessed copies here, so this method must not read
|
|
1486
1083
|
user-authored fields from them.
|
|
1487
1084
|
|
|
1488
1085
|
Returns:
|
|
@@ -1490,7 +1087,6 @@ class PlaybookAggregator:
|
|
|
1490
1087
|
"""
|
|
1491
1088
|
if not response:
|
|
1492
1089
|
return None
|
|
1493
|
-
response, _placeholder_count = self._sanitize_aggregation_response(response)
|
|
1494
1090
|
|
|
1495
1091
|
structured = response.playbook
|
|
1496
1092
|
if structured is None:
|