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
|
@@ -33,6 +33,74 @@ logger = logging.getLogger(__name__)
|
|
|
33
33
|
# Playbook-specific Pydantic Output Schemas for LLM
|
|
34
34
|
# ===============================
|
|
35
35
|
|
|
36
|
+
NewIdField = str | list[str]
|
|
37
|
+
ExistingIdField = int | list[int]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _strip_prompt_brackets(value: str) -> str:
|
|
41
|
+
"""Strip the display brackets used in prompt labels, e.g. ``[NEW-0]``."""
|
|
42
|
+
stripped = value.strip()
|
|
43
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
44
|
+
return stripped[1:-1].strip()
|
|
45
|
+
return stripped
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _dedupe_preserving_order(values: list[str]) -> list[str]:
|
|
49
|
+
seen: set[str] = set()
|
|
50
|
+
deduped: list[str] = []
|
|
51
|
+
for value in values:
|
|
52
|
+
if value in seen:
|
|
53
|
+
continue
|
|
54
|
+
seen.add(value)
|
|
55
|
+
deduped.append(value)
|
|
56
|
+
return deduped
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _coerce_new_id(value: object) -> str:
|
|
60
|
+
"""Normalize a prompt-format NEW id to ``NEW-N``.
|
|
61
|
+
|
|
62
|
+
LLMs sometimes echo the rendered display label (``[NEW-0]``) or wrap the
|
|
63
|
+
id in a one-item list. Keep the apply path keyed on canonical ``NEW-N``.
|
|
64
|
+
"""
|
|
65
|
+
if isinstance(value, list):
|
|
66
|
+
if len(value) != 1:
|
|
67
|
+
raise ValueError(f"new_id expected one NEW id, got {value!r}")
|
|
68
|
+
return _coerce_new_id(value[0])
|
|
69
|
+
if not isinstance(value, str):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"new_id must be 'NEW-N' label, got {type(value).__name__}: {value!r}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
stripped = _strip_prompt_brackets(value)
|
|
75
|
+
for prefix in ("NEW-", "NEW_", "new-", "new_"):
|
|
76
|
+
if stripped.startswith(prefix):
|
|
77
|
+
suffix = stripped[len(prefix) :]
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(f"new_id must be 'NEW-N' label, got {value!r}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
parsed = int(suffix)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise ValueError(f"new_id must be 'NEW-N' label, got {value!r}") from exc
|
|
86
|
+
if parsed < 0:
|
|
87
|
+
raise ValueError(f"new_id must be >= 0, got {value!r}")
|
|
88
|
+
return f"NEW-{parsed}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _coerce_new_ids(value: object, *, allow_many: bool) -> NewIdField:
|
|
92
|
+
values = value if isinstance(value, list) else [value]
|
|
93
|
+
coerced = _dedupe_preserving_order([_coerce_new_id(item) for item in values])
|
|
94
|
+
if not coerced:
|
|
95
|
+
raise ValueError("new_id must include at least one NEW id")
|
|
96
|
+
if not allow_many and len(coerced) != 1:
|
|
97
|
+
raise ValueError(f"new_id expected one NEW id, got {coerced!r}")
|
|
98
|
+
return coerced if len(coerced) > 1 else coerced[0]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _new_ids_from_field(value: NewIdField) -> list[str]:
|
|
102
|
+
return list(value) if isinstance(value, list) else [value]
|
|
103
|
+
|
|
36
104
|
|
|
37
105
|
def _coerce_existing_position(value: object) -> int:
|
|
38
106
|
"""Accept either a bare int position or an ``"EXISTING-N"`` label.
|
|
@@ -71,12 +139,16 @@ def _coerce_existing_position(value: object) -> int:
|
|
|
71
139
|
# ``bool`` is a subclass of ``int`` in Python; reject explicitly so a
|
|
72
140
|
# stray ``True`` doesn't silently become position 1.
|
|
73
141
|
raise ValueError(f"existing-position must be int, got bool: {value!r}")
|
|
142
|
+
if isinstance(value, list):
|
|
143
|
+
if len(value) != 1:
|
|
144
|
+
raise ValueError(f"existing-position expected one id, got {value!r}")
|
|
145
|
+
return _coerce_existing_position(value[0])
|
|
74
146
|
if isinstance(value, int):
|
|
75
147
|
if value < 0:
|
|
76
148
|
raise ValueError(f"existing-position must be >= 0, got {value!r}")
|
|
77
149
|
return value
|
|
78
150
|
if isinstance(value, str):
|
|
79
|
-
stripped = value
|
|
151
|
+
stripped = _strip_prompt_brackets(value)
|
|
80
152
|
for prefix in ("EXISTING-", "EXISTING_", "existing-", "existing_"):
|
|
81
153
|
if stripped.startswith(prefix):
|
|
82
154
|
stripped = stripped[len(prefix) :]
|
|
@@ -95,6 +167,30 @@ def _coerce_existing_position(value: object) -> int:
|
|
|
95
167
|
)
|
|
96
168
|
|
|
97
169
|
|
|
170
|
+
def _coerce_existing_positions(value: object, *, allow_many: bool) -> ExistingIdField:
|
|
171
|
+
values = value if isinstance(value, list) else [value]
|
|
172
|
+
coerced = [_coerce_existing_position(item) for item in values]
|
|
173
|
+
if not coerced:
|
|
174
|
+
raise ValueError("existing-position must include at least one id")
|
|
175
|
+
if not allow_many and len(coerced) != 1:
|
|
176
|
+
raise ValueError(f"existing-position expected one id, got {coerced!r}")
|
|
177
|
+
return coerced if len(coerced) > 1 else coerced[0]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _existing_ids_from_field(value: ExistingIdField) -> list[int]:
|
|
181
|
+
return list(value) if isinstance(value, list) else [value]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _new_id_log_label(value: NewIdField) -> str:
|
|
185
|
+
return ",".join(_new_ids_from_field(value)) if isinstance(value, list) else value
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _existing_id_log_label(value: ExistingIdField) -> str:
|
|
189
|
+
if isinstance(value, list):
|
|
190
|
+
return ",".join(str(item) for item in value)
|
|
191
|
+
return str(value)
|
|
192
|
+
|
|
193
|
+
|
|
98
194
|
class UnifyDecision(BaseModel):
|
|
99
195
|
"""Collapse NEW (+ 0..N EXISTING) into one row with LLM-supplied content.
|
|
100
196
|
|
|
@@ -112,13 +208,18 @@ class UnifyDecision(BaseModel):
|
|
|
112
208
|
"""
|
|
113
209
|
|
|
114
210
|
kind: Literal["unify"] = "unify"
|
|
115
|
-
new_id:
|
|
211
|
+
new_id: NewIdField
|
|
116
212
|
archive_existing_ids: list[int] = Field(default_factory=list)
|
|
117
213
|
content: str
|
|
118
214
|
trigger: str
|
|
119
215
|
rationale: str
|
|
120
216
|
reason: str = ""
|
|
121
217
|
|
|
218
|
+
@field_validator("new_id", mode="before")
|
|
219
|
+
@classmethod
|
|
220
|
+
def _coerce_new_id(cls, value: object) -> NewIdField:
|
|
221
|
+
return _coerce_new_ids(value, allow_many=True)
|
|
222
|
+
|
|
122
223
|
@field_validator("archive_existing_ids", mode="before")
|
|
123
224
|
@classmethod
|
|
124
225
|
def _coerce_archive_ids(cls, value: object) -> object:
|
|
@@ -126,7 +227,11 @@ class UnifyDecision(BaseModel):
|
|
|
126
227
|
return []
|
|
127
228
|
if isinstance(value, list):
|
|
128
229
|
return [_coerce_existing_position(item) for item in value]
|
|
129
|
-
return value
|
|
230
|
+
return [_coerce_existing_position(value)]
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def new_ids(self) -> list[str]:
|
|
234
|
+
return _new_ids_from_field(self.new_id)
|
|
130
235
|
|
|
131
236
|
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
132
237
|
|
|
@@ -142,14 +247,27 @@ class RejectNewDecision(BaseModel):
|
|
|
142
247
|
"""
|
|
143
248
|
|
|
144
249
|
kind: Literal["reject_new"] = "reject_new"
|
|
145
|
-
new_id:
|
|
146
|
-
superseded_by_existing_id:
|
|
250
|
+
new_id: NewIdField
|
|
251
|
+
superseded_by_existing_id: ExistingIdField
|
|
147
252
|
reason: str = ""
|
|
148
253
|
|
|
254
|
+
@field_validator("new_id", mode="before")
|
|
255
|
+
@classmethod
|
|
256
|
+
def _coerce_new_id(cls, value: object) -> NewIdField:
|
|
257
|
+
return _coerce_new_ids(value, allow_many=True)
|
|
258
|
+
|
|
149
259
|
@field_validator("superseded_by_existing_id", mode="before")
|
|
150
260
|
@classmethod
|
|
151
|
-
def _coerce_superseded_id(cls, value: object) ->
|
|
152
|
-
return
|
|
261
|
+
def _coerce_superseded_id(cls, value: object) -> ExistingIdField:
|
|
262
|
+
return _coerce_existing_positions(value, allow_many=True)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def new_ids(self) -> list[str]:
|
|
266
|
+
return _new_ids_from_field(self.new_id)
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def superseded_by_existing_ids(self) -> list[int]:
|
|
270
|
+
return _existing_ids_from_field(self.superseded_by_existing_id)
|
|
153
271
|
|
|
154
272
|
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
155
273
|
|
|
@@ -171,10 +289,18 @@ class DifferentiateDecision(BaseModel):
|
|
|
171
289
|
refined_existing_trigger: str
|
|
172
290
|
reason: str = ""
|
|
173
291
|
|
|
292
|
+
@field_validator("new_id", mode="before")
|
|
293
|
+
@classmethod
|
|
294
|
+
def _coerce_new_id(cls, value: object) -> str:
|
|
295
|
+
return _coerce_new_id(value)
|
|
296
|
+
|
|
174
297
|
@field_validator("existing_id", mode="before")
|
|
175
298
|
@classmethod
|
|
176
299
|
def _coerce_existing_id(cls, value: object) -> int:
|
|
177
|
-
|
|
300
|
+
coerced = _coerce_existing_positions(value, allow_many=False)
|
|
301
|
+
if not isinstance(coerced, int):
|
|
302
|
+
raise ValueError(f"existing_id expected one id, got {coerced!r}")
|
|
303
|
+
return coerced
|
|
178
304
|
|
|
179
305
|
@field_validator("refined_new_trigger", "refined_existing_trigger")
|
|
180
306
|
@classmethod
|
|
@@ -190,9 +316,18 @@ class IndependentDecision(BaseModel):
|
|
|
190
316
|
"""Unrelated to any existing row: insert new as-is, no archive."""
|
|
191
317
|
|
|
192
318
|
kind: Literal["independent"] = "independent"
|
|
193
|
-
new_id:
|
|
319
|
+
new_id: NewIdField
|
|
194
320
|
reason: str = ""
|
|
195
321
|
|
|
322
|
+
@field_validator("new_id", mode="before")
|
|
323
|
+
@classmethod
|
|
324
|
+
def _coerce_new_id(cls, value: object) -> NewIdField:
|
|
325
|
+
return _coerce_new_ids(value, allow_many=True)
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def new_ids(self) -> list[str]:
|
|
329
|
+
return _new_ids_from_field(self.new_id)
|
|
330
|
+
|
|
196
331
|
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
197
332
|
|
|
198
333
|
|
|
@@ -679,8 +814,22 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
679
814
|
)
|
|
680
815
|
except Exception as exc: # noqa: BLE001 — per-decision isolation
|
|
681
816
|
result_counters.failed_count += 1
|
|
682
|
-
|
|
683
|
-
|
|
817
|
+
raw_new_id = getattr(decision, "new_id", "unknown")
|
|
818
|
+
new_id_str = (
|
|
819
|
+
_new_id_log_label(raw_new_id)
|
|
820
|
+
if isinstance(raw_new_id, (str, list))
|
|
821
|
+
else "unknown"
|
|
822
|
+
)
|
|
823
|
+
raw_existing_id = getattr(
|
|
824
|
+
decision,
|
|
825
|
+
"existing_id",
|
|
826
|
+
getattr(decision, "superseded_by_existing_id", "unknown"),
|
|
827
|
+
)
|
|
828
|
+
existing_id_str = (
|
|
829
|
+
_existing_id_log_label(raw_existing_id)
|
|
830
|
+
if isinstance(raw_existing_id, (int, list))
|
|
831
|
+
else "unknown"
|
|
832
|
+
)
|
|
684
833
|
with sentry_tags(
|
|
685
834
|
subsystem="playbook_consolidator",
|
|
686
835
|
op="apply_decision",
|
|
@@ -779,6 +928,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
779
928
|
if isinstance(decision, RejectNewDecision):
|
|
780
929
|
return self._apply_reject_new(
|
|
781
930
|
decision,
|
|
931
|
+
candidates_by_id=candidates_by_id,
|
|
782
932
|
existing_by_id=existing_by_id,
|
|
783
933
|
existing_by_position=existing_by_position,
|
|
784
934
|
)
|
|
@@ -834,14 +984,18 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
834
984
|
so the merge is materialized by the caller, not here.
|
|
835
985
|
|
|
836
986
|
Raises:
|
|
837
|
-
KeyError: If ``decision.
|
|
838
|
-
candidate.
|
|
987
|
+
KeyError: If any id in ``decision.new_ids`` does not resolve to a
|
|
988
|
+
known candidate.
|
|
839
989
|
ValueError: If an ``archive_existing_ids`` entry has no matching
|
|
840
990
|
``EXISTING-{idx}`` row in the position map.
|
|
841
991
|
"""
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
992
|
+
new_ids = decision.new_ids
|
|
993
|
+
candidates: list[UserPlaybook] = []
|
|
994
|
+
for new_id in new_ids:
|
|
995
|
+
candidate = candidates_by_id.get(new_id)
|
|
996
|
+
if candidate is None:
|
|
997
|
+
raise KeyError(f"unify references unknown NEW id: {new_id}")
|
|
998
|
+
candidates.append(candidate)
|
|
845
999
|
|
|
846
1000
|
existing_members: list[UserPlaybook] = []
|
|
847
1001
|
for existing_position in decision.archive_existing_ids:
|
|
@@ -870,74 +1024,92 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
870
1024
|
# apply logic; the merge still proceeds.
|
|
871
1025
|
logger.warning(
|
|
872
1026
|
"event=consolidation_over_budget new_id=%s len=%d budget=%d",
|
|
873
|
-
|
|
1027
|
+
",".join(new_ids),
|
|
874
1028
|
content_len,
|
|
875
1029
|
budget,
|
|
876
1030
|
)
|
|
877
1031
|
|
|
878
|
-
|
|
1032
|
+
primary_candidate = candidates[0]
|
|
1033
|
+
combined_source_ids = self._merge_source_ids([*candidates, *existing_members])
|
|
879
1034
|
unified_row = UserPlaybook(
|
|
880
1035
|
user_playbook_id=0,
|
|
881
|
-
user_id=
|
|
882
|
-
agent_version=
|
|
1036
|
+
user_id=primary_candidate.user_id,
|
|
1037
|
+
agent_version=primary_candidate.agent_version,
|
|
883
1038
|
request_id=request_id,
|
|
884
|
-
playbook_name=
|
|
1039
|
+
playbook_name=primary_candidate.playbook_name,
|
|
885
1040
|
created_at=int(datetime.now(UTC).timestamp()),
|
|
886
1041
|
content=decision.content,
|
|
887
1042
|
trigger=decision.trigger,
|
|
888
1043
|
rationale=decision.rationale,
|
|
889
|
-
status=
|
|
890
|
-
source=
|
|
1044
|
+
status=primary_candidate.status,
|
|
1045
|
+
source=primary_candidate.source,
|
|
891
1046
|
source_interaction_ids=combined_source_ids,
|
|
892
1047
|
)
|
|
893
|
-
return [unified_row],
|
|
1048
|
+
return [unified_row], new_ids, merge_source_ids
|
|
894
1049
|
|
|
895
1050
|
def _apply_reject_new(
|
|
896
1051
|
self,
|
|
897
1052
|
decision: RejectNewDecision,
|
|
898
1053
|
*,
|
|
1054
|
+
candidates_by_id: dict[str, UserPlaybook],
|
|
899
1055
|
existing_by_id: dict[int, UserPlaybook],
|
|
900
1056
|
existing_by_position: dict[str, UserPlaybook],
|
|
901
1057
|
) -> tuple[list[UserPlaybook], list[str], list[int]]:
|
|
902
|
-
"""No-op apply: the existing row
|
|
1058
|
+
"""No-op apply: the existing row(s) win and the new candidate(s) dropped.
|
|
903
1059
|
|
|
904
|
-
Resolve
|
|
905
|
-
then as a DB ``user_playbook_id`` for backwards
|
|
906
|
-
|
|
907
|
-
malformed: we log a warning and return
|
|
908
|
-
|
|
909
|
-
extracted data.
|
|
1060
|
+
Resolve each superseding integer against the rendered ``EXISTING-N``
|
|
1061
|
+
position first, then as a DB ``user_playbook_id`` for backwards
|
|
1062
|
+
compatibility. If ANY referenced existing row does not resolve, the
|
|
1063
|
+
decision is treated as malformed: we log a warning and return
|
|
1064
|
+
``([], [], [])`` so the safety fallback re-inserts every named
|
|
1065
|
+
candidate rather than silently dropping extracted data.
|
|
910
1066
|
|
|
911
1067
|
Args:
|
|
912
1068
|
decision: The ``RejectNewDecision`` to apply.
|
|
1069
|
+
candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
|
|
913
1070
|
existing_by_id: Mapping ``user_playbook_id`` -> existing playbook,
|
|
914
|
-
used as a fallback for ``decision.
|
|
1071
|
+
used as a fallback for ``decision.superseded_by_existing_ids``.
|
|
915
1072
|
existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook.
|
|
916
1073
|
|
|
917
1074
|
Returns:
|
|
918
|
-
Tuple of ``([], [consumed NEW-N
|
|
919
|
-
resolves, or ``([], [], [])`` when
|
|
920
|
-
|
|
921
|
-
|
|
1075
|
+
Tuple of ``([], [consumed NEW-N ids], [])`` when every superseding
|
|
1076
|
+
id resolves, or ``([], [], [])`` when any is unknown. Never produces
|
|
1077
|
+
a merge group — the existing rows are kept as-is (no archive, no
|
|
1078
|
+
survivor).
|
|
1079
|
+
|
|
1080
|
+
Raises:
|
|
1081
|
+
KeyError: If any id in ``decision.new_ids`` does not resolve to a
|
|
1082
|
+
known candidate.
|
|
922
1083
|
"""
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1084
|
+
new_ids = decision.new_ids
|
|
1085
|
+
missing_new_ids = [
|
|
1086
|
+
new_id for new_id in new_ids if new_id not in candidates_by_id
|
|
1087
|
+
]
|
|
1088
|
+
if missing_new_ids:
|
|
1089
|
+
raise KeyError(f"reject_new references unknown NEW ids: {missing_new_ids}")
|
|
1090
|
+
|
|
1091
|
+
existing_ids = decision.superseded_by_existing_ids
|
|
1092
|
+
existing_members = [
|
|
1093
|
+
self._resolve_existing_reference(
|
|
1094
|
+
existing_id,
|
|
1095
|
+
existing_by_position=existing_by_position,
|
|
1096
|
+
existing_by_id=existing_by_id,
|
|
1097
|
+
)
|
|
1098
|
+
for existing_id in existing_ids
|
|
1099
|
+
]
|
|
1100
|
+
if any(existing is None for existing in existing_members):
|
|
929
1101
|
logger.warning(
|
|
930
|
-
"event=consolidation_reject_new_invalid new_id=%s existing_id=%
|
|
931
|
-
|
|
932
|
-
|
|
1102
|
+
"event=consolidation_reject_new_invalid new_id=%s existing_id=%s",
|
|
1103
|
+
",".join(new_ids),
|
|
1104
|
+
existing_ids,
|
|
933
1105
|
)
|
|
934
1106
|
return [], [], []
|
|
935
1107
|
logger.info(
|
|
936
|
-
"event=consolidation_reject_new new_id=%s existing_id=%
|
|
937
|
-
|
|
938
|
-
|
|
1108
|
+
"event=consolidation_reject_new new_id=%s existing_id=%s",
|
|
1109
|
+
",".join(new_ids),
|
|
1110
|
+
existing_ids,
|
|
939
1111
|
)
|
|
940
|
-
return [],
|
|
1112
|
+
return [], new_ids, []
|
|
941
1113
|
|
|
942
1114
|
def _apply_differentiate(
|
|
943
1115
|
self,
|
|
@@ -1028,10 +1200,13 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1028
1200
|
Tuple of ``([candidate row], [consumed NEW-N id], [])`` — no archive,
|
|
1029
1201
|
so never a merge group.
|
|
1030
1202
|
"""
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1203
|
+
rows: list[UserPlaybook] = []
|
|
1204
|
+
for new_id in decision.new_ids:
|
|
1205
|
+
candidate = candidates_by_id.get(new_id)
|
|
1206
|
+
if candidate is None:
|
|
1207
|
+
raise KeyError(f"independent references unknown NEW id: {new_id}")
|
|
1208
|
+
rows.append(candidate)
|
|
1209
|
+
return rows, decision.new_ids, []
|
|
1035
1210
|
|
|
1036
1211
|
@staticmethod
|
|
1037
1212
|
def _merge_source_ids(playbooks: list[UserPlaybook]) -> list[int]:
|
|
@@ -1104,8 +1279,14 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1104
1279
|
existing_by_id: Mapping ``user_playbook_id`` -> existing playbook.
|
|
1105
1280
|
"""
|
|
1106
1281
|
kind = decision.kind
|
|
1107
|
-
|
|
1108
|
-
|
|
1282
|
+
raw_new_id = getattr(decision, "new_id", "")
|
|
1283
|
+
new_ids = (
|
|
1284
|
+
_new_ids_from_field(raw_new_id)
|
|
1285
|
+
if isinstance(raw_new_id, (str, list))
|
|
1286
|
+
else []
|
|
1287
|
+
)
|
|
1288
|
+
new_id_label = ",".join(new_ids)
|
|
1289
|
+
new_pb = candidates_by_id.get(new_ids[0]) if new_ids else None
|
|
1109
1290
|
|
|
1110
1291
|
# UnifyDecision archives by position (EXISTING-{idx}) rather than a
|
|
1111
1292
|
# single existing_id; log a synthetic "multi" so the probe parser sees
|
|
@@ -1118,34 +1299,42 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1118
1299
|
"playbook_consolidation.decision kind=%s new_id=%s existing_id=%s "
|
|
1119
1300
|
"trigger_match=%s",
|
|
1120
1301
|
kind,
|
|
1121
|
-
|
|
1302
|
+
new_id_label,
|
|
1122
1303
|
existing_id_label,
|
|
1123
1304
|
"unknown",
|
|
1124
1305
|
)
|
|
1125
1306
|
return
|
|
1126
1307
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1308
|
+
existing_ids: list[int] = []
|
|
1309
|
+
if isinstance(decision, RejectNewDecision):
|
|
1310
|
+
existing_ids = decision.superseded_by_existing_ids
|
|
1311
|
+
elif isinstance(decision, DifferentiateDecision):
|
|
1312
|
+
existing_ids = [decision.existing_id]
|
|
1313
|
+
|
|
1314
|
+
existing_pb = (
|
|
1315
|
+
PlaybookConsolidator._resolve_existing_reference(
|
|
1316
|
+
existing_ids[0],
|
|
1317
|
+
existing_by_position=existing_by_position,
|
|
1318
|
+
existing_by_id=existing_by_id,
|
|
1319
|
+
)
|
|
1320
|
+
if existing_ids
|
|
1321
|
+
else None
|
|
1138
1322
|
)
|
|
1139
1323
|
trigger_match = (
|
|
1140
1324
|
new_pb is not None
|
|
1141
1325
|
and existing_pb is not None
|
|
1142
1326
|
and new_pb.trigger == existing_pb.trigger
|
|
1143
1327
|
)
|
|
1328
|
+
existing_id_label = (
|
|
1329
|
+
",".join(str(existing_id) for existing_id in existing_ids)
|
|
1330
|
+
if existing_ids
|
|
1331
|
+
else "none"
|
|
1332
|
+
)
|
|
1144
1333
|
logger.info(
|
|
1145
1334
|
"playbook_consolidation.decision kind=%s new_id=%s existing_id=%s "
|
|
1146
1335
|
"trigger_match=%s",
|
|
1147
1336
|
kind,
|
|
1148
|
-
|
|
1149
|
-
|
|
1337
|
+
new_id_label,
|
|
1338
|
+
existing_id_label,
|
|
1150
1339
|
str(trigger_match).lower(),
|
|
1151
1340
|
)
|
|
@@ -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)
|