claude-smart 0.2.42 → 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.
Files changed (392) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +1 -1
  3. package/package.json +9 -3
  4. package/plugin/.claude-plugin/plugin.json +9 -3
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/README.md +2 -2
  7. package/plugin/pyproject.toml +2 -2
  8. package/plugin/scripts/_lib.sh +91 -0
  9. package/plugin/scripts/backend-service.sh +1 -0
  10. package/plugin/scripts/cli.sh +1 -0
  11. package/plugin/scripts/codex-hook.js +72 -4
  12. package/plugin/scripts/dashboard-build.sh +1 -0
  13. package/plugin/scripts/dashboard-service.sh +1 -0
  14. package/plugin/scripts/hook_entry.sh +1 -0
  15. package/plugin/scripts/smart-install.sh +1 -0
  16. package/plugin/src/claude_smart/context_format.py +11 -12
  17. package/plugin/src/claude_smart/cs_cite.py +26 -12
  18. package/plugin/src/claude_smart/ids.py +13 -5
  19. package/plugin/uv.lock +1 -1
  20. package/plugin/vendor/reflexio/.env.example +53 -0
  21. package/plugin/vendor/reflexio/LICENSE +201 -0
  22. package/plugin/vendor/reflexio/README.md +338 -0
  23. package/plugin/vendor/reflexio/pyproject.toml +271 -0
  24. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  25. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  26. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  27. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  28. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  29. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  37. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  38. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  39. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  40. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  68. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  69. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  70. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  71. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  148. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  149. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  150. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  151. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  162. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  163. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  164. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  165. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  166. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
  180. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  181. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  182. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  183. package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
  184. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  185. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  186. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  187. package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
  188. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  189. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  190. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  195. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  196. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  197. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  198. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  199. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
  200. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  201. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
  214. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  215. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  216. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  217. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  218. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  282. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
  380. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  381. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  382. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  383. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  384. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  385. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  386. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  387. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  388. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  389. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  390. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  391. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  392. 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