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,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] # SM-2 回忆质量评分 0=完全忘 5=完美
17
+
18
+
19
+ @dataclass
20
+ class KnowledgeItem:
21
+ """一个知识条目 = 一次学习采集"""
22
+ content: str # 知识点内容
23
+ topic: str = "general" # 主题
24
+ source: str = "" # 来源:书名/课程/文章/Readwise
25
+ tags: list = field(default_factory=list)
26
+ notes: list = field(default_factory=list) # 用户读时加的笔记 [{text, ts}]
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 # 难度因子(SM-2 起始 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 # 遗忘次数(评分<3)
45
+
46
+ @property
47
+ def mastery(self) -> float:
48
+ """掌握度 0~1:综合连续正确次数和难度因子。"""
49
+ rep_score = min(self.repetitions / 6.0, 1.0) # 6 次连对算掌握
50
+ ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3) # 1.3~2.5 归一
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,70 @@
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
+ # 负荷偏重阈值:未来窗口日均复习量 ≥ 该值即算偏重,交给可选 LLM 复核是否提醒。
15
+ HEAVY_DAILY = 20.0
16
+
17
+
18
+ class LearningReviewReasoner(DomainReasoner):
19
+ """subject = states(list[ReviewState])。裁决:approve(学习状态健康) / confirm(需干预)。"""
20
+
21
+ def __init__(self, horizon_days: int = 3, llm=None, llm_enabled: bool = False):
22
+ self.horizon_days = horizon_days
23
+ self.llm = llm
24
+ self.llm_enabled = llm_enabled
25
+
26
+ def observe(self, states) -> dict:
27
+ load = SpacedRepetition.learning_load(states)
28
+ return {"total": len(states), "overdue": load["overdue"],
29
+ "daily_avg": load["daily_avg"], "total_due": load["total_due"]}
30
+
31
+ def hypothesize(self, obs: dict) -> list:
32
+ h = []
33
+ if obs["overdue"]:
34
+ h.append("可能有逾期未复习")
35
+ if obs["daily_avg"] >= HEAVY_DAILY:
36
+ h.append("可能复习负荷过重")
37
+ return h or ["学习状态可能健康"]
38
+
39
+ def gather_evidence(self, states, obs: dict) -> list:
40
+ risky = SpacedRepetition.forgetting_risk(states, self.horizon_days) # 规则=裁决证据
41
+ ev = [Evidence(factor="forgetting_risk",
42
+ detail=r["reason"] + f"(item {r['item_id']})", weight=1.0, raw=r)
43
+ for r in risky]
44
+ if not risky:
45
+ ev.append(Evidence("no_urgent_risk", "无即将到期且掌握度低的知识点", 0.5))
46
+ # informational(raw=None):学习负荷,不参与裁决
47
+ ev.append(Evidence("learning_load",
48
+ f"未来窗口共 {obs['total_due']} 项待复习,日均 {obs['daily_avg']}", 0.3))
49
+ return ev
50
+
51
+ def arbitrate(self, states, obs, hyps, ev) -> ReasonedVerdict:
52
+ triggered = [e for e in ev if e.raw is not None]
53
+ if triggered:
54
+ # 【规则权威】存在高遗忘风险 → confirm(建议立即复习),绝不被 LLM 推翻/隐藏
55
+ top = triggered[:3]
56
+ return ReasonedVerdict(
57
+ "confirm", f"发现 {len(triggered)} 个高遗忘风险知识点,建议优先复习:"
58
+ + ";".join(e.detail for e in top),
59
+ ev, hyps, "rules")
60
+ if DomainReasoner.llm_available(self.llm, self.llm_enabled) and obs["daily_avg"] >= HEAVY_DAILY:
61
+ if DomainReasoner.llm_should_escalate(self.llm, *self._llm_prompt(obs, ev)):
62
+ return ReasonedVerdict("confirm", "复习负荷偏重,LLM 建议调整计划", ev, hyps, "llm")
63
+ return ReasonedVerdict("approve", "学习状态健康:无高遗忘风险知识点", ev, hyps, "rules")
64
+
65
+ def _llm_prompt(self, obs, ev):
66
+ ev_text = "\n".join(f"- {e.factor}: {e.detail}" for e in ev) or "(无)"
67
+ system = ("你是学习助手。当前无高遗忘风险知识点,但复习负荷偏重。"
68
+ "判断是否建议用户调整复习计划。只回答 CONFIRM(建议调整)或 OK(无需)。")
69
+ user = f"负荷:日均 {obs['daily_avg']} 项。\n证据:\n{ev_text}\n\n只回答一个词:CONFIRM 或 OK。"
70
+ return system, user
@@ -0,0 +1,363 @@
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 = {} # id -> KnowledgeItem(内存缓存;知识真源=HKC)
24
+ self.review_states: dict = {} # item_id -> ReviewState
25
+ self.sr = SpacedRepetition()
26
+ self.hmr = hmr
27
+ self._hkc_provider = hkc_provider # 惰性拿 HKC(知识真源);None=纯内存(测试用)
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
+ # persist_path 现在只作「离线兜底队列 + 旧 learning.json 迁移源」;知识真源是 HKC。
34
+ self._persist_path = Path(persist_path) if persist_path else None
35
+ self._hkc_loaded = False # HKC 首次成功载入的门闩(惰性,见 _ensure_hkc_loaded)
36
+ self._load_file() # 先把离线队列/旧文件载入内存(__init__ 时 HKC 常未就绪)
37
+ self._ensure_hkc_loaded() # HKC 若已就绪则立即从真源载入;否则留待后续调用重试
38
+
39
+ def _make_embedder(self, hmr):
40
+ """尝试复用 HMR 的 embedding;失败则返回 None(图谱自动降级哈希)。"""
41
+ try:
42
+ store = getattr(hmr._hmr, "vector_store", None)
43
+ if store and hasattr(store, "_embed"):
44
+ return lambda t: list(store._embed(t))
45
+ except Exception:
46
+ pass
47
+ return None
48
+
49
+ # ── 存储:知识内容+笔记+复习状态 全落 HKC(真源);HKC 拿不到时落离线队列(兜底 A)──
50
+ def _graph_store(self):
51
+ """惰性拿 HKC 的 graph_store。拿不到(HKC 未装配/未配/异常)返回 None。"""
52
+ if not self._hkc_provider:
53
+ return None
54
+ try:
55
+ hkc = self._hkc_provider()
56
+ c = getattr(hkc, "container", None) if hkc else None
57
+ return getattr(c, "graph_store", None) if c else None
58
+ except Exception:
59
+ return None
60
+
61
+ def _item_to_ku(self, item):
62
+ """KnowledgeItem → 一个连续的 ConceptKU:source_text=全文;notes/复习状态/kind 存 extra。"""
63
+ from hkc_core.models.ku import ConceptKU
64
+ rs = self.review_states.get(item.id)
65
+ ku = ConceptKU(
66
+ ku_id=item.id,
67
+ name=(item.topic or "学习知识点")[:80],
68
+ summary=(item.content or "")[:200],
69
+ tags=list(item.tags or []) + ["learning_note"],
70
+ domain=item.topic or "learning",
71
+ source_text=item.content or "",
72
+ )
73
+ ku.extra.update({
74
+ "kind": "learning_note",
75
+ "notes": list(item.notes or []),
76
+ "note_source": item.source or "",
77
+ "learn_created_at": item.created_at,
78
+ "review": asdict(rs) if rs is not None else None,
79
+ })
80
+ return ku
81
+
82
+ def _ku_to_item(self, ku):
83
+ """HKC 的 learning_note KU → (KnowledgeItem, review_dict)。"""
84
+ ex = getattr(ku, "extra", {}) or {}
85
+ content = getattr(ku, "source_text", "") or ex.get("source_text", "") or ku.summary
86
+ tags = [t for t in (ku.tags or []) if t != "learning_note"]
87
+ item = KnowledgeItem(
88
+ content=content,
89
+ topic=ku.name or ku.domain or "general",
90
+ source=ex.get("note_source", ""),
91
+ tags=tags,
92
+ notes=list(ex.get("notes", []) or []),
93
+ created_at=ex.get("learn_created_at") or ku.created_at,
94
+ id=ku.ku_id,
95
+ )
96
+ return item, ex.get("review")
97
+
98
+ def _load_file(self):
99
+ """把离线队列 / 旧 learning.json 载入内存(__init__ 时 HKC 常未就绪,先撑住 UI)。"""
100
+ if not (self._persist_path and self._persist_path.exists()):
101
+ return
102
+ try:
103
+ data = json.loads(self._persist_path.read_text(encoding="utf-8"))
104
+ fields = KnowledgeItem.__dataclass_fields__
105
+ for d in data.get("items", []):
106
+ item = KnowledgeItem(**{k: v for k, v in d.items() if k in fields})
107
+ if item.id in self.items:
108
+ continue
109
+ self.items[item.id] = item
110
+ self.graph.add_item(item)
111
+ for d in data.get("reviews", []):
112
+ try:
113
+ rs = ReviewState(**d)
114
+ self.review_states.setdefault(rs.item_id, rs)
115
+ except Exception:
116
+ pass
117
+ except Exception:
118
+ pass
119
+
120
+ def _ensure_hkc_loaded(self):
121
+ """惰性从 HKC(知识真源)载入 learning_note KU —— 只成功一次。
122
+ 铁律要点:HKC 惰性初始化,__init__ 时(_hkc_api_key 尚未设)常拿不到 gs,
123
+ 故必须在后续任意读写(analyze 轮询/learn/...)里重试载入,否则迁移删文件后
124
+ 重启会「知识凭空消失」(真源仍在 hkc.db,只是没被读回)。加载后把内存里
125
+ 文件残留的条目回迁 HKC 并删本地队列文件。"""
126
+ if self._hkc_loaded:
127
+ return
128
+ gs = self._graph_store()
129
+ if gs is None: # HKC 还没就绪 → 下次调用再试
130
+ return
131
+ try: # 1) 从 HKC 载入(内存已有的以文件版为准,不覆盖)
132
+ for ku in gs.query_all(limit=5000):
133
+ ex = getattr(ku, "extra", {}) or {}
134
+ if ex.get("kind") != "learning_note":
135
+ continue
136
+ if getattr(ku, "status", "active") == "deleted":
137
+ continue
138
+ item, review = self._ku_to_item(ku)
139
+ if item.id in self.items:
140
+ continue
141
+ self.items[item.id] = item
142
+ self.graph.add_item(item)
143
+ if review:
144
+ try:
145
+ self.review_states.setdefault(item.id, ReviewState(**review))
146
+ except Exception:
147
+ pass
148
+ except Exception:
149
+ return # 载入失败不置门闩,留待下次重试
150
+ self._hkc_loaded = True
151
+ if self._persist_path and self._persist_path.exists(): # 2) 文件残留回迁 HKC + 删文件
152
+ self._save()
153
+
154
+ def _save(self):
155
+ """把所有知识点 put 进 HKC(真源,upsert)。HKC 拿不到 → 写离线队列文件(兜底 A)。"""
156
+ with self._lock:
157
+ items = list(self.items.values())
158
+ gs = self._graph_store()
159
+ if gs is not None:
160
+ ok = True
161
+ for it in items:
162
+ try:
163
+ gs.put(self._item_to_ku(it))
164
+ except Exception:
165
+ ok = False
166
+ if ok:
167
+ if self._persist_path and self._persist_path.exists():
168
+ try:
169
+ self._persist_path.unlink() # 已全部进 HKC → 删掉本地队列文件
170
+ except Exception:
171
+ pass
172
+ return
173
+ if self._persist_path: # 离线兜底:HKC 不可用/部分失败 → 落队列文件
174
+ try:
175
+ data = {
176
+ "items": [asdict(it) for it in items],
177
+ "reviews": [asdict(rs) for rs in self.review_states.values()],
178
+ }
179
+ self._persist_path.parent.mkdir(parents=True, exist_ok=True)
180
+ tmp = self._persist_path.with_suffix(".tmp")
181
+ tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
182
+ tmp.replace(self._persist_path)
183
+ except Exception:
184
+ pass
185
+
186
+ # ── 采集 + 沉淀 ───────────────────────────────────────
187
+ def learn(self, content: str, topic: str = "general",
188
+ source: str = "", tags: list = None) -> dict:
189
+ """采集一个知识点 → 存进 HKC(知识真源)+ 间隔重复队列 + 图谱。
190
+ 铁律:知识内容+笔记归 HKC,不进 HMR(HMR 只装 token 上下文/工作运行记忆)。"""
191
+ self._ensure_hkc_loaded()
192
+ item = KnowledgeItem(content=content, topic=topic,
193
+ source=source, tags=tags or [])
194
+ with self._lock:
195
+ self.items[item.id] = item
196
+ self.review_states[item.id] = ReviewState(item_id=item.id)
197
+ self.graph.add_item(item)
198
+ self._save() # 存进 HKC(HKC 不可用则离线队列兜底,恢复后自动补进)
199
+ return {"item": item.to_dict(),
200
+ "review": self.review_states[item.id].to_dict()}
201
+
202
+ def import_csv(self, path: str) -> int:
203
+ """从 CSV 导入知识点。列:content,topic,source,tags
204
+ 外部工具(Readwise/Obsidian/Anki MCP)接口可在此扩展。"""
205
+ count = 0
206
+ with open(Path(path), encoding="utf-8") as f:
207
+ for row in csv.DictReader(f):
208
+ tags = [t.strip() for t in (row.get("tags") or "").split("|") if t.strip()]
209
+ self.learn(content=row["content"],
210
+ topic=row.get("topic", "general"),
211
+ source=row.get("source", ""), tags=tags)
212
+ count += 1
213
+ return count
214
+
215
+ # ── 固化:复习 ────────────────────────────────────────
216
+ def review(self, item_id: str, grade: int) -> dict:
217
+ """复习一个知识点,提交回忆质量评分(0~5),更新掌握度。"""
218
+ self._ensure_hkc_loaded()
219
+ state = self.review_states.get(item_id)
220
+ if not state:
221
+ return {"error": "未知知识点"}
222
+ self.sr.review(state, grade)
223
+ self._save()
224
+ return state.to_dict()
225
+
226
+ def delete(self, item_id: str) -> bool:
227
+ """删除一个知识点(内存 + 复习状态 + 图谱)并落盘。返回是否命中。"""
228
+ self._ensure_hkc_loaded()
229
+ with self._lock:
230
+ found = item_id in self.items
231
+ self.items.pop(item_id, None)
232
+ self.review_states.pop(item_id, None)
233
+ try:
234
+ self.graph.remove_item(item_id)
235
+ except Exception:
236
+ pass
237
+ gs = self._graph_store()
238
+ if gs is not None:
239
+ try:
240
+ gs.delete(item_id) # HKC 软删除(status='deleted')
241
+ except Exception:
242
+ pass
243
+ if found:
244
+ self._save()
245
+ return found
246
+
247
+ def add_note(self, item_id: str, text: str) -> dict:
248
+ """给一个知识点追加一条笔记(有内容才写盘;没新增就不存)。"""
249
+ self._ensure_hkc_loaded()
250
+ text = (text or "").strip()
251
+ if not text:
252
+ return {"error": "笔记为空"}
253
+ with self._lock:
254
+ item = self.items.get(item_id)
255
+ if item is None:
256
+ return {"error": "未知知识点"}
257
+ item.notes.append({"text": text, "ts": datetime.now().isoformat()})
258
+ notes = list(item.notes)
259
+ self._save() # 只有新增了笔记才落盘
260
+ return {"notes": notes}
261
+
262
+ def due_today(self) -> list:
263
+ """今天该复习的知识点(最弱的优先)。"""
264
+ due = self.sr.due_items(list(self.review_states.values()))
265
+ return [{"item": self.items[s.item_id].to_dict(),
266
+ "review": s.to_dict()} for s in due]
267
+
268
+ # ── 连接:构建图谱 ────────────────────────────────────
269
+ def build_connections(self, semantic_threshold: float = 0.55) -> dict:
270
+ """自动构建知识连接(同主题 + 语义)。"""
271
+ topic_edges = self.graph.auto_link_by_topic()
272
+ semantic_edges = self.graph.auto_link_by_semantic(semantic_threshold)
273
+ return {"topic_edges": topic_edges,
274
+ "semantic_edges": semantic_edges,
275
+ "graph": self.graph.stats()}
276
+
277
+ def related(self, item_id: str) -> list:
278
+ """某知识点的关联知识(连接发现)。"""
279
+ out = []
280
+ for nid, etype, weight, reason in self.graph.neighbors(item_id):
281
+ if nid in self.items:
282
+ out.append({"item": self.items[nid].to_dict(),
283
+ "edge_type": etype, "weight": weight, "reason": reason})
284
+ return out
285
+
286
+ # ── 学习策略(对应财务守门人)─────────────────────────
287
+ def strategy_check(self, daily_cap: int = 20) -> list:
288
+ """学习策略裁决:复习提醒、薄弱主题、负荷预警、连接建议。"""
289
+ verdicts = []
290
+ states = list(self.review_states.values())
291
+
292
+ # 到期复习
293
+ due = self.sr.due_items(states)
294
+ if due:
295
+ verdicts.append(LearningVerdict(
296
+ rule_id="review_due", action="review_due", severity="info",
297
+ message=f"今天有 {len(due)} 个知识点待复习",
298
+ payload={"count": len(due)}))
299
+
300
+ # 遗忘风险(薄弱项)
301
+ risky = self.sr.forgetting_risk(states)
302
+ if risky:
303
+ verdicts.append(LearningVerdict(
304
+ rule_id="weak_alert", action="weak_topic_alert", severity="warn",
305
+ message=f"{len(risky)} 个知识点处于高遗忘风险,建议优先复习",
306
+ payload={"risky": risky[:5]}))
307
+
308
+ # 负荷预警
309
+ load = self.sr.learning_load(states)
310
+ if load["total_due"] > daily_cap:
311
+ verdicts.append(LearningVerdict(
312
+ rule_id="overload", action="overload_warn", severity="warn",
313
+ message=f"未来复习量 {load['total_due']} 超过每日上限 {daily_cap},建议分批",
314
+ payload=load))
315
+
316
+ # 孤立知识点(无连接)→ 建议建立关联
317
+ connected = set()
318
+ for e in self.graph.edges:
319
+ connected.add(e.source_id)
320
+ connected.add(e.target_id)
321
+ isolated = [i for i in self.items if i not in connected]
322
+ if isolated:
323
+ verdicts.append(LearningVerdict(
324
+ rule_id="suggest_connect", action="suggest_connect", severity="info",
325
+ message=f"{len(isolated)} 个知识点尚无关联,可建立连接以加深理解",
326
+ payload={"isolated_count": len(isolated)}))
327
+ return verdicts
328
+
329
+ # ── 综合分析(office Agent 核心输出)──────────────────
330
+ def analyze(self) -> dict:
331
+ self._ensure_hkc_loaded() # HKC 就绪后从真源载回知识(迁移删文件后重启也不丢)
332
+ states = list(self.review_states.values())
333
+ masteries = [s.mastery for s in states]
334
+ avg_mastery = round(sum(masteries) / len(masteries), 3) if masteries else 0.0
335
+
336
+ # 按主题统计掌握度
337
+ from collections import defaultdict
338
+ topic_mastery = defaultdict(list)
339
+ for iid, item in self.items.items():
340
+ st = self.review_states.get(iid)
341
+ if st:
342
+ topic_mastery[item.topic].append(st.mastery)
343
+ topics = {t: round(sum(m) / len(m), 3) for t, m in topic_mastery.items()}
344
+
345
+ return {
346
+ "total_items": len(self.items),
347
+ "avg_mastery": avg_mastery,
348
+ "topic_mastery": topics,
349
+ "weakest_topic": (min(topics, key=topics.get) if topics else None),
350
+ "due_today": len(self.sr.due_items(states)),
351
+ "forgetting_risk": self.sr.forgetting_risk(states),
352
+ "learning_load": self.sr.learning_load(states),
353
+ "graph": self.graph.stats(),
354
+ "strategy": [v.to_dict() for v in self.strategy_check()],
355
+ }
356
+
357
+ def status(self) -> dict:
358
+ self._ensure_hkc_loaded()
359
+ return {
360
+ "items": len(self.items),
361
+ "reviews_tracked": len(self.review_states),
362
+ "graph": self.graph.stats(),
363
+ }
@@ -0,0 +1,96 @@
1
+ """
2
+ Foxclaw 学习子系统 — 间隔重复引擎(固化层)
3
+ 实现 SM-2 算法(Anki 核心算法的经典版本)。
4
+ 核心思想:学习是时间连续系统——追踪每个知识点的记忆强度随时间衰减,
5
+ 在最佳时机安排复习,让"越用越熟"变成可计算的机制。
6
+ """
7
+
8
+ from datetime import datetime, timedelta
9
+ from .models import ReviewState
10
+
11
+
12
+ class SpacedRepetition:
13
+ """SM-2 间隔重复算法。
14
+
15
+ 评分 grade(0~5):
16
+ 0-2 = 没记住(算 lapse,重置间隔)
17
+ 3 = 勉强记住
18
+ 4 = 记住了
19
+ 5 = 完美回忆
20
+ """
21
+
22
+ @staticmethod
23
+ def review(state: ReviewState, grade: int) -> ReviewState:
24
+ """根据本次回忆质量,更新间隔重复状态。返回更新后的 state。"""
25
+ # grade 钳制到 SM-2 定义域 [0,5],防止越界输入让 EF 公式算出无意义值
26
+ grade = max(0, min(5, int(grade)))
27
+ now = datetime.now()
28
+ state.total_reviews += 1
29
+ state.last_review = now.isoformat()
30
+
31
+ if grade < 3:
32
+ # 没记住:重置,短期内再复习
33
+ state.repetitions = 0
34
+ state.interval = 1
35
+ state.lapses += 1
36
+ else:
37
+ # 记住了:按 SM-2 推进间隔
38
+ if state.repetitions == 0:
39
+ state.interval = 1
40
+ elif state.repetitions == 1:
41
+ state.interval = 6
42
+ else:
43
+ state.interval = round(state.interval * state.ease_factor)
44
+ state.repetitions += 1
45
+
46
+ # 更新难度因子(SM-2 公式)
47
+ ef = state.ease_factor + (0.1 - (5 - grade) * (0.08 + (5 - grade) * 0.02))
48
+ state.ease_factor = max(1.3, round(ef, 3))
49
+
50
+ # 下次复习时间
51
+ state.due_date = (now + timedelta(days=state.interval)).isoformat()
52
+ return state
53
+
54
+ @staticmethod
55
+ def due_items(states: list) -> list:
56
+ """返回所有到期需复习的状态,按掌握度升序(最弱的先复习)。"""
57
+ due = [s for s in states if s.is_due]
58
+ return sorted(due, key=lambda s: s.mastery)
59
+
60
+ @staticmethod
61
+ def forgetting_risk(states: list, horizon_days: int = 3) -> list:
62
+ """遗忘风险检测(对应财务异常检测):
63
+ 即将到期且掌握度低的知识点 = 高遗忘风险。"""
64
+ risky = []
65
+ for s in states:
66
+ if s.days_until_due <= horizon_days and s.mastery < 0.5:
67
+ risky.append({
68
+ "item_id": s.item_id,
69
+ "mastery": s.mastery,
70
+ "days_until_due": s.days_until_due,
71
+ "lapses": s.lapses,
72
+ "risk": round((1 - s.mastery) * (1 if s.is_due else 0.6), 3),
73
+ "reason": f"掌握度仅 {s.mastery},"
74
+ + ("已到期" if s.is_due else f"{s.days_until_due} 天内到期"),
75
+ })
76
+ return sorted(risky, key=lambda x: -x["risk"])
77
+
78
+ @staticmethod
79
+ def learning_load(states: list, days: int = 7) -> dict:
80
+ """学习负荷(对应财务烧钱速度):未来 N 天的复习量。"""
81
+ from collections import defaultdict
82
+ by_day = defaultdict(int)
83
+ for s in states:
84
+ d = s.days_until_due
85
+ if 0 <= d <= days:
86
+ by_day[d] += 1
87
+ elif d < 0:
88
+ by_day[0] += 1 # 逾期的算今天
89
+ total_due = sum(by_day.values())
90
+ return {
91
+ "window_days": days,
92
+ "total_due": total_due,
93
+ "daily_avg": round(total_due / days, 1),
94
+ "by_day": dict(sorted(by_day.items())),
95
+ "overdue": sum(1 for s in states if s.is_due),
96
+ }