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,623 @@
1
+ """
2
+ hkc-kde / extractor.py
3
+ 知识提取器:把 Chunk 文本交给 LLM,提取结构化 KU。
4
+
5
+ 核心规则:
6
+ - Fact :可被客观验证,confidence 固定 1.0
7
+ - Claim :依赖理论或存在对立观点,给出 0.0-1.0 置信度
8
+ - Entity:真实存在的人 / 机构 / 产品
9
+ - Concept:抽象知识单元
10
+
11
+ v1:调用 Anthropic API(claude-sonnet-4-6)
12
+ v2:可替换为本地模型,接口不变
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import os
19
+ import re
20
+ import time
21
+ from typing import Optional
22
+
23
+ from .chunker import Chunk
24
+ from .models import RawExtraction, ExtractionResult
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ # ── System Prompt ─────────────────────────────────────────────
30
+
31
+ _SYSTEM_PROMPT = """\
32
+ 你是知识提取专家。从用户提供的文本中提取结构化知识。
33
+
34
+ ## 严格规则
35
+
36
+ **Fact vs Claim 必须区分:**
37
+ - Fact:可被客观验证、不随时间/理论变化的陈述(人物生卒年、历史事件、测量值)
38
+ → confidence 固定 1.0,不需要估算
39
+ - Claim:依赖理论框架、存在对立观点、或需要解释的观点/结论
40
+ → 给出 0.0-1.0 的置信度,0.5 表示不确定
41
+
42
+ **输出格式:严格 JSON,不要任何解释文字、不要 Markdown 代码块**
43
+
44
+ ## 输出 Schema
45
+
46
+ {
47
+ "facts": [
48
+ {"statement": "陈述原文", "source_hint": "来源线索(可选)"}
49
+ ],
50
+ "claims": [
51
+ {"statement": "观点陈述", "confidence": 0.0-1.0, "domain": "领域"}
52
+ ],
53
+ "entities": [
54
+ {"name": "实体名", "type": "Person|Organization|Product|Event|Place", "aliases": []}
55
+ ],
56
+ "concepts": [
57
+ {"name": "概念名", "domain": "领域", "definition": "简短定义"}
58
+ ]
59
+ }
60
+
61
+ ## 判断示例
62
+
63
+ Fact(✓): "巴菲特生于1930年" / "GDP增长率为3.2%" / "该公司成立于1994年"
64
+ Claim(✓): "价值投资长期跑赢市场" / "高脂饮食有益代谢健康" / "这项技术将颠覆行业"
65
+
66
+ 每类最多提取 5 个最重要的条目。如果文本中没有某类知识,返回空列表。\
67
+ """
68
+
69
+
70
+ # 批量抽取提示词(opt-in,HKC_EXTRACT_BATCH>1 时用):一次调用处理多段,
71
+ # 省掉逐段模式里每段重复一遍系统提示词的 token 开销。
72
+ _BATCH_SYSTEM_PROMPT = """\
73
+ 你是知识提取专家。用户会提供 N 段文本,每段用 <chunk id="0">...</chunk> 这样的标签包裹,id 从 0 递增到 N-1。
74
+ 对**每一段分别**提取结构化知识,输出一个 JSON 对象:顶层 key 是段的 id(字符串),value 是该段的知识。
75
+
76
+ {
77
+ "0": { "facts": [...], "claims": [...], "entities": [...], "concepts": [...] },
78
+ "1": { ... }
79
+ }
80
+
81
+ 每段独立提取、互不混淆。每类最多提取 5 个最重要的条目;某段没有某类知识就返回空列表。
82
+
83
+ ## 严格规则
84
+ - Fact vs Claim 必须区分:Fact 可客观验证(confidence 固定 1.0);Claim 依赖理论/有对立观点(给 0.0-1.0 置信度)。
85
+ - 每个 value 的 Schema:
86
+ facts: [{"statement": "陈述原文", "source_hint": "来源线索(可选)"}]
87
+ claims: [{"statement": "观点陈述", "confidence": 0.0-1.0, "domain": "领域"}]
88
+ entities: [{"name": "实体名", "type": "Person|Organization|Product|Event|Place", "aliases": []}]
89
+ concepts: [{"name": "概念名", "domain": "领域", "definition": "简短定义"}]
90
+
91
+ 严格只输出 JSON,不要任何解释文字、不要 Markdown 代码块。\
92
+ """
93
+
94
+
95
+ class Extractor:
96
+
97
+ def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None,
98
+ provider: str = "anthropic", base_url: Optional[str] = None):
99
+ """
100
+ provider: 'anthropic'(默认) | 'deepseek' | 'openai' | 'openai-compatible'
101
+ - anthropic: 用 anthropic SDK,model 默认 claude-sonnet-4-6
102
+ - deepseek : 用 openai SDK(兼容),base_url=https://api.deepseek.com,model 默认 deepseek-chat
103
+ - openai / openai-compatible: 用 openai SDK,可自定义 base_url
104
+ """
105
+ self._apply_config(provider, api_key, model, base_url)
106
+ self._client = None
107
+ # 最后一次抽取异常:全段失败时由 _run_pipeline 抛出,供上层显示真实原因
108
+ self._last_error = None
109
+
110
+ def _apply_config(self, provider, api_key, model, base_url):
111
+ """应用 LLM 配置(init 和运行时 reconfigure 共用)。"""
112
+ self.provider = (provider or "anthropic").lower()
113
+ self.api_key = api_key
114
+ self.base_url = base_url
115
+ # 各 provider 的默认 model
116
+ if model:
117
+ self.model = model
118
+ elif self.provider == "deepseek":
119
+ self.model = "deepseek-chat"
120
+ elif self.provider in ("openai", "openai-compatible"):
121
+ self.model = "gpt-4o-mini"
122
+ else:
123
+ self.model = "claude-sonnet-4-6"
124
+ # deepseek 的默认 base_url
125
+ if self.provider == "deepseek" and not self.base_url:
126
+ self.base_url = "https://api.deepseek.com"
127
+
128
+ def reconfigure(self, provider=None, api_key=None, model=None, base_url=None):
129
+ """运行时重设 LLM 配置(界面配置用),清掉缓存的 client。"""
130
+ self._apply_config(provider, api_key, model, base_url)
131
+ self._client = None
132
+
133
+ def config_summary(self) -> dict:
134
+ """返回当前配置(api_key 脱敏),供界面展示。"""
135
+ k = self.api_key or ""
136
+ masked = (k[:6] + "…" + k[-4:]) if len(k) > 12 else ("已设置" if k else "")
137
+ return {
138
+ "provider": self.provider,
139
+ "model": self.model,
140
+ "base_url": self.base_url or "",
141
+ "api_key_masked": masked,
142
+ "has_key": bool(k),
143
+ }
144
+
145
+ def _get_client(self):
146
+ if self._client is not None:
147
+ return self._client
148
+ if self.provider == "anthropic":
149
+ try:
150
+ import anthropic
151
+ self._client = anthropic.Anthropic(api_key=self.api_key)
152
+ except ImportError:
153
+ raise ImportError("anthropic SDK 未安装,请运行: pip install anthropic")
154
+ else:
155
+ # deepseek / openai / openai-compatible 都用 openai SDK
156
+ try:
157
+ from openai import OpenAI
158
+ self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
159
+ except ImportError:
160
+ raise ImportError("openai SDK 未安装,请运行: pip install openai")
161
+ return self._client
162
+
163
+ def extract(self, chunks: list[Chunk], progress_cb=None) -> ExtractionResult:
164
+ """
165
+ 批量提取:每个 Chunk 单独调用 LLM,汇总为 ExtractionResult。
166
+
167
+ progress_cb: 可选 (processed, total) -> None 回调,每处理完一个 chunk 触发一次,
168
+ 供异步摄入上报真进度。
169
+ """
170
+ if not chunks:
171
+ return ExtractionResult(doc_id="", source="", items=[])
172
+
173
+ doc_id = chunks[0].doc_id
174
+ source = doc_id # ExtractionResult.source 存 doc_id,真实路径由 Packager 从外部传入
175
+ items: list[RawExtraction] = []
176
+ fail_count = 0
177
+ total = len(chunks)
178
+
179
+ # 并发抽取:每段一次 LLM 调用是 I/O 密集,串行 500 段要 40+ 分钟。用有限并发
180
+ # (默认 4,可经 HKC_EXTRACT_CONCURRENCY 调)把整本书从几十分钟压到几分钟。
181
+ # 并发度要温和,避免触发各 provider 的限流(429);429 已有 _retry_llm 兜底
182
+ # (指数退避 + Retry-After header),并发可适度放松但仍建议 4-8 区间。
183
+ import os
184
+ try:
185
+ workers = max(1, int(os.getenv("HKC_EXTRACT_CONCURRENCY", "4")))
186
+ except ValueError:
187
+ workers = 4
188
+ try:
189
+ llm_timeout = float(os.getenv("HKC_LLM_TIMEOUT", "60"))
190
+ except ValueError:
191
+ llm_timeout = 60.0
192
+ # 批量抽取(opt-in):一次 LLM 调用处理多段,省掉逐段模式里每段重复一遍系统提示词的 token。
193
+ # 默认 1 = 逐段抽取(行为不变);设 HKC_EXTRACT_BATCH=N 则 N 段一次调用。
194
+ try:
195
+ batch_size = max(1, int(os.getenv("HKC_EXTRACT_BATCH", "1")))
196
+ except ValueError:
197
+ batch_size = 1
198
+ if batch_size > 1:
199
+ return self._extract_batched(chunks, workers, llm_timeout, batch_size, progress_cb)
200
+ workers = min(workers, total)
201
+ done = 0
202
+
203
+ def _tick(raw):
204
+ nonlocal fail_count, done
205
+ if raw:
206
+ items.append(raw)
207
+ else:
208
+ fail_count += 1
209
+ done += 1
210
+ logger.info("提取 Chunk %d/%d (doc=%s)", done, total, doc_id)
211
+ if progress_cb is not None:
212
+ try:
213
+ progress_cb(done, total)
214
+ except Exception:
215
+ pass # 进度上报失败不影响摄入本身
216
+
217
+ if workers <= 1:
218
+ for chunk in chunks:
219
+ _tick(self._extract_chunk(chunk))
220
+ else:
221
+ from concurrent.futures import ThreadPoolExecutor, as_completed
222
+ with ThreadPoolExecutor(max_workers=workers, thread_name_prefix="extract") as pool:
223
+ futures = [pool.submit(self._extract_chunk, ch, llm_timeout) for ch in chunks]
224
+ for fut in as_completed(futures):
225
+ try:
226
+ raw = fut.result()
227
+ except Exception as e:
228
+ logger.error("Chunk 提取线程异常: %s", e)
229
+ raw = None
230
+ _tick(raw)
231
+
232
+ # 全部 chunk 都抽取失败 → 分两种情况,不能一刀切:
233
+ # 1) 根本没配 key:这是**设计内的降级**(无 LLM 时知识库照常读写,只是抽不出 KU,
234
+ # 文件摄入/格式门等链路仍要能跑通),返回空结果,不抛。
235
+ # 2) 配了 key 却全失败(401/超时/模型不存在):这才是真故障。必须抛,否则 API 层
236
+ # 返回 ku_count=0 + 200,用户以为录进去了,实际库是空的(复习永远 0 卡)。
237
+ if fail_count == len(chunks) and len(chunks) > 0:
238
+ reason = self._last_error
239
+ logger.error(
240
+ "知识抽取全部失败 (%d/%d chunks)。provider=%s, model=%s, has_key=%s。最后错误: %s",
241
+ fail_count, len(chunks), self.provider, self.model, bool(self.api_key), reason,
242
+ )
243
+ if self.api_key:
244
+ raise RuntimeError(
245
+ f"知识抽取失败:所有 {len(chunks)} 段都没能抽出内容。"
246
+ f"当前 provider={self.provider} model={self.model}。"
247
+ f"最后错误: {reason or '未知(模型返回为空或解析失败)'}"
248
+ )
249
+ logger.warning("未配置 API Key —— 按降级处理,本次不产出知识单元。")
250
+
251
+ return ExtractionResult(doc_id=doc_id, source=source, items=items)
252
+
253
+ def _retry_llm(self, func, *args, max_retries: int = 3, base_delay: float = 1.0, max_delay: float = 8.0, **kwargs):
254
+ """
255
+ 通用 LLM retry 包装:处理 429 RateLimit / 5xx / Timeout,指数退避 + Retry-After。
256
+
257
+ 用法:
258
+ resp = self._retry_llm(
259
+ client.messages.create,
260
+ model=..., max_tokens=..., timeout=..., system=..., messages=[...],
261
+ )
262
+
263
+ 可重试的异常:
264
+ - status_code == 429 (限流,anthropic/openai SDK 都会设置)
265
+ - 500 <= status_code < 600 (服务端错误)
266
+ - ConnectionError / TimeoutError (网络层)
267
+
268
+ 退避策略:
269
+ - 优先读取异常 headers['Retry-After'] (秒),作为本次等待时间
270
+ - 否则用 base_delay,每次 * 2 直到 max_delay
271
+ - 最多 max_retries+1 次尝试 (默认 4 次 = 1 + 3 retry)
272
+ """
273
+ delay = base_delay
274
+ for attempt in range(max_retries + 1):
275
+ try:
276
+ return func(*args, **kwargs)
277
+ except Exception as e:
278
+ status = getattr(e, 'status_code', None)
279
+ if status is None:
280
+ status = getattr(e, 'status', None)
281
+ is_429 = (status == 429) or 'RateLimit' in type(e).__name__
282
+ is_5xx = isinstance(status, int) and 500 <= status < 600
283
+ is_conn = isinstance(e, (ConnectionError, TimeoutError)) or 'Timeout' in type(e).__name__
284
+ retryable = is_429 or is_5xx or is_conn
285
+ if not retryable or attempt == max_retries:
286
+ raise
287
+ # 取 Retry-After header
288
+ wait = delay
289
+ headers = getattr(e, 'headers', None)
290
+ if headers and 'Retry-After' in headers:
291
+ try:
292
+ wait = float(headers['Retry-After'])
293
+ except (TypeError, ValueError):
294
+ pass
295
+ wait = min(wait, max_delay)
296
+ logger.warning(
297
+ "LLM %s (status=%s, attempt %d/%d) retry after %.1fs: %s",
298
+ type(e).__name__, status, attempt + 1, max_retries, wait, e,
299
+ )
300
+ time.sleep(wait)
301
+ delay = min(delay * 2, max_delay)
302
+
303
+ def complete(self, system: str, user: str, max_tokens: int = 1500, timeout: float = 60.0) -> str:
304
+ """
305
+ 通用单轮补全(非抽取):给定 system+user,返回模型文本。
306
+ 供「综合页 LLM 综述」等派生视图复用同一套 provider 配置,保持模型无关。
307
+ 与 _extract_chunk 一样兼容 anthropic 与 openai 系两条路径。
308
+ """
309
+ client = self._get_client()
310
+ if self.provider == "anthropic":
311
+ resp = self._retry_llm(
312
+ client.messages.create,
313
+ model=self.model, max_tokens=max_tokens, timeout=timeout,
314
+ system=system,
315
+ messages=[{"role": "user", "content": user}],
316
+ )
317
+ return resp.content[0].text.strip()
318
+ resp = self._retry_llm(
319
+ client.chat.completions.create,
320
+ model=self.model, max_tokens=max_tokens, timeout=timeout,
321
+ messages=[{"role": "system", "content": system},
322
+ {"role": "user", "content": user}],
323
+ )
324
+ return resp.choices[0].message.content.strip()
325
+
326
+ def _extract_chunk(self, chunk: Chunk, timeout: float = 60.0) -> Optional[RawExtraction]:
327
+ """
328
+ 单个 Chunk 的提取。
329
+ LLM 调用失败时记录日志并返回 None,不中断整个流程。
330
+ """
331
+ try:
332
+ client = self._get_client()
333
+ if self.provider == "anthropic":
334
+ resp = self._retry_llm(
335
+ client.messages.create,
336
+ model = self.model,
337
+ max_tokens = 2048,
338
+ timeout = timeout,
339
+ system = _SYSTEM_PROMPT,
340
+ messages = [{"role": "user", "content": chunk.content}],
341
+ )
342
+ raw_text = resp.content[0].text.strip()
343
+ else:
344
+ # OpenAI 兼容格式(DeepSeek / OpenAI):system 作为一条 message
345
+ resp = self._retry_llm(
346
+ client.chat.completions.create,
347
+ model = self.model,
348
+ max_tokens = 2048,
349
+ timeout = timeout,
350
+ messages = [
351
+ {"role": "system", "content": _SYSTEM_PROMPT},
352
+ {"role": "user", "content": chunk.content},
353
+ ],
354
+ )
355
+ raw_text = resp.choices[0].message.content.strip()
356
+ raw = self._parse_response(raw_text, chunk.source_hint)
357
+ if raw:
358
+ # 把本 chunk 的原文段落挂到每条抽取结果上,供 packager 存进 KU,
359
+ # 前端点击知识节点时可显示"原文段落"(见 KU.source_text)。
360
+ src = chunk.content
361
+ for d in (raw.facts + raw.claims + raw.concepts + raw.entities):
362
+ d["_src"] = src
363
+ return raw
364
+
365
+ except Exception as e:
366
+ logger.error("Chunk 提取失败 (chunk_id=%s): %s", chunk.chunk_id, e)
367
+ # 留给 _run_pipeline:全部失败时把真实原因(401/超时/模型不存在)带出去,
368
+ # 而不是只回一句"0 条"。
369
+ self._last_error = e
370
+ return None
371
+
372
+ def _extract_batched(self, chunks, workers, llm_timeout, batch_size, progress_cb):
373
+ """批量抽取:把 chunks 按 batch_size 分组,每组一次 LLM 调用。返回 ExtractionResult。"""
374
+ total = len(chunks)
375
+ groups = [chunks[i:i + batch_size] for i in range(0, total, batch_size)]
376
+ n_groups = len(groups)
377
+ results = [None] * n_groups # 每组对齐结果 list[Optional[RawExtraction]]
378
+ done = 0
379
+
380
+ def _collect(gi, raws):
381
+ nonlocal done
382
+ results[gi] = raws
383
+ done += len(groups[gi])
384
+ if progress_cb is not None:
385
+ try:
386
+ progress_cb(min(done, total), total)
387
+ except Exception:
388
+ pass
389
+
390
+ if workers <= 1 or n_groups == 1:
391
+ for gi, g in enumerate(groups):
392
+ _collect(gi, self._extract_batch(g, llm_timeout))
393
+ else:
394
+ from concurrent.futures import ThreadPoolExecutor, as_completed
395
+ with ThreadPoolExecutor(max_workers=min(workers, n_groups),
396
+ thread_name_prefix="extract") as pool:
397
+ futs = {pool.submit(self._extract_batch, g, llm_timeout): gi
398
+ for gi, g in enumerate(groups)}
399
+ for fut in as_completed(futs):
400
+ gi = futs[fut]
401
+ try:
402
+ raws = fut.result()
403
+ except Exception as e:
404
+ logger.error("批量抽取线程异常 (group=%d): %s", gi, e)
405
+ raws = [None] * len(groups[gi])
406
+ _collect(gi, raws)
407
+
408
+ items = []
409
+ fail_count = 0
410
+ for gi in range(n_groups):
411
+ for r in (results[gi] or []):
412
+ if r is None:
413
+ fail_count += 1
414
+ else:
415
+ items.append(r)
416
+
417
+ # 全部失败 → 大概率 LLM 配置问题(与逐段路径同一诊断口径)
418
+ if fail_count == total and total > 0:
419
+ logger.error(
420
+ "知识抽取全部失败 (%d/%d chunks)。provider=%s, model=%s。"
421
+ "若上方有「JSON 解析失败」,说明输出被截断:调大 HKC_EXTRACT_MAX_TOKENS "
422
+ "或调小 HKC_EXTRACT_BATCH;否则检查 API Key / 网络。",
423
+ fail_count, total, self.provider, self.model,
424
+ )
425
+
426
+ doc_id = chunks[0].doc_id
427
+ return ExtractionResult(doc_id=doc_id, source=doc_id, items=items)
428
+
429
+ @staticmethod
430
+ def _batch_max_tokens(n: int) -> int:
431
+ """单批输出上限:随段数放大(每段最多 5×4 条),硬顶可配。
432
+
433
+ 旧公式是 1024*n,批 4 段正好 4096 —— 恰好撞在 deepseek-chat 的单次输出
434
+ 硬顶上。内容密的中文文档会在 JSON 写到一半被截断,整批 4 段全部丢弃,
435
+ 表象就是「摄入完成但 0 条知识」。这里给足余量并允许用
436
+ HKC_EXTRACT_MAX_TOKENS 覆盖(上限 8192)。
437
+ """
438
+ try:
439
+ cap = max(2048, int(os.getenv("HKC_EXTRACT_MAX_TOKENS", "8192")))
440
+ except ValueError:
441
+ cap = 8192
442
+ return max(2048, min(cap, 1280 * n))
443
+
444
+ def _extract_batch(self, chunks, timeout, max_tokens: Optional[int] = None):
445
+ """一次 LLM 调用抽取一组 chunks,返回与 chunks 对齐的 list[Optional[RawExtraction]]。"""
446
+ n = len(chunks)
447
+ parts = [f'<chunk id="{i}">\n{ch.content}\n</chunk>' for i, ch in enumerate(chunks)]
448
+ user = "\n\n".join(parts)
449
+ if max_tokens is None:
450
+ max_tokens = self._batch_max_tokens(n)
451
+ try:
452
+ client = self._get_client()
453
+ if self.provider == "anthropic":
454
+ resp = self._retry_llm(
455
+ client.messages.create,
456
+ model = self.model,
457
+ max_tokens = max_tokens,
458
+ timeout = timeout,
459
+ system = _BATCH_SYSTEM_PROMPT,
460
+ messages = [{"role": "user", "content": user}],
461
+ )
462
+ raw_text = resp.content[0].text.strip()
463
+ else:
464
+ resp = self._retry_llm(
465
+ client.chat.completions.create,
466
+ model = self.model,
467
+ max_tokens = max_tokens,
468
+ timeout = timeout,
469
+ messages = [
470
+ {"role": "system", "content": _BATCH_SYSTEM_PROMPT},
471
+ {"role": "user", "content": user},
472
+ ],
473
+ )
474
+ raw_text = resp.choices[0].message.content.strip()
475
+ except Exception as e:
476
+ logger.error("批量提取失败 (首 chunk=%s): %s",
477
+ chunks[0].chunk_id if chunks else "?", e)
478
+ return [None] * n
479
+
480
+ parsed = self._parse_batch_response(raw_text, n)
481
+ # 整批解析失败(JSON 被截断 / 空响应):拆半重试,不把整批丢掉。
482
+ # 逐层对半拆到单段为止;单段仍失败,就放大输出上限再试一次。
483
+ if n > 1 and all(p is None for p in parsed):
484
+ mid = n // 2
485
+ logger.warning("批量抽取整批解析失败,拆分为 %d + %d 重试", mid, n - mid)
486
+ return (self._extract_batch(chunks[:mid], timeout)
487
+ + self._extract_batch(chunks[mid:], timeout))
488
+ if n == 1 and parsed[0] is None:
489
+ bigger = min(8192, max_tokens * 2)
490
+ if bigger > max_tokens:
491
+ logger.warning("单段抽取解析失败,放大 max_tokens %d → %d 重试",
492
+ max_tokens, bigger)
493
+ return self._extract_batch(chunks, timeout, bigger)
494
+ for i, raw in enumerate(parsed):
495
+ if raw is None:
496
+ continue
497
+ raw.source_hint = chunks[i].source_hint
498
+ src = chunks[i].content
499
+ for d in (raw.facts + raw.claims + raw.concepts + raw.entities):
500
+ d["_src"] = src
501
+ return parsed
502
+
503
+ @staticmethod
504
+ def _strip_fences(text: str) -> str:
505
+ """去掉 LLM 偶尔包裹的 Markdown 代码块围栏。"""
506
+ text = re.sub(r'^```(?:json)?\s*', '', text, flags=re.MULTILINE)
507
+ text = re.sub(r'\s*```$', '', text, flags=re.MULTILINE)
508
+ return text.strip()
509
+
510
+ def _raw_from_dict(self, data, source_hint: str) -> Optional[RawExtraction]:
511
+ """把单个 chunk 的 JSON dict 校验并规范化为 RawExtraction(空则 None)。"""
512
+ if not isinstance(data, dict):
513
+ return None
514
+ facts = self._validate_facts(data.get("facts", []))
515
+ claims = self._validate_claims(data.get("claims", []))
516
+ entities = self._validate_entities(data.get("entities", []))
517
+ concepts = self._validate_concepts(data.get("concepts", []))
518
+
519
+ if not any([facts, claims, entities, concepts]):
520
+ return None
521
+
522
+ return RawExtraction(
523
+ facts = facts,
524
+ claims = claims,
525
+ entities = entities,
526
+ concepts = concepts,
527
+ source_hint = source_hint,
528
+ )
529
+
530
+ def _parse_response(self, text: str, source_hint: str) -> Optional[RawExtraction]:
531
+ """
532
+ 解析单段 LLM 返回的 JSON。
533
+ 容错处理:去掉可能的 Markdown 代码块包裹。
534
+ """
535
+ text = self._strip_fences(text)
536
+ try:
537
+ data = json.loads(text)
538
+ except json.JSONDecodeError as e:
539
+ logger.warning("JSON 解析失败: %s\n原文: %.200s", e, text)
540
+ return None
541
+ return self._raw_from_dict(data, source_hint)
542
+
543
+ def _parse_batch_response(self, text: str, n: int) -> list[Optional[RawExtraction]]:
544
+ """解析批量 LLM 返回的 JSON:顶层 key 为段 id(字符串 "0".."n-1")。"""
545
+ text = self._strip_fences(text)
546
+ try:
547
+ data = json.loads(text)
548
+ except json.JSONDecodeError as e:
549
+ logger.warning("批量 JSON 解析失败: %s\n原文: %.200s", e, text)
550
+ return [None] * n
551
+ if not isinstance(data, dict):
552
+ return [None] * n
553
+ return [self._raw_from_dict(data.get(str(i)), "") for i in range(n)]
554
+
555
+ # ── 校验方法:确保字段存在且类型正确 ────────────────────
556
+
557
+ def _validate_facts(self, items: list) -> list[dict]:
558
+ result = []
559
+ for item in items[:5]:
560
+ if not isinstance(item, dict):
561
+ continue
562
+ stmt = str(item.get("statement", "")).strip()
563
+ if stmt:
564
+ result.append({
565
+ "statement": stmt,
566
+ "source_hint": str(item.get("source_hint", "")),
567
+ })
568
+ return result
569
+
570
+ def _validate_claims(self, items: list) -> list[dict]:
571
+ result = []
572
+ for item in items[:5]:
573
+ if not isinstance(item, dict):
574
+ continue
575
+ stmt = str(item.get("statement", "")).strip()
576
+ if not stmt:
577
+ continue
578
+ conf = item.get("confidence", 0.5)
579
+ try:
580
+ conf = float(conf)
581
+ conf = max(0.0, min(1.0, conf))
582
+ except (TypeError, ValueError):
583
+ conf = 0.5
584
+ result.append({
585
+ "statement": stmt,
586
+ "confidence": conf,
587
+ "domain": str(item.get("domain", "")),
588
+ })
589
+ return result
590
+
591
+ def _validate_entities(self, items: list) -> list[dict]:
592
+ valid_types = {"Person", "Organization", "Product", "Event", "Place"}
593
+ result = []
594
+ for item in items[:5]:
595
+ if not isinstance(item, dict):
596
+ continue
597
+ name = str(item.get("name", "")).strip()
598
+ if not name:
599
+ continue
600
+ entity_type = item.get("type", "Person")
601
+ if entity_type not in valid_types:
602
+ entity_type = "Person"
603
+ result.append({
604
+ "name": name,
605
+ "type": entity_type,
606
+ "aliases": list(item.get("aliases", [])),
607
+ })
608
+ return result
609
+
610
+ def _validate_concepts(self, items: list) -> list[dict]:
611
+ result = []
612
+ for item in items[:5]:
613
+ if not isinstance(item, dict):
614
+ continue
615
+ name = str(item.get("name", "")).strip()
616
+ if not name:
617
+ continue
618
+ result.append({
619
+ "name": name,
620
+ "domain": str(item.get("domain", "")),
621
+ "definition": str(item.get("definition", "")),
622
+ })
623
+ return result