foxclaw 0.1.0 → 0.1.3

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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,630 @@
1
+ """
2
+ hkc-api / routes / knowledge.py
3
+ 知识摄入与 KU 查询路由。
4
+ """
5
+ from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form, Body
6
+
7
+ from ..container import HKCContainer, get_container
8
+ from ..schemas import (
9
+ IngestTextRequest, IngestURLRequest, IngestResponse, KUResponse,
10
+ GraphResponse, CrystallizeRequest,
11
+ )
12
+ from hkc_kde.kde import DuplicateDocumentError
13
+ import logging
14
+ import os
15
+ import tempfile
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ router = APIRouter(prefix="/knowledge", tags=["knowledge"])
20
+
21
+ # 单次摄入文本上限(约 2MB UTF-8),防止 OOM
22
+ MAX_INGEST_CHARS = 2_000_000
23
+
24
+
25
+ def _ku_to_response(ku, *, include_source_text: bool = True) -> KUResponse:
26
+ extra = ku.to_dict().get("extra", {})
27
+ # 星球全量拉取(/graph)时剔除 source_text:原文段落占整个图谱负载的 ~92%,
28
+ # 而渲染节点根本用不到它,只有点开单个节点看详情才需要 → 那时按 /ku/{id} 单独拉全量。
29
+ if not include_source_text and "source_text" in extra:
30
+ extra = {k: v for k, v in extra.items() if k != "source_text"}
31
+ return KUResponse(
32
+ ku_id = ku.ku_id,
33
+ ku_type = ku.ku_type.value,
34
+ name = ku.name,
35
+ summary = ku.summary,
36
+ domain = ku.domain,
37
+ confidence = ku.confidence,
38
+ status = ku.status,
39
+ tags = ku.tags,
40
+ extra = extra,
41
+ )
42
+
43
+
44
+ def _ingest_response(kus) -> IngestResponse:
45
+ counts = {}
46
+ for ku in kus:
47
+ t = ku.ku_type.value
48
+ counts[t] = counts.get(t, 0) + 1
49
+ return IngestResponse(
50
+ ku_count = len(kus),
51
+ ku_ids = [ku.ku_id for ku in kus],
52
+ counts = counts,
53
+ )
54
+
55
+
56
+ @router.post("/ingest/text", response_model=IngestResponse)
57
+ def ingest_text(
58
+ req: IngestTextRequest,
59
+ c: HKCContainer = Depends(get_container),
60
+ ):
61
+ """摄入文本字符串,触发完整 KDE pipeline。"""
62
+ # 请求体大小限制:防止超大文本 OOM
63
+ if len(req.text) > MAX_INGEST_CHARS:
64
+ raise HTTPException(
65
+ status_code=413,
66
+ detail=f"文本过大({len(req.text)} 字符),上限 {MAX_INGEST_CHARS}"
67
+ )
68
+ try:
69
+ kus = c.kde.ingest_text(
70
+ text = req.text,
71
+ source = req.source,
72
+ source_title = req.source_title,
73
+ source_year = req.source_year,
74
+ domain = req.domain,
75
+ )
76
+ return _ingest_response(kus)
77
+ except HTTPException:
78
+ raise
79
+ except Exception as e:
80
+ raise HTTPException(status_code=500, detail=f"摄入失败: {e}")
81
+
82
+
83
+ @router.post("/ingest/url", response_model=IngestResponse)
84
+ def ingest_url(
85
+ req: IngestURLRequest,
86
+ c: HKCContainer = Depends(get_container),
87
+ ):
88
+ """摄入网页 URL。"""
89
+ try:
90
+ kus = c.kde.ingest_url(
91
+ url = req.url,
92
+ source_title = req.source_title,
93
+ source_year = req.source_year,
94
+ domain = req.domain,
95
+ )
96
+ return _ingest_response(kus)
97
+ except HTTPException:
98
+ raise
99
+ except Exception as e:
100
+ raise HTTPException(status_code=500, detail=f"URL 摄入失败: {e}")
101
+
102
+
103
+ @router.post("/crystallize", response_model=IngestResponse)
104
+ def crystallize(req: CrystallizeRequest, c: HKCContainer = Depends(get_container)):
105
+ """
106
+ Crystallizer 集成层入口:外部系统推送「知识候选」,经边界转换 → KDE → KEE。
107
+ 守边界:候选只是事件视角的产物,知识身份/去重仍由 KEE 裁决(Principle 07)。
108
+ """
109
+ from hkc_crystallizer.candidate import KnowledgeCandidate, CandidateEvidence
110
+ if not (req.content or "").strip():
111
+ raise HTTPException(status_code=400, detail="content 不能为空")
112
+ ev = [CandidateEvidence(
113
+ evidence_type = req.evidence_type or "document",
114
+ source_id = req.source_id or "",
115
+ agent = req.agent or None,
116
+ )]
117
+ cand = KnowledgeCandidate(
118
+ content=req.content, title=req.title, domain_hint=req.domain,
119
+ evidence=ev, event_refs=req.event_refs,
120
+ )
121
+ try:
122
+ kus = c.crystallizer.ingest(cand) or []
123
+ except HTTPException:
124
+ raise
125
+ except Exception as e:
126
+ raise HTTPException(status_code=500, detail=f"结晶失败: {e}")
127
+ return _ingest_response(kus)
128
+
129
+
130
+ # 单次上传文件大小上限(约 20MB),防止 OOM
131
+ MAX_UPLOAD_BYTES = 20 * 1024 * 1024
132
+ ALLOWED_SUFFIXES = (".pdf", ".md", ".markdown", ".txt", ".epub", ".mobi", ".azw3", ".azw",
133
+ ".docx", ".xlsx")
134
+
135
+
136
+ @router.post("/ingest/file", response_model=IngestResponse)
137
+ async def ingest_file(
138
+ file: UploadFile = File(...),
139
+ source_title: str = Form(""),
140
+ domain: str = Form(""),
141
+ c: HKCContainer = Depends(get_container),
142
+ ):
143
+ """
144
+ 摄入上传的单个文件(.pdf / .md / .txt)。
145
+ 前端的文件/文件夹上传会对每个文件分别调用本端点。
146
+ """
147
+ filename = file.filename or "uploaded"
148
+ suffix = os.path.splitext(filename)[1].lower()
149
+ if suffix not in ALLOWED_SUFFIXES:
150
+ raise HTTPException(
151
+ status_code=415,
152
+ detail=f"不支持的格式 {suffix},支持: .pdf .md .txt .epub .mobi .azw3 .docx .xlsx",
153
+ )
154
+
155
+ data = await file.read()
156
+ if len(data) > MAX_UPLOAD_BYTES:
157
+ raise HTTPException(
158
+ status_code=413,
159
+ detail=f"文件过大({len(data)} 字节),上限 {MAX_UPLOAD_BYTES}",
160
+ )
161
+
162
+ # 方向一:内容哈希查重,重复上传直接 409(写临时文件前拦截)
163
+ if c.kde.is_duplicate_content(data):
164
+ raise HTTPException(status_code=409, detail="该文件内容已摄入过,请勿重复上传")
165
+
166
+ # 存到临时文件供 KDE 按路径加载,完后清理
167
+ tmp_path = None
168
+ try:
169
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
170
+ tf.write(data)
171
+ tmp_path = tf.name
172
+ kus = c.kde.ingest_file(
173
+ path = tmp_path,
174
+ source_title = source_title or os.path.splitext(filename)[0],
175
+ domain = domain,
176
+ source = filename, # 原始上传文件名作来源标识,而非临时盘路径
177
+ )
178
+ return _ingest_response(kus)
179
+ except HTTPException:
180
+ raise
181
+ except DuplicateDocumentError as e:
182
+ raise HTTPException(status_code=409, detail=str(e))
183
+ except Exception as e:
184
+ raise HTTPException(status_code=500, detail=f"文件摄入失败: {e}")
185
+ finally:
186
+ if tmp_path and os.path.exists(tmp_path):
187
+ try:
188
+ os.unlink(tmp_path)
189
+ except OSError:
190
+ pass
191
+
192
+
193
+ @router.post("/ingest/file/async")
194
+ async def ingest_file_async(
195
+ file: UploadFile = File(...),
196
+ source_title: str = Form(""),
197
+ domain: str = Form(""),
198
+ collection: str = Form(""), # 文件夹名(批量摄入时传);非空→建/挂合集节点
199
+ c: HKCContainer = Depends(get_container),
200
+ ):
201
+ """
202
+ 异步摄入:立即返回 job_id,真正的解析+逐 chunk 抽取在后台线程池执行。
203
+ 前端轮询 GET /knowledge/ingest/jobs/{job_id} 拿真进度,期间可继续上传下一个。
204
+ """
205
+ filename = file.filename or "uploaded"
206
+ suffix = os.path.splitext(filename)[1].lower()
207
+ if suffix not in ALLOWED_SUFFIXES:
208
+ raise HTTPException(
209
+ status_code=415,
210
+ detail=f"不支持的格式 {suffix},支持: .pdf .md .txt .epub .mobi .azw3 .docx .xlsx",
211
+ )
212
+ data = await file.read()
213
+ if len(data) > MAX_UPLOAD_BYTES:
214
+ raise HTTPException(
215
+ status_code=413,
216
+ detail=f"文件过大({len(data)} 字节),上限 {MAX_UPLOAD_BYTES}",
217
+ )
218
+
219
+ # 方向一:内容哈希查重,重复上传直接 409(创建 job 前拦截,避免后台空跑)
220
+ if c.kde.is_duplicate_content(data):
221
+ raise HTTPException(status_code=409, detail="该文件内容已摄入过,请勿重复上传")
222
+
223
+ # 存临时文件供后台线程按路径加载(完成/失败后由 work() 清理)
224
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
225
+ tf.write(data)
226
+ tmp_path = tf.name
227
+
228
+ job = c.ingest_jobs.create(filename)
229
+
230
+ def work(progress):
231
+ try:
232
+ return c.kde.ingest_file(
233
+ path = tmp_path,
234
+ source_title = source_title or os.path.splitext(filename)[0],
235
+ domain = domain,
236
+ source = filename, # 原始文件名作来源标识
237
+ collection = collection, # 文件夹名(有则建/挂合集节点)
238
+ progress_cb = progress,
239
+ )
240
+ finally:
241
+ if os.path.exists(tmp_path):
242
+ try:
243
+ os.unlink(tmp_path)
244
+ except OSError:
245
+ pass
246
+
247
+ c.ingest_jobs.submit(job, work)
248
+ return {"job_id": job.job_id, "status": job.status, "filename": filename}
249
+
250
+
251
+ @router.get("/ingest/jobs/{job_id}")
252
+ def get_ingest_job(job_id: str, c: HKCContainer = Depends(get_container)):
253
+ """查询异步摄入任务的状态与进度。"""
254
+ job = c.ingest_jobs.get(job_id)
255
+ if job is None:
256
+ raise HTTPException(status_code=404, detail=f"摄入任务不存在: {job_id}")
257
+ return job
258
+
259
+
260
+ @router.post("/reset")
261
+ def reset_knowledge(c: HKCContainer = Depends(get_container)):
262
+ """清空整个知识库 + vector/bm25 索引 + 冷构建状态,用于"清库重来"。**不可逆**。
263
+
264
+ 完整清理(之前版本只清 graph_store + 调 build_index(),库空时 build_index
265
+ 直接 return 不动 vector —— 残留 14181 条死引用 + 旧 v1 manifest 触发冷构建):
266
+ 1. IndexBuildManager 后台线程(cancel + join 3s,避开 hybrid._write_lock 抢锁)
267
+ 2. graph_store (SQLite) + KEE dedup/conflict 索引
268
+ 3. in-memory vector + bm25(在 hybrid._write_lock 内,vector._reset + bm25 重建)
269
+ 4. IndexBuildManager 内存状态 → state=idle
270
+ 5. 磁盘文件: search_vector.faiss/.meta + index_build_state.json
271
+
272
+ 不需要重启服务:reset 后 IndexBuildManager.state=idle,vector_size=0,
273
+ 下次 _sync_ku 走 _do_sync_ku 同步路径(append_ku),不会触发冷构建,不会搜到死引用。
274
+ """
275
+ # 1. 取消 IndexBuildManager 后台线程(避免与 hybrid._write_lock 抢锁 —
276
+ # build 线程在 batch 边界持锁几秒,3s 兜底超时不让用户久等)
277
+ if c.index_build_manager is not None:
278
+ try:
279
+ c.index_build_manager.cancel()
280
+ if not c.index_build_manager.join(timeout=3.0):
281
+ logger.warning("IndexBuildManager 3s 内未退出,reset 强行继续")
282
+ except Exception as e:
283
+ logger.warning("cancel/join IndexBuildManager 失败: %s", e)
284
+
285
+ # 2. graph_store + KEE dedup/conflict 索引
286
+ n = c.graph_store.clear()
287
+ try:
288
+ c.kee.dedup.rebuild_index() # 重置去重指纹索引
289
+ except Exception:
290
+ pass
291
+ try:
292
+ c.kee.reset_conflict_index() # 失效 open 冲突对索引
293
+ except Exception:
294
+ pass
295
+
296
+ # 3. 清 in-memory vector + bm25(持 hybrid._write_lock,排他)
297
+ # vector._reset() 自身不持锁(vector_search.py:355),调用方负责持锁
298
+ try:
299
+ with c.search._write_lock:
300
+ c.search.vector._reset() # 清 _index + 4 个 list
301
+ c.search._rebuild_bm25_from_store() # 库空 → 自动清 5 字段 + _bm25=None
302
+ except Exception as e:
303
+ logger.warning("清 vector/bm25 失败(非致命,后续写时会重建): %s", e)
304
+
305
+ # 4. 删 vector 磁盘文件 + IndexBuildManager state.json
306
+ vec_path = str(c.data_dir / "search_vector")
307
+ for ext in (".faiss", ".meta"):
308
+ p = vec_path + ext
309
+ if os.path.exists(p):
310
+ try:
311
+ os.unlink(p)
312
+ except Exception as e:
313
+ logger.warning("删 %s 失败(非致命): %s", p, e)
314
+
315
+ if c.index_build_manager is not None:
316
+ sp = getattr(c.index_build_manager, "state_path", None)
317
+ if sp and os.path.exists(sp):
318
+ try:
319
+ os.unlink(sp)
320
+ except Exception as e:
321
+ logger.warning("删 state.json 失败(非致命): %s", e)
322
+
323
+ # 5. 重置 IndexBuildManager 内存状态(让 stats() 立刻显示干净,不等磁盘 reload)
324
+ if c.index_build_manager is not None:
325
+ mgr = c.index_build_manager
326
+ with mgr._lock:
327
+ mgr.state = "idle"
328
+ mgr.task_id = ""
329
+ mgr.index_version = ""
330
+ mgr.source_total = 0
331
+ mgr.scanned = 0
332
+ mgr.ready = 0
333
+ mgr.failed = 0
334
+ mgr.failures = []
335
+ mgr.error_summary = ""
336
+ mgr.started_at = None
337
+ mgr.finished_at = None
338
+ mgr.last_checkpoint_at = None
339
+
340
+ return {
341
+ "cleared_kus": n,
342
+ "status": "ok",
343
+ "vector_size": c.search.vector.size(),
344
+ "index_state": c.index_build_manager.state if c.index_build_manager else "n/a",
345
+ }
346
+
347
+
348
+ def _delete_source_cascade(c: HKCContainer, source: str) -> dict:
349
+ """按来源(原始文件名)删除一本书及其独占知识。
350
+
351
+ 语义(安全):
352
+ - 软删该来源的所有 EvidenceKU(书节点);
353
+ - 仅属于该书的知识 KU(Concept/Fact/Claim 的 sources/supports/contradicts
354
+ 全部来自该书)一并软删;
355
+ - 共享知识(还引用了其它书)只移除该书引用,不删本体;
356
+ - 实体移除该书来的 MENTIONS 关系,若无其它来源则软删;
357
+ - 清理悬空关系 + 关闭涉及被删 claim 的 open 冲突卡;
358
+ - 重建检索/去重/冲突索引。
359
+ """
360
+ from hkc_core.models.enums import KUType, ConflictStatus
361
+
362
+ store = c.graph_store
363
+ BIG = 10_000_000
364
+
365
+ # 1. 找该来源的所有 EvidenceKU(活动)
366
+ evds = [ku for ku in store.query_by_type(KUType.EVIDENCE, status="active", limit=BIG)
367
+ if getattr(ku, "source", "") == source]
368
+ if not evds:
369
+ return {"deleted_kus": 0, "deleted_relations": 0,
370
+ "deleted_conflicts": 0, "found": False}
371
+ evd_ids = {e.ku_id for e in evds}
372
+
373
+ # 2. 全量活动 KU,清理引用 / 标记独占删除
374
+ all_kus: list = []
375
+ for t in KUType:
376
+ all_kus.extend(store.query_by_type(t, status="active", limit=BIG))
377
+
378
+ to_delete = set(evd_ids)
379
+ for ku in all_kus:
380
+ if ku.ku_id in evd_ids:
381
+ continue
382
+ dirty = False
383
+ if ku.ku_type in (KUType.CONCEPT, KUType.FACT):
384
+ new_src = [s for s in (ku.sources or []) if s not in evd_ids]
385
+ if new_src != (ku.sources or []):
386
+ ku.sources = new_src
387
+ dirty = True
388
+ if getattr(ku, "source_ref", "") in evd_ids:
389
+ ku.source_ref = ""
390
+ dirty = True
391
+ if ku.ku_type == KUType.CLAIM:
392
+ new_sup = [s for s in (ku.supports or []) if s not in evd_ids]
393
+ new_con = [s for s in (ku.contradicts or []) if s not in evd_ids]
394
+ if new_sup != (ku.supports or []) or new_con != (ku.contradicts or []):
395
+ ku.supports = new_sup
396
+ ku.contradicts = new_con
397
+ dirty = True
398
+ if dirty:
399
+ # 不再有任何来源/支持/矛盾引用 → 仅属本书,删除;否则去引用后写回
400
+ if not (ku.sources or []) and not (getattr(ku, "supports", []) or []) \
401
+ and not (getattr(ku, "contradicts", []) or []):
402
+ to_delete.add(ku.ku_id)
403
+ else:
404
+ # 只写「被清掉引用」的那几处,不整行回写:整行 put() 会把期间别人
405
+ # 写进 extra 的内容(如复习进度 extra.review)按旧快照覆盖掉。
406
+ ext = {}
407
+ if ku.ku_type in (KUType.CONCEPT, KUType.FACT):
408
+ ext["source_ref"] = getattr(ku, "source_ref", "")
409
+ if ku.ku_type == KUType.CLAIM:
410
+ ext["supports"] = list(ku.supports or [])
411
+ ext["contradicts"] = list(ku.contradicts or [])
412
+ if store.patch_ku(
413
+ ku.ku_id, columns={"sources": list(ku.sources or [])}, extra=ext or None
414
+ ) == 0:
415
+ store.put(ku)
416
+
417
+ # 3. 实体:删掉本书来的 MENTIONS 关系;无其它来源的实体软删
418
+ for evd in evds:
419
+ for r in store.get_relations(evd.ku_id, direction="out"):
420
+ if r.rel_type == "MENTIONS" and r.to_ku not in evd_ids:
421
+ store.delete_relation(r.rel_id)
422
+ ent = store.get(r.to_ku)
423
+ if ent is not None:
424
+ remaining = [x for x in store.get_relations(ent.ku_id, direction="in")
425
+ if x.rel_type == "MENTIONS"]
426
+ if not remaining:
427
+ to_delete.add(ent.ku_id)
428
+
429
+ # 4. 软删除
430
+ for kid in to_delete:
431
+ store.delete(kid)
432
+
433
+ # 4.5 阶段 B:sqlite_store.delete 改返 bool 不再发事件,调用方按业务语义显式发。
434
+ # 这里发一条批量 DELETED,让 _sync_ku 把每条 KU 从 vector 移除(否则级联删了
435
+ # 知识,检索仍召回已删 KU,会非常误导)。_on_ku_event 兼容 ku_id 单数/ku_ids 复数。
436
+ if to_delete:
437
+ from hkc_kep.event_bus import KEPEvents
438
+ c.event_bus.publish({
439
+ "event": KEPEvents.KNOWLEDGE_DELETED,
440
+ "source": "routes.knowledge.delete_source",
441
+ "payload": {
442
+ "ku_ids": sorted(to_delete),
443
+ "reason": "source_cascade_delete",
444
+ }
445
+ })
446
+
447
+ # 5. 清理悬空关系(任一端已删除)
448
+ del_rels = 0
449
+ for r in store.get_all_relations(limit=BIG):
450
+ if r.from_ku in to_delete or r.to_ku in to_delete:
451
+ store.delete_relation(r.rel_id)
452
+ del_rels += 1
453
+
454
+ # 6. 关闭涉及被删 claim 的 open 冲突卡
455
+ del_cf = 0
456
+ from datetime import datetime, timezone
457
+ for card in store.list_conflicts(status="open"):
458
+ if card.claim_a_id in to_delete or card.claim_b_id in to_delete:
459
+ card.status = ConflictStatus.RESOLVED
460
+ card.resolution_note = "来源已删除"
461
+ card.resolved_by = "system:source-delete"
462
+ card.resolved_at = datetime.now(timezone.utc).isoformat()
463
+ store.save_conflict(card)
464
+ del_cf += 1
465
+
466
+ # 7. 重建索引
467
+ try:
468
+ c.kee.dedup.rebuild_index()
469
+ except Exception:
470
+ pass
471
+ try:
472
+ c.kee.reset_conflict_index()
473
+ except Exception:
474
+ pass
475
+ try:
476
+ c.search.build_index()
477
+ except Exception:
478
+ pass
479
+
480
+ return {"deleted_kus": len(to_delete), "deleted_relations": del_rels,
481
+ "deleted_conflicts": del_cf, "found": True}
482
+
483
+
484
+ @router.get("/sources")
485
+ def list_sources(c: HKCContainer = Depends(get_container)):
486
+ """列出所有已摄入的书籍/来源(按 EvidenceKU.source 去重),供「删除书籍」面板。"""
487
+ from hkc_core.models.enums import KUType
488
+ evds = c.graph_store.query_by_type(KUType.EVIDENCE, status="active", limit=10_000_000)
489
+ seen: dict = {}
490
+ for e in evds:
491
+ key = getattr(e, "source", "") or getattr(e, "name", "")
492
+ if not key:
493
+ continue
494
+ if key not in seen:
495
+ seen[key] = {"source": key, "name": e.name,
496
+ "year": getattr(e, "year", 0)}
497
+ books = sorted(seen.values(), key=lambda b: b["name"] or b["source"])
498
+ return {"sources": books, "count": len(books)}
499
+
500
+
501
+ @router.post("/sources/delete")
502
+ def delete_source(source: str = Body(..., embed=True), c: HKCContainer = Depends(get_container)):
503
+ """按来源(原始文件名)删除一本书及其独占知识;共享知识只去掉该书引用。"""
504
+ result = _delete_source_cascade(c, source)
505
+ if not result.get("found"):
506
+ raise HTTPException(status_code=404, detail=f"未找到来源: {source}")
507
+ return result
508
+
509
+
510
+ # 全量图护栏:单次最多返回的 KU 数,防止超大库 OOM
511
+ MAX_GRAPH_KUS = 5000
512
+
513
+
514
+ @router.get("/graph", response_model=GraphResponse)
515
+ def get_graph(
516
+ limit: int = 2000,
517
+ c: HKCContainer = Depends(get_container),
518
+ ):
519
+ """
520
+ 一次性返回整个知识图谱:所有非删除 KU + 所有关系 + 统计。
521
+ 供前端 3D 星球一次拉取全量(取代逐领域 + 逐节点拼图的 N+1 方式)。
522
+ """
523
+ limit = max(1, min(limit, MAX_GRAPH_KUS))
524
+ kus = c.graph_store.query_all(limit=limit)
525
+ ku_ids = {ku.ku_id for ku in kus}
526
+
527
+ # 只保留两端都在已返回 KU 集合里的关系,避免悬空边
528
+ rels = c.graph_store.get_all_relations(limit=MAX_GRAPH_KUS * 4)
529
+ relations = [
530
+ {
531
+ "from_ku": r.from_ku,
532
+ "to_ku": r.to_ku,
533
+ "rel_type": r.rel_type,
534
+ "weight": r.weight,
535
+ }
536
+ for r in rels
537
+ if r.from_ku in ku_ids and r.to_ku in ku_ids
538
+ ]
539
+
540
+ return GraphResponse(
541
+ # 剔除 source_text 让星球负载瘦身 ~92%(点节点看详情时再按 /ku/{id} 拉全量)
542
+ kus = [_ku_to_response(ku, include_source_text=False) for ku in kus],
543
+ relations = relations,
544
+ stats = c.graph_store.stats(),
545
+ )
546
+
547
+
548
+ @router.get("/ku/{ku_id}", response_model=KUResponse)
549
+ def get_ku(
550
+ ku_id: str,
551
+ c: HKCContainer = Depends(get_container),
552
+ ):
553
+ """按 ID 获取单个 KU。"""
554
+ ku = c.graph_store.get(ku_id)
555
+ if not ku:
556
+ raise HTTPException(status_code=404, detail=f"KU 不存在: {ku_id}")
557
+ return _ku_to_response(ku)
558
+
559
+
560
+ @router.get("/ku/{ku_id}/neighbors")
561
+ def get_ku_neighbors(
562
+ ku_id: str,
563
+ max_depth: int = 2,
564
+ top_k: int = 20,
565
+ c: HKCContainer = Depends(get_container),
566
+ ):
567
+ """获取 KU 的图谱邻居。"""
568
+ ku = c.graph_store.get(ku_id)
569
+ if not ku:
570
+ raise HTTPException(status_code=404, detail=f"KU 不存在: {ku_id}")
571
+
572
+ hits = c.search.search_neighbors(ku_id, max_depth=max_depth, top_k=top_k)
573
+ return {
574
+ "ku_id": ku_id,
575
+ "neighbors": [
576
+ {
577
+ "ku_id": h.ku_id,
578
+ "score": h.score,
579
+ "name": h.name,
580
+ "summary": h.summary,
581
+ "rel_type": getattr(h, "rel_type", ""),
582
+ }
583
+ for h in hits
584
+ ],
585
+ }
586
+
587
+
588
+ @router.get("/ku/{ku_id}/synthesis")
589
+ def get_synthesis(ku_id: str, c: HKCContainer = Depends(get_container)):
590
+ """读取已缓存的综述(不触发生成/不调 LLM)。无缓存时 has_cache=false。"""
591
+ cached = c.synthesis.get_cached(ku_id)
592
+ if not cached:
593
+ return {"ku_id": ku_id, "has_cache": False}
594
+ return {"ku_id": ku_id, "has_cache": True, **cached}
595
+
596
+
597
+ @router.post("/ku/{ku_id}/synthesis")
598
+ def make_synthesis(
599
+ ku_id: str,
600
+ force: bool = False,
601
+ c: HKCContainer = Depends(get_container),
602
+ ):
603
+ """
604
+ 生成(或返回缓存)某节点的综述。force=true 强制重新生成。
605
+ ★ 派生只读视图:只重组已有知识,不建 KU、不写知识图(Principle 07)。
606
+ """
607
+ try:
608
+ return c.synthesis.generate(ku_id, force=force)
609
+ except KeyError:
610
+ raise HTTPException(status_code=404, detail=f"KU 不存在: {ku_id}")
611
+ except Exception as e:
612
+ raise HTTPException(
613
+ status_code=502,
614
+ detail=f"综述生成失败(请检查 LLM 配置/网络): {e}",
615
+ )
616
+
617
+
618
+ @router.get("/domain/{domain}")
619
+ def list_by_domain(
620
+ domain: str,
621
+ limit: int = 50,
622
+ c: HKCContainer = Depends(get_container),
623
+ ):
624
+ """列出某领域的所有 KU。"""
625
+ kus = c.graph_store.query_by_domain(domain, limit=limit)
626
+ return {
627
+ "domain": domain,
628
+ "count": len(kus),
629
+ "kus": [_ku_to_response(ku).model_dump() for ku in kus],
630
+ }