claude-smart 0.2.42 → 0.2.44
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 +3 -3
- package/README.md +1 -1
- package/bin/claude-smart.js +2 -2
- package/package.json +9 -3
- package/plugin/.claude-plugin/plugin.json +9 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +23 -3
- package/plugin/pyproject.toml +3 -3
- package/plugin/scripts/_lib.sh +91 -0
- package/plugin/scripts/backend-service.sh +51 -4
- package/plugin/scripts/cli.sh +3 -1
- package/plugin/scripts/codex-hook.js +72 -4
- package/plugin/scripts/dashboard-build.sh +1 -0
- package/plugin/scripts/dashboard-service.sh +1 -0
- package/plugin/scripts/ensure-plugin-root.sh +1 -0
- package/plugin/scripts/hook_entry.sh +6 -3
- package/plugin/scripts/smart-install.sh +3 -2
- package/plugin/src/README.md +57 -0
- package/plugin/src/claude_smart/context_format.py +11 -12
- package/plugin/src/claude_smart/cs_cite.py +26 -12
- package/plugin/src/claude_smart/ids.py +13 -5
- package/plugin/uv.lock +126 -5
- package/plugin/vendor/reflexio/.env.example +62 -0
- package/plugin/vendor/reflexio/LICENSE +201 -0
- package/plugin/vendor/reflexio/README.md +338 -0
- package/plugin/vendor/reflexio/pyproject.toml +274 -0
- package/plugin/vendor/reflexio/reflexio/README.md +184 -0
- package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
- package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
- package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
- package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +265 -0
- package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +964 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
- package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
- package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
- package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
- package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
- package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
- package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
- package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
- package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
- package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
- package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
- package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
- package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1112 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +768 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +333 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
- package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +622 -0
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +2868 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +562 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
- package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +359 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1871 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +140 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +479 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +338 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +187 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +716 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +283 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +137 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +336 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1668 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +358 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +535 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +779 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +94 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +702 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +1045 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +772 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +737 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +803 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +80 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +756 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +384 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +572 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
- package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
- package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
- package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Playbook consolidation service that merges duplicate user playbook entries using LLM
|
|
3
|
+
and hybrid search against existing entries in the database.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from typing import Annotated, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
12
|
+
|
|
13
|
+
from reflexio.models.api_schema.retriever_schema import SearchUserPlaybookRequest
|
|
14
|
+
from reflexio.models.api_schema.service_schemas import UserPlaybook
|
|
15
|
+
from reflexio.models.config_schema import (
|
|
16
|
+
EMBEDDING_DIMENSIONS,
|
|
17
|
+
DeduplicationConfig,
|
|
18
|
+
SearchOptions,
|
|
19
|
+
)
|
|
20
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
21
|
+
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
22
|
+
from reflexio.server.services.deduplication_utils import (
|
|
23
|
+
BaseDeduplicator,
|
|
24
|
+
format_dedup_timestamp,
|
|
25
|
+
)
|
|
26
|
+
from reflexio.server.tracing import sentry_tags
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ===============================
|
|
32
|
+
# Playbook-specific Pydantic Output Schemas for LLM
|
|
33
|
+
# ===============================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _coerce_existing_position(value: object) -> int:
|
|
37
|
+
"""Accept either a bare int position or an ``"EXISTING-N"`` label.
|
|
38
|
+
|
|
39
|
+
Wired ONLY to ``UnifyDecision.archive_existing_ids`` — the one field whose
|
|
40
|
+
int contract is genuinely a list **position** (resolved via
|
|
41
|
+
``existing_by_position`` as ``f"EXISTING-{idx}"`` in ``_apply_unify``).
|
|
42
|
+
``RejectNewDecision.superseded_by_existing_id`` and
|
|
43
|
+
``DifferentiateDecision.existing_id`` are DB ``user_playbook_id`` values
|
|
44
|
+
(resolved via ``existing_by_id`` in ``_apply_reject_new`` /
|
|
45
|
+
``_apply_differentiate``) — coercing ``"EXISTING-N"`` to ``N`` on those
|
|
46
|
+
fields would silently misroute decisions when the position int doesn't
|
|
47
|
+
map to a real DB id.
|
|
48
|
+
|
|
49
|
+
The consolidation prompt labels rows as ``[EXISTING-0]``, ``[EXISTING-1]``
|
|
50
|
+
etc. (see ``_format_playbooks_with_prefix``) and ``_apply_unify``
|
|
51
|
+
reconstructs ``f"EXISTING-{position}"`` from the integer the LLM returns.
|
|
52
|
+
Strong structured-output models (GPT-4o, Claude) honor the ``list[int]``
|
|
53
|
+
schema and return the bare integer ``0``; weaker models (e.g. MiniMax-M3)
|
|
54
|
+
ignore the int constraint and return the literal label ``"EXISTING-0"``
|
|
55
|
+
instead — which then fails pydantic validation and the whole
|
|
56
|
+
consolidation batch dies.
|
|
57
|
+
|
|
58
|
+
Strip the prefix when present so the schema tolerates both shapes
|
|
59
|
+
without changing the int contract downstream consumers rely on. Plain
|
|
60
|
+
numeric strings (``"5"``) are also accepted for symmetry with how
|
|
61
|
+
most JSON-coerced models handle ID-like values. Negative values are
|
|
62
|
+
rejected — list positions are always ``>= 0``.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: when ``value`` is not a non-negative int or a recognized
|
|
66
|
+
position-label / numeric string.
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(value, bool):
|
|
69
|
+
# ``bool`` is a subclass of ``int`` in Python; reject explicitly so a
|
|
70
|
+
# stray ``True`` doesn't silently become position 1.
|
|
71
|
+
raise ValueError(f"existing-position must be int, got bool: {value!r}")
|
|
72
|
+
if isinstance(value, int):
|
|
73
|
+
if value < 0:
|
|
74
|
+
raise ValueError(f"existing-position must be >= 0, got {value!r}")
|
|
75
|
+
return value
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
stripped = value.strip()
|
|
78
|
+
for prefix in ("EXISTING-", "EXISTING_", "existing-", "existing_"):
|
|
79
|
+
if stripped.startswith(prefix):
|
|
80
|
+
stripped = stripped[len(prefix) :]
|
|
81
|
+
break
|
|
82
|
+
try:
|
|
83
|
+
parsed = int(stripped)
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"existing-position must be int or 'EXISTING-N' label, got {value!r}"
|
|
87
|
+
) from exc
|
|
88
|
+
if parsed < 0:
|
|
89
|
+
raise ValueError(f"existing-position must be >= 0, got {value!r}")
|
|
90
|
+
return parsed
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"existing-position must be int or 'EXISTING-N' label, got {type(value).__name__}: {value!r}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UnifyDecision(BaseModel):
|
|
97
|
+
"""Collapse NEW (+ 0..N EXISTING) into one row with LLM-supplied content.
|
|
98
|
+
|
|
99
|
+
Subsumes the legacy ``duplicate`` and ``prefer_new`` kinds AND the
|
|
100
|
+
``compose`` case: the LLM picks the final ``content`` / ``trigger`` /
|
|
101
|
+
``rationale`` and lists which EXISTING ids (if any) are absorbed. An empty
|
|
102
|
+
``archive_existing_ids`` is allowed and behaves as an insert-without-archive
|
|
103
|
+
distinguished from ``independent`` by the prompt's intent contract.
|
|
104
|
+
|
|
105
|
+
A unified skill MAY hold mixed-polarity rules (do-rules and avoid-rules for
|
|
106
|
+
different sub-aspects of the one task). There is no mechanical polarity
|
|
107
|
+
field or apply-time polarity check: the no-self-contradiction judgment
|
|
108
|
+
(do not merge rules that contradict on the same situation) is made by the
|
|
109
|
+
LLM in the consolidation prompt, not by the apply path.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
kind: Literal["unify"] = "unify"
|
|
113
|
+
new_id: str
|
|
114
|
+
archive_existing_ids: list[int] = Field(default_factory=list)
|
|
115
|
+
content: str
|
|
116
|
+
trigger: str
|
|
117
|
+
rationale: str
|
|
118
|
+
reason: str = ""
|
|
119
|
+
|
|
120
|
+
@field_validator("archive_existing_ids", mode="before")
|
|
121
|
+
@classmethod
|
|
122
|
+
def _coerce_archive_ids(cls, value: object) -> object:
|
|
123
|
+
if value is None:
|
|
124
|
+
return []
|
|
125
|
+
if isinstance(value, list):
|
|
126
|
+
return [_coerce_existing_position(item) for item in value]
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class RejectNewDecision(BaseModel):
|
|
133
|
+
"""The new candidate is redundant; an existing row supersedes it (storage no-op).
|
|
134
|
+
|
|
135
|
+
``superseded_by_existing_id`` is a DB ``user_playbook_id`` (resolved
|
|
136
|
+
against ``existing_by_id`` in ``_apply_reject_new``), NOT a list
|
|
137
|
+
position — no ``"EXISTING-N"`` coercion is wired here. If the LLM
|
|
138
|
+
returns ``"EXISTING-N"`` for this field, pydantic rejects it loudly
|
|
139
|
+
rather than silently misrouting the decision (see
|
|
140
|
+
``_coerce_existing_position`` docstring).
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
kind: Literal["reject_new"] = "reject_new"
|
|
144
|
+
new_id: str
|
|
145
|
+
superseded_by_existing_id: int
|
|
146
|
+
reason: str = ""
|
|
147
|
+
|
|
148
|
+
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class DifferentiateDecision(BaseModel):
|
|
152
|
+
"""Both rules valid in distinct contexts: refine both triggers.
|
|
153
|
+
|
|
154
|
+
``existing_id`` is a DB ``user_playbook_id`` (resolved against
|
|
155
|
+
``existing_by_id`` in ``_apply_differentiate``), NOT a list position —
|
|
156
|
+
no ``"EXISTING-N"`` coercion is wired here. See
|
|
157
|
+
``_coerce_existing_position`` docstring.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
kind: Literal["differentiate"] = "differentiate"
|
|
161
|
+
new_id: str
|
|
162
|
+
existing_id: int
|
|
163
|
+
refined_new_trigger: str
|
|
164
|
+
refined_existing_trigger: str
|
|
165
|
+
reason: str = ""
|
|
166
|
+
|
|
167
|
+
@field_validator("refined_new_trigger", "refined_existing_trigger")
|
|
168
|
+
@classmethod
|
|
169
|
+
def _non_empty(cls, v: str) -> str:
|
|
170
|
+
if not v or not v.strip():
|
|
171
|
+
raise ValueError("differentiate requires non-empty refined triggers")
|
|
172
|
+
return v
|
|
173
|
+
|
|
174
|
+
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class IndependentDecision(BaseModel):
|
|
178
|
+
"""Unrelated to any existing row: insert new as-is, no archive."""
|
|
179
|
+
|
|
180
|
+
kind: Literal["independent"] = "independent"
|
|
181
|
+
new_id: str
|
|
182
|
+
reason: str = ""
|
|
183
|
+
|
|
184
|
+
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
ConsolidationDecision = Annotated[
|
|
188
|
+
UnifyDecision | RejectNewDecision | DifferentiateDecision | IndependentDecision,
|
|
189
|
+
Field(discriminator="kind"),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class PlaybookConsolidationOutput(BaseModel):
|
|
194
|
+
"""Output schema for playbook consolidation as a 4-kind discriminated union.
|
|
195
|
+
|
|
196
|
+
Each decision is one of ``UnifyDecision``, ``RejectNewDecision``,
|
|
197
|
+
``DifferentiateDecision``, or ``IndependentDecision``; the ``kind`` literal
|
|
198
|
+
selects the concrete shape.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
decisions: list[ConsolidationDecision] = Field(default_factory=list)
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class PlaybookConsolidationResult(BaseModel):
|
|
207
|
+
"""Per-kind counters tracked over one consolidation batch.
|
|
208
|
+
|
|
209
|
+
Bumped once per successfully-applied decision; ``failed_count`` is bumped
|
|
210
|
+
when a single decision's apply path raises, allowing the rest of the batch
|
|
211
|
+
to proceed unaffected.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
unify_count: int = 0
|
|
215
|
+
reject_new_count: int = 0
|
|
216
|
+
differentiate_count: int = 0
|
|
217
|
+
independent_count: int = 0
|
|
218
|
+
failed_count: int = 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
_COUNTER_BY_KIND: dict[str, str] = {
|
|
222
|
+
"unify": "unify_count",
|
|
223
|
+
"reject_new": "reject_new_count",
|
|
224
|
+
"differentiate": "differentiate_count",
|
|
225
|
+
"independent": "independent_count",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class PlaybookConsolidator(BaseDeduplicator):
|
|
230
|
+
"""
|
|
231
|
+
Consolidates new user playbook entries against each other and against existing entries
|
|
232
|
+
in the database using hybrid search (vector + FTS) and LLM-based merging.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
DEDUPLICATION_PROMPT_ID = "playbook_consolidation"
|
|
236
|
+
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
request_context: RequestContext,
|
|
240
|
+
llm_client: LiteLLMClient,
|
|
241
|
+
dedup_config: DeduplicationConfig | None = None,
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Initialize the playbook consolidator.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
request_context: Request context with storage and prompt manager
|
|
248
|
+
llm_client: Unified LLM client for LLM calls
|
|
249
|
+
dedup_config: Optional consolidation search parameters (threshold, top_k)
|
|
250
|
+
"""
|
|
251
|
+
super().__init__(request_context, llm_client)
|
|
252
|
+
self._dedup_config = dedup_config or DeduplicationConfig()
|
|
253
|
+
|
|
254
|
+
def _get_prompt_id(self) -> str:
|
|
255
|
+
"""Get the prompt ID for playbook consolidation."""
|
|
256
|
+
return self.DEDUPLICATION_PROMPT_ID
|
|
257
|
+
|
|
258
|
+
def _get_item_count_key(self) -> str:
|
|
259
|
+
"""Get the key name for item count in prompt variables."""
|
|
260
|
+
return "new_playbook_count"
|
|
261
|
+
|
|
262
|
+
def _get_items_key(self) -> str:
|
|
263
|
+
"""Get the key name for items in prompt variables."""
|
|
264
|
+
return "new_playbooks"
|
|
265
|
+
|
|
266
|
+
def _get_output_schema_class(self) -> type[BaseModel]:
|
|
267
|
+
"""Return the discriminated-union output schema for consolidation."""
|
|
268
|
+
return PlaybookConsolidationOutput
|
|
269
|
+
|
|
270
|
+
def _format_items_for_prompt(self, playbooks: list[UserPlaybook]) -> str:
|
|
271
|
+
"""
|
|
272
|
+
Format user playbook entries list for LLM prompt with NEW-N prefix.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
playbooks: List of user playbook entries
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Formatted string representation
|
|
279
|
+
"""
|
|
280
|
+
return self._format_playbooks_with_prefix(playbooks, "NEW")
|
|
281
|
+
|
|
282
|
+
def _format_playbooks_with_prefix(
|
|
283
|
+
self, playbooks: list[UserPlaybook], prefix: str
|
|
284
|
+
) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Format user playbook entries with a given prefix (NEW or EXISTING).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
playbooks: List of user playbook entries to format
|
|
290
|
+
prefix: Prefix string for indices
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Formatted string
|
|
294
|
+
"""
|
|
295
|
+
if not playbooks:
|
|
296
|
+
return "(None)"
|
|
297
|
+
lines = []
|
|
298
|
+
for idx, playbook in enumerate(playbooks):
|
|
299
|
+
playbook_name = playbook.playbook_name or "unknown"
|
|
300
|
+
source = playbook.source or "unknown"
|
|
301
|
+
created_date = format_dedup_timestamp(playbook.created_at)
|
|
302
|
+
# ``Trigger`` and ``Rationale`` are included alongside ``Content``
|
|
303
|
+
# so the model can actually compare the fields it is asked to
|
|
304
|
+
# refine (``differentiate``, same-trigger contradictions, trigger
|
|
305
|
+
# refinements). Without ``trigger`` exposed the decisions become
|
|
306
|
+
# guesswork.
|
|
307
|
+
lines.append(
|
|
308
|
+
f'[{prefix}-{idx}] Content: "{playbook.content}"'
|
|
309
|
+
f' | Trigger: "{playbook.trigger or ""}"'
|
|
310
|
+
f' | Rationale: "{playbook.rationale or ""}"'
|
|
311
|
+
f" | Name: {playbook_name}"
|
|
312
|
+
f" | Source: {source} | Last Modified: {created_date}"
|
|
313
|
+
)
|
|
314
|
+
return "\n".join(lines)
|
|
315
|
+
|
|
316
|
+
def _retrieve_existing_playbooks(
|
|
317
|
+
self,
|
|
318
|
+
new_playbooks: list[UserPlaybook],
|
|
319
|
+
user_id: str | None = None,
|
|
320
|
+
agent_version: str | None = None,
|
|
321
|
+
) -> list[UserPlaybook]:
|
|
322
|
+
"""
|
|
323
|
+
Retrieve existing user playbook entries from the database using hybrid search.
|
|
324
|
+
|
|
325
|
+
For each new entry, uses its trigger field as the query with
|
|
326
|
+
pre-computed embeddings for vector search.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
new_playbooks: List of new entries to search against
|
|
330
|
+
user_id: Optional user ID to scope the search
|
|
331
|
+
agent_version: Optional agent version to scope the search
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Deduplicated list of existing UserPlaybook objects from the database
|
|
335
|
+
"""
|
|
336
|
+
storage = self.request_context.storage
|
|
337
|
+
|
|
338
|
+
# Collect trigger strings for embedding
|
|
339
|
+
query_texts = []
|
|
340
|
+
for playbook in new_playbooks:
|
|
341
|
+
trigger = playbook.trigger or playbook.content
|
|
342
|
+
if trigger and trigger.strip():
|
|
343
|
+
query_texts.append(trigger.strip())
|
|
344
|
+
|
|
345
|
+
if not query_texts:
|
|
346
|
+
return []
|
|
347
|
+
|
|
348
|
+
# Batch-generate embeddings
|
|
349
|
+
try:
|
|
350
|
+
embeddings = self.client.get_embeddings(
|
|
351
|
+
query_texts, dimensions=EMBEDDING_DIMENSIONS
|
|
352
|
+
)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.warning("Failed to generate embeddings for dedup search: %s", e)
|
|
355
|
+
# Fall back to text-only search
|
|
356
|
+
embeddings = [None] * len(query_texts)
|
|
357
|
+
|
|
358
|
+
# Search for each new entry
|
|
359
|
+
seen_ids: set[int] = set()
|
|
360
|
+
existing_playbooks: list[UserPlaybook] = []
|
|
361
|
+
|
|
362
|
+
for i, query_text in enumerate(query_texts):
|
|
363
|
+
try:
|
|
364
|
+
search_request = SearchUserPlaybookRequest(
|
|
365
|
+
query=query_text,
|
|
366
|
+
user_id=user_id,
|
|
367
|
+
agent_version=agent_version,
|
|
368
|
+
status_filter=[None], # Only current entries
|
|
369
|
+
threshold=self._dedup_config.search_threshold,
|
|
370
|
+
top_k=self._dedup_config.search_top_k,
|
|
371
|
+
)
|
|
372
|
+
search_options = SearchOptions(query_embedding=embeddings[i])
|
|
373
|
+
results = storage.search_user_playbooks( # type: ignore[reportOptionalMemberAccess]
|
|
374
|
+
search_request, search_options
|
|
375
|
+
)
|
|
376
|
+
for fb in results:
|
|
377
|
+
if fb.user_playbook_id and fb.user_playbook_id not in seen_ids:
|
|
378
|
+
seen_ids.add(fb.user_playbook_id)
|
|
379
|
+
existing_playbooks.append(fb)
|
|
380
|
+
except Exception as e: # noqa: PERF203
|
|
381
|
+
logger.warning(
|
|
382
|
+
"Failed to search existing entries for query %d: %s", i, e
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
logger.info(
|
|
386
|
+
"Retrieved %d unique existing user playbook entries for deduplication "
|
|
387
|
+
"(scoped to user_id=%r agent_version=%r)",
|
|
388
|
+
len(existing_playbooks),
|
|
389
|
+
user_id,
|
|
390
|
+
agent_version,
|
|
391
|
+
)
|
|
392
|
+
return existing_playbooks
|
|
393
|
+
|
|
394
|
+
def _format_new_and_existing_for_prompt(
|
|
395
|
+
self,
|
|
396
|
+
new_playbooks: list[UserPlaybook],
|
|
397
|
+
existing_playbooks: list[UserPlaybook],
|
|
398
|
+
) -> tuple[str, str]:
|
|
399
|
+
"""
|
|
400
|
+
Format new and existing entries for the deduplication prompt.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
new_playbooks: New entries to deduplicate
|
|
404
|
+
existing_playbooks: Existing entries from the database
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Tuple of (new_playbooks_text, existing_playbooks_text)
|
|
408
|
+
"""
|
|
409
|
+
new_text = self._format_playbooks_with_prefix(new_playbooks, "NEW")
|
|
410
|
+
existing_text = self._format_playbooks_with_prefix(
|
|
411
|
+
existing_playbooks, "EXISTING"
|
|
412
|
+
)
|
|
413
|
+
return new_text, existing_text
|
|
414
|
+
|
|
415
|
+
def _consolidation_decisions(
|
|
416
|
+
self,
|
|
417
|
+
new_playbooks: list[UserPlaybook],
|
|
418
|
+
existing_playbooks: list[UserPlaybook],
|
|
419
|
+
) -> PlaybookConsolidationOutput:
|
|
420
|
+
"""Render the consolidation prompt for NEW + EXISTING playbooks and run the
|
|
421
|
+
LLM decision step (prompt render + LLM call + parse only — no hybrid search,
|
|
422
|
+
no apply). Returns the parsed decisions, or an empty ``PlaybookConsolidationOutput``
|
|
423
|
+
if the LLM returned the wrong shape.
|
|
424
|
+
|
|
425
|
+
EXISTING-id <-> prompt-label mapping (for downstream eval providers that
|
|
426
|
+
must map a returned decision back to a source playbook):
|
|
427
|
+
* Both ``new_playbooks`` and ``existing_playbooks`` are rendered by
|
|
428
|
+
``_format_playbooks_with_prefix``, which labels rows by **list
|
|
429
|
+
position**, not by ``user_playbook_id``: NEW rows become
|
|
430
|
+
``[NEW-0]``, ``[NEW-1]``, ... and EXISTING rows become
|
|
431
|
+
``[EXISTING-0]``, ``[EXISTING-1]``, ... in the order passed in.
|
|
432
|
+
* Consequently the integer ids returned in decisions are interpreted
|
|
433
|
+
against EITHER positions OR ``user_playbook_id`` depending on the
|
|
434
|
+
decision kind, in the apply path (``_build_deduplicated_results``):
|
|
435
|
+
- ``UnifyDecision.archive_existing_ids`` -> **list positions**
|
|
436
|
+
(resolved as ``EXISTING-{idx}``).
|
|
437
|
+
- ``DifferentiateDecision.existing_id`` and
|
|
438
|
+
``RejectNewDecision.superseded_by_existing_id`` ->
|
|
439
|
+
``user_playbook_id`` (resolved against ``existing_by_id``).
|
|
440
|
+
- All decisions' ``new_id`` is the ``NEW-{idx}`` position label of
|
|
441
|
+
the candidate.
|
|
442
|
+
* A provider that controls the inputs should therefore choose its
|
|
443
|
+
``existing_playbooks`` ordering and ``user_playbook_id`` values so it
|
|
444
|
+
can map a returned ``existing_id`` (position for unify;
|
|
445
|
+
``user_playbook_id`` for differentiate/reject_new) back to its case.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
new_playbooks: Flattened list of new (candidate) entries.
|
|
449
|
+
existing_playbooks: Existing entries to consolidate against.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Parsed ``PlaybookConsolidationOutput``; an empty output (no
|
|
453
|
+
decisions) if the LLM returned an unexpected response shape.
|
|
454
|
+
"""
|
|
455
|
+
# Format for prompt
|
|
456
|
+
new_text, existing_text = self._format_new_and_existing_for_prompt(
|
|
457
|
+
new_playbooks, existing_playbooks
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Build and call LLM
|
|
461
|
+
prompt = self.request_context.prompt_manager.render_prompt(
|
|
462
|
+
self._get_prompt_id(),
|
|
463
|
+
{
|
|
464
|
+
"new_playbook_count": len(new_playbooks),
|
|
465
|
+
"new_playbooks": new_text,
|
|
466
|
+
"existing_playbooks": existing_text,
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
output_schema_class = self._get_output_schema_class()
|
|
471
|
+
|
|
472
|
+
from reflexio.server.services.service_utils import (
|
|
473
|
+
log_llm_messages,
|
|
474
|
+
log_model_response,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
log_llm_messages(
|
|
478
|
+
logger,
|
|
479
|
+
"Playbook consolidation",
|
|
480
|
+
[{"role": "user", "content": prompt}],
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
response = self.client.generate_chat_response(
|
|
484
|
+
messages=[{"role": "user", "content": prompt}],
|
|
485
|
+
model=self.model_name,
|
|
486
|
+
response_format=output_schema_class,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
log_model_response(logger, "Consolidation response", response)
|
|
490
|
+
|
|
491
|
+
if not isinstance(response, PlaybookConsolidationOutput):
|
|
492
|
+
logger.warning(
|
|
493
|
+
"Unexpected response type from consolidation LLM: %s",
|
|
494
|
+
type(response),
|
|
495
|
+
)
|
|
496
|
+
return PlaybookConsolidationOutput()
|
|
497
|
+
|
|
498
|
+
return response
|
|
499
|
+
|
|
500
|
+
def deduplicate(
|
|
501
|
+
self,
|
|
502
|
+
results: list[list[UserPlaybook]],
|
|
503
|
+
request_id: str,
|
|
504
|
+
agent_version: str,
|
|
505
|
+
user_id: str | None = None,
|
|
506
|
+
) -> tuple[list[UserPlaybook], list[int]]:
|
|
507
|
+
"""
|
|
508
|
+
Consolidate user playbook entries across extractors and against existing entries in DB.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
results: List of entry lists from extractors (each extractor returns list[UserPlaybook])
|
|
512
|
+
request_id: Request ID for context
|
|
513
|
+
agent_version: Agent version for context
|
|
514
|
+
user_id: Optional user ID to scope the existing entry search
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Tuple of (consolidated entries, list of existing entry IDs to delete after save)
|
|
518
|
+
"""
|
|
519
|
+
# Check if mock mode is enabled
|
|
520
|
+
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
|
|
521
|
+
logger.info("Mock mode: skipping consolidation")
|
|
522
|
+
all_playbooks: list[UserPlaybook] = []
|
|
523
|
+
for result in results:
|
|
524
|
+
if isinstance(result, list):
|
|
525
|
+
all_playbooks.extend(result)
|
|
526
|
+
return all_playbooks, []
|
|
527
|
+
|
|
528
|
+
# Flatten all new entries
|
|
529
|
+
new_playbooks: list[UserPlaybook] = []
|
|
530
|
+
for result in results:
|
|
531
|
+
if isinstance(result, list):
|
|
532
|
+
new_playbooks.extend(result)
|
|
533
|
+
|
|
534
|
+
if not new_playbooks:
|
|
535
|
+
return [], []
|
|
536
|
+
|
|
537
|
+
# Retrieve existing entries via hybrid search
|
|
538
|
+
existing_playbooks = self._retrieve_existing_playbooks(
|
|
539
|
+
new_playbooks, user_id=user_id, agent_version=agent_version
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Run the LLM decision step (prompt render + LLM call + parse only).
|
|
543
|
+
try:
|
|
544
|
+
dedup_output = self._consolidation_decisions(
|
|
545
|
+
new_playbooks, existing_playbooks
|
|
546
|
+
)
|
|
547
|
+
except Exception as e:
|
|
548
|
+
with sentry_tags(
|
|
549
|
+
subsystem="playbook_consolidator",
|
|
550
|
+
op="identify_duplicates",
|
|
551
|
+
org_id=self.request_context.org_id,
|
|
552
|
+
user_id=user_id,
|
|
553
|
+
request_id=request_id,
|
|
554
|
+
agent_version=agent_version,
|
|
555
|
+
error_type=type(e).__name__,
|
|
556
|
+
):
|
|
557
|
+
logger.exception("Failed to identify duplicates")
|
|
558
|
+
return new_playbooks, []
|
|
559
|
+
|
|
560
|
+
if not dedup_output.decisions:
|
|
561
|
+
logger.info(
|
|
562
|
+
"No consolidation decisions returned for request %s", request_id
|
|
563
|
+
)
|
|
564
|
+
return new_playbooks, []
|
|
565
|
+
|
|
566
|
+
logger.info(
|
|
567
|
+
"Received %d consolidation decisions for request %s",
|
|
568
|
+
len(dedup_output.decisions),
|
|
569
|
+
request_id,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Build consolidated result via the discriminated-union apply path
|
|
573
|
+
return self._build_deduplicated_results(
|
|
574
|
+
new_playbooks=new_playbooks,
|
|
575
|
+
existing_playbooks=existing_playbooks,
|
|
576
|
+
dedup_output=dedup_output,
|
|
577
|
+
request_id=request_id,
|
|
578
|
+
agent_version=agent_version,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# ===============================
|
|
582
|
+
# Apply path: discriminated-union decisions -> (new rows, archive ids)
|
|
583
|
+
# ===============================
|
|
584
|
+
|
|
585
|
+
def _build_deduplicated_results(
|
|
586
|
+
self,
|
|
587
|
+
new_playbooks: list[UserPlaybook],
|
|
588
|
+
existing_playbooks: list[UserPlaybook],
|
|
589
|
+
dedup_output: PlaybookConsolidationOutput,
|
|
590
|
+
request_id: str,
|
|
591
|
+
agent_version: str, # noqa: ARG002
|
|
592
|
+
) -> tuple[list[UserPlaybook], list[int]]:
|
|
593
|
+
"""
|
|
594
|
+
Build the deduplicated entry list from LLM decisions.
|
|
595
|
+
|
|
596
|
+
Dispatches each ``ConsolidationDecision`` to its kind-specific apply
|
|
597
|
+
method, accumulates resulting rows + archive ids, and adds any NEW
|
|
598
|
+
playbooks the LLM didn't reference as a safety fallback so a
|
|
599
|
+
misbehaving LLM cannot silently drop extracted playbooks.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
new_playbooks: Flattened list of new (candidate) entries.
|
|
603
|
+
existing_playbooks: List of existing entries from the DB.
|
|
604
|
+
dedup_output: LLM decisions output (discriminated union).
|
|
605
|
+
request_id: Request ID stamped onto newly-built rows.
|
|
606
|
+
agent_version: Agent version (currently unused, kept for symmetry).
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Tuple of (entries ready to save, existing entry IDs to delete).
|
|
610
|
+
"""
|
|
611
|
+
candidates_by_id = {
|
|
612
|
+
f"NEW-{idx}": playbook for idx, playbook in enumerate(new_playbooks)
|
|
613
|
+
}
|
|
614
|
+
existing_by_id = {
|
|
615
|
+
playbook.user_playbook_id: playbook
|
|
616
|
+
for playbook in existing_playbooks
|
|
617
|
+
if playbook.user_playbook_id
|
|
618
|
+
}
|
|
619
|
+
existing_by_position = {
|
|
620
|
+
f"EXISTING-{idx}": playbook
|
|
621
|
+
for idx, playbook in enumerate(existing_playbooks)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
result_counters = PlaybookConsolidationResult()
|
|
625
|
+
archive_ids: list[int] = []
|
|
626
|
+
seen_archive: set[int] = set()
|
|
627
|
+
new_rows: list[UserPlaybook] = []
|
|
628
|
+
handled_new_ids: set[str] = set()
|
|
629
|
+
|
|
630
|
+
for decision in dedup_output.decisions:
|
|
631
|
+
try:
|
|
632
|
+
rows, marked_new_ids = self._apply_one(
|
|
633
|
+
decision=decision,
|
|
634
|
+
candidates_by_id=candidates_by_id,
|
|
635
|
+
existing_by_id=existing_by_id,
|
|
636
|
+
existing_by_position=existing_by_position,
|
|
637
|
+
archive_ids=archive_ids,
|
|
638
|
+
seen_archive=seen_archive,
|
|
639
|
+
request_id=request_id,
|
|
640
|
+
)
|
|
641
|
+
except Exception as exc: # noqa: BLE001 — per-decision isolation
|
|
642
|
+
result_counters.failed_count += 1
|
|
643
|
+
new_id_str = getattr(decision, "new_id", "unknown")
|
|
644
|
+
existing_id_str = getattr(decision, "existing_id", "unknown")
|
|
645
|
+
with sentry_tags(
|
|
646
|
+
subsystem="playbook_consolidator",
|
|
647
|
+
op="apply_decision",
|
|
648
|
+
org_id=self.request_context.org_id,
|
|
649
|
+
request_id=request_id,
|
|
650
|
+
kind=decision.kind,
|
|
651
|
+
new_id=new_id_str,
|
|
652
|
+
existing_id=existing_id_str,
|
|
653
|
+
error_type=type(exc).__name__,
|
|
654
|
+
):
|
|
655
|
+
logger.exception(
|
|
656
|
+
"event=consolidation_apply_failed kind=%s new_id=%s existing_id=%s",
|
|
657
|
+
decision.kind,
|
|
658
|
+
new_id_str,
|
|
659
|
+
existing_id_str,
|
|
660
|
+
)
|
|
661
|
+
continue
|
|
662
|
+
new_rows.extend(rows)
|
|
663
|
+
handled_new_ids.update(marked_new_ids)
|
|
664
|
+
self._bump_counter(result_counters, decision.kind)
|
|
665
|
+
self._log_decision(decision, candidates_by_id, existing_by_id)
|
|
666
|
+
|
|
667
|
+
# Safety fallback: add any NEW entries the LLM did not reference, so a
|
|
668
|
+
# misbehaving model cannot silently drop extracted playbooks.
|
|
669
|
+
for new_id, candidate in candidates_by_id.items():
|
|
670
|
+
if new_id not in handled_new_ids:
|
|
671
|
+
logger.warning(
|
|
672
|
+
"event=consolidation_unhandled_new id=%s — adding as-is",
|
|
673
|
+
new_id,
|
|
674
|
+
)
|
|
675
|
+
new_rows.append(candidate)
|
|
676
|
+
|
|
677
|
+
logger.info(
|
|
678
|
+
"event=playbook_consolidation_done unify=%d reject_new=%d "
|
|
679
|
+
"differentiate=%d independent=%d failed=%d",
|
|
680
|
+
result_counters.unify_count,
|
|
681
|
+
result_counters.reject_new_count,
|
|
682
|
+
result_counters.differentiate_count,
|
|
683
|
+
result_counters.independent_count,
|
|
684
|
+
result_counters.failed_count,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
return new_rows, archive_ids
|
|
688
|
+
|
|
689
|
+
def _apply_one(
|
|
690
|
+
self,
|
|
691
|
+
*,
|
|
692
|
+
decision: ConsolidationDecision,
|
|
693
|
+
candidates_by_id: dict[str, UserPlaybook],
|
|
694
|
+
existing_by_id: dict[int, UserPlaybook],
|
|
695
|
+
existing_by_position: dict[str, UserPlaybook],
|
|
696
|
+
archive_ids: list[int],
|
|
697
|
+
seen_archive: set[int],
|
|
698
|
+
request_id: str,
|
|
699
|
+
) -> tuple[list[UserPlaybook], list[str]]:
|
|
700
|
+
"""Dispatch a single decision to its kind-specific apply method.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
decision: The decision to apply (one of four kinds).
|
|
704
|
+
candidates_by_id: Mapping ``"NEW-N"`` -> candidate ``UserPlaybook``.
|
|
705
|
+
existing_by_id: Mapping ``user_playbook_id`` -> existing playbook.
|
|
706
|
+
existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook
|
|
707
|
+
(used by ``unify`` to resolve the EXISTING-M ids it archives in
|
|
708
|
+
``archive_existing_ids``).
|
|
709
|
+
archive_ids: Accumulator list mutated with ids to archive/delete.
|
|
710
|
+
seen_archive: Accumulator set guarding ``archive_ids`` against
|
|
711
|
+
duplicate ids.
|
|
712
|
+
request_id: Request ID stamped onto newly-built rows.
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
Tuple of ``(rows_to_insert, handled_new_ids)`` where the second
|
|
716
|
+
element is the set of ``"NEW-N"`` candidate ids consumed by this
|
|
717
|
+
decision (used to suppress the safety fallback).
|
|
718
|
+
"""
|
|
719
|
+
if isinstance(decision, UnifyDecision):
|
|
720
|
+
return self._apply_unify(
|
|
721
|
+
decision,
|
|
722
|
+
candidates_by_id=candidates_by_id,
|
|
723
|
+
existing_by_position=existing_by_position,
|
|
724
|
+
archive_ids=archive_ids,
|
|
725
|
+
seen_archive=seen_archive,
|
|
726
|
+
request_id=request_id,
|
|
727
|
+
)
|
|
728
|
+
if isinstance(decision, RejectNewDecision):
|
|
729
|
+
return self._apply_reject_new(
|
|
730
|
+
decision,
|
|
731
|
+
existing_by_id=existing_by_id,
|
|
732
|
+
)
|
|
733
|
+
if isinstance(decision, DifferentiateDecision):
|
|
734
|
+
return self._apply_differentiate(
|
|
735
|
+
decision,
|
|
736
|
+
candidates_by_id=candidates_by_id,
|
|
737
|
+
existing_by_id=existing_by_id,
|
|
738
|
+
archive_ids=archive_ids,
|
|
739
|
+
seen_archive=seen_archive,
|
|
740
|
+
request_id=request_id,
|
|
741
|
+
)
|
|
742
|
+
if isinstance(decision, IndependentDecision):
|
|
743
|
+
return self._apply_independent(decision, candidates_by_id=candidates_by_id)
|
|
744
|
+
raise ValueError(f"unknown decision kind: {decision}")
|
|
745
|
+
|
|
746
|
+
def _apply_unify(
|
|
747
|
+
self,
|
|
748
|
+
decision: UnifyDecision,
|
|
749
|
+
*,
|
|
750
|
+
candidates_by_id: dict[str, UserPlaybook],
|
|
751
|
+
existing_by_position: dict[str, UserPlaybook],
|
|
752
|
+
archive_ids: list[int],
|
|
753
|
+
seen_archive: set[int],
|
|
754
|
+
request_id: str,
|
|
755
|
+
) -> tuple[list[UserPlaybook], list[str]]:
|
|
756
|
+
"""Collapse / compose NEW (+ 0..N EXISTING) into one row.
|
|
757
|
+
|
|
758
|
+
Looks up each ``archive_existing_ids`` entry by position
|
|
759
|
+
(``EXISTING-{idx}``) and archives it. The unified skill may carry
|
|
760
|
+
mixed-polarity rules (do-rules and avoid-rules for different
|
|
761
|
+
sub-aspects); there is **no** mechanical same-polarity check here. The
|
|
762
|
+
no-self-contradiction judgment (do not merge rules that contradict on
|
|
763
|
+
the same situation) is made by the LLM in the consolidation prompt, not
|
|
764
|
+
the apply path. The new row is built by copying identity/metadata from
|
|
765
|
+
the NEW candidate and overlaying ``content``, ``trigger``, and
|
|
766
|
+
``rationale`` from the decision.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
decision: The ``UnifyDecision`` to apply.
|
|
770
|
+
candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
|
|
771
|
+
existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook.
|
|
772
|
+
archive_ids: Accumulator mutated with EXISTING ids to archive.
|
|
773
|
+
seen_archive: Dedup set for ``archive_ids``.
|
|
774
|
+
request_id: Request ID stamped on the unified row.
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
Tuple of ([unified_row], [consumed NEW-N ids]).
|
|
778
|
+
|
|
779
|
+
Raises:
|
|
780
|
+
KeyError: If ``decision.new_id`` does not resolve to a known
|
|
781
|
+
candidate.
|
|
782
|
+
ValueError: If an ``archive_existing_ids`` entry has no matching
|
|
783
|
+
``EXISTING-{idx}`` row in the position map.
|
|
784
|
+
"""
|
|
785
|
+
candidate = candidates_by_id.get(decision.new_id)
|
|
786
|
+
if candidate is None:
|
|
787
|
+
raise KeyError(f"unify references unknown NEW id: {decision.new_id}")
|
|
788
|
+
|
|
789
|
+
existing_members: list[UserPlaybook] = []
|
|
790
|
+
for existing_position in decision.archive_existing_ids:
|
|
791
|
+
existing = existing_by_position.get(f"EXISTING-{existing_position}")
|
|
792
|
+
if existing is None:
|
|
793
|
+
raise ValueError(
|
|
794
|
+
f"unify references unknown existing_id={existing_position}"
|
|
795
|
+
)
|
|
796
|
+
existing_members.append(existing)
|
|
797
|
+
|
|
798
|
+
for existing in existing_members:
|
|
799
|
+
pid = existing.user_playbook_id
|
|
800
|
+
if pid and pid not in seen_archive:
|
|
801
|
+
seen_archive.add(pid)
|
|
802
|
+
archive_ids.append(pid)
|
|
803
|
+
|
|
804
|
+
budget = self._dedup_config.max_unified_content_chars
|
|
805
|
+
content_len = len(decision.content)
|
|
806
|
+
if content_len > budget:
|
|
807
|
+
# Soft backstop only: the prompt instructs the model to prefer
|
|
808
|
+
# `differentiate` over an over-long unify. We log a signal rather
|
|
809
|
+
# than hard-fail or downgrade so we don't destabilize the 4-kind
|
|
810
|
+
# apply logic; the merge still proceeds.
|
|
811
|
+
logger.warning(
|
|
812
|
+
"event=consolidation_over_budget new_id=%s len=%d budget=%d",
|
|
813
|
+
decision.new_id,
|
|
814
|
+
content_len,
|
|
815
|
+
budget,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
combined_source_ids = self._merge_source_ids([candidate, *existing_members])
|
|
819
|
+
unified_row = UserPlaybook(
|
|
820
|
+
user_playbook_id=0,
|
|
821
|
+
user_id=candidate.user_id,
|
|
822
|
+
agent_version=candidate.agent_version,
|
|
823
|
+
request_id=request_id,
|
|
824
|
+
playbook_name=candidate.playbook_name,
|
|
825
|
+
created_at=int(datetime.now(UTC).timestamp()),
|
|
826
|
+
content=decision.content,
|
|
827
|
+
trigger=decision.trigger,
|
|
828
|
+
rationale=decision.rationale,
|
|
829
|
+
status=candidate.status,
|
|
830
|
+
source=candidate.source,
|
|
831
|
+
source_interaction_ids=combined_source_ids,
|
|
832
|
+
)
|
|
833
|
+
return [unified_row], [decision.new_id]
|
|
834
|
+
|
|
835
|
+
def _apply_reject_new(
|
|
836
|
+
self,
|
|
837
|
+
decision: RejectNewDecision,
|
|
838
|
+
*,
|
|
839
|
+
existing_by_id: dict[int, UserPlaybook],
|
|
840
|
+
) -> tuple[list[UserPlaybook], list[str]]:
|
|
841
|
+
"""No-op apply: the existing row wins and the new candidate is dropped.
|
|
842
|
+
|
|
843
|
+
If ``decision.superseded_by_existing_id`` does not resolve to a known
|
|
844
|
+
existing row, the decision is treated as malformed: we log a warning
|
|
845
|
+
and return ``([], [])`` so the safety fallback re-inserts the candidate
|
|
846
|
+
rather than silently dropping extracted data.
|
|
847
|
+
|
|
848
|
+
Args:
|
|
849
|
+
decision: The ``RejectNewDecision`` to apply.
|
|
850
|
+
existing_by_id: Mapping ``user_playbook_id`` -> existing playbook,
|
|
851
|
+
used to validate ``decision.superseded_by_existing_id``.
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
Tuple of ([], [consumed NEW-N id]) when the existing id resolves,
|
|
855
|
+
or ``([], [])`` when the existing id is unknown.
|
|
856
|
+
"""
|
|
857
|
+
if decision.superseded_by_existing_id not in existing_by_id:
|
|
858
|
+
logger.warning(
|
|
859
|
+
"event=consolidation_reject_new_invalid new_id=%s existing_id=%d",
|
|
860
|
+
decision.new_id,
|
|
861
|
+
decision.superseded_by_existing_id,
|
|
862
|
+
)
|
|
863
|
+
return [], []
|
|
864
|
+
logger.info(
|
|
865
|
+
"event=consolidation_reject_new new_id=%s existing_id=%d",
|
|
866
|
+
decision.new_id,
|
|
867
|
+
decision.superseded_by_existing_id,
|
|
868
|
+
)
|
|
869
|
+
return [], [decision.new_id]
|
|
870
|
+
|
|
871
|
+
def _apply_differentiate(
|
|
872
|
+
self,
|
|
873
|
+
decision: DifferentiateDecision,
|
|
874
|
+
*,
|
|
875
|
+
candidates_by_id: dict[str, UserPlaybook],
|
|
876
|
+
existing_by_id: dict[int, UserPlaybook],
|
|
877
|
+
archive_ids: list[int],
|
|
878
|
+
seen_archive: set[int],
|
|
879
|
+
request_id: str,
|
|
880
|
+
) -> tuple[list[UserPlaybook], list[str]]:
|
|
881
|
+
"""Archive the existing row and emit two refined rows in its place.
|
|
882
|
+
|
|
883
|
+
Builds one ``UserPlaybook`` from the candidate's content/polarity with
|
|
884
|
+
``refined_new_trigger``, and a second from the existing row's
|
|
885
|
+
content/polarity with ``refined_existing_trigger``. Polarity is
|
|
886
|
+
threaded through unchanged for each side.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
decision: The ``DifferentiateDecision`` to apply.
|
|
890
|
+
candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
|
|
891
|
+
existing_by_id: Mapping ``user_playbook_id`` -> existing playbook.
|
|
892
|
+
archive_ids: Accumulator mutated with the existing id to archive.
|
|
893
|
+
seen_archive: Dedup set for ``archive_ids``.
|
|
894
|
+
request_id: Request ID stamped on both new rows.
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
Tuple of ([refined_new_row, refined_existing_row], [NEW-N id]).
|
|
898
|
+
"""
|
|
899
|
+
candidate = candidates_by_id.get(decision.new_id)
|
|
900
|
+
if candidate is None:
|
|
901
|
+
raise KeyError(
|
|
902
|
+
f"differentiate references unknown NEW id: {decision.new_id}"
|
|
903
|
+
)
|
|
904
|
+
existing = existing_by_id.get(decision.existing_id)
|
|
905
|
+
if existing is None:
|
|
906
|
+
raise KeyError(
|
|
907
|
+
f"differentiate references unknown EXISTING id: {decision.existing_id}"
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
if decision.existing_id not in seen_archive:
|
|
911
|
+
seen_archive.add(decision.existing_id)
|
|
912
|
+
archive_ids.append(decision.existing_id)
|
|
913
|
+
|
|
914
|
+
now_ts = int(datetime.now(UTC).timestamp())
|
|
915
|
+
refined_candidate = candidate.model_copy(
|
|
916
|
+
update={
|
|
917
|
+
"user_playbook_id": 0,
|
|
918
|
+
"request_id": request_id,
|
|
919
|
+
"trigger": decision.refined_new_trigger,
|
|
920
|
+
"created_at": now_ts,
|
|
921
|
+
}
|
|
922
|
+
)
|
|
923
|
+
refined_existing = existing.model_copy(
|
|
924
|
+
update={
|
|
925
|
+
"user_playbook_id": 0,
|
|
926
|
+
"request_id": request_id,
|
|
927
|
+
"trigger": decision.refined_existing_trigger,
|
|
928
|
+
"created_at": now_ts,
|
|
929
|
+
"source_interaction_ids": list(existing.source_interaction_ids),
|
|
930
|
+
}
|
|
931
|
+
)
|
|
932
|
+
return [refined_candidate, refined_existing], [decision.new_id]
|
|
933
|
+
|
|
934
|
+
def _apply_independent(
|
|
935
|
+
self,
|
|
936
|
+
decision: IndependentDecision,
|
|
937
|
+
*,
|
|
938
|
+
candidates_by_id: dict[str, UserPlaybook],
|
|
939
|
+
) -> tuple[list[UserPlaybook], list[str]]:
|
|
940
|
+
"""Insert the new candidate unchanged; no archive.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
decision: The ``IndependentDecision`` to apply.
|
|
944
|
+
candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
Tuple of ([candidate row], [consumed NEW-N id]).
|
|
948
|
+
"""
|
|
949
|
+
candidate = candidates_by_id.get(decision.new_id)
|
|
950
|
+
if candidate is None:
|
|
951
|
+
raise KeyError(f"independent references unknown NEW id: {decision.new_id}")
|
|
952
|
+
return [candidate], [decision.new_id]
|
|
953
|
+
|
|
954
|
+
@staticmethod
|
|
955
|
+
def _merge_source_ids(playbooks: list[UserPlaybook]) -> list[int]:
|
|
956
|
+
"""Combine ``source_interaction_ids`` across playbooks, preserving order.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
playbooks: The playbooks whose source ids should be combined.
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
Order-preserving deduplicated list of source interaction ids.
|
|
963
|
+
"""
|
|
964
|
+
seen: set[int] = set()
|
|
965
|
+
combined: list[int] = []
|
|
966
|
+
for playbook in playbooks:
|
|
967
|
+
for sid in playbook.source_interaction_ids:
|
|
968
|
+
if sid not in seen:
|
|
969
|
+
seen.add(sid)
|
|
970
|
+
combined.append(sid)
|
|
971
|
+
return combined
|
|
972
|
+
|
|
973
|
+
@staticmethod
|
|
974
|
+
def _bump_counter(result: PlaybookConsolidationResult, kind: str) -> None:
|
|
975
|
+
"""Increment the per-kind counter on ``result`` for a successful apply.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
result: The result counters object to mutate.
|
|
979
|
+
kind: One of ``unify``, ``reject_new``, ``differentiate``, or
|
|
980
|
+
``independent``.
|
|
981
|
+
"""
|
|
982
|
+
field = _COUNTER_BY_KIND[kind]
|
|
983
|
+
setattr(result, field, getattr(result, field) + 1)
|
|
984
|
+
|
|
985
|
+
@staticmethod
|
|
986
|
+
def _log_decision(
|
|
987
|
+
decision: ConsolidationDecision,
|
|
988
|
+
candidates_by_id: dict[str, UserPlaybook],
|
|
989
|
+
existing_by_id: dict[int, UserPlaybook],
|
|
990
|
+
) -> None:
|
|
991
|
+
"""Emit a structured per-decision log line for probe ingest.
|
|
992
|
+
|
|
993
|
+
Emits ``playbook_consolidation.decision`` with the 4-kind name,
|
|
994
|
+
new/existing ids, and trigger_match. Polarity is intentionally NOT
|
|
995
|
+
derived or logged: under Option B a skill may hold mixed-polarity
|
|
996
|
+
rules, so a single whole-content polarity label is no longer
|
|
997
|
+
meaningful. The no-self-contradiction judgment lives in the LLM.
|
|
998
|
+
|
|
999
|
+
Args:
|
|
1000
|
+
decision: The applied consolidation decision.
|
|
1001
|
+
candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
|
|
1002
|
+
existing_by_id: Mapping ``user_playbook_id`` -> existing playbook.
|
|
1003
|
+
"""
|
|
1004
|
+
kind = decision.kind
|
|
1005
|
+
new_id: str = getattr(decision, "new_id", "")
|
|
1006
|
+
new_pb = candidates_by_id.get(new_id)
|
|
1007
|
+
|
|
1008
|
+
# UnifyDecision archives by position (EXISTING-{idx}) rather than a
|
|
1009
|
+
# single existing_id; log a synthetic "multi" so the probe parser sees
|
|
1010
|
+
# one line per decision regardless of arity.
|
|
1011
|
+
if isinstance(decision, UnifyDecision):
|
|
1012
|
+
existing_id_label: str = (
|
|
1013
|
+
"multi" if decision.archive_existing_ids else "none"
|
|
1014
|
+
)
|
|
1015
|
+
logger.info(
|
|
1016
|
+
"playbook_consolidation.decision kind=%s new_id=%s existing_id=%s "
|
|
1017
|
+
"trigger_match=%s",
|
|
1018
|
+
kind,
|
|
1019
|
+
new_id,
|
|
1020
|
+
existing_id_label,
|
|
1021
|
+
"unknown",
|
|
1022
|
+
)
|
|
1023
|
+
return
|
|
1024
|
+
|
|
1025
|
+
# RejectNewDecision exposes ``superseded_by_existing_id``; the other
|
|
1026
|
+
# two surviving kinds expose ``existing_id`` directly.
|
|
1027
|
+
existing_id_raw: int = getattr(
|
|
1028
|
+
decision,
|
|
1029
|
+
"existing_id",
|
|
1030
|
+
getattr(decision, "superseded_by_existing_id", 0),
|
|
1031
|
+
)
|
|
1032
|
+
existing_pb = existing_by_id.get(existing_id_raw)
|
|
1033
|
+
trigger_match = (
|
|
1034
|
+
new_pb is not None
|
|
1035
|
+
and existing_pb is not None
|
|
1036
|
+
and new_pb.trigger == existing_pb.trigger
|
|
1037
|
+
)
|
|
1038
|
+
logger.info(
|
|
1039
|
+
"playbook_consolidation.decision kind=%s new_id=%s existing_id=%s "
|
|
1040
|
+
"trigger_match=%s",
|
|
1041
|
+
kind,
|
|
1042
|
+
new_id,
|
|
1043
|
+
existing_id_raw,
|
|
1044
|
+
str(trigger_match).lower(),
|
|
1045
|
+
)
|