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,484 @@
1
+ """
2
+ hkc-search / vector_search.py
3
+ 向量检索(Warm 层)。
4
+
5
+ 向量生成委托给可插拔的 EmbeddingBackend(见 embedding_backends.py):
6
+ LocalSTBackend 本地 sentence-transformers
7
+ TEIBackend 远程 TEI 服务
8
+ StubBackend 离线/测试
9
+
10
+ FAISS IndexFlatIP 做近似最近邻,向量已 L2 归一化,内积 = 余弦相似度。
11
+ VectorIndex 不再关心模型如何加载,只调用 backend.encode()。
12
+
13
+ 阶段 A:manifest 升级
14
+ ────────────────────
15
+ 旧 manifest(v1,无版本号)只存 `ku_ids / names / summaries / backend`,隐患:
16
+ 1. 加载时不校验 backend/dim 与实际后端是否匹配 → 旧 faiss 向量配新 backend 会
17
+ 让检索结果全错(向量空间不同)。
18
+ 2. 不存文本 hash → 阶段 B update_ku 无法知道"内容是否真变",每条都重建向量
19
+ (5000 条 × 重新 encode 一次 ≈ 几十秒)。
20
+ 3. 不存 dim → 切换后端后 faiss 向量维度可能与新后端 dim 不一致,加载即崩。
21
+
22
+ 新 manifest(v2):
23
+ format_version: int (当前 2)
24
+ backend: str (后端名,如 "stub"/"local:bge-m3"/"tei:xxx")
25
+ dim: int (后端向量维度,加索引文件维度)
26
+ ku_ids: list[str]
27
+ names: list[str]
28
+ summaries: list[str]
29
+ text_hashes: list[str] ← 阶段 B 用,长度 == len(ku_ids)
30
+
31
+ 加载 8 步校验:任何一步不过 → _reset() 清空,让上层走兜底(全量重建)。
32
+ """
33
+ from __future__ import annotations
34
+ import hashlib
35
+ import logging
36
+ import os
37
+ import pickle
38
+ import sys
39
+ import threading
40
+ from dataclasses import dataclass
41
+
42
+
43
+ import numpy as np
44
+ from .embedding_backends import EmbeddingBackend, make_backend
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ # ── manifest 常量 ─────────────────────────────────────────────────────
50
+ # 升级 manifest 格式时务必 +1,旧 manifest 在 load() 校验会失败→ 触发 _reset() → 兜底全量重建
51
+ _MANIFEST_FORMAT_VERSION = 2
52
+ # _ku_text() 的语义哈希:任何对 name/summary/tags/statement 的拼接逻辑修改,都会改
53
+ # text 拼接结果 → 旧 hash 必不匹配 → 阶段 B update_ku 会按"内容变了"重 encode,不会出错。
54
+ # 但若你**同时**改了 text 拼接 + 升级了 manifest version,二者会叠加;推荐:只改其一。
55
+ _KU_TEXT_FORMAT_VERSION = 1
56
+
57
+
58
+ def _hash_text(text: str) -> str:
59
+ """稳定的内容指纹:阶段 B 用它做 'update_ku 实际是否真改了内容' 检测。
60
+ SHA1 16 字符足够防误判(碰撞概率 ~1/2^64);hex 短,存盘小。
61
+ 故意不用 blake2 —— _compact 的内存清洗会让 hash 形式不可读,SHA1 hex 短更易排查。"""
62
+ return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16]
63
+
64
+
65
+ # 阶段 C:Vector 索引自动升级阈值。
66
+ # 当累积向量数 >= 这个阈值后,IndexFlatIP(暴力精确)自动升级到 IndexIVFFlat(聚类倒排,10x 检索加速)。
67
+ # 通过环境变量 HKC_VECTOR_IVF_THRESHOLD 可调(默认 10000;测试时设 200 即可触发升级)。
68
+ # 设 0 = 禁用升级,始终用 IndexFlatIP(用于回归测试 / 精度对比)。
69
+ HKC_VECTOR_IVF_THRESHOLD = int(os.environ.get("HKC_VECTOR_IVF_THRESHOLD", "10000"))
70
+
71
+
72
+ @dataclass
73
+ class VectorResult:
74
+ ku_id: str
75
+ score: float # 余弦相似度 0-1
76
+ name: str
77
+ summary: str
78
+
79
+
80
+ class VectorIndex:
81
+
82
+ def __init__(
83
+ self,
84
+ backend: EmbeddingBackend | None = None,
85
+ model_name: str | None = None, # 向后兼容旧调用
86
+ ):
87
+ """
88
+ backend: 显式传入 embedding 后端。
89
+ 不传则用 make_backend("auto")(本地优先,回退 stub)。
90
+ model_name: 兼容旧接口,等价于 make_backend("local", model_name=...)。
91
+ """
92
+ if backend is not None:
93
+ self._backend = backend
94
+ elif model_name is not None:
95
+ self._backend = make_backend("local", model_name=model_name)
96
+ else:
97
+ self._backend = make_backend("auto")
98
+
99
+ self._index = None
100
+ self._ku_ids: list[str] = []
101
+ self._names: list[str] = []
102
+ self._summaries: list[str] = []
103
+ # 阶段 A:每条 KU 对应一个文本 hash(由 _ku_text(ku) 计算),与 _ku_ids 一一对应。
104
+ # 阶段 B 的 update_ku 用它做"内容真伪变化"检测 —— 不变则跳过 encode,变才重编。
105
+ # save/load 都要带;load 缺失则视为旧 v1 manifest,触发 _reset() 兜底。
106
+ self._text_hashes: list[str] = []
107
+ # B1+B2 写-写互斥锁(RLock 支持 update_ku 内部嵌套调
108
+ # remove_ku/append_ku 不死锁)。append_bulk 内部 encode 在锁外,
109
+ # 仅 faiss.add + 4 list extend 持锁(微秒级)—— 避免和 _sync_ku
110
+ # 抢锁导致 ingest 阻塞。读(search)不持此锁,与写不互斥。
111
+ self._write_lock = threading.RLock()
112
+
113
+ @property
114
+ def backend_name(self) -> str:
115
+ return self._backend.name
116
+
117
+ @property
118
+ def dim(self) -> int:
119
+ return self._backend.dim
120
+
121
+ # ── 构建 ──────────────────────────────────────────────────
122
+
123
+ def build(self, kus: list) -> None:
124
+ if not kus:
125
+ return
126
+ texts = [self._ku_text(ku) for ku in kus]
127
+ embeddings = self._backend.encode(texts) # encode 锁外(慢)
128
+ with self._write_lock: # 锁内(微秒)
129
+ self._init_index()
130
+ self._index.add(embeddings)
131
+ self._maybe_upgrade_to_ivf() # 阶段 C:达阈值时 Flat → IVF
132
+ self._ku_ids = [ku.ku_id for ku in kus]
133
+ self._names = [ku.name for ku in kus]
134
+ self._summaries = [ku.summary[:100] for ku in kus]
135
+ self._text_hashes = [_hash_text(t) for t in texts]
136
+ logger.info("向量索引构建完成,共 %d 条 (backend=%s)",
137
+ len(self._ku_ids), self._backend.name)
138
+
139
+ def append_ku(self, ku) -> None:
140
+ # 先 encode(触发后端维度校正),再用校正后的 dim 初始化索引
141
+ text = self._ku_text(ku)
142
+ vec = self._backend.encode([text]) # encode 锁外(慢)
143
+ with self._write_lock: # 锁内(微秒)
144
+ if self._index is None:
145
+ self._init_index()
146
+ self._index.add(vec)
147
+ self._maybe_upgrade_to_ivf() # 阶段 C:达阈值时 Flat → IVF
148
+ self._ku_ids.append(ku.ku_id)
149
+ self._names.append(ku.name)
150
+ self._summaries.append(ku.summary[:100])
151
+ self._text_hashes.append(_hash_text(text))
152
+
153
+ def append_bulk(self, kus: list) -> None:
154
+ """批量追加 KU(一次 encode 调用,比循环 append_ku 快 ~10x)。
155
+ 用于启动时把磁盘索引中缺失的 KU 增量补齐:N=几百到几千时秒级返回。
156
+ B1+B2:encode 在锁外(慢,几十~几百 ms/batch),仅 faiss.add + 4 list
157
+ extend 持 _write_lock(微秒级)—— 避免和 _sync_ku 抢锁导致 ingest
158
+ 阻塞。RLock 允许同线程嵌套(update_ku 内部也调)。"""
159
+ if not kus:
160
+ return
161
+ texts = [self._ku_text(ku) for ku in kus]
162
+ embeddings = self._backend.encode(texts) # 锁外(慢)
163
+ with self._write_lock: # 锁内(微秒)
164
+ if self._index is None:
165
+ self._init_index()
166
+ self._index.add(embeddings)
167
+ self._maybe_upgrade_to_ivf() # 阶段 C:达阈值时 Flat → IVF
168
+ self._ku_ids .extend([ku.ku_id for ku in kus])
169
+ self._names .extend([ku.name for ku in kus])
170
+ self._summaries .extend([ku.summary[:100] for ku in kus])
171
+ self._text_hashes.extend([_hash_text(t) for t in texts])
172
+
173
+ def update_ku(self, ku) -> bool:
174
+ """更新某 KU 的向量。返回是否真发生编码(同 hash 跳过)。
175
+ 阶段 B:由 hybrid.update_ku 在 _lock 内调用,经 text_hash 检测:
176
+ - hash 未变 → 跳过 encode(节省 1 次大模型调用,大库下显著)
177
+ - hash 变 → remove 旧条 + append 新条
178
+ B1+B2:整体持 _write_lock(RLock 支持内部 remove_ku/append_ku
179
+ 嵌套 acquire 不死锁);encode 在 append_ku 内已锁外。
180
+ 注:IndexFlatIP add 总在末尾 push(id 自增),故 index == 在 5 个 list 里的位置。
181
+ """
182
+ if self._index is None:
183
+ return False
184
+ if ku.ku_id not in self._ku_ids:
185
+ return False
186
+ text = self._ku_text(ku)
187
+ new_hash = _hash_text(text)
188
+ idx = self._ku_ids.index(ku.ku_id)
189
+ if self._text_hashes[idx] == new_hash:
190
+ return False # 同 hash → 跳过 encode
191
+ with self._write_lock:
192
+ self.remove_ku(ku.ku_id)
193
+ self.append_ku(ku)
194
+ return True
195
+
196
+ def remove_ku(self, ku_id: str) -> bool:
197
+ """从 faiss 删一条 + 同步 5 个 list。返回是否真删了。
198
+ 阶段 B:用于 KEE 状态机变更(把 superseded/disputed 等从检索剔除)、
199
+ delete_source 级联删、manifest 校验失败后清空旧数据。
200
+ IndexFlatIP.add 永远 push 末尾,id 与 list 位置一一对应。
201
+ B1+B2:整体持 _write_lock(RLock 支持嵌套)。
202
+
203
+ faiss 1.13 注:Index.remove_ids(selector) 是 **in-place** 操作,直接修改
204
+ 内部存储并返 int(剩余数量),**不**返新 Index。若用 `new_idx = idx.remove_ids()`
205
+ 会让 new_idx 变成 int 而非 Index,后续操作全崩。**这是测试 E3/E4 早期失败
206
+ 的根因**。
207
+ """
208
+ if self._index is None:
209
+ return False
210
+ if ku_id not in self._ku_ids:
211
+ return False
212
+ with self._write_lock:
213
+ import faiss
214
+ idx = self._ku_ids.index(ku_id)
215
+ sel = faiss.IDSelectorBatch(np.array([idx], dtype=np.int64))
216
+ self._index.remove_ids(sel)
217
+ # 同步 4 个 list。faiss 内部已经把后续 id 重映射(连续 0..n-1),
218
+ # 我们的 _ku_ids/_names/... 是按位置存的,删 idx 后 4 list 同步剔 idx 位置。
219
+ self._ku_ids = self._ku_ids[:idx] + self._ku_ids[idx+1:]
220
+ self._names = self._names[:idx] + self._names[idx+1:]
221
+ self._summaries = self._summaries[:idx] + self._summaries[idx+1:]
222
+ self._text_hashes= self._text_hashes[:idx]+ self._text_hashes[idx+1:]
223
+ return True
224
+
225
+ # ── 检索 ──────────────────────────────────────────────────
226
+
227
+ def search(self, query: str, top_k: int = 10) -> list[VectorResult]:
228
+ if self._index is None or self._index.ntotal == 0:
229
+ return []
230
+ vec = self._backend.encode([query])
231
+ k = min(top_k, self._index.ntotal)
232
+ distances, indices = self._index.search(vec, k)
233
+
234
+ results = []
235
+ for dist, idx in zip(distances[0], indices[0]):
236
+ if idx < 0 or idx >= len(self._ku_ids):
237
+ continue
238
+ score = float(dist)
239
+ if score < 0.1:
240
+ continue
241
+ results.append(VectorResult(
242
+ ku_id = self._ku_ids[idx],
243
+ score = score,
244
+ name = self._names[idx],
245
+ summary = self._summaries[idx],
246
+ ))
247
+ return results
248
+
249
+ def size(self) -> int:
250
+ return self._index.ntotal if self._index else 0
251
+
252
+ # ── 持久化 ────────────────────────────────────────────────
253
+
254
+ def save(self, path: str) -> None:
255
+ import faiss
256
+ if self._index is None:
257
+ logger.warning("索引为空,跳过保存")
258
+ return
259
+ faiss.write_index(self._index, path + ".faiss")
260
+ # 阶段 A:manifest 升 v2,加 format_version/dim/text_hashes。
261
+ # 4 个 list 长度必须一致,save 前用 _check_invariant 校验,失败抛 AssertionError
262
+ # —— 不让不一致的 manifest 落盘(下次加载必 8 步校验失败→ _reset 兜底,徒增重建)。
263
+ self._check_invariant()
264
+ with open(path + ".meta", "wb") as f:
265
+ pickle.dump({
266
+ "format_version": _MANIFEST_FORMAT_VERSION,
267
+ "ku_text_version": _KU_TEXT_FORMAT_VERSION,
268
+ "backend": self._backend.name,
269
+ "dim": self._backend.dim,
270
+ "ku_ids": self._ku_ids,
271
+ "names": self._names,
272
+ "summaries": self._summaries,
273
+ "text_hashes": self._text_hashes,
274
+ }, f)
275
+ logger.info("向量索引已保存: %s (v%d, %d 条, dim=%d)",
276
+ path, _MANIFEST_FORMAT_VERSION,
277
+ len(self._ku_ids), self._backend.dim)
278
+
279
+ def load(self, path: str) -> bool:
280
+ """加载磁盘索引。返回 True 表示成功,False 表示文件不存在 / 校验失败(_reset 已清空)。"""
281
+ import faiss
282
+ faiss_path = path + ".faiss"
283
+ meta_path = path + ".meta"
284
+ if not (os.path.exists(faiss_path) and os.path.exists(meta_path)):
285
+ return False
286
+ try:
287
+ with open(meta_path, "rb") as f:
288
+ meta = pickle.load(f)
289
+ except Exception as e:
290
+ logger.warning("meta 损坏无法加载,触发 _reset 兜底: %s", e)
291
+ self._reset()
292
+ return False
293
+ # 8 步校验:任一不过 → _reset() → 上层走兜底全量重建
294
+ try:
295
+ self._validate_manifest(meta, faiss_path)
296
+ except _ManifestValidationError as e:
297
+ logger.warning("manifest 校验失败[%s],触发 _reset 兜底: %s",
298
+ e.reason, e)
299
+ self._reset()
300
+ return False
301
+ # 全部通过:加载
302
+ self._index = faiss.read_index(faiss_path)
303
+ self._ku_ids = meta["ku_ids"]
304
+ self._names = meta["names"]
305
+ self._summaries = meta["summaries"]
306
+ self._text_hashes = meta["text_hashes"]
307
+ logger.info("向量索引已加载: %d 条 (v%d, backend=%s, dim=%d)",
308
+ self._index.ntotal, meta.get("format_version", 1),
309
+ meta.get("backend"), meta.get("dim"))
310
+ return True
311
+
312
+ def _validate_manifest(self, meta: dict, faiss_path: str) -> None:
313
+ """阶段 A 8 步校验。任一不通过抛 _ManifestValidationError,load() 捕获后 _reset()。
314
+
315
+ 8 步:
316
+ 1. format_version 存在且 == _MANIFEST_FORMAT_VERSION(旧 v1 无此字段 → 不过)
317
+ 2. ku_text_version 存在且 == _KU_TEXT_FORMAT_VERSION(_ku_text 拼接改了就过不了)
318
+ 3. backend 字段存在(str)
319
+ 4. dim 字段存在(int)
320
+ 5. len(ku_ids) == len(names) == len(summaries) == len(text_hashes)
321
+ 6. 索引文件 faiss 存在(已在 caller 校验)
322
+ 7. faiss 索引的 d == meta["dim"]
323
+ 8. faiss 索引 ntotal == len(meta["ku_ids"])
324
+ """
325
+ if not isinstance(meta.get("format_version"), int):
326
+ raise _ManifestValidationError("missing_format_version",
327
+ "format_version 字段缺失或非 int(旧 v1 manifest)")
328
+ if meta["format_version"] != _MANIFEST_FORMAT_VERSION:
329
+ raise _ManifestValidationError(
330
+ "format_version_mismatch",
331
+ f"format_version={meta['format_version']} 但当前期望 {_MANIFEST_FORMAT_VERSION}",
332
+ )
333
+ if meta.get("ku_text_version") != _KU_TEXT_FORMAT_VERSION:
334
+ raise _ManifestValidationError(
335
+ "ku_text_version_mismatch",
336
+ f"ku_text_version={meta.get('ku_text_version')!r} 期望 {_KU_TEXT_FORMAT_VERSION}"
337
+ f"(_ku_text 拼接逻辑变了,旧 hash 无意义,必须全量重建)",
338
+ )
339
+ if not isinstance(meta.get("backend"), str):
340
+ raise _ManifestValidationError("missing_backend", "backend 字段缺失或非 str")
341
+ if not isinstance(meta.get("dim"), int) or meta["dim"] <= 0:
342
+ raise _ManifestValidationError("missing_dim", "dim 字段缺失或非法")
343
+ lens = (len(meta.get("ku_ids", [])),
344
+ len(meta.get("names", [])),
345
+ len(meta.get("summaries", [])),
346
+ len(meta.get("text_hashes", [])))
347
+ if len(set(lens)) != 1:
348
+ raise _ManifestValidationError(
349
+ "list_length_mismatch",
350
+ f"4 个 list 长度不一致: {lens}",
351
+ )
352
+ # 6/7/8 涉及 faiss 实际维度
353
+ import faiss
354
+ idx = faiss.read_index(faiss_path)
355
+ if idx.d != meta["dim"]:
356
+ raise _ManifestValidationError(
357
+ "faiss_dim_mismatch",
358
+ f"faiss d={idx.d} 但 meta dim={meta['dim']}",
359
+ )
360
+ if idx.ntotal != lens[0]:
361
+ raise _ManifestValidationError(
362
+ "faiss_ntotal_mismatch",
363
+ f"faiss ntotal={idx.ntotal} 但 meta ku_ids 数={lens[0]}",
364
+ )
365
+
366
+ def _reset(self) -> None:
367
+ """加载失败兜底:清空所有 in-memory 状态,让上层走全量重建。
368
+ 不删磁盘文件 —— 排查时常常需要看「旧 manifest 究竟哪里坏了」;上层兜底重建后
369
+ 会写新 manifest 把旧 .meta 覆盖。"""
370
+ self._index = None
371
+ self._ku_ids = []
372
+ self._names = []
373
+ self._summaries = []
374
+ self._text_hashes = []
375
+ logger.warning("VectorIndex _reset: in-memory 索引已清空,等待上层兜底重建")
376
+
377
+ def _check_invariant(self) -> None:
378
+ """save 前的内部一致性:5 个 list 长度 + dim 已初始化。"""
379
+ if self._index is None:
380
+ raise AssertionError("save() 时 _index 还没初始化")
381
+ n = self._index.ntotal
382
+ for name, lst in (("ku_ids", self._ku_ids),
383
+ ("names", self._names),
384
+ ("summaries", self._summaries),
385
+ ("text_hashes", self._text_hashes)):
386
+ if len(lst) != n:
387
+ raise AssertionError(
388
+ f"save() invariant 失败:{name} len={len(lst)} 但 faiss ntotal={n}"
389
+ )
390
+
391
+ # ── 内部方法 ─────────────────────────────────────────────
392
+
393
+ def _init_index(self) -> None:
394
+ try:
395
+ import faiss
396
+ self._index = faiss.IndexFlatIP(self._backend.dim)
397
+ except ImportError:
398
+ raise ImportError("faiss-cpu 未安装: pip install faiss-cpu")
399
+
400
+ def _maybe_upgrade_to_ivf(self) -> None:
401
+ """累积向量数达阈值时,IndexFlatIP → IndexIVFFlat 自动升级。
402
+
403
+ 设计:
404
+ - < HKC_VECTOR_IVF_THRESHOLD: 保持 IndexFlatIP(精确,无训练开销)
405
+ - >= HKC_VECTOR_IVF_THRESHOLD: 升级(10x 检索加速)
406
+
407
+ 升级流程(在调用方的 _write_lock 内,持锁 1-3s):
408
+ 1. reconstruct 所有当前向量(0..n-1)
409
+ 2. nlist = max(64, int(sqrt(n)))
410
+ 3. IndexIVFFlat(quantizer=Flat, dim, nlist, METRIC_IP)
411
+ 4. train + add + nprobe=16(精度保留)
412
+ 5. 替换 self._index
413
+
414
+ 一次性:升级后 isinstance 检查直接 return,后续 append_ku 直接 add 到 IVF。
415
+ save/load 透明:faiss.write_index/read_index 自动处理 IVF,manifest v2 不变。
416
+ """
417
+ import faiss
418
+ import numpy as np
419
+ if HKC_VECTOR_IVF_THRESHOLD <= 0:
420
+ return
421
+ # faiss.read_index() 反序列化会把 IndexFlatIP 降为基类 IndexFlat
422
+ # (丢失 Python 子类信息),用 isinstance 永远 False → 走 type name 判断
423
+ type_name = type(self._index).__name__
424
+ if "IVF" in type_name:
425
+ return # 已是 IVF
426
+ if type_name not in ("IndexFlat", "IndexFlatIP"):
427
+ return # 未知 Flat 变体,不动
428
+ n = self._index.ntotal
429
+ if n < HKC_VECTOR_IVF_THRESHOLD:
430
+ return
431
+ nlist = max(64, int(n ** 0.5))
432
+ # n ≥ HKC_VECTOR_IVF_THRESHOLD > 0 ⇒ n ≥ max(64, sqrt(n)) ⇒ n ≥ nlist
433
+ # (对 n≥1 必有 sqrt(n) ≤ n),因此 n >= nlist 在升级瞬间必然成立。
434
+ # faiss 1.13 train() 自己会校验 n >= nlist 并抛 RuntimeError,被外层 try/except 兜住,
435
+ # 不再单独 return(防御性分支只会让阈值恰好=1 且 n=1 这类边缘情况被静默忽略,
436
+ # 而那种情况本就该让 faiss 自己报错)。
437
+ try:
438
+ vectors = np.vstack(
439
+ [self._index.reconstruct(i) for i in range(n)]
440
+ ).astype(np.float32)
441
+ # L2 归一化兜底:对已归一化向量(bge-m3 等)幂等;对未归一化
442
+ # backend 防止 IVF(IP 距离)与原 IndexFlatIP 行为不一致导致
443
+ # 搜索结果"看起来变差"。0 向量(L2 norm=0)用一个常量 norm 替代,
444
+ # 避免除零后所有分量变成 nan。
445
+ norms = np.linalg.norm(vectors, axis=1, keepdims=True)
446
+ norms = np.where(norms > 0, norms, 1.0)
447
+ vectors = (vectors / norms).astype(np.float32)
448
+ quantizer = faiss.IndexFlatIP(self._backend.dim)
449
+ new_index = faiss.IndexIVFFlat(
450
+ quantizer, self._backend.dim, nlist, faiss.METRIC_INNER_PRODUCT)
451
+ new_index.train(vectors)
452
+ new_index.add(vectors)
453
+ new_index.nprobe = 16 # 搜 16 个聚类,精度保留(~99%)
454
+ self._index = new_index
455
+ msg = (f"VectorIndex 升级: Flat → IVF (n={n}, nlist={nlist}, "
456
+ f"nprobe=16, dim={self._backend.dim})")
457
+ logger.info(msg)
458
+ # logger 通过 root handler 输出,uvicorn 自带 logging 配置
459
+ # 不一定把 hkc_search 接到 stderr —— 加 stderr 直接 print 兜底,
460
+ # 下次阈值正常触发时(ivf_status 显示进度 100%),人工能直接看到。
461
+ print(msg, file=sys.stderr, flush=True)
462
+ except Exception as e:
463
+ warn_msg = f"VectorIndex 升级失败,继续用 Flat: {e}"
464
+ logger.warning(warn_msg)
465
+ print(warn_msg, file=sys.stderr, flush=True)
466
+
467
+ @staticmethod
468
+ def _ku_text(ku) -> str:
469
+ parts = [ku.name]
470
+ if ku.summary:
471
+ parts.append(ku.summary)
472
+ if ku.tags:
473
+ parts.append(" ".join(ku.tags))
474
+ stmt = getattr(ku, "statement", "")
475
+ if stmt:
476
+ parts.append(stmt)
477
+ return " ".join(parts)
478
+
479
+
480
+ class _ManifestValidationError(Exception):
481
+ """load() 8 步校验失败的私有异常类。reason 是给 logger 用的机器可读码。"""
482
+ def __init__(self, reason: str, msg: str):
483
+ super().__init__(msg)
484
+ self.reason = reason
File without changes
File without changes