foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,520 @@
1
+ """
2
+ VectorStore - 语义索引与检索
3
+ 修复内容:
4
+ 1. 向量持久化到磁盘(重启不丢失)
5
+ 2. 真实 Embedding(三层 fallback:OpenAI → sentence-transformers → TF-IDF)
6
+ 3. 启动时自动从磁盘加载已有向量
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import math
12
+ import re
13
+ import time
14
+ import hashlib
15
+ import threading
16
+ from pathlib import Path
17
+ from typing import List, Dict, Any, Optional
18
+ from collections import Counter
19
+
20
+
21
+ # ============================================================================
22
+ # Embedding Provider(三层 fallback)
23
+ # ============================================================================
24
+
25
+ class EmbeddingProvider:
26
+ """
27
+ 三层 fallback Embedding:
28
+ 1. OpenAI API(最好,需要 API Key)— 默认跳过(用户用云 LLM 主对话,embedding 走 bge-m3)
29
+ 2. sentence-transformers(本地模型,pip install sentence-transformers)
30
+ 3. TF-IDF 字符级 n-gram(纯 Python,始终可用,比哈希有真实语义)
31
+
32
+ 【P1·全系统 2 模型】模型解析顺序:
33
+ 显式传参 > HKC_EMBEDDING_MODEL(bge-m3)> FOXCLAW_EMBEDDING_MODEL > MiniLM 兜底。
34
+ 【P1·share-bge-m3】ST 加载优先调 HKC 共享缓存 get_st_model(),同名复用 instance,
35
+ 避免启动期重复加载 bge-m3 占双倍内存。
36
+ """
37
+
38
+ def __init__(self, model: str = None):
39
+ # 【P2·单 embedding 入口】模型解析链 —— 与 harness.py / evidence.py 一致:
40
+ # 显式 > EMBEDDING_MODEL(系统单入口) > HKC_EMBEDDING_MODEL > FOXCLAW_EMBEDDING_MODEL > MiniLM 兜底
41
+ self.model = (model
42
+ or os.environ.get("EMBEDDING_MODEL")
43
+ or os.environ.get("HKC_EMBEDDING_MODEL")
44
+ or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
45
+ or "all-MiniLM-L6-v2")
46
+ self._provider = None
47
+ self._vocab: Dict[str, int] = {}
48
+ self._idf: Dict[str, float] = {}
49
+ self._dim = 512 # TF-IDF fallback 维度
50
+ self._lock = threading.Lock()
51
+
52
+ self._init_provider()
53
+
54
+ def _init_provider(self):
55
+ # 【P1·全系统 2 模型】OpenAI 优先级:仅当用户显式开 HMR_USE_OPENAI_EMBEDDING=1 才走,
56
+ # 否则跳过(用户主对话已用 MiniMax 云 LLM,embedding 一律 bge-m3,不再切到 OpenAI)。
57
+ if os.environ.get("HMR_USE_OPENAI_EMBEDDING", "").strip() == "1":
58
+ try:
59
+ import openai
60
+ key = os.environ.get("OPENAI_API_KEY", "")
61
+ if key:
62
+ self._client = openai.OpenAI(api_key=key)
63
+ self._provider = "openai"
64
+ print("[HMR Embedding] 使用 OpenAI Embedding")
65
+ return
66
+ except ImportError:
67
+ pass
68
+ except Exception as e:
69
+ print(f"[HMR Embedding] OpenAI 初始化失败,降级:{e}")
70
+
71
+ # 尝试 sentence-transformers
72
+ # 【P1·share-bge-m3】优先用 HKC 共享缓存(同名复用 instance,启动期 0 重复加载)。
73
+ # 注意:sentence-transformers 已安装但模型权重下载失败(离线/内网)时抛 OSError,
74
+ # 不能只 catch ImportError,否则会直接崩溃而非降级到 TF-IDF。
75
+ try:
76
+ _shared = None
77
+ try:
78
+ from hkc_search.embedding_backends import get_st_model as _hk_get
79
+ _shared = _hk_get(self.model)
80
+ except Exception as _hk_e:
81
+ # 【P2·share-bge-m3】打印根因,不再静默吞错,便于排查 sys.path / 缓存 key 问题
82
+ print(f"[HMR-HKC 共享跳过] {_hk_e.__class__.__name__}: {_hk_e} → HMR 兜底独立 load SentenceTransformer")
83
+ if _shared is not None:
84
+ self._st_model = _shared
85
+ # 用实际模型维度校正 dim(bge-m3=1024, MiniLM=384)
86
+ _fn = (getattr(_shared, "get_embedding_dimension", None)
87
+ or getattr(_shared, "get_sentence_embedding_dimension", None))
88
+ if _fn:
89
+ self._dim = _fn()
90
+ else:
91
+ from sentence_transformers import SentenceTransformer
92
+ self._st_model = SentenceTransformer(self.model)
93
+ _fn = (getattr(self._st_model, "get_embedding_dimension", None)
94
+ or getattr(self._st_model, "get_sentence_embedding_dimension", None))
95
+ if _fn:
96
+ self._dim = _fn()
97
+ self._provider = "sentence_transformers"
98
+ print(f"[HMR Embedding] 使用 sentence-transformers ({self.model}, dim={self._dim})")
99
+ return
100
+ except ImportError:
101
+ pass
102
+ except Exception as e:
103
+ print(f"[HMR Embedding] sentence-transformers 不可用(可能模型未缓存/离线),降级到 TF-IDF:{e}")
104
+
105
+ # fallback: TF-IDF 字符级 n-gram(纯本地,无需任何下载,离线环境保底可用)
106
+ self._provider = "tfidf"
107
+ print("[HMR Embedding] 使用 TF-IDF n-gram Embedding(安装 openai 或 sentence-transformers 可获得更好效果)")
108
+
109
+ def embed(self, text: str) -> List[float]:
110
+ if self._provider == "openai":
111
+ return self._embed_openai(text)
112
+ elif self._provider == "sentence_transformers":
113
+ return self._embed_st(text)
114
+ else:
115
+ return self._embed_tfidf(text)
116
+
117
+ def embed_batch(self, texts: List[str]) -> List[List[float]]:
118
+ if self._provider == "openai":
119
+ return self._embed_openai_batch(texts)
120
+ elif self._provider == "sentence_transformers":
121
+ return self._embed_st_batch(texts)
122
+ else:
123
+ return [self._embed_tfidf(t) for t in texts]
124
+
125
+ # --- OpenAI ---
126
+
127
+ def _embed_openai(self, text: str) -> List[float]:
128
+ try:
129
+ resp = self._client.embeddings.create(
130
+ model=self.model,
131
+ input=text[:8000]
132
+ )
133
+ return resp.data[0].embedding
134
+ except Exception as e:
135
+ print(f"[HMR Embedding] OpenAI 失败,降级到 TF-IDF: {e}")
136
+ self._provider = "tfidf"
137
+ return self._embed_tfidf(text)
138
+
139
+ def _embed_openai_batch(self, texts: List[str]) -> List[List[float]]:
140
+ try:
141
+ resp = self._client.embeddings.create(
142
+ model=self.model,
143
+ input=[t[:8000] for t in texts]
144
+ )
145
+ return [item.embedding for item in resp.data]
146
+ except Exception as e:
147
+ print(f"[HMR Embedding] OpenAI 批量失败: {e}")
148
+ return [self._embed_tfidf(t) for t in texts]
149
+
150
+ # --- sentence-transformers ---
151
+
152
+ def _embed_st(self, text: str) -> List[float]:
153
+ vec = self._st_model.encode(text, normalize_embeddings=True)
154
+ return vec.tolist()
155
+
156
+ def _embed_st_batch(self, texts: List[str]) -> List[List[float]]:
157
+ vecs = self._st_model.encode(texts, normalize_embeddings=True)
158
+ return [v.tolist() for v in vecs]
159
+
160
+ # --- TF-IDF 字符级 n-gram(真实语义近似)---
161
+
162
+ def _tokenize(self, text: str) -> List[str]:
163
+ """字符级 2-gram + 词级 1-gram 组合"""
164
+ text = text.lower()
165
+ words = re.findall(r'\b\w+\b', text)
166
+ # 词级 unigram
167
+ tokens = list(words)
168
+ # 字符级 bigram(捕捉词根相似性)
169
+ for word in words:
170
+ for i in range(len(word) - 1):
171
+ tokens.append(word[i:i+2])
172
+ return tokens
173
+
174
+ def _update_vocab(self, tokens: List[str]):
175
+ with self._lock:
176
+ for t in tokens:
177
+ if t not in self._vocab:
178
+ self._vocab[t] = len(self._vocab)
179
+
180
+ def _embed_tfidf(self, text: str) -> List[float]:
181
+ tokens = self._tokenize(text)
182
+ self._update_vocab(tokens)
183
+
184
+ # TF
185
+ tf = Counter(tokens)
186
+ total = max(len(tokens), 1)
187
+
188
+ # 构建向量(固定维度,用哈希分桶)
189
+ vec = [0.0] * self._dim
190
+ for token, count in tf.items():
191
+ # 用 token 本身做哈希分桶(不同于纯哈希embed,这里有TF加权)
192
+ bucket = int(hashlib.md5(token.encode()).hexdigest(), 16) % self._dim
193
+ tf_val = count / total
194
+ # IDF(全局文档频率近似)
195
+ idf = self._idf.get(token, 1.0)
196
+ vec[bucket] += tf_val * idf
197
+
198
+ # L2 归一化
199
+ norm = math.sqrt(sum(x * x for x in vec))
200
+ if norm > 0:
201
+ vec = [x / norm for x in vec]
202
+
203
+ return vec
204
+
205
+ def update_idf(self, corpus_tokens: List[List[str]], total_docs: int):
206
+ """更新 IDF(当有足够文档时调用)"""
207
+ doc_freq: Dict[str, int] = Counter()
208
+ for tokens in corpus_tokens:
209
+ for t in set(tokens):
210
+ doc_freq[t] += 1
211
+ self._idf = {
212
+ t: math.log((total_docs + 1) / (freq + 1)) + 1
213
+ for t, freq in doc_freq.items()
214
+ }
215
+
216
+ @property
217
+ def dimension(self) -> int:
218
+ if self._provider == "openai":
219
+ return 1536
220
+ elif self._provider == "sentence_transformers":
221
+ return self._dim
222
+ else:
223
+ return self._dim
224
+
225
+
226
+ # ============================================================================
227
+ # VectorStore(持久化版本)
228
+ # ============================================================================
229
+
230
+ class VectorStore:
231
+ """
232
+ 向量存储(持久化版本)
233
+
234
+ 修复:
235
+ - 向量保存到磁盘(JSON 格式),重启后自动加载
236
+ - 使用真实 Embedding(EmbeddingProvider)
237
+ - 写入时加文件锁防止并发损坏
238
+ """
239
+
240
+ def __init__(
241
+ self,
242
+ embedding_model: str = None, # 【P2】默认 None → EmbeddingProvider 内部按 env 链解析
243
+ storage_path: Optional[str] = None
244
+ ):
245
+ self.embedding_model = (embedding_model
246
+ or os.environ.get("EMBEDDING_MODEL")
247
+ or os.environ.get("HKC_EMBEDDING_MODEL")
248
+ or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
249
+ or "all-MiniLM-L6-v2")
250
+ self.storage_path = Path(storage_path) if storage_path else None
251
+ self._lock = threading.Lock()
252
+
253
+ # 内存存储
254
+ self.vectors: Dict[str, List[float]] = {}
255
+ self.metadata: Dict[str, Dict[str, Any]] = {}
256
+
257
+ # Embedding provider
258
+ self.embedder = EmbeddingProvider(model=embedding_model)
259
+
260
+ # 从磁盘加载已有向量
261
+ if self.storage_path:
262
+ self._load_from_disk()
263
+
264
+ # -------------------------------------------------------------------------
265
+ # 核心 API
266
+ # -------------------------------------------------------------------------
267
+
268
+ def embed(self, text: str) -> List[float]:
269
+ """生成真实 Embedding"""
270
+ return self.embedder.embed(text)
271
+
272
+ def add(
273
+ self,
274
+ id: str,
275
+ embedding: List[float],
276
+ metadata: Dict[str, Any]
277
+ ):
278
+ """添加向量,并持久化到磁盘"""
279
+ with self._lock:
280
+ self.vectors[id] = embedding
281
+ self.metadata[id] = metadata
282
+ if self.storage_path:
283
+ self._save_to_disk()
284
+
285
+ def search(
286
+ self,
287
+ query_embedding: List[float],
288
+ top_k: int = 5,
289
+ filters: Optional[Dict[str, Any]] = None
290
+ ) -> List[Dict[str, Any]]:
291
+ """语义搜索"""
292
+ results = []
293
+
294
+ with self._lock:
295
+ for vec_id, vector in self.vectors.items():
296
+ if filters:
297
+ meta = self.metadata.get(vec_id, {})
298
+ if not self._matches_filters(meta, filters):
299
+ continue
300
+
301
+ similarity = self._cosine_similarity(query_embedding, vector)
302
+ results.append({
303
+ "id": vec_id,
304
+ "score": similarity,
305
+ "metadata": self.metadata.get(vec_id, {})
306
+ })
307
+
308
+ results.sort(key=lambda x: x["score"], reverse=True)
309
+ return results[:top_k]
310
+
311
+ def delete(self, id: str) -> bool:
312
+ with self._lock:
313
+ if id in self.vectors:
314
+ del self.vectors[id]
315
+ del self.metadata[id]
316
+ if self.storage_path:
317
+ self._save_to_disk()
318
+ return True
319
+ return False
320
+
321
+ def rebuild_from_memories(self, memories):
322
+ """
323
+ 从 MemoryFS 重建向量索引(修复持久化裂缝)
324
+ 当 VectorStore 为空但 MemoryFS 有数据时调用
325
+ """
326
+ if not memories:
327
+ return
328
+
329
+ print(f"[HMR VectorStore] 从 MemoryFS 重建索引,共 {len(memories)} 条记忆...")
330
+
331
+ texts = [m.semantic_summary or m.content[:500] for m in memories]
332
+ embeddings = self.embedder.embed_batch(texts)
333
+
334
+ with self._lock:
335
+ for memory, embedding in zip(memories, embeddings):
336
+ self.vectors[memory.id] = embedding
337
+ self.metadata[memory.id] = {
338
+ "type": memory.type,
339
+ "title": memory.title,
340
+ "tags": memory.tags,
341
+ "temporal_weight": memory.temporal_weight
342
+ }
343
+ if self.storage_path:
344
+ self._save_to_disk()
345
+
346
+ print(f"[HMR VectorStore] 重建完成")
347
+
348
+ def get_stats(self) -> Dict[str, Any]:
349
+ return {
350
+ "total_vectors": len(self.vectors),
351
+ "embedding_provider": self.embedder._provider,
352
+ "embedding_dimension": self.embedder.dimension,
353
+ "memory_mb": self._estimate_memory_mb(),
354
+ "persisted": self.storage_path is not None
355
+ }
356
+
357
+ # -------------------------------------------------------------------------
358
+ # 持久化
359
+ # -------------------------------------------------------------------------
360
+
361
+ def _save_to_disk(self):
362
+ """保存向量到磁盘(JSON 格式,线程安全内已调用)"""
363
+ if not self.storage_path:
364
+ return
365
+
366
+ self.storage_path.mkdir(parents=True, exist_ok=True)
367
+ vec_path = self.storage_path / "vectors.json"
368
+ meta_path = self.storage_path / "vector_metadata.json"
369
+
370
+ # 写临时文件再原子替换,防止写到一半崩溃
371
+ tmp_vec = vec_path.with_suffix(".tmp")
372
+ tmp_meta = meta_path.with_suffix(".tmp")
373
+
374
+ # 必须显式 utf-8:不指定时 Py3 在 Windows 简体中文环境按 cp936 落盘,
375
+ # 而 metadata 里含中文标题/摘要(ensure_ascii=False),写出的是 GBK 字节。
376
+ # 一旦有进程以 UTF-8 模式运行(-X utf8 / PYTHONUTF8=1)就读不回来。
377
+ with open(tmp_vec, "w", encoding="utf-8") as f:
378
+ json.dump(self.vectors, f)
379
+ with open(tmp_meta, "w", encoding="utf-8") as f:
380
+ json.dump(self.metadata, f, ensure_ascii=False, indent=2)
381
+
382
+ tmp_vec.replace(vec_path)
383
+ tmp_meta.replace(meta_path)
384
+
385
+ def _load_from_disk(self):
386
+ """启动时从磁盘加载向量。
387
+
388
+ 历史缺陷:读写两侧都没指定 encoding。Windows 简体中文环境下 open() 默认
389
+ cp936,metadata(含中文标题/摘要)以 GBK 落盘;之后只要有进程以 UTF-8 模式
390
+ 运行(-X utf8 / PYTHONUTF8=1)读取就解码失败 → 走进 except 把 vectors 和
391
+ metadata 清空 → 下一次 add 触发 _save_to_disk,用空集覆盖磁盘。
392
+ 症状是"召回突然失灵,但记忆文件看着都还在",且真实向量已不可恢复。
393
+
394
+ 现在:写入统一 UTF-8;读取按 utf-8 → gbk 依次尝试(兼容历史 GBK 文件);
395
+ 仍然读不出来就把原文件改名留存,再以空索引启动 —— 宁可少加载,也不让
396
+ 空集静默盖掉原数据。
397
+ """
398
+ if not self.storage_path:
399
+ return
400
+
401
+ vec_path = self.storage_path / "vectors.json"
402
+ meta_path = self.storage_path / "vector_metadata.json"
403
+
404
+ if vec_path.exists() and meta_path.exists():
405
+ try:
406
+ self.vectors = self._read_json_file(vec_path)
407
+ self.metadata = self._read_json_file(meta_path)
408
+ print(f"[HMR VectorStore] 从磁盘加载 {len(self.vectors)} 条向量")
409
+ except Exception as e:
410
+ kept = self._quarantine_unreadable(vec_path, meta_path)
411
+ print(f"[HMR VectorStore] ⚠ 加载失败({e.__class__.__name__}: {e}),"
412
+ f"原文件已留存为 {kept},本次以空索引启动")
413
+
414
+ def purge_incompatible(self, dimension: int) -> bool:
415
+ """丢弃维度与当前模型不符的向量(换 embedding 模型后),返回是否发生丢弃。
416
+
417
+ 为什么必须主动清:换模型后旧向量照常从磁盘加载,但 _cosine_similarity 对
418
+ 长度不等的一律 return 0.0 —— 表现是"召回有条目、分数全 0、排序随机",
419
+ 既不报错也不为空,是最难发现的一种失效。清空前把原文件改名保留,
420
+ 回滚只需把 env 改回去、再把备份改回原名。
421
+ """
422
+ if not self.vectors:
423
+ return False
424
+
425
+ stale_dims = sorted({len(v) for v in self.vectors.values()
426
+ if len(v) != dimension})
427
+ if not stale_dims:
428
+ return False
429
+
430
+ count = sum(1 for v in self.vectors.values() if len(v) in stale_dims)
431
+
432
+ kept = ""
433
+ if self.storage_path:
434
+ stamp = time.strftime("%Y%m%d-%H%M%S")
435
+ tag = "dim" + "-".join(str(d) for d in stale_dims)
436
+ names = []
437
+ for name in ("vectors.json", "vector_metadata.json"):
438
+ p = self.storage_path / name
439
+ if not p.exists():
440
+ continue
441
+ target = p.with_name(f"{p.name}.{tag}-{stamp}")
442
+ try:
443
+ p.rename(target)
444
+ names.append(target.name)
445
+ except OSError:
446
+ pass
447
+ kept = ", ".join(names)
448
+
449
+ with self._lock:
450
+ self.vectors.clear()
451
+ self.metadata.clear()
452
+
453
+ print(f"[HMR VectorStore] ⚠ 旧向量维度 {stale_dims} ≠ 当前模型 {dimension},"
454
+ f"已清空 {count} 条" + (f"(原文件备份: {kept})" if kept else ""))
455
+ return True
456
+
457
+ # 历史文件可能是 cp936 写出的,读取时按序试;不做 latin-1 兜底 —— 它能把任意
458
+ # 损坏字节解成乱码并"成功"解析,反而掩盖问题,让乱码随后覆盖真实数据。
459
+ _READ_ENCODINGS = ("utf-8", "gbk")
460
+
461
+ @classmethod
462
+ def _read_json_file(cls, path: Path):
463
+ last_err: Optional[Exception] = None
464
+ for enc in cls._READ_ENCODINGS:
465
+ try:
466
+ with open(path, "r", encoding=enc) as f:
467
+ return json.load(f)
468
+ except Exception as e: # UnicodeDecodeError / JSONDecodeError 都是 ValueError
469
+ last_err = e
470
+ raise last_err
471
+
472
+ @staticmethod
473
+ def _quarantine_unreadable(vec_path: Path, meta_path: Path) -> str:
474
+ """把读不了的文件改名保留,供事后人工恢复;返回结果描述。"""
475
+ stamp = time.strftime("%Y%m%d-%H%M%S")
476
+ kept = []
477
+ for p in (vec_path, meta_path):
478
+ if not p.exists():
479
+ continue
480
+ target = p.with_name(f"{p.name}.unreadable-{stamp}")
481
+ try:
482
+ p.rename(target)
483
+ kept.append(target.name)
484
+ except OSError:
485
+ pass
486
+ return ", ".join(kept) or "(备份失败,原文件可能被占用)"
487
+
488
+ # -------------------------------------------------------------------------
489
+ # 工具方法
490
+ # -------------------------------------------------------------------------
491
+
492
+ @staticmethod
493
+ def _cosine_similarity(vec1: List[float], vec2: List[float]) -> float:
494
+ if len(vec1) != len(vec2):
495
+ return 0.0
496
+ dot = sum(a * b for a, b in zip(vec1, vec2))
497
+ n1 = math.sqrt(sum(x * x for x in vec1))
498
+ n2 = math.sqrt(sum(x * x for x in vec2))
499
+ if n1 == 0 or n2 == 0:
500
+ return 0.0
501
+ return dot / (n1 * n2)
502
+
503
+ @staticmethod
504
+ def _matches_filters(metadata: Dict[str, Any], filters: Dict[str, Any]) -> bool:
505
+ for key, value in filters.items():
506
+ if key not in metadata:
507
+ return False
508
+ meta_val = metadata[key]
509
+ if isinstance(value, list):
510
+ if meta_val not in value:
511
+ return False
512
+ else:
513
+ if meta_val != value:
514
+ return False
515
+ return True
516
+
517
+ def _estimate_memory_mb(self) -> float:
518
+ n = len(self.vectors)
519
+ dim = self.embedder.dimension
520
+ return round(n * dim * 4 / (1024 * 1024), 2)