claude-smart 0.2.49 → 0.2.50
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 +2 -2
- package/README.md +10 -43
- package/bin/claude-smart.js +105 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +197 -1
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +757 -119
- package/plugin/scripts/codex-hook.js +63 -225
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +117 -136
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/smart-install.sh +15 -1
- package/plugin/src/claude_smart/cli.py +14 -0
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +4 -1
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -13,7 +13,6 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
13
13
|
from reflexio.models.api_schema.retriever_schema import SearchUserPlaybookRequest
|
|
14
14
|
from reflexio.models.api_schema.service_schemas import UserPlaybook
|
|
15
15
|
from reflexio.models.config_schema import (
|
|
16
|
-
EMBEDDING_DIMENSIONS,
|
|
17
16
|
DeduplicationConfig,
|
|
18
17
|
SearchOptions,
|
|
19
18
|
)
|
|
@@ -23,6 +22,10 @@ from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
|
23
22
|
from reflexio.server.services.deduplication_utils import (
|
|
24
23
|
BaseDeduplicator,
|
|
25
24
|
format_dedup_timestamp,
|
|
25
|
+
resolve_dedup_query_embeddings,
|
|
26
|
+
)
|
|
27
|
+
from reflexio.server.services.profile.profile_generation_service_utils import (
|
|
28
|
+
check_string_token_overlap,
|
|
26
29
|
)
|
|
27
30
|
from reflexio.server.tracing import sentry_tags
|
|
28
31
|
|
|
@@ -191,6 +194,22 @@ def _existing_id_log_label(value: ExistingIdField) -> str:
|
|
|
191
194
|
return str(value)
|
|
192
195
|
|
|
193
196
|
|
|
197
|
+
def _normalize_generation_request_id(
|
|
198
|
+
generation_request_id: str | None,
|
|
199
|
+
*,
|
|
200
|
+
request_id: str | None = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
if generation_request_id is not None:
|
|
203
|
+
if request_id is not None and request_id != generation_request_id:
|
|
204
|
+
raise TypeError(
|
|
205
|
+
"generation_request_id and request_id must match when both are provided"
|
|
206
|
+
)
|
|
207
|
+
return generation_request_id
|
|
208
|
+
if request_id is not None:
|
|
209
|
+
return request_id
|
|
210
|
+
raise TypeError("generation_request_id is required")
|
|
211
|
+
|
|
212
|
+
|
|
194
213
|
class UnifyDecision(BaseModel):
|
|
195
214
|
"""Collapse NEW (+ 0..N EXISTING) into one row with LLM-supplied content.
|
|
196
215
|
|
|
@@ -379,6 +398,50 @@ _COUNTER_BY_KIND: dict[str, str] = {
|
|
|
379
398
|
}
|
|
380
399
|
|
|
381
400
|
|
|
401
|
+
def _decision_new_ids(decision: ConsolidationDecision) -> list[str]:
|
|
402
|
+
if isinstance(decision, DifferentiateDecision):
|
|
403
|
+
return [decision.new_id]
|
|
404
|
+
return decision.new_ids
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def validate_consolidation_output(
|
|
408
|
+
new_playbooks: list[UserPlaybook],
|
|
409
|
+
output: PlaybookConsolidationOutput,
|
|
410
|
+
) -> list[str]:
|
|
411
|
+
"""Return partition-contract errors for a parsed consolidation output."""
|
|
412
|
+
known_new_ids = {f"NEW-{idx}" for idx in range(len(new_playbooks))}
|
|
413
|
+
seen_by_id: dict[str, list[str]] = {}
|
|
414
|
+
errors: list[str] = []
|
|
415
|
+
|
|
416
|
+
for decision_index, decision in enumerate(output.decisions):
|
|
417
|
+
label = f"decision[{decision_index}] {decision.kind}"
|
|
418
|
+
for new_id in _decision_new_ids(decision):
|
|
419
|
+
if new_id not in known_new_ids:
|
|
420
|
+
errors.append(f"{label} references unknown NEW id {new_id}.")
|
|
421
|
+
continue
|
|
422
|
+
seen_by_id.setdefault(new_id, []).append(label)
|
|
423
|
+
|
|
424
|
+
missing = sorted(known_new_ids - set(seen_by_id))
|
|
425
|
+
if missing:
|
|
426
|
+
errors.append(
|
|
427
|
+
"Every NEW id must appear exactly once; missing NEW ids: "
|
|
428
|
+
+ ", ".join(missing)
|
|
429
|
+
+ "."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
duplicates = {
|
|
433
|
+
new_id: labels for new_id, labels in seen_by_id.items() if len(labels) > 1
|
|
434
|
+
}
|
|
435
|
+
for new_id, labels in sorted(duplicates.items()):
|
|
436
|
+
errors.append(
|
|
437
|
+
f"Every NEW id must appear exactly once; {new_id} appears in "
|
|
438
|
+
+ "; ".join(labels)
|
|
439
|
+
+ "."
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return errors
|
|
443
|
+
|
|
444
|
+
|
|
382
445
|
class PlaybookConsolidator(BaseDeduplicator):
|
|
383
446
|
"""
|
|
384
447
|
Consolidates new user playbook entries against each other and against existing entries
|
|
@@ -498,15 +561,12 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
498
561
|
if not query_texts:
|
|
499
562
|
return []
|
|
500
563
|
|
|
501
|
-
#
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
logger.warning("Failed to generate embeddings for dedup search: %s", e)
|
|
508
|
-
# Fall back to text-only search
|
|
509
|
-
embeddings = [None] * len(query_texts)
|
|
564
|
+
# Embed dedup queries with the same model that indexed the store —
|
|
565
|
+
# see resolve_dedup_query_embeddings for why the client's default
|
|
566
|
+
# embedding model must not be used here.
|
|
567
|
+
embeddings = resolve_dedup_query_embeddings(
|
|
568
|
+
storage, self.client, query_texts, entity_label="Playbook"
|
|
569
|
+
)
|
|
510
570
|
|
|
511
571
|
# Search for each new entry
|
|
512
572
|
seen_ids: set[int] = set()
|
|
@@ -565,6 +625,36 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
565
625
|
)
|
|
566
626
|
return new_text, existing_text
|
|
567
627
|
|
|
628
|
+
def _render_consolidation_prompt(
|
|
629
|
+
self,
|
|
630
|
+
new_playbooks: list[UserPlaybook],
|
|
631
|
+
existing_playbooks: list[UserPlaybook],
|
|
632
|
+
) -> str:
|
|
633
|
+
"""Render the consolidation prompt for the given NEW + EXISTING rows.
|
|
634
|
+
|
|
635
|
+
Rendering is deterministic, so the repair path calls this again to
|
|
636
|
+
reconstruct the exact first-turn prompt when building the follow-up
|
|
637
|
+
conversation.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
new_playbooks: New entries to deduplicate.
|
|
641
|
+
existing_playbooks: Existing entries from the database.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
The fully rendered consolidation prompt.
|
|
645
|
+
"""
|
|
646
|
+
new_text, existing_text = self._format_new_and_existing_for_prompt(
|
|
647
|
+
new_playbooks, existing_playbooks
|
|
648
|
+
)
|
|
649
|
+
return self.request_context.prompt_manager.render_prompt(
|
|
650
|
+
self._get_prompt_id(),
|
|
651
|
+
{
|
|
652
|
+
"new_playbook_count": len(new_playbooks),
|
|
653
|
+
"new_playbooks": new_text,
|
|
654
|
+
"existing_playbooks": existing_text,
|
|
655
|
+
},
|
|
656
|
+
)
|
|
657
|
+
|
|
568
658
|
def _consolidation_decisions(
|
|
569
659
|
self,
|
|
570
660
|
new_playbooks: list[UserPlaybook],
|
|
@@ -605,20 +695,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
605
695
|
Parsed ``PlaybookConsolidationOutput``; an empty output (no
|
|
606
696
|
decisions) if the LLM returned an unexpected response shape.
|
|
607
697
|
"""
|
|
608
|
-
|
|
609
|
-
new_text, existing_text = self._format_new_and_existing_for_prompt(
|
|
610
|
-
new_playbooks, existing_playbooks
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
# Build and call LLM
|
|
614
|
-
prompt = self.request_context.prompt_manager.render_prompt(
|
|
615
|
-
self._get_prompt_id(),
|
|
616
|
-
{
|
|
617
|
-
"new_playbook_count": len(new_playbooks),
|
|
618
|
-
"new_playbooks": new_text,
|
|
619
|
-
"existing_playbooks": existing_text,
|
|
620
|
-
},
|
|
621
|
-
)
|
|
698
|
+
prompt = self._render_consolidation_prompt(new_playbooks, existing_playbooks)
|
|
622
699
|
|
|
623
700
|
output_schema_class = self._get_output_schema_class()
|
|
624
701
|
|
|
@@ -653,16 +730,18 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
653
730
|
def deduplicate(
|
|
654
731
|
self,
|
|
655
732
|
results: list[list[UserPlaybook]],
|
|
656
|
-
|
|
657
|
-
agent_version: str,
|
|
733
|
+
generation_request_id: str | None = None,
|
|
734
|
+
agent_version: str | None = None,
|
|
658
735
|
user_id: str | None = None,
|
|
736
|
+
*,
|
|
737
|
+
request_id: str | None = None,
|
|
659
738
|
) -> tuple[list[UserPlaybook], list[int], list[tuple[int, list[int]]]]:
|
|
660
739
|
"""
|
|
661
740
|
Consolidate user playbook entries across extractors and against existing entries in DB.
|
|
662
741
|
|
|
663
742
|
Args:
|
|
664
743
|
results: List of entry lists from extractors (each extractor returns list[UserPlaybook])
|
|
665
|
-
|
|
744
|
+
generation_request_id: Request ID for context
|
|
666
745
|
agent_version: Agent version for context
|
|
667
746
|
user_id: Optional user ID to scope the existing entry search
|
|
668
747
|
|
|
@@ -680,6 +759,12 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
680
759
|
non-merge archives such as ``differentiate``'s split source); the
|
|
681
760
|
caller subtracts the merge-covered ids to find pure-delete leftovers.
|
|
682
761
|
"""
|
|
762
|
+
generation_request_id = _normalize_generation_request_id(
|
|
763
|
+
generation_request_id, request_id=request_id
|
|
764
|
+
)
|
|
765
|
+
if agent_version is None:
|
|
766
|
+
raise TypeError("agent_version is required")
|
|
767
|
+
|
|
683
768
|
# Check if mock mode is enabled
|
|
684
769
|
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
|
|
685
770
|
logger.info("Mock mode: skipping consolidation")
|
|
@@ -714,7 +799,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
714
799
|
op="identify_duplicates",
|
|
715
800
|
org_id=self.request_context.org_id,
|
|
716
801
|
user_id=user_id,
|
|
717
|
-
request_id=
|
|
802
|
+
request_id=generation_request_id,
|
|
718
803
|
agent_version=agent_version,
|
|
719
804
|
error_type=type(e).__name__,
|
|
720
805
|
):
|
|
@@ -723,14 +808,21 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
723
808
|
|
|
724
809
|
if not dedup_output.decisions:
|
|
725
810
|
logger.info(
|
|
726
|
-
"No consolidation decisions returned for request %s",
|
|
811
|
+
"No consolidation decisions returned for request %s",
|
|
812
|
+
generation_request_id,
|
|
727
813
|
)
|
|
728
814
|
return new_playbooks, [], []
|
|
729
815
|
|
|
730
816
|
logger.info(
|
|
731
817
|
"Received %d consolidation decisions for request %s",
|
|
732
818
|
len(dedup_output.decisions),
|
|
733
|
-
|
|
819
|
+
generation_request_id,
|
|
820
|
+
)
|
|
821
|
+
dedup_output = self._repair_consolidation_output_if_needed(
|
|
822
|
+
new_playbooks=new_playbooks,
|
|
823
|
+
existing_playbooks=existing_playbooks,
|
|
824
|
+
dedup_output=dedup_output,
|
|
825
|
+
request_id=request_id,
|
|
734
826
|
)
|
|
735
827
|
|
|
736
828
|
# Build consolidated result via the discriminated-union apply path
|
|
@@ -738,10 +830,215 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
738
830
|
new_playbooks=new_playbooks,
|
|
739
831
|
existing_playbooks=existing_playbooks,
|
|
740
832
|
dedup_output=dedup_output,
|
|
741
|
-
|
|
833
|
+
generation_request_id=generation_request_id,
|
|
742
834
|
agent_version=agent_version,
|
|
743
835
|
)
|
|
744
836
|
|
|
837
|
+
def _repair_consolidation_output_if_needed(
|
|
838
|
+
self,
|
|
839
|
+
*,
|
|
840
|
+
new_playbooks: list[UserPlaybook],
|
|
841
|
+
existing_playbooks: list[UserPlaybook],
|
|
842
|
+
dedup_output: PlaybookConsolidationOutput,
|
|
843
|
+
request_id: str | None,
|
|
844
|
+
) -> PlaybookConsolidationOutput:
|
|
845
|
+
errors = validate_consolidation_output(new_playbooks, dedup_output)
|
|
846
|
+
if not errors:
|
|
847
|
+
errors = self._find_suspicious_under_consumed_new_rows(
|
|
848
|
+
new_playbooks, dedup_output
|
|
849
|
+
)
|
|
850
|
+
if not errors:
|
|
851
|
+
return dedup_output
|
|
852
|
+
|
|
853
|
+
repaired = self._repair_consolidation_output(
|
|
854
|
+
original_prompt=self._render_consolidation_prompt(
|
|
855
|
+
new_playbooks, existing_playbooks
|
|
856
|
+
),
|
|
857
|
+
invalid_output=dedup_output,
|
|
858
|
+
errors=errors,
|
|
859
|
+
request_id=request_id,
|
|
860
|
+
)
|
|
861
|
+
if repaired is None:
|
|
862
|
+
return dedup_output
|
|
863
|
+
|
|
864
|
+
repaired_errors = validate_consolidation_output(new_playbooks, repaired)
|
|
865
|
+
if repaired_errors:
|
|
866
|
+
logger.warning(
|
|
867
|
+
"event=consolidation_repair_failed request_id=%s errors=%s",
|
|
868
|
+
request_id,
|
|
869
|
+
repaired_errors,
|
|
870
|
+
)
|
|
871
|
+
return dedup_output
|
|
872
|
+
|
|
873
|
+
logger.info(
|
|
874
|
+
"event=consolidation_repaired request_id=%s decision_count=%d",
|
|
875
|
+
request_id,
|
|
876
|
+
len(repaired.decisions),
|
|
877
|
+
)
|
|
878
|
+
return repaired
|
|
879
|
+
|
|
880
|
+
def _repair_consolidation_output(
|
|
881
|
+
self,
|
|
882
|
+
*,
|
|
883
|
+
original_prompt: str,
|
|
884
|
+
invalid_output: PlaybookConsolidationOutput,
|
|
885
|
+
errors: list[str],
|
|
886
|
+
request_id: str | None,
|
|
887
|
+
) -> PlaybookConsolidationOutput | None:
|
|
888
|
+
"""Ask the model to fix its consolidation output in a follow-up turn.
|
|
889
|
+
|
|
890
|
+
The repair is sent as the third turn of the original conversation —
|
|
891
|
+
[user: consolidation prompt, assistant: the invalid decisions, user:
|
|
892
|
+
validation errors + fix instruction] — so the model retains the full
|
|
893
|
+
first-turn context (rendered rows, decision-kind rules, output format)
|
|
894
|
+
without restating any of it.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
original_prompt: The exact rendered first-turn consolidation prompt.
|
|
898
|
+
invalid_output: The parsed decisions that failed validation.
|
|
899
|
+
errors: Human-readable validation errors to feed back.
|
|
900
|
+
request_id: Request ID for log correlation.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
The repaired ``PlaybookConsolidationOutput``, or None when the
|
|
904
|
+
repair call fails or returns an unexpected shape.
|
|
905
|
+
"""
|
|
906
|
+
logger.warning(
|
|
907
|
+
"event=consolidation_repair_attempted request_id=%s errors=%s",
|
|
908
|
+
request_id,
|
|
909
|
+
errors,
|
|
910
|
+
)
|
|
911
|
+
followup = "\n".join(
|
|
912
|
+
[
|
|
913
|
+
"Your decisions above violate the NEW-id coverage contract:",
|
|
914
|
+
"",
|
|
915
|
+
"\n".join(f"- {error}" for error in errors),
|
|
916
|
+
"",
|
|
917
|
+
"Return corrected decisions that cover every NEW id exactly once. "
|
|
918
|
+
"If a unified rule uses facts/details from multiple NEW rows, `new_id` must list all of them. "
|
|
919
|
+
"If your decisions are semantically correct apart from these errors, "
|
|
920
|
+
"keep them and fix only the coverage.",
|
|
921
|
+
"",
|
|
922
|
+
'Respond ONLY with valid JSON matching PlaybookConsolidationOutput: {"decisions": [...]}.',
|
|
923
|
+
]
|
|
924
|
+
)
|
|
925
|
+
messages = [
|
|
926
|
+
{"role": "user", "content": original_prompt},
|
|
927
|
+
{"role": "assistant", "content": invalid_output.model_dump_json()},
|
|
928
|
+
{"role": "user", "content": followup},
|
|
929
|
+
]
|
|
930
|
+
|
|
931
|
+
from reflexio.server.services.service_utils import (
|
|
932
|
+
log_llm_messages,
|
|
933
|
+
log_model_response,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
log_llm_messages(logger, "Playbook consolidation repair", messages)
|
|
937
|
+
|
|
938
|
+
try:
|
|
939
|
+
response = self.client.generate_chat_response(
|
|
940
|
+
messages=messages,
|
|
941
|
+
model=self.model_name,
|
|
942
|
+
response_format=self._get_output_schema_class(),
|
|
943
|
+
)
|
|
944
|
+
except Exception as exc: # noqa: BLE001 — repair is best-effort
|
|
945
|
+
logger.warning(
|
|
946
|
+
"event=consolidation_repair_failed request_id=%s error_type=%s",
|
|
947
|
+
request_id,
|
|
948
|
+
type(exc).__name__,
|
|
949
|
+
exc_info=True,
|
|
950
|
+
)
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
log_model_response(logger, "Consolidation repair response", response)
|
|
954
|
+
|
|
955
|
+
if not isinstance(response, PlaybookConsolidationOutput):
|
|
956
|
+
logger.warning(
|
|
957
|
+
"event=consolidation_repair_failed request_id=%s unexpected_type=%s",
|
|
958
|
+
request_id,
|
|
959
|
+
type(response),
|
|
960
|
+
)
|
|
961
|
+
return None
|
|
962
|
+
return response
|
|
963
|
+
|
|
964
|
+
@staticmethod
|
|
965
|
+
def _find_suspicious_under_consumed_new_rows(
|
|
966
|
+
new_playbooks: list[UserPlaybook],
|
|
967
|
+
dedup_output: PlaybookConsolidationOutput,
|
|
968
|
+
) -> list[str]:
|
|
969
|
+
candidates_by_id = {
|
|
970
|
+
f"NEW-{idx}": playbook for idx, playbook in enumerate(new_playbooks)
|
|
971
|
+
}
|
|
972
|
+
# Every kind except reject_new stores a row for its consumed candidates
|
|
973
|
+
# (raw for independent, refined-trigger copy for differentiate, the
|
|
974
|
+
# re-synthesized decision content for a sibling unify), so any of them
|
|
975
|
+
# can duplicate a unify's merged row. Only reject_new drops the row
|
|
976
|
+
# outright and is never a duplicate-storage risk worth a repair call.
|
|
977
|
+
consuming_kind_by_new_id = {
|
|
978
|
+
new_id: decision.kind
|
|
979
|
+
for decision in dedup_output.decisions
|
|
980
|
+
for new_id in _decision_new_ids(decision)
|
|
981
|
+
}
|
|
982
|
+
source_ids_by_decision = [
|
|
983
|
+
{
|
|
984
|
+
source_id
|
|
985
|
+
for new_id in _decision_new_ids(decision)
|
|
986
|
+
if new_id in candidates_by_id
|
|
987
|
+
for source_id in candidates_by_id[new_id].source_interaction_ids
|
|
988
|
+
}
|
|
989
|
+
for decision in dedup_output.decisions
|
|
990
|
+
]
|
|
991
|
+
errors: list[str] = []
|
|
992
|
+
|
|
993
|
+
for decision_index, decision in enumerate(dedup_output.decisions):
|
|
994
|
+
if not isinstance(decision, UnifyDecision):
|
|
995
|
+
continue
|
|
996
|
+
consumed_ids = set(decision.new_ids)
|
|
997
|
+
consumed_source_ids = source_ids_by_decision[decision_index]
|
|
998
|
+
if not consumed_source_ids:
|
|
999
|
+
continue
|
|
1000
|
+
|
|
1001
|
+
# Candidates stored (near-)verbatim by another decision whose facts
|
|
1002
|
+
# this unify's merged content may have absorbed.
|
|
1003
|
+
for other_id, other in candidates_by_id.items():
|
|
1004
|
+
if other_id in consumed_ids:
|
|
1005
|
+
continue
|
|
1006
|
+
if consuming_kind_by_new_id.get(other_id) not in (
|
|
1007
|
+
"independent",
|
|
1008
|
+
"differentiate",
|
|
1009
|
+
):
|
|
1010
|
+
continue
|
|
1011
|
+
if not consumed_source_ids.intersection(other.source_interaction_ids):
|
|
1012
|
+
continue
|
|
1013
|
+
if check_string_token_overlap(decision.content, other.content):
|
|
1014
|
+
errors.append(
|
|
1015
|
+
f"decision[{decision_index}] unify likely absorbed facts from {other_id} but did not list it in new_id; "
|
|
1016
|
+
"if the unified rule absorbed this row's facts, add its id to new_id; "
|
|
1017
|
+
"if not, leave the decisions unchanged."
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Sibling unify decisions each persist their own survivor row, so
|
|
1021
|
+
# two same-source unifies with heavily overlapping FINAL contents
|
|
1022
|
+
# are the same duplicate-storage class. Compare stored content to
|
|
1023
|
+
# stored content and flag each pair once (j > i).
|
|
1024
|
+
for sibling_index in range(decision_index + 1, len(dedup_output.decisions)):
|
|
1025
|
+
sibling = dedup_output.decisions[sibling_index]
|
|
1026
|
+
if not isinstance(sibling, UnifyDecision):
|
|
1027
|
+
continue
|
|
1028
|
+
if not consumed_source_ids.intersection(
|
|
1029
|
+
source_ids_by_decision[sibling_index]
|
|
1030
|
+
):
|
|
1031
|
+
continue
|
|
1032
|
+
if check_string_token_overlap(decision.content, sibling.content):
|
|
1033
|
+
errors.append(
|
|
1034
|
+
f"decision[{decision_index}] and decision[{sibling_index}] are both unify decisions over "
|
|
1035
|
+
"same-source NEW rows with highly overlapping content; if they describe the same skill, "
|
|
1036
|
+
"merge them into one unify decision whose new_id lists every consumed NEW id; "
|
|
1037
|
+
"if they are genuinely distinct skills, leave the decisions unchanged."
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
return errors
|
|
1041
|
+
|
|
745
1042
|
# ===============================
|
|
746
1043
|
# Apply path: discriminated-union decisions -> (new rows, archive ids)
|
|
747
1044
|
# ===============================
|
|
@@ -751,8 +1048,10 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
751
1048
|
new_playbooks: list[UserPlaybook],
|
|
752
1049
|
existing_playbooks: list[UserPlaybook],
|
|
753
1050
|
dedup_output: PlaybookConsolidationOutput,
|
|
754
|
-
|
|
755
|
-
agent_version: str, # noqa: ARG002
|
|
1051
|
+
generation_request_id: str | None = None,
|
|
1052
|
+
agent_version: str | None = None, # noqa: ARG002
|
|
1053
|
+
*,
|
|
1054
|
+
request_id: str | None = None,
|
|
756
1055
|
) -> tuple[list[UserPlaybook], list[int], list[tuple[int, list[int]]]]:
|
|
757
1056
|
"""
|
|
758
1057
|
Build the deduplicated entry list from LLM decisions.
|
|
@@ -766,7 +1065,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
766
1065
|
new_playbooks: Flattened list of new (candidate) entries.
|
|
767
1066
|
existing_playbooks: List of existing entries from the DB.
|
|
768
1067
|
dedup_output: LLM decisions output (discriminated union).
|
|
769
|
-
|
|
1068
|
+
generation_request_id: Request ID stamped onto newly-built rows.
|
|
770
1069
|
agent_version: Agent version (currently unused, kept for symmetry).
|
|
771
1070
|
|
|
772
1071
|
Returns:
|
|
@@ -781,6 +1080,10 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
781
1080
|
two rows (no single survivor), so its archived id appears in the
|
|
782
1081
|
delete set but NOT in any merge group.
|
|
783
1082
|
"""
|
|
1083
|
+
generation_request_id = _normalize_generation_request_id(
|
|
1084
|
+
generation_request_id, request_id=request_id
|
|
1085
|
+
)
|
|
1086
|
+
|
|
784
1087
|
candidates_by_id = {
|
|
785
1088
|
f"NEW-{idx}": playbook for idx, playbook in enumerate(new_playbooks)
|
|
786
1089
|
}
|
|
@@ -810,7 +1113,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
810
1113
|
existing_by_position=existing_by_position,
|
|
811
1114
|
archive_ids=archive_ids,
|
|
812
1115
|
seen_archive=seen_archive,
|
|
813
|
-
|
|
1116
|
+
generation_request_id=generation_request_id,
|
|
814
1117
|
)
|
|
815
1118
|
except Exception as exc: # noqa: BLE001 — per-decision isolation
|
|
816
1119
|
result_counters.failed_count += 1
|
|
@@ -834,7 +1137,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
834
1137
|
subsystem="playbook_consolidator",
|
|
835
1138
|
op="apply_decision",
|
|
836
1139
|
org_id=self.request_context.org_id,
|
|
837
|
-
request_id=
|
|
1140
|
+
request_id=generation_request_id,
|
|
838
1141
|
kind=decision.kind,
|
|
839
1142
|
new_id=new_id_str,
|
|
840
1143
|
existing_id=existing_id_str,
|
|
@@ -890,7 +1193,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
890
1193
|
existing_by_position: dict[str, UserPlaybook],
|
|
891
1194
|
archive_ids: list[int],
|
|
892
1195
|
seen_archive: set[int],
|
|
893
|
-
|
|
1196
|
+
generation_request_id: str,
|
|
894
1197
|
) -> tuple[list[UserPlaybook], list[str], list[int]]:
|
|
895
1198
|
"""Dispatch a single decision to its kind-specific apply method.
|
|
896
1199
|
|
|
@@ -904,7 +1207,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
904
1207
|
archive_ids: Accumulator list mutated with ids to archive/delete.
|
|
905
1208
|
seen_archive: Accumulator set guarding ``archive_ids`` against
|
|
906
1209
|
duplicate ids.
|
|
907
|
-
|
|
1210
|
+
generation_request_id: Request ID stamped onto newly-built rows.
|
|
908
1211
|
|
|
909
1212
|
Returns:
|
|
910
1213
|
Tuple of ``(rows_to_insert, handled_new_ids, merge_source_ids)``.
|
|
@@ -923,7 +1226,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
923
1226
|
existing_by_position=existing_by_position,
|
|
924
1227
|
archive_ids=archive_ids,
|
|
925
1228
|
seen_archive=seen_archive,
|
|
926
|
-
|
|
1229
|
+
generation_request_id=generation_request_id,
|
|
927
1230
|
)
|
|
928
1231
|
if isinstance(decision, RejectNewDecision):
|
|
929
1232
|
return self._apply_reject_new(
|
|
@@ -940,10 +1243,14 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
940
1243
|
existing_by_position=existing_by_position,
|
|
941
1244
|
archive_ids=archive_ids,
|
|
942
1245
|
seen_archive=seen_archive,
|
|
943
|
-
|
|
1246
|
+
generation_request_id=generation_request_id,
|
|
944
1247
|
)
|
|
945
1248
|
if isinstance(decision, IndependentDecision):
|
|
946
|
-
return self._apply_independent(
|
|
1249
|
+
return self._apply_independent(
|
|
1250
|
+
decision,
|
|
1251
|
+
candidates_by_id=candidates_by_id,
|
|
1252
|
+
generation_request_id=generation_request_id,
|
|
1253
|
+
)
|
|
947
1254
|
raise ValueError(f"unknown decision kind: {decision}")
|
|
948
1255
|
|
|
949
1256
|
def _apply_unify(
|
|
@@ -954,7 +1261,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
954
1261
|
existing_by_position: dict[str, UserPlaybook],
|
|
955
1262
|
archive_ids: list[int],
|
|
956
1263
|
seen_archive: set[int],
|
|
957
|
-
|
|
1264
|
+
generation_request_id: str,
|
|
958
1265
|
) -> tuple[list[UserPlaybook], list[str], list[int]]:
|
|
959
1266
|
"""Collapse / compose NEW (+ 0..N EXISTING) into one row.
|
|
960
1267
|
|
|
@@ -974,7 +1281,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
974
1281
|
existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook.
|
|
975
1282
|
archive_ids: Accumulator mutated with EXISTING ids to archive.
|
|
976
1283
|
seen_archive: Dedup set for ``archive_ids``.
|
|
977
|
-
|
|
1284
|
+
generation_request_id: Request ID stamped on the unified row.
|
|
978
1285
|
|
|
979
1286
|
Returns:
|
|
980
1287
|
Tuple of ``([unified_row], [consumed NEW-N ids], merge_source_ids)``
|
|
@@ -1035,7 +1342,8 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1035
1342
|
user_playbook_id=0,
|
|
1036
1343
|
user_id=primary_candidate.user_id,
|
|
1037
1344
|
agent_version=primary_candidate.agent_version,
|
|
1038
|
-
|
|
1345
|
+
# Legacy storage/API field; value may be synthetic for manual/rerun flows.
|
|
1346
|
+
request_id=generation_request_id,
|
|
1039
1347
|
playbook_name=primary_candidate.playbook_name,
|
|
1040
1348
|
created_at=int(datetime.now(UTC).timestamp()),
|
|
1041
1349
|
content=decision.content,
|
|
@@ -1120,7 +1428,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1120
1428
|
existing_by_position: dict[str, UserPlaybook],
|
|
1121
1429
|
archive_ids: list[int],
|
|
1122
1430
|
seen_archive: set[int],
|
|
1123
|
-
|
|
1431
|
+
generation_request_id: str,
|
|
1124
1432
|
) -> tuple[list[UserPlaybook], list[str], list[int]]:
|
|
1125
1433
|
"""Archive the existing row and emit two refined rows in its place.
|
|
1126
1434
|
|
|
@@ -1136,7 +1444,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1136
1444
|
existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook.
|
|
1137
1445
|
archive_ids: Accumulator mutated with the existing id to archive.
|
|
1138
1446
|
seen_archive: Dedup set for ``archive_ids``.
|
|
1139
|
-
|
|
1447
|
+
generation_request_id: Request ID stamped on both new rows.
|
|
1140
1448
|
|
|
1141
1449
|
Returns:
|
|
1142
1450
|
Tuple of ``([refined_new_row, refined_existing_row], [NEW-N id],
|
|
@@ -1168,7 +1476,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1168
1476
|
refined_candidate = candidate.model_copy(
|
|
1169
1477
|
update={
|
|
1170
1478
|
"user_playbook_id": 0,
|
|
1171
|
-
"request_id":
|
|
1479
|
+
"request_id": generation_request_id,
|
|
1172
1480
|
"trigger": decision.refined_new_trigger,
|
|
1173
1481
|
"created_at": now_ts,
|
|
1174
1482
|
}
|
|
@@ -1176,7 +1484,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1176
1484
|
refined_existing = existing.model_copy(
|
|
1177
1485
|
update={
|
|
1178
1486
|
"user_playbook_id": 0,
|
|
1179
|
-
"request_id":
|
|
1487
|
+
"request_id": generation_request_id,
|
|
1180
1488
|
"trigger": decision.refined_existing_trigger,
|
|
1181
1489
|
"created_at": now_ts,
|
|
1182
1490
|
"source_interaction_ids": list(existing.source_interaction_ids),
|
|
@@ -1189,6 +1497,7 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1189
1497
|
decision: IndependentDecision,
|
|
1190
1498
|
*,
|
|
1191
1499
|
candidates_by_id: dict[str, UserPlaybook],
|
|
1500
|
+
generation_request_id: str,
|
|
1192
1501
|
) -> tuple[list[UserPlaybook], list[str], list[int]]:
|
|
1193
1502
|
"""Insert the new candidate unchanged; no archive.
|
|
1194
1503
|
|
|
@@ -1205,7 +1514,9 @@ class PlaybookConsolidator(BaseDeduplicator):
|
|
|
1205
1514
|
candidate = candidates_by_id.get(new_id)
|
|
1206
1515
|
if candidate is None:
|
|
1207
1516
|
raise KeyError(f"independent references unknown NEW id: {new_id}")
|
|
1208
|
-
rows.append(
|
|
1517
|
+
rows.append(
|
|
1518
|
+
candidate.model_copy(update={"request_id": generation_request_id})
|
|
1519
|
+
)
|
|
1209
1520
|
return rows, decision.new_ids, []
|
|
1210
1521
|
|
|
1211
1522
|
@staticmethod
|