claude-smart 0.2.41 → 0.2.43
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 +17 -0
- package/README.md +1 -1
- package/bin/claude-smart.js +86 -48
- package/package.json +10 -3
- package/plugin/.claude-plugin/plugin.json +9 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +2 -2
- package/plugin/dashboard/next.config.ts +9 -1
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +91 -0
- package/plugin/scripts/backend-service.sh +46 -15
- package/plugin/scripts/cli.sh +29 -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 +7 -14
- package/plugin/scripts/hook_entry.sh +1 -0
- package/plugin/scripts/smart-install.sh +18 -2
- package/plugin/src/claude_smart/cli.py +72 -38
- 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 +1 -1
- package/plugin/vendor/reflexio/.env.example +53 -0
- package/plugin/vendor/reflexio/LICENSE +201 -0
- package/plugin/vendor/reflexio/README.md +338 -0
- package/plugin/vendor/reflexio/pyproject.toml +271 -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 +266 -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 +961 -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 +1103 -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 +766 -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 +280 -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 +616 -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 +2714 -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 +540 -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 +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -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 +210 -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 +255 -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 +177 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -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_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 +279 -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 +127 -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/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 +340 -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 +1626 -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/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 +351 -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 +468 -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 +777 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -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 +960 -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 +741 -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 +734 -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 +800 -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 +70 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -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 +1253 -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 +372 -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 +568 -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,741 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Profile deduplication service that merges duplicate profiles from multiple extractors
|
|
3
|
+
and against existing profiles in the database using hybrid search and LLM.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from reflexio.models.api_schema.retriever_schema import SearchUserProfileRequest
|
|
14
|
+
from reflexio.models.api_schema.service_schemas import Status, UserProfile
|
|
15
|
+
from reflexio.models.config_schema import EMBEDDING_DIMENSIONS
|
|
16
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
17
|
+
from reflexio.server.llm.litellm_client import LiteLLMClient
|
|
18
|
+
from reflexio.server.services.deduplication_utils import (
|
|
19
|
+
BaseDeduplicator,
|
|
20
|
+
format_dedup_timestamp,
|
|
21
|
+
parse_item_id,
|
|
22
|
+
)
|
|
23
|
+
from reflexio.server.services.profile.profile_generation_service_utils import (
|
|
24
|
+
ProfileTimeToLive,
|
|
25
|
+
calculate_expiration_timestamp,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Backward-compat alias — existing unit tests import this name from this
|
|
32
|
+
# module. Delegates to the shared helper in deduplication_utils.
|
|
33
|
+
_format_profile_timestamp = format_dedup_timestamp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Canonical prefix emitted by the extractor for forget/delete requests. The
|
|
37
|
+
# dedup LLM routes matching NEW profiles into `deletions`; any fallback path
|
|
38
|
+
# that skips the LLM step must strip these markers before returning so they
|
|
39
|
+
# are never persisted as facts.
|
|
40
|
+
_DELETION_MARKER_PREFIX = "Requested removal of"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _strip_deletion_markers(
|
|
44
|
+
profiles: list[UserProfile],
|
|
45
|
+
) -> list[UserProfile]:
|
|
46
|
+
"""
|
|
47
|
+
Drop profiles whose content is a canonical deletion marker.
|
|
48
|
+
|
|
49
|
+
Used on fallback paths (LLM error, unexpected response type, empty dedup
|
|
50
|
+
output) to prevent "Requested removal of …" markers emitted by the
|
|
51
|
+
extractor from being persisted as regular profile facts when the dedup
|
|
52
|
+
LLM step is skipped or yields no deletions. Persisting such markers would
|
|
53
|
+
recreate the exact zombie-profile failure mode the deletion-directive
|
|
54
|
+
channel was introduced to eliminate.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
profiles (list[UserProfile]): Profiles to filter.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
list[UserProfile]: Profiles with deletion markers removed.
|
|
61
|
+
"""
|
|
62
|
+
return [
|
|
63
|
+
p
|
|
64
|
+
for p in profiles
|
|
65
|
+
if not (p.content or "").lstrip().startswith(_DELETION_MARKER_PREFIX)
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ===============================
|
|
70
|
+
# Profile-specific Pydantic Output Schemas for LLM
|
|
71
|
+
# ===============================
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ProfileDuplicateGroup(BaseModel):
|
|
75
|
+
"""
|
|
76
|
+
Represents a group of duplicate profiles across NEW and EXISTING sets.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
item_ids: List of item IDs matching prompt format (e.g., 'NEW-0', 'EXISTING-1')
|
|
80
|
+
merged_content: The consolidated profile content combining information from all duplicates
|
|
81
|
+
merged_time_to_live: The chosen time_to_live for the merged profile
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
item_ids: list[str] = Field(
|
|
85
|
+
description="IDs of items in this group matching prompt format (e.g., 'NEW-0', 'EXISTING-1')"
|
|
86
|
+
)
|
|
87
|
+
merged_content: str = Field(
|
|
88
|
+
description="Consolidated profile content combining all duplicate information"
|
|
89
|
+
)
|
|
90
|
+
merged_time_to_live: str = Field(
|
|
91
|
+
description="Time to live for merged profile: one_day, one_week, one_month, one_quarter, one_year, infinity"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(
|
|
95
|
+
extra="allow",
|
|
96
|
+
json_schema_extra={"additionalProperties": False},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ProfileDeletionDirective(BaseModel):
|
|
101
|
+
"""
|
|
102
|
+
Represents a NEW profile that is a meta-request to forget an EXISTING fact.
|
|
103
|
+
|
|
104
|
+
Used when the user explicitly asks the system to erase a previously-stored
|
|
105
|
+
profile (e.g. "forget that I like X"). Unlike a duplicate group, a deletion
|
|
106
|
+
directive removes the matched EXISTING profile(s) without writing any merged
|
|
107
|
+
or replacement profile — the NEW directive is consumed, not retained.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
new_id: ID of the NEW profile that expresses the deletion directive (e.g. 'NEW-0')
|
|
111
|
+
existing_ids: IDs of EXISTING profiles to delete without replacement (e.g. ['EXISTING-0'])
|
|
112
|
+
reasoning: Brief explanation of why this was classified as a deletion directive
|
|
113
|
+
rather than a fact update
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
new_id: str = Field(
|
|
117
|
+
description="ID of the NEW profile that is a deletion directive (e.g. 'NEW-0')"
|
|
118
|
+
)
|
|
119
|
+
existing_ids: list[str] = Field(
|
|
120
|
+
description="IDs of EXISTING profiles to delete without replacement (e.g. ['EXISTING-0'])"
|
|
121
|
+
)
|
|
122
|
+
reasoning: str = Field(
|
|
123
|
+
description="Brief explanation of the deletion classification"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
model_config = ConfigDict(
|
|
127
|
+
extra="allow",
|
|
128
|
+
json_schema_extra={"additionalProperties": False},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ProfileDeduplicationOutput(BaseModel):
|
|
133
|
+
"""
|
|
134
|
+
Output schema for profile deduplication with NEW/EXISTING format.
|
|
135
|
+
|
|
136
|
+
Attributes:
|
|
137
|
+
duplicate_groups: List of duplicate groups to merge
|
|
138
|
+
unique_ids: List of IDs of unique NEW profiles (e.g., 'NEW-2')
|
|
139
|
+
deletions: List of deletion directives — NEW profiles that are pure
|
|
140
|
+
meta-requests to erase an EXISTING profile. Both the NEW and the
|
|
141
|
+
matched EXISTING profile(s) are removed; no merged replacement is
|
|
142
|
+
produced.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
duplicate_groups: list[ProfileDuplicateGroup] = Field(
|
|
146
|
+
default=[], description="Groups of duplicate profiles that should be merged"
|
|
147
|
+
)
|
|
148
|
+
unique_ids: list[str] = Field(
|
|
149
|
+
default=[],
|
|
150
|
+
description="IDs of unique NEW profiles (e.g., 'NEW-2')",
|
|
151
|
+
)
|
|
152
|
+
deletions: list[ProfileDeletionDirective] = Field(
|
|
153
|
+
default=[],
|
|
154
|
+
description=(
|
|
155
|
+
"NEW profiles that are pure deletion directives (the user asked to "
|
|
156
|
+
"forget/remove a stored fact). Both the NEW and matched EXISTING "
|
|
157
|
+
"profiles are removed; no merged replacement is written."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
model_config = ConfigDict(
|
|
162
|
+
extra="allow",
|
|
163
|
+
json_schema_extra={"additionalProperties": False},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ProfileDeduplicator(BaseDeduplicator):
|
|
168
|
+
"""
|
|
169
|
+
Deduplicates new profiles against each other and against existing profiles
|
|
170
|
+
in the database using hybrid search (vector + FTS) and LLM-based merging.
|
|
171
|
+
|
|
172
|
+
Follows the same pattern as PlaybookConsolidator.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
DEDUPLICATION_PROMPT_ID = "profile_deduplication"
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
request_context: RequestContext,
|
|
180
|
+
llm_client: LiteLLMClient,
|
|
181
|
+
output_pending_status: bool = False,
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
Initialize the profile deduplicator.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
request_context: Request context with storage and prompt manager
|
|
188
|
+
llm_client: Unified LLM client for LLM calls
|
|
189
|
+
output_pending_status: When True (rerun preview mode), the deduplicator
|
|
190
|
+
searches against existing PENDING profiles instead of CURRENT
|
|
191
|
+
profiles. This makes rerun idempotent against pending state and
|
|
192
|
+
leaves the user's current profile set untouched. When False
|
|
193
|
+
(normal extraction), searches against CURRENT profiles so newly
|
|
194
|
+
extracted facts can supersede stale ones.
|
|
195
|
+
"""
|
|
196
|
+
super().__init__(request_context, llm_client)
|
|
197
|
+
self.output_pending_status = output_pending_status
|
|
198
|
+
|
|
199
|
+
def _get_prompt_id(self) -> str:
|
|
200
|
+
"""Get the prompt ID for profile deduplication."""
|
|
201
|
+
return self.DEDUPLICATION_PROMPT_ID
|
|
202
|
+
|
|
203
|
+
def _get_item_count_key(self) -> str:
|
|
204
|
+
"""Get the key name for item count in prompt variables."""
|
|
205
|
+
return "new_profile_count"
|
|
206
|
+
|
|
207
|
+
def _get_items_key(self) -> str:
|
|
208
|
+
"""Get the key name for items in prompt variables."""
|
|
209
|
+
return "new_profiles"
|
|
210
|
+
|
|
211
|
+
def _get_output_schema_class(self) -> type[BaseModel]:
|
|
212
|
+
"""Get the profile-specific output schema with NEW/EXISTING format."""
|
|
213
|
+
return ProfileDeduplicationOutput
|
|
214
|
+
|
|
215
|
+
def _format_items_for_prompt(self, profiles: list[UserProfile]) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Format profiles list for LLM prompt with NEW-N prefix.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
profiles: List of profiles
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Formatted string representation
|
|
224
|
+
"""
|
|
225
|
+
return self._format_profiles_with_prefix(profiles, "NEW")
|
|
226
|
+
|
|
227
|
+
def _format_profiles_with_prefix(
|
|
228
|
+
self, profiles: list[UserProfile], prefix: str
|
|
229
|
+
) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Format profiles with a given prefix (NEW or EXISTING).
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
profiles: List of profiles to format
|
|
235
|
+
prefix: Prefix string for indices
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Formatted string
|
|
239
|
+
"""
|
|
240
|
+
if not profiles:
|
|
241
|
+
return "(None)"
|
|
242
|
+
lines = []
|
|
243
|
+
for idx, profile in enumerate(profiles):
|
|
244
|
+
ttl = (
|
|
245
|
+
profile.profile_time_to_live.value
|
|
246
|
+
if profile.profile_time_to_live
|
|
247
|
+
else "unknown"
|
|
248
|
+
)
|
|
249
|
+
source = profile.source or "unknown"
|
|
250
|
+
modified_date = _format_profile_timestamp(profile.last_modified_timestamp)
|
|
251
|
+
lines.append(
|
|
252
|
+
f'[{prefix}-{idx}] Content: "{profile.content}" | TTL: {ttl} | Source: {source} | Last Modified: {modified_date}'
|
|
253
|
+
)
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
def _format_new_and_existing_for_prompt(
|
|
257
|
+
self,
|
|
258
|
+
new_profiles: list[UserProfile],
|
|
259
|
+
existing_profiles: list[UserProfile],
|
|
260
|
+
) -> tuple[str, str]:
|
|
261
|
+
"""
|
|
262
|
+
Format new and existing profiles for the deduplication prompt.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
new_profiles: New profiles to deduplicate
|
|
266
|
+
existing_profiles: Existing profiles from the database
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Tuple of (new_profiles_text, existing_profiles_text)
|
|
270
|
+
"""
|
|
271
|
+
new_text = self._format_profiles_with_prefix(new_profiles, "NEW")
|
|
272
|
+
existing_text = self._format_profiles_with_prefix(existing_profiles, "EXISTING")
|
|
273
|
+
return new_text, existing_text
|
|
274
|
+
|
|
275
|
+
def _retrieve_existing_profiles(
|
|
276
|
+
self,
|
|
277
|
+
new_profiles: list[UserProfile],
|
|
278
|
+
user_id: str,
|
|
279
|
+
) -> list[UserProfile]:
|
|
280
|
+
"""
|
|
281
|
+
Retrieve existing profiles from the database using hybrid search.
|
|
282
|
+
|
|
283
|
+
For each new profile, uses its profile_content as the query with
|
|
284
|
+
pre-computed embeddings for vector search.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
new_profiles: List of new profiles to search against
|
|
288
|
+
user_id: User ID to scope the search
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Deduplicated list of existing UserProfile objects from the database
|
|
292
|
+
"""
|
|
293
|
+
storage = self.request_context.storage
|
|
294
|
+
|
|
295
|
+
# Collect profile content strings for embedding
|
|
296
|
+
query_texts = []
|
|
297
|
+
for profile in new_profiles:
|
|
298
|
+
text = profile.content
|
|
299
|
+
if text and text.strip():
|
|
300
|
+
query_texts.append(text.strip())
|
|
301
|
+
|
|
302
|
+
if not query_texts:
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
# Batch-generate embeddings
|
|
306
|
+
try:
|
|
307
|
+
embeddings = self.client.get_embeddings(
|
|
308
|
+
query_texts, dimensions=EMBEDDING_DIMENSIONS
|
|
309
|
+
)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.warning("Failed to generate embeddings for dedup search: %s", e)
|
|
312
|
+
embeddings = [None] * len(query_texts)
|
|
313
|
+
|
|
314
|
+
# Search for each new profile.
|
|
315
|
+
#
|
|
316
|
+
# When output_pending_status=True (rerun preview mode), search against
|
|
317
|
+
# existing PENDING profiles so dedup is idempotent: a rerun should
|
|
318
|
+
# collapse duplicates within the pending preview set without ever
|
|
319
|
+
# touching the user's CURRENT profiles. Searching against [None]
|
|
320
|
+
# (current) here would cause newly-extracted profiles to be flagged
|
|
321
|
+
# as duplicates of current ones, and the downstream deletion step
|
|
322
|
+
# in ProfileGenerationService._process_results would then delete the
|
|
323
|
+
# CURRENT profiles -- collapsing the user's profile set to zero
|
|
324
|
+
# during what is supposed to be a non-destructive preview.
|
|
325
|
+
if self.output_pending_status:
|
|
326
|
+
search_status_filter: list[Status | None] = [Status.PENDING]
|
|
327
|
+
else:
|
|
328
|
+
search_status_filter = [None] # Only current profiles
|
|
329
|
+
|
|
330
|
+
seen_ids: set[str] = set()
|
|
331
|
+
existing_profiles: list[UserProfile] = []
|
|
332
|
+
|
|
333
|
+
for i, query_text in enumerate(query_texts):
|
|
334
|
+
try:
|
|
335
|
+
results = storage.search_user_profile( # type: ignore[reportOptionalMemberAccess]
|
|
336
|
+
SearchUserProfileRequest(
|
|
337
|
+
query=query_text,
|
|
338
|
+
user_id=user_id,
|
|
339
|
+
top_k=10,
|
|
340
|
+
threshold=0.4,
|
|
341
|
+
),
|
|
342
|
+
status_filter=search_status_filter,
|
|
343
|
+
query_embedding=embeddings[i],
|
|
344
|
+
)
|
|
345
|
+
for profile in results:
|
|
346
|
+
if profile.profile_id and profile.profile_id not in seen_ids:
|
|
347
|
+
seen_ids.add(profile.profile_id)
|
|
348
|
+
existing_profiles.append(profile)
|
|
349
|
+
except Exception as e: # noqa: PERF203
|
|
350
|
+
logger.warning(
|
|
351
|
+
"Failed to search existing profiles for query %d: %s", i, e
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
logger.info(
|
|
355
|
+
"Retrieved %d unique existing profiles for deduplication "
|
|
356
|
+
"(status_filter=%s, output_pending_status=%s)",
|
|
357
|
+
len(existing_profiles),
|
|
358
|
+
search_status_filter,
|
|
359
|
+
self.output_pending_status,
|
|
360
|
+
)
|
|
361
|
+
return existing_profiles
|
|
362
|
+
|
|
363
|
+
def deduplicate(
|
|
364
|
+
self,
|
|
365
|
+
new_profiles: list[UserProfile],
|
|
366
|
+
user_id: str,
|
|
367
|
+
request_id: str,
|
|
368
|
+
) -> tuple[list[UserProfile], list[str], list[UserProfile]]:
|
|
369
|
+
"""
|
|
370
|
+
Deduplicate profiles across extractors and against existing profiles in DB.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
new_profiles: List of new UserProfile objects from extractors
|
|
374
|
+
request_id: Request ID for context
|
|
375
|
+
user_id: User ID to scope the existing profile search
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Tuple of (deduplicated profiles, existing profile IDs to delete, superseded existing profiles)
|
|
379
|
+
"""
|
|
380
|
+
# Check if mock mode is enabled
|
|
381
|
+
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
|
|
382
|
+
logger.info("Mock mode: skipping deduplication")
|
|
383
|
+
return new_profiles, [], []
|
|
384
|
+
|
|
385
|
+
if not new_profiles:
|
|
386
|
+
return [], [], []
|
|
387
|
+
|
|
388
|
+
# Retrieve existing profiles via hybrid search
|
|
389
|
+
existing_profiles = self._retrieve_existing_profiles(new_profiles, user_id)
|
|
390
|
+
|
|
391
|
+
# Format for prompt
|
|
392
|
+
new_text, existing_text = self._format_new_and_existing_for_prompt(
|
|
393
|
+
new_profiles, existing_profiles
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Build and call LLM
|
|
397
|
+
prompt = self.request_context.prompt_manager.render_prompt(
|
|
398
|
+
self._get_prompt_id(),
|
|
399
|
+
{
|
|
400
|
+
"new_profile_count": len(new_profiles),
|
|
401
|
+
"new_profiles": new_text,
|
|
402
|
+
"existing_profile_count": len(existing_profiles),
|
|
403
|
+
"existing_profiles": existing_text,
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
output_schema_class = self._get_output_schema_class()
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
from reflexio.server.services.service_utils import (
|
|
411
|
+
log_llm_messages,
|
|
412
|
+
log_model_response,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
log_llm_messages(
|
|
416
|
+
logger, "Profile deduplication", [{"role": "user", "content": prompt}]
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
response = self.client.generate_chat_response(
|
|
420
|
+
messages=[{"role": "user", "content": prompt}],
|
|
421
|
+
model=self.model_name,
|
|
422
|
+
response_format=output_schema_class,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
log_model_response(logger, "Deduplication response", response)
|
|
426
|
+
|
|
427
|
+
if not isinstance(response, ProfileDeduplicationOutput):
|
|
428
|
+
logger.warning(
|
|
429
|
+
"Unexpected response type from deduplication LLM: %s",
|
|
430
|
+
type(response),
|
|
431
|
+
)
|
|
432
|
+
return _strip_deletion_markers(new_profiles), [], []
|
|
433
|
+
|
|
434
|
+
dedup_output = response
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error("Failed to identify duplicates: %s", str(e))
|
|
437
|
+
return _strip_deletion_markers(new_profiles), [], []
|
|
438
|
+
|
|
439
|
+
if not dedup_output.duplicate_groups and not dedup_output.deletions:
|
|
440
|
+
logger.info("No duplicate or deletion actions for request %s", request_id)
|
|
441
|
+
return _strip_deletion_markers(new_profiles), [], []
|
|
442
|
+
|
|
443
|
+
logger.info(
|
|
444
|
+
"Found %d duplicate profile groups and %d deletion directives for request %s",
|
|
445
|
+
len(dedup_output.duplicate_groups),
|
|
446
|
+
len(dedup_output.deletions),
|
|
447
|
+
request_id,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Build deduplicated result
|
|
451
|
+
return self._build_deduplicated_results(
|
|
452
|
+
new_profiles=new_profiles,
|
|
453
|
+
existing_profiles=existing_profiles,
|
|
454
|
+
dedup_output=dedup_output,
|
|
455
|
+
user_id=user_id,
|
|
456
|
+
request_id=request_id,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def _build_deduplicated_results(
|
|
460
|
+
self,
|
|
461
|
+
new_profiles: list[UserProfile],
|
|
462
|
+
existing_profiles: list[UserProfile],
|
|
463
|
+
dedup_output: ProfileDeduplicationOutput,
|
|
464
|
+
user_id: str,
|
|
465
|
+
request_id: str,
|
|
466
|
+
) -> tuple[list[UserProfile], list[str], list[UserProfile]]:
|
|
467
|
+
"""
|
|
468
|
+
Build the deduplicated profile list from LLM output.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
new_profiles: Flattened list of new profiles
|
|
472
|
+
existing_profiles: List of existing profiles from DB
|
|
473
|
+
dedup_output: LLM deduplication output
|
|
474
|
+
user_id: User ID
|
|
475
|
+
request_id: Request ID
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Tuple of (profiles ready to save, existing profile IDs to delete, superseded existing profiles)
|
|
479
|
+
"""
|
|
480
|
+
handled_new_indices: set[int] = set()
|
|
481
|
+
result_profiles: list[UserProfile] = []
|
|
482
|
+
existing_ids_to_delete: list[str] = []
|
|
483
|
+
seen_delete_ids: set[str] = set()
|
|
484
|
+
superseded_profiles: list[UserProfile] = []
|
|
485
|
+
|
|
486
|
+
now_ts = int(datetime.now(UTC).timestamp())
|
|
487
|
+
|
|
488
|
+
# Process deletion directives first. A directive is a NEW profile that
|
|
489
|
+
# is a meta-request to forget an EXISTING profile. Both the NEW and the
|
|
490
|
+
# matched EXISTING profile(s) are removed with no merged replacement.
|
|
491
|
+
self._apply_deletion_directives(
|
|
492
|
+
dedup_output.deletions,
|
|
493
|
+
new_profiles=new_profiles,
|
|
494
|
+
existing_profiles=existing_profiles,
|
|
495
|
+
handled_new_indices=handled_new_indices,
|
|
496
|
+
existing_ids_to_delete=existing_ids_to_delete,
|
|
497
|
+
seen_delete_ids=seen_delete_ids,
|
|
498
|
+
superseded_profiles=superseded_profiles,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Process duplicate groups
|
|
502
|
+
for group in dedup_output.duplicate_groups:
|
|
503
|
+
group_new_indices: list[int] = []
|
|
504
|
+
group_existing_indices: list[int] = []
|
|
505
|
+
|
|
506
|
+
for item_id in group.item_ids:
|
|
507
|
+
parsed = parse_item_id(item_id)
|
|
508
|
+
if parsed is None:
|
|
509
|
+
continue
|
|
510
|
+
prefix, idx = parsed
|
|
511
|
+
if prefix == "NEW":
|
|
512
|
+
group_new_indices.append(idx)
|
|
513
|
+
elif prefix == "EXISTING":
|
|
514
|
+
group_existing_indices.append(idx)
|
|
515
|
+
|
|
516
|
+
# Reject groups that overlap with profiles already consumed by a
|
|
517
|
+
# deletion directive. Merging such a group would write a
|
|
518
|
+
# replacement profile containing content the user asked to forget.
|
|
519
|
+
conflicting_new = [i for i in group_new_indices if i in handled_new_indices]
|
|
520
|
+
conflicting_existing = [
|
|
521
|
+
i
|
|
522
|
+
for i in group_existing_indices
|
|
523
|
+
if 0 <= i < len(existing_profiles)
|
|
524
|
+
and existing_profiles[i].profile_id
|
|
525
|
+
and existing_profiles[i].profile_id in seen_delete_ids
|
|
526
|
+
]
|
|
527
|
+
if conflicting_new or conflicting_existing:
|
|
528
|
+
logger.warning(
|
|
529
|
+
"Skipping duplicate group %s: overlaps with deletion "
|
|
530
|
+
"directives (NEW indices=%s, EXISTING indices=%s)",
|
|
531
|
+
group.item_ids,
|
|
532
|
+
conflicting_new,
|
|
533
|
+
conflicting_existing,
|
|
534
|
+
)
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
# Mark NEW indices as handled only after the overlap check passes.
|
|
538
|
+
for idx in group_new_indices:
|
|
539
|
+
handled_new_indices.add(idx)
|
|
540
|
+
|
|
541
|
+
# Collect existing profile IDs to delete and their profiles for changelog (deduplicated)
|
|
542
|
+
for eidx in group_existing_indices:
|
|
543
|
+
self._mark_existing_for_deletion(
|
|
544
|
+
f"EXISTING-{eidx}",
|
|
545
|
+
existing_profiles,
|
|
546
|
+
existing_ids_to_delete,
|
|
547
|
+
seen_delete_ids,
|
|
548
|
+
superseded_profiles,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Get template from first NEW profile in group (for metadata)
|
|
552
|
+
template_profile: UserProfile | None = None
|
|
553
|
+
if group_new_indices:
|
|
554
|
+
first_new_idx = group_new_indices[0]
|
|
555
|
+
if 0 <= first_new_idx < len(new_profiles):
|
|
556
|
+
template_profile = new_profiles[first_new_idx]
|
|
557
|
+
|
|
558
|
+
if template_profile is None:
|
|
559
|
+
logger.warning("Could not find template profile for group, skipping")
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
# Merge custom_features from all NEW profiles in group
|
|
563
|
+
group_new_profiles = [
|
|
564
|
+
new_profiles[i] for i in group_new_indices if 0 <= i < len(new_profiles)
|
|
565
|
+
]
|
|
566
|
+
merged_custom_features = self._merge_custom_features(group_new_profiles)
|
|
567
|
+
|
|
568
|
+
# Merge extractor_names from all NEW profiles in group
|
|
569
|
+
merged_extractor_names = self._merge_extractor_names(group_new_profiles)
|
|
570
|
+
|
|
571
|
+
# Determine TTL
|
|
572
|
+
try:
|
|
573
|
+
ttl = ProfileTimeToLive(group.merged_time_to_live)
|
|
574
|
+
except ValueError:
|
|
575
|
+
ttl = template_profile.profile_time_to_live
|
|
576
|
+
logger.warning(
|
|
577
|
+
"Invalid TTL '%s' from LLM, using template TTL '%s'",
|
|
578
|
+
group.merged_time_to_live,
|
|
579
|
+
ttl.value,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
merged_profile = UserProfile(
|
|
583
|
+
profile_id=str(uuid.uuid4()),
|
|
584
|
+
user_id=user_id,
|
|
585
|
+
content=group.merged_content,
|
|
586
|
+
last_modified_timestamp=now_ts,
|
|
587
|
+
generated_from_request_id=request_id,
|
|
588
|
+
profile_time_to_live=ttl,
|
|
589
|
+
expiration_timestamp=calculate_expiration_timestamp(now_ts, ttl),
|
|
590
|
+
custom_features=merged_custom_features,
|
|
591
|
+
source=template_profile.source,
|
|
592
|
+
status=template_profile.status,
|
|
593
|
+
extractor_names=merged_extractor_names,
|
|
594
|
+
)
|
|
595
|
+
result_profiles.append(merged_profile)
|
|
596
|
+
|
|
597
|
+
# Add unique NEW profiles
|
|
598
|
+
for uid in dedup_output.unique_ids:
|
|
599
|
+
parsed = parse_item_id(uid)
|
|
600
|
+
if parsed is None:
|
|
601
|
+
continue
|
|
602
|
+
prefix, idx = parsed
|
|
603
|
+
if (
|
|
604
|
+
prefix == "NEW"
|
|
605
|
+
and idx not in handled_new_indices
|
|
606
|
+
and 0 <= idx < len(new_profiles)
|
|
607
|
+
):
|
|
608
|
+
result_profiles.append(new_profiles[idx])
|
|
609
|
+
handled_new_indices.add(idx)
|
|
610
|
+
|
|
611
|
+
# Safety fallback: add any NEW profiles not mentioned by LLM
|
|
612
|
+
for idx, profile in enumerate(new_profiles):
|
|
613
|
+
if idx not in handled_new_indices:
|
|
614
|
+
logger.warning(
|
|
615
|
+
"New profile at index %d was not handled by LLM, adding as-is",
|
|
616
|
+
idx,
|
|
617
|
+
)
|
|
618
|
+
result_profiles.append(profile)
|
|
619
|
+
|
|
620
|
+
return result_profiles, existing_ids_to_delete, superseded_profiles
|
|
621
|
+
|
|
622
|
+
def _apply_deletion_directives(
|
|
623
|
+
self,
|
|
624
|
+
directives: list[ProfileDeletionDirective],
|
|
625
|
+
*,
|
|
626
|
+
new_profiles: list[UserProfile],
|
|
627
|
+
existing_profiles: list[UserProfile],
|
|
628
|
+
handled_new_indices: set[int],
|
|
629
|
+
existing_ids_to_delete: list[str],
|
|
630
|
+
seen_delete_ids: set[str],
|
|
631
|
+
superseded_profiles: list[UserProfile],
|
|
632
|
+
) -> None:
|
|
633
|
+
"""
|
|
634
|
+
Apply deletion directives in place: consume the NEW profile and mark matched
|
|
635
|
+
EXISTING profile(s) for deletion without producing a merged replacement.
|
|
636
|
+
|
|
637
|
+
A directive is a NEW profile whose content is a meta-request to forget an
|
|
638
|
+
EXISTING profile (e.g. "Requested removal of interest in X from stored
|
|
639
|
+
profiles"). The NEW is suppressed from the result set and the matched
|
|
640
|
+
EXISTING rows are added to the deletion list.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
directives: Deletion directives from the LLM.
|
|
644
|
+
new_profiles: Flat list of NEW profiles (indexed by NEW-N id).
|
|
645
|
+
existing_profiles: List of EXISTING profiles (indexed by EXISTING-M id).
|
|
646
|
+
handled_new_indices: Set of NEW indices already accounted for; this
|
|
647
|
+
method adds the consumed directive indices to it.
|
|
648
|
+
existing_ids_to_delete: Output list of profile IDs to delete; this
|
|
649
|
+
method appends to it.
|
|
650
|
+
seen_delete_ids: Set used to deduplicate IDs across all deletion paths.
|
|
651
|
+
superseded_profiles: Output list of deleted profile objects for the
|
|
652
|
+
changelog; this method appends to it.
|
|
653
|
+
"""
|
|
654
|
+
for directive in directives:
|
|
655
|
+
self._consume_new_index(
|
|
656
|
+
directive.new_id, len(new_profiles), handled_new_indices
|
|
657
|
+
)
|
|
658
|
+
for eid in directive.existing_ids:
|
|
659
|
+
self._mark_existing_for_deletion(
|
|
660
|
+
eid,
|
|
661
|
+
existing_profiles,
|
|
662
|
+
existing_ids_to_delete,
|
|
663
|
+
seen_delete_ids,
|
|
664
|
+
superseded_profiles,
|
|
665
|
+
)
|
|
666
|
+
logger.info(
|
|
667
|
+
"Profile deletion directive %s -> delete %s: %s",
|
|
668
|
+
directive.new_id,
|
|
669
|
+
directive.existing_ids,
|
|
670
|
+
directive.reasoning,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
@staticmethod
|
|
674
|
+
def _consume_new_index(
|
|
675
|
+
new_id: str, new_profile_count: int, handled_new_indices: set[int]
|
|
676
|
+
) -> None:
|
|
677
|
+
"""Mark a NEW-N id as handled so the safety fallback does not re-add it."""
|
|
678
|
+
parsed = parse_item_id(new_id)
|
|
679
|
+
if parsed is None:
|
|
680
|
+
return
|
|
681
|
+
prefix, idx = parsed
|
|
682
|
+
if prefix == "NEW" and 0 <= idx < new_profile_count:
|
|
683
|
+
handled_new_indices.add(idx)
|
|
684
|
+
|
|
685
|
+
@staticmethod
|
|
686
|
+
def _mark_existing_for_deletion(
|
|
687
|
+
existing_id: str,
|
|
688
|
+
existing_profiles: list[UserProfile],
|
|
689
|
+
existing_ids_to_delete: list[str],
|
|
690
|
+
seen_delete_ids: set[str],
|
|
691
|
+
superseded_profiles: list[UserProfile],
|
|
692
|
+
) -> None:
|
|
693
|
+
"""Resolve an EXISTING-N id to a profile_id and queue it for deletion."""
|
|
694
|
+
parsed = parse_item_id(existing_id)
|
|
695
|
+
if parsed is None:
|
|
696
|
+
return
|
|
697
|
+
prefix, idx = parsed
|
|
698
|
+
if prefix != "EXISTING" or not (0 <= idx < len(existing_profiles)):
|
|
699
|
+
return
|
|
700
|
+
pid = existing_profiles[idx].profile_id
|
|
701
|
+
if pid and pid not in seen_delete_ids:
|
|
702
|
+
seen_delete_ids.add(pid)
|
|
703
|
+
existing_ids_to_delete.append(pid)
|
|
704
|
+
superseded_profiles.append(existing_profiles[idx])
|
|
705
|
+
|
|
706
|
+
def _merge_custom_features(self, profiles: list[UserProfile]) -> dict | None:
|
|
707
|
+
"""
|
|
708
|
+
Merge custom_features from multiple profiles.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
profiles: List of profiles to merge custom_features from
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Merged custom_features dict or None if no custom_features
|
|
715
|
+
"""
|
|
716
|
+
merged = {}
|
|
717
|
+
for profile in profiles:
|
|
718
|
+
if profile.custom_features:
|
|
719
|
+
merged.update(profile.custom_features)
|
|
720
|
+
|
|
721
|
+
return merged or None
|
|
722
|
+
|
|
723
|
+
def _merge_extractor_names(self, profiles: list[UserProfile]) -> list[str] | None:
|
|
724
|
+
"""
|
|
725
|
+
Merge extractor_names from multiple profiles, preserving order and removing duplicates.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
profiles: List of profiles to merge extractor_names from
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Merged list of unique extractor names or None if no extractor_names
|
|
732
|
+
"""
|
|
733
|
+
seen: set[str] = set()
|
|
734
|
+
merged: list[str] = []
|
|
735
|
+
for profile in profiles:
|
|
736
|
+
if profile.extractor_names:
|
|
737
|
+
for name in profile.extractor_names:
|
|
738
|
+
if name not in seen:
|
|
739
|
+
seen.add(name)
|
|
740
|
+
merged.append(name)
|
|
741
|
+
return merged or None
|