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,144 @@
1
+ """
2
+ Foxclaw 学习子系统 — 知识图谱(连接层)
3
+ 把分散的知识点串成网。三种边:
4
+ manual(手动标注)、same_topic(同主题/标签自动)、semantic(语义相近自动)
5
+ 核心思想:学习不只是记住单点,更是发现知识之间的连接。
6
+ """
7
+
8
+ import hashlib
9
+ from collections import defaultdict
10
+ from .models import KnowledgeItem, KnowledgeEdge
11
+
12
+
13
+ class KnowledgeGraph:
14
+ """轻量知识图谱。语义相似度优先用外部 embedder(HMR),
15
+ 不可用时降级为字符 n-gram 哈希向量余弦(与 Harness 同思路)。"""
16
+
17
+ DIM = 256
18
+
19
+ def __init__(self, embedder=None):
20
+ self.items: dict = {}
21
+ self.edges: list = []
22
+ self._embedder = embedder
23
+
24
+ def add_item(self, item: KnowledgeItem):
25
+ self.items[item.id] = item
26
+
27
+ def remove_item(self, item_id: str):
28
+ """移除一个知识点及其相关的所有边。"""
29
+ self.items.pop(item_id, None)
30
+ self.edges = [e for e in self.edges
31
+ if e.source_id != item_id and e.target_id != item_id]
32
+
33
+ def link_manual(self, source_id: str, target_id: str, reason: str = ""):
34
+ self.edges.append(KnowledgeEdge(
35
+ source_id=source_id, target_id=target_id,
36
+ edge_type="manual", weight=1.0, reason=reason or "手动关联"))
37
+
38
+ def auto_link_by_topic(self):
39
+ """同主题 / 共享标签的知识点自动连接。"""
40
+ ids = list(self.items.keys())
41
+ new_edges = 0
42
+ for i in range(len(ids)):
43
+ for j in range(i + 1, len(ids)):
44
+ a, b = self.items[ids[i]], self.items[ids[j]]
45
+ shared_tags = set(a.tags) & set(b.tags)
46
+ if a.topic == b.topic and a.topic != "general":
47
+ self.edges.append(KnowledgeEdge(
48
+ source_id=a.id, target_id=b.id, edge_type="same_topic",
49
+ weight=0.7, reason=f"同主题:{a.topic}"))
50
+ new_edges += 1
51
+ elif shared_tags:
52
+ self.edges.append(KnowledgeEdge(
53
+ source_id=a.id, target_id=b.id, edge_type="same_topic",
54
+ weight=0.5, reason=f"共享标签:{','.join(shared_tags)}"))
55
+ new_edges += 1
56
+ return new_edges
57
+
58
+ def auto_link_by_semantic(self, threshold: float = 0.55):
59
+ """语义相近的知识点自动连接(余弦相似度)。"""
60
+ ids = list(self.items.keys())
61
+ vecs = {i: self._embed(self.items[i].content) for i in ids}
62
+ new_edges = 0
63
+ for i in range(len(ids)):
64
+ for j in range(i + 1, len(ids)):
65
+ sim = self._cosine(vecs[ids[i]], vecs[ids[j]])
66
+ if sim >= threshold:
67
+ self.edges.append(KnowledgeEdge(
68
+ source_id=ids[i], target_id=ids[j], edge_type="semantic",
69
+ weight=round(sim, 3),
70
+ reason=f"语义相近({sim:.2f})"))
71
+ new_edges += 1
72
+ return new_edges
73
+
74
+ def neighbors(self, item_id: str) -> list:
75
+ """某知识点的所有关联点。"""
76
+ out = []
77
+ for e in self.edges:
78
+ if e.source_id == item_id:
79
+ out.append((e.target_id, e.edge_type, e.weight, e.reason))
80
+ elif e.target_id == item_id:
81
+ out.append((e.source_id, e.edge_type, e.weight, e.reason))
82
+ return sorted(out, key=lambda x: -x[2])
83
+
84
+ def clusters(self) -> list:
85
+ """连通分量 = 知识簇。返回每个簇的成员 id 列表。"""
86
+ adj = defaultdict(set)
87
+ for e in self.edges:
88
+ adj[e.source_id].add(e.target_id)
89
+ adj[e.target_id].add(e.source_id)
90
+ seen, result = set(), []
91
+ for node in self.items:
92
+ if node in seen:
93
+ continue
94
+ stack, comp = [node], []
95
+ while stack:
96
+ n = stack.pop()
97
+ if n in seen:
98
+ continue
99
+ seen.add(n)
100
+ comp.append(n)
101
+ stack.extend(adj[n] - seen)
102
+ result.append(comp)
103
+ return result
104
+
105
+ def stats(self) -> dict:
106
+ type_count: defaultdict[str, int] = defaultdict(int)
107
+ for e in self.edges:
108
+ type_count[e.edge_type] += 1
109
+ return {
110
+ "items": len(self.items),
111
+ "edges": len(self.edges),
112
+ "edge_types": dict(type_count),
113
+ "clusters": len(self.clusters()),
114
+ }
115
+
116
+ def _embed(self, text: str):
117
+ if self._embedder is not None:
118
+ try:
119
+ return self._embedder(text)
120
+ except Exception:
121
+ pass
122
+ return self._hash_embed(text)
123
+
124
+ def _hash_embed(self, text: str):
125
+ import math
126
+ vec = [0.0] * self.DIM
127
+ text = (text or "").lower().strip()
128
+ grams = [text[i:i+3] for i in range(max(len(text) - 2, 1))]
129
+ for g in grams:
130
+ h = int(hashlib.md5(g.encode("utf-8")).hexdigest(), 16)
131
+ idx = h % self.DIM
132
+ vec[idx] += 1.0 if (h // self.DIM) % 2 == 0 else -1.0
133
+ norm = math.sqrt(sum(v * v for v in vec))
134
+ return [v / norm for v in vec] if norm > 0 else vec
135
+
136
+ @staticmethod
137
+ def _cosine(a, b) -> float:
138
+ import math
139
+ dot = sum(x * y for x, y in zip(a, b))
140
+ na = math.sqrt(sum(x * x for x in a))
141
+ nb = math.sqrt(sum(x * x for x in b))
142
+ if na == 0 or nb == 0:
143
+ return 0.0
144
+ return max(0.0, min(1.0, dot / (na * nb)))
@@ -0,0 +1,93 @@
1
+ """
2
+ Foxclaw 学习子系统 — 数据模型
3
+ 学习行为操作系统的四类结构:
4
+ KnowledgeItem(知识条目)、ReviewState(间隔重复状态)、
5
+ KnowledgeEdge(知识图谱边)、LearningVerdict(学习策略裁决)
6
+ 核心思想:学习 = 可追踪的记忆行为轨迹(纵向掌握度 + 横向知识连接)。
7
+ """
8
+
9
+ from dataclasses import dataclass, field, asdict
10
+ from datetime import datetime
11
+ from typing import Optional, Literal
12
+ import uuid
13
+
14
+
15
+ EdgeType = Literal["same_topic", "concept_ref", "semantic", "manual"]
16
+ Grade = Literal[0, 1, 2, 3, 4, 5]
17
+
18
+
19
+ @dataclass
20
+ class KnowledgeItem:
21
+ """一个知识条目 = 一次学习采集"""
22
+ content: str
23
+ topic: str = "general"
24
+ source: str = ""
25
+ tags: list = field(default_factory=list)
26
+ notes: list = field(default_factory=list)
27
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
28
+ id: str = field(default_factory=lambda: f"ki_{uuid.uuid4().hex[:10]}")
29
+
30
+ def to_dict(self) -> dict:
31
+ return asdict(self)
32
+
33
+
34
+ @dataclass
35
+ class ReviewState:
36
+ """SM-2 间隔重复状态(固化层核心)"""
37
+ item_id: str
38
+ repetitions: int = 0
39
+ interval: int = 0
40
+ ease_factor: float = 2.5
41
+ due_date: str = field(default_factory=lambda: datetime.now().isoformat())
42
+ last_review: Optional[str] = None
43
+ total_reviews: int = 0
44
+ lapses: int = 0
45
+
46
+ @property
47
+ def mastery(self) -> float:
48
+ """掌握度 0~1:综合连续正确次数和难度因子。"""
49
+ rep_score = min(self.repetitions / 6.0, 1.0)
50
+ ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3)
51
+ ease_score = max(0.0, min(1.0, ease_score))
52
+ return round(0.6 * rep_score + 0.4 * ease_score, 3)
53
+
54
+ @property
55
+ def is_due(self) -> bool:
56
+ return datetime.fromisoformat(self.due_date) <= datetime.now()
57
+
58
+ @property
59
+ def days_until_due(self) -> int:
60
+ delta = datetime.fromisoformat(self.due_date) - datetime.now()
61
+ return delta.days
62
+
63
+ def to_dict(self) -> dict:
64
+ d = asdict(self)
65
+ d.update(mastery=self.mastery, is_due=self.is_due,
66
+ days_until_due=self.days_until_due)
67
+ return d
68
+
69
+
70
+ @dataclass
71
+ class KnowledgeEdge:
72
+ """知识图谱的一条边(连接层核心)"""
73
+ source_id: str
74
+ target_id: str
75
+ edge_type: EdgeType
76
+ weight: float = 1.0
77
+ reason: str = ""
78
+
79
+ def to_dict(self) -> dict:
80
+ return asdict(self)
81
+
82
+
83
+ @dataclass
84
+ class LearningVerdict:
85
+ """学习策略裁决(对应财务守门人)"""
86
+ rule_id: str
87
+ action: Literal["review_due", "weak_topic_alert", "overload_warn", "suggest_connect"]
88
+ message: str
89
+ severity: Literal["info", "warn"] = "info"
90
+ payload: dict = field(default_factory=dict)
91
+
92
+ def to_dict(self) -> dict:
93
+ return asdict(self)
@@ -0,0 +1,67 @@
1
+ """
2
+ LearningReviewReasoner —— 学习复习推理的认知化(v2 ③ Step4)。
3
+
4
+ 把 SpacedRepetition.forgetting_risk / learning_load 的规则裁决,形式化为
5
+ 观察 → 假设 → 证据 → 裁决,
6
+ **规则永远权威**(存在高遗忘风险知识点 → confirm=建议立即复习,绝不被 LLM 推翻);
7
+ 可选 LLM 层(默认关)只对"无高风险、但负荷偏重"的微妙情形**升级** approve→confirm。
8
+ 裁决带证据链(可审计)。顾问域(非治理闸口):LLM 出错→维持规则裁决。
9
+ """
10
+ from __future__ import annotations
11
+ from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
12
+ from .spaced_repetition import SpacedRepetition
13
+
14
+ HEAVY_DAILY = 20.0
15
+
16
+
17
+ class LearningReviewReasoner(DomainReasoner):
18
+ """subject = states(list[ReviewState])。裁决:approve(学习状态健康) / confirm(需干预)。"""
19
+
20
+ def __init__(self, horizon_days: int = 3, llm=None, llm_enabled: bool = False):
21
+ self.horizon_days = horizon_days
22
+ self.llm = llm
23
+ self.llm_enabled = llm_enabled
24
+
25
+ def observe(self, states) -> dict:
26
+ load = SpacedRepetition.learning_load(states)
27
+ return {"total": len(states), "overdue": load["overdue"],
28
+ "daily_avg": load["daily_avg"], "total_due": load["total_due"]}
29
+
30
+ def hypothesize(self, obs: dict) -> list:
31
+ h = []
32
+ if obs["overdue"]:
33
+ h.append("可能有逾期未复习")
34
+ if obs["daily_avg"] >= HEAVY_DAILY:
35
+ h.append("可能复习负荷过重")
36
+ return h or ["学习状态可能健康"]
37
+
38
+ def gather_evidence(self, states, obs: dict) -> list:
39
+ risky = SpacedRepetition.forgetting_risk(states, self.horizon_days)
40
+ ev = [Evidence(factor="forgetting_risk",
41
+ detail=r["reason"] + f"(item {r['item_id']})", weight=1.0, raw=r)
42
+ for r in risky]
43
+ if not risky:
44
+ ev.append(Evidence("no_urgent_risk", "无即将到期且掌握度低的知识点", 0.5))
45
+ ev.append(Evidence("learning_load",
46
+ f"未来窗口共 {obs['total_due']} 项待复习,日均 {obs['daily_avg']}", 0.3))
47
+ return ev
48
+
49
+ def arbitrate(self, states, obs, hyps, ev) -> ReasonedVerdict:
50
+ triggered = [e for e in ev if e.raw is not None]
51
+ if triggered:
52
+ top = triggered[:3]
53
+ return ReasonedVerdict(
54
+ "confirm", f"发现 {len(triggered)} 个高遗忘风险知识点,建议优先复习:"
55
+ + ";".join(e.detail for e in top),
56
+ ev, hyps, "rules")
57
+ if DomainReasoner.llm_available(self.llm, self.llm_enabled) and obs["daily_avg"] >= HEAVY_DAILY:
58
+ if DomainReasoner.llm_should_escalate(self.llm, *self._llm_prompt(obs, ev)):
59
+ return ReasonedVerdict("confirm", "复习负荷偏重,LLM 建议调整计划", ev, hyps, "llm")
60
+ return ReasonedVerdict("approve", "学习状态健康:无高遗忘风险知识点", ev, hyps, "rules")
61
+
62
+ def _llm_prompt(self, obs, ev):
63
+ ev_text = "\n".join(f"- {e.factor}: {e.detail}" for e in ev) or "(无)"
64
+ system = ("你是学习助手。当前无高遗忘风险知识点,但复习负荷偏重。"
65
+ "判断是否建议用户调整复习计划。只回答 CONFIRM(建议调整)或 OK(无需)。")
66
+ user = f"负荷:日均 {obs['daily_avg']} 项。\n证据:\n{ev_text}\n\n只回答一个词:CONFIRM 或 OK。"
67
+ return system, user
@@ -0,0 +1,351 @@
1
+ """
2
+ Foxclaw 学习子系统 — Learning Runtime
3
+ 学习行为操作系统:采集 → 沉淀(HMR + 双结构) → 固化(间隔重复) → 连接(知识图谱)。
4
+ 对应财务 Runtime,office Agent 的"赫尔墨斯式自学"内核。
5
+ """
6
+
7
+ import csv
8
+ import json
9
+ import threading
10
+ from dataclasses import asdict
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+
14
+ from .models import KnowledgeItem, ReviewState, LearningVerdict
15
+ from .spaced_repetition import SpacedRepetition
16
+ from .knowledge_graph import KnowledgeGraph
17
+
18
+
19
+ class LearningRuntime:
20
+ """office Agent 的学习内核。"""
21
+
22
+ def __init__(self, hmr=None, hkc_provider=None, persist_path=None):
23
+ self.items: dict = {}
24
+ self.review_states: dict = {}
25
+ self.sr = SpacedRepetition()
26
+ self.hmr = hmr
27
+ self._hkc_provider = hkc_provider
28
+ self._lock = threading.RLock()
29
+ embedder = None
30
+ if hmr is not None and hasattr(hmr, "_hmr"):
31
+ embedder = self._make_embedder(hmr)
32
+ self.graph = KnowledgeGraph(embedder=embedder)
33
+ self._persist_path = Path(persist_path) if persist_path else None
34
+ self._hkc_loaded = False
35
+ self._load_file()
36
+ self._ensure_hkc_loaded()
37
+
38
+ def _make_embedder(self, hmr):
39
+ """尝试复用 HMR 的 embedding;失败则返回 None(图谱自动降级哈希)。"""
40
+ try:
41
+ store = getattr(hmr._hmr, "vector_store", None)
42
+ if store and hasattr(store, "_embed"):
43
+ return lambda t: list(store._embed(t))
44
+ except Exception:
45
+ pass
46
+ return None
47
+
48
+ def _graph_store(self):
49
+ """惰性拿 HKC 的 graph_store。拿不到(HKC 未装配/未配/异常)返回 None。"""
50
+ if not self._hkc_provider:
51
+ return None
52
+ try:
53
+ hkc = self._hkc_provider()
54
+ c = getattr(hkc, "container", None) if hkc else None
55
+ return getattr(c, "graph_store", None) if c else None
56
+ except Exception:
57
+ return None
58
+
59
+ def _item_to_ku(self, item):
60
+ """KnowledgeItem → 一个连续的 ConceptKU:source_text=全文;notes/复习状态/kind 存 extra。"""
61
+ from hkc_core.models.ku import ConceptKU
62
+ rs = self.review_states.get(item.id)
63
+ ku = ConceptKU(
64
+ ku_id=item.id,
65
+ name=(item.topic or "学习知识点")[:80],
66
+ summary=(item.content or "")[:200],
67
+ tags=list(item.tags or []) + ["learning_note"],
68
+ domain=item.topic or "learning",
69
+ source_text=item.content or "",
70
+ )
71
+ ku.extra.update({
72
+ "kind": "learning_note",
73
+ "notes": list(item.notes or []),
74
+ "note_source": item.source or "",
75
+ "learn_created_at": item.created_at,
76
+ "review": asdict(rs) if rs is not None else None,
77
+ })
78
+ return ku
79
+
80
+ def _ku_to_item(self, ku):
81
+ """HKC 的 learning_note KU → (KnowledgeItem, review_dict)。"""
82
+ ex = getattr(ku, "extra", {}) or {}
83
+ content = getattr(ku, "source_text", "") or ex.get("source_text", "") or ku.summary
84
+ tags = [t for t in (ku.tags or []) if t != "learning_note"]
85
+ item = KnowledgeItem(
86
+ content=content,
87
+ topic=ku.name or ku.domain or "general",
88
+ source=ex.get("note_source", ""),
89
+ tags=tags,
90
+ notes=list(ex.get("notes", []) or []),
91
+ created_at=ex.get("learn_created_at") or ku.created_at,
92
+ id=ku.ku_id,
93
+ )
94
+ return item, ex.get("review")
95
+
96
+ def _load_file(self):
97
+ """把离线队列 / 旧 learning.json 载入内存(__init__ 时 HKC 常未就绪,先撑住 UI)。"""
98
+ if not (self._persist_path and self._persist_path.exists()):
99
+ return
100
+ try:
101
+ data = json.loads(self._persist_path.read_text(encoding="utf-8"))
102
+ fields = KnowledgeItem.__dataclass_fields__
103
+ for d in data.get("items", []):
104
+ item = KnowledgeItem(**{k: v for k, v in d.items() if k in fields})
105
+ if item.id in self.items:
106
+ continue
107
+ self.items[item.id] = item
108
+ self.graph.add_item(item)
109
+ for d in data.get("reviews", []):
110
+ try:
111
+ rs = ReviewState(**d)
112
+ self.review_states.setdefault(rs.item_id, rs)
113
+ except Exception:
114
+ pass
115
+ except Exception:
116
+ pass
117
+
118
+ def _ensure_hkc_loaded(self):
119
+ """惰性从 HKC(知识真源)载入 learning_note KU —— 只成功一次。
120
+ 铁律要点:HKC 惰性初始化,__init__ 时(_hkc_api_key 尚未设)常拿不到 gs,
121
+ 故必须在后续任意读写(analyze 轮询/learn/...)里重试载入,否则迁移删文件后
122
+ 重启会「知识凭空消失」(真源仍在 hkc.db,只是没被读回)。加载后把内存里
123
+ 文件残留的条目回迁 HKC 并删本地队列文件。"""
124
+ if self._hkc_loaded:
125
+ return
126
+ gs = self._graph_store()
127
+ if gs is None:
128
+ return
129
+ try:
130
+ for ku in gs.query_all(limit=5000):
131
+ ex = getattr(ku, "extra", {}) or {}
132
+ if ex.get("kind") != "learning_note":
133
+ continue
134
+ if getattr(ku, "status", "active") == "deleted":
135
+ continue
136
+ item, review = self._ku_to_item(ku)
137
+ if item.id in self.items:
138
+ continue
139
+ self.items[item.id] = item
140
+ self.graph.add_item(item)
141
+ if review:
142
+ try:
143
+ self.review_states.setdefault(item.id, ReviewState(**review))
144
+ except Exception:
145
+ pass
146
+ except Exception:
147
+ return
148
+ self._hkc_loaded = True
149
+ if self._persist_path and self._persist_path.exists():
150
+ self._save()
151
+
152
+ def _save(self):
153
+ """把所有知识点 put 进 HKC(真源,upsert)。HKC 拿不到 → 写离线队列文件(兜底 A)。"""
154
+ with self._lock:
155
+ items = list(self.items.values())
156
+ gs = self._graph_store()
157
+ if gs is not None:
158
+ ok = True
159
+ for it in items:
160
+ try:
161
+ gs.put(self._item_to_ku(it))
162
+ except Exception:
163
+ ok = False
164
+ if ok:
165
+ if self._persist_path and self._persist_path.exists():
166
+ try:
167
+ self._persist_path.unlink()
168
+ except Exception:
169
+ pass
170
+ return
171
+ if self._persist_path:
172
+ try:
173
+ data = {
174
+ "items": [asdict(it) for it in items],
175
+ "reviews": [asdict(rs) for rs in self.review_states.values()],
176
+ }
177
+ self._persist_path.parent.mkdir(parents=True, exist_ok=True)
178
+ tmp = self._persist_path.with_suffix(".tmp")
179
+ tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
180
+ tmp.replace(self._persist_path)
181
+ except Exception:
182
+ pass
183
+
184
+ def learn(self, content: str, topic: str = "general",
185
+ source: str = "", tags: list | None = None) -> dict:
186
+ """采集一个知识点 → 存进 HKC(知识真源)+ 间隔重复队列 + 图谱。
187
+ 铁律:知识内容+笔记归 HKC,不进 HMR(HMR 只装 token 上下文/工作运行记忆)。"""
188
+ self._ensure_hkc_loaded()
189
+ item = KnowledgeItem(content=content, topic=topic,
190
+ source=source, tags=tags or [])
191
+ with self._lock:
192
+ self.items[item.id] = item
193
+ self.review_states[item.id] = ReviewState(item_id=item.id)
194
+ self.graph.add_item(item)
195
+ self._save()
196
+ return {"item": item.to_dict(),
197
+ "review": self.review_states[item.id].to_dict()}
198
+
199
+ def import_csv(self, path: str) -> int:
200
+ """从 CSV 导入知识点。列:content,topic,source,tags
201
+ 外部工具(Readwise/Obsidian/Anki MCP)接口可在此扩展。"""
202
+ count = 0
203
+ with open(Path(path), encoding="utf-8") as f:
204
+ for row in csv.DictReader(f):
205
+ tags = [t.strip() for t in (row.get("tags") or "").split("|") if t.strip()]
206
+ self.learn(content=row["content"],
207
+ topic=row.get("topic", "general"),
208
+ source=row.get("source", ""), tags=tags)
209
+ count += 1
210
+ return count
211
+
212
+ def review(self, item_id: str, grade: int) -> dict:
213
+ """复习一个知识点,提交回忆质量评分(0~5),更新掌握度。"""
214
+ self._ensure_hkc_loaded()
215
+ state = self.review_states.get(item_id)
216
+ if not state:
217
+ return {"error": "未知知识点"}
218
+ self.sr.review(state, grade)
219
+ self._save()
220
+ return state.to_dict()
221
+
222
+ def delete(self, item_id: str) -> bool:
223
+ """删除一个知识点(内存 + 复习状态 + 图谱)并落盘。返回是否命中。"""
224
+ self._ensure_hkc_loaded()
225
+ with self._lock:
226
+ found = item_id in self.items
227
+ self.items.pop(item_id, None)
228
+ self.review_states.pop(item_id, None)
229
+ try:
230
+ self.graph.remove_item(item_id)
231
+ except Exception:
232
+ pass
233
+ gs = self._graph_store()
234
+ if gs is not None:
235
+ try:
236
+ gs.delete(item_id)
237
+ except Exception:
238
+ pass
239
+ if found:
240
+ self._save()
241
+ return found
242
+
243
+ def add_note(self, item_id: str, text: str) -> dict:
244
+ """给一个知识点追加一条笔记(有内容才写盘;没新增就不存)。"""
245
+ self._ensure_hkc_loaded()
246
+ text = (text or "").strip()
247
+ if not text:
248
+ return {"error": "笔记为空"}
249
+ with self._lock:
250
+ item = self.items.get(item_id)
251
+ if item is None:
252
+ return {"error": "未知知识点"}
253
+ item.notes.append({"text": text, "ts": datetime.now().isoformat()})
254
+ notes = list(item.notes)
255
+ self._save()
256
+ return {"notes": notes}
257
+
258
+ def due_today(self) -> list:
259
+ """今天该复习的知识点(最弱的优先)。"""
260
+ due = self.sr.due_items(list(self.review_states.values()))
261
+ return [{"item": self.items[s.item_id].to_dict(),
262
+ "review": s.to_dict()} for s in due]
263
+
264
+ def build_connections(self, semantic_threshold: float = 0.55) -> dict:
265
+ """自动构建知识连接(同主题 + 语义)。"""
266
+ topic_edges = self.graph.auto_link_by_topic()
267
+ semantic_edges = self.graph.auto_link_by_semantic(semantic_threshold)
268
+ return {"topic_edges": topic_edges,
269
+ "semantic_edges": semantic_edges,
270
+ "graph": self.graph.stats()}
271
+
272
+ def related(self, item_id: str) -> list:
273
+ """某知识点的关联知识(连接发现)。"""
274
+ out = []
275
+ for nid, etype, weight, reason in self.graph.neighbors(item_id):
276
+ if nid in self.items:
277
+ out.append({"item": self.items[nid].to_dict(),
278
+ "edge_type": etype, "weight": weight, "reason": reason})
279
+ return out
280
+
281
+ def strategy_check(self, daily_cap: int = 20) -> list:
282
+ """学习策略裁决:复习提醒、薄弱主题、负荷预警、连接建议。"""
283
+ verdicts = []
284
+ states = list(self.review_states.values())
285
+
286
+ due = self.sr.due_items(states)
287
+ if due:
288
+ verdicts.append(LearningVerdict(
289
+ rule_id="review_due", action="review_due", severity="info",
290
+ message=f"今天有 {len(due)} 个知识点待复习",
291
+ payload={"count": len(due)}))
292
+
293
+ risky = self.sr.forgetting_risk(states)
294
+ if risky:
295
+ verdicts.append(LearningVerdict(
296
+ rule_id="weak_alert", action="weak_topic_alert", severity="warn",
297
+ message=f"{len(risky)} 个知识点处于高遗忘风险,建议优先复习",
298
+ payload={"risky": risky[:5]}))
299
+
300
+ load = self.sr.learning_load(states)
301
+ if load["total_due"] > daily_cap:
302
+ verdicts.append(LearningVerdict(
303
+ rule_id="overload", action="overload_warn", severity="warn",
304
+ message=f"未来复习量 {load['total_due']} 超过每日上限 {daily_cap},建议分批",
305
+ payload=load))
306
+
307
+ connected = set()
308
+ for e in self.graph.edges:
309
+ connected.add(e.source_id)
310
+ connected.add(e.target_id)
311
+ isolated = [i for i in self.items if i not in connected]
312
+ if isolated:
313
+ verdicts.append(LearningVerdict(
314
+ rule_id="suggest_connect", action="suggest_connect", severity="info",
315
+ message=f"{len(isolated)} 个知识点尚无关联,可建立连接以加深理解",
316
+ payload={"isolated_count": len(isolated)}))
317
+ return verdicts
318
+
319
+ def analyze(self) -> dict:
320
+ self._ensure_hkc_loaded()
321
+ states = list(self.review_states.values())
322
+ masteries = [s.mastery for s in states]
323
+ avg_mastery = round(sum(masteries) / len(masteries), 3) if masteries else 0.0
324
+
325
+ from collections import defaultdict
326
+ topic_mastery = defaultdict(list)
327
+ for iid, item in self.items.items():
328
+ st = self.review_states.get(iid)
329
+ if st:
330
+ topic_mastery[item.topic].append(st.mastery)
331
+ topics = {t: round(sum(m) / len(m), 3) for t, m in topic_mastery.items()}
332
+
333
+ return {
334
+ "total_items": len(self.items),
335
+ "avg_mastery": avg_mastery,
336
+ "topic_mastery": topics,
337
+ "weakest_topic": (min(topics, key=lambda t: topics[t]) if topics else None),
338
+ "due_today": len(self.sr.due_items(states)),
339
+ "forgetting_risk": self.sr.forgetting_risk(states),
340
+ "learning_load": self.sr.learning_load(states),
341
+ "graph": self.graph.stats(),
342
+ "strategy": [v.to_dict() for v in self.strategy_check()],
343
+ }
344
+
345
+ def status(self) -> dict:
346
+ self._ensure_hkc_loaded()
347
+ return {
348
+ "items": len(self.items),
349
+ "reviews_tracked": len(self.review_states),
350
+ "graph": self.graph.stats(),
351
+ }