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,242 @@
1
+ """
2
+ 联网搜索工具 —— 给 Agent 提供实时信息检索能力。
3
+
4
+ DeepSeek/OpenAI 等基础 API 不自带联网,本模块对接百度 AI 搜索
5
+ (千帆 ai_search,底层百度搜索 + ERNIE),国内访问稳定、每日有免费额度。
6
+ 百度 AI 搜索是"搜索+生成"一体接口:输入问题,它联网检索后直接产出
7
+ 带引用的实时答案,本模块取其答案作为"联网上下文"注入回 Agent 的模型。
8
+
9
+ 配置(在 .env):
10
+ BAIDU_SEARCH_KEY=bce-v3/ALTAK-xxx/xxx # 千帆 AppBuilder API Key
11
+ BAIDU_SEARCH_MODEL=ernie-4.5-turbo-32k # 可选,默认 ernie-3.5-8k
12
+ BAIDU_SEARCH_BASE=https://qianfan.baidubce.com/v2/ai_search # 可选
13
+
14
+ 降级策略:未配 key / 网络不通 / 失败 → 返回空,不影响主流程,
15
+ Agent 如实告知"暂未查到",不编造。
16
+ """
17
+ from __future__ import annotations
18
+ import os
19
+ import re
20
+ import json
21
+ import urllib.request
22
+ import urllib.error
23
+ from . import settings
24
+
25
+
26
+ # 触发联网搜索的关键词:问到实时/时效性信息时才搜
27
+ # ── 是否需要联网:话题信号(强) vs 时间词(弱) vs 个人数据(否决)──────────────
28
+ # 教训:曾把纯时间词(今天/本月/最近…)也当触发,结果「分析一下本月支出」这类**问自己账本**
29
+ # 的任务被误判为需要联网 → 白跑一次 ~22s 的付费深度搜索。而 Foxclaw 的领域(个人财务/
30
+ # 行程/学习)天生满是时间词,这不是个别误判,是**一整类合法任务**在白交时间和钱。
31
+ # 现在:时间词单独**不足以**触发;必须出现"外部世界话题";且个人数据一律否决。
32
+ # (与既有的 now⊂knowledge / current⊂concurrent 词边界修复同源:都是过度触发。)
33
+
34
+ # 强触发:出现即说明要的是外部实时信息
35
+ TOPIC_TRIGGERS = [
36
+ "版本", "几个版本", "第几", "价格", "多少钱", "汇率", "股价", "新闻",
37
+ "发布", "上线", "更新到", "迭代", "排行", "排名", "天气",
38
+ # 问"某产品/技术的模型/型号/出没出/更没更"——都是外部时效信息,过去漏搜→编造。
39
+ # 只留明确指向外部产品的词;"有哪些/可用/支持"太宽(会误伤"我有哪些任务"),不加。
40
+ "模型", "型号", "出了没", "发布没", "更新没", "新版",
41
+ "latest", "news", "version", "price", "models",
42
+ ]
43
+ # 弱信号:纯时间词。单独出现不触发(个人数据问题天生带这些词)
44
+ TIME_WORDS = [
45
+ "最新", "现在", "目前", "今天", "今日", "昨天", "本周", "本月", "今年",
46
+ "最近", "当前", "today", "current", "now",
47
+ ]
48
+ # 否决:问的是用户自己的账/行程/学习/记忆 —— 这些答案在本地领域内核里,不在网上。
49
+ # 注意别放"我的"这类过宽的词:「我的股票现在多少钱」是真需要联网的。
50
+ PERSONAL_MARKERS = [
51
+ "支出", "收入", "预算", "交易", "账单", "花了", "消费", "记账", "结余", "余额",
52
+ "行程", "日程", "出行", "复习", "知识点", "学习计划", "记忆",
53
+ ]
54
+
55
+ SEARCH_TRIGGERS = TOPIC_TRIGGERS # 对外保持同名(旧引用不破)
56
+
57
+ # 中文触发词按「子串」匹配(中文无词边界);英文触发词按「词边界」匹配,
58
+ # 否则短词会误命中常见词:"now"⊂"knowledge"、"current"⊂"concurrent"、
59
+ # "now"⊂"known" —— 在这个满口 knowledge 的项目里会频繁无谓地打百度。
60
+ _CJK_TRIGGERS = [k for k in SEARCH_TRIGGERS if not k.isascii()]
61
+ _ASCII_TRIGGERS = [k.lower() for k in SEARCH_TRIGGERS if k.isascii()]
62
+ _ASCII_RE = (
63
+ re.compile(r"\b(" + "|".join(re.escape(k) for k in _ASCII_TRIGGERS) + r")\b")
64
+ if _ASCII_TRIGGERS else None
65
+ )
66
+
67
+
68
+ def is_personal_query(query: str) -> bool:
69
+ """问的是不是**用户自己的数据**(账本/行程/学习/记忆)。
70
+
71
+ 单一真源:凡是"要不要把这个问题发到外部"的判断都用它 —— 联网增强(needs_search)、
72
+ 技能路由的外部技能闸,都必须走这里。教训:这条保护原先只写在 needs_search 里,
73
+ 结果技能派发默认开启后,「查一下最新的本月支出」照样把私人财务问题发去外部搜索
74
+ —— 同一个 bug 从没设防的旁路回来了。**保护必须放在共同咽喉,不能各写一份。**
75
+ """
76
+ if not query:
77
+ return False
78
+ return any(m in query.lower() for m in PERSONAL_MARKERS)
79
+
80
+
81
+ def needs_search(query: str) -> bool:
82
+ """是否值得为这个问题联网。宁可漏搜也不要错搜:
83
+ 错搜的代价是每次 ~22s + 付费调用(且答案本来就在本地);漏搜只是少一点时效信息。"""
84
+ if not query:
85
+ return False
86
+ q = query.lower()
87
+ if is_personal_query(q):
88
+ return False # 问的是自己的数据 → 答案在本地内核,绝不联网
89
+ # 自我模型/身份问题(你是什么模型 / 你现在是哪个模型 …)→ 由 ModelRouter 运行时注入本地即答,不联网。
90
+ # 否则会因含"模型"触发词白跑一次 ~20s 百度搜索。"你"与"模型"相邻(≤8字)才算自问;
91
+ # "你帮我查 X 的模型"(相距远)仍走搜索。
92
+ if re.search(r"你[^,。?!]{0,8}模型", q):
93
+ return False
94
+ if any(k in q for k in _CJK_TRIGGERS):
95
+ return True # 外部世界话题 → 联网
96
+ return bool(_ASCII_RE and _ASCII_RE.search(q))
97
+ # 注:只含时间词(今天/本月/最近…)而无话题 → 不触发(见上方 TIME_WORDS 说明)
98
+
99
+
100
+ def _key() -> str:
101
+ return os.environ.get("BAIDU_SEARCH_KEY", "").strip()
102
+
103
+
104
+ def _model() -> str:
105
+ return os.environ.get("BAIDU_SEARCH_MODEL", "ernie-3.5-8k").strip()
106
+
107
+
108
+ def _base() -> str:
109
+ return os.environ.get(
110
+ "BAIDU_SEARCH_BASE", "https://qianfan.baidubce.com/v2/ai_search"
111
+ ).rstrip("/")
112
+
113
+
114
+ def _timeout() -> int:
115
+ """读取超时(秒)。深度搜索较慢,默认 45s;可经 BAIDU_SEARCH_TIMEOUT 调。"""
116
+ return max(5, int(settings.get_safe("BAIDU_SEARCH_TIMEOUT")))
117
+
118
+
119
+ def _deep() -> bool:
120
+ """是否开启深度搜索(更全但更慢)。默认开;BAIDU_SEARCH_DEEP=0 关掉换速度。"""
121
+ return os.environ.get("BAIDU_SEARCH_DEEP", "1").strip() not in ("0", "false", "False", "")
122
+
123
+
124
+ def search_answer(query: str) -> dict:
125
+ """调用百度 AI 搜索,返回 {answer, references}。
126
+ answer=联网生成的实时答案文本;references=参考资料列表。失败返回空 dict。"""
127
+ key = _key()
128
+ if not key:
129
+ return {}
130
+ from .tracing import span as _span
131
+ with _span("web.search"):
132
+ return _search_answer_inner(query, key)
133
+
134
+
135
+ def _search_answer_inner(query: str, key: str) -> dict:
136
+ try:
137
+ payload = json.dumps({
138
+ "messages": [{"role": "user", "content": query}],
139
+ "stream": False,
140
+ "model": _model(),
141
+ "enable_deep_search": _deep(),
142
+ "enable_corner_markers": False, # 不要答案里的 ^[1]^ 角标(参考资料已单列)
143
+ }).encode("utf-8")
144
+ req = urllib.request.Request(
145
+ f"{_base()}/chat/completions",
146
+ data=payload,
147
+ headers={
148
+ "Content-Type": "application/json",
149
+ "Authorization": f"Bearer {key}",
150
+ },
151
+ method="POST",
152
+ )
153
+ with urllib.request.urlopen(req, timeout=_timeout()) as resp:
154
+ data = json.loads(resp.read().decode("utf-8", "ignore"))
155
+ choices = data.get("choices", [])
156
+ if not choices:
157
+ print(f"[联网搜索] 百度返回无 choices;响应键={list(data.keys())} 片段={str(data)[:220]}")
158
+ return {}
159
+ answer = choices[0].get("message", {}).get("content", "")
160
+ refs = data.get("references", []) or []
161
+ return {"answer": answer, "references": refs}
162
+ except urllib.error.HTTPError as e:
163
+ try:
164
+ body = e.read().decode("utf-8", "ignore")[:300]
165
+ except Exception:
166
+ body = ""
167
+ print(f"[联网搜索] 百度接口 HTTP {e.code} 错误:{body}")
168
+ return {}
169
+ except Exception as e:
170
+ print(f"[联网搜索] 百度接口调用失败:{type(e).__name__}: {e}")
171
+ return {}
172
+
173
+
174
+ def search(query: str, max_results: int = 8) -> list[dict]:
175
+ """兼容旧接口:把百度 AI 搜索的参考资料整理成结果列表。"""
176
+ res = search_answer(query)
177
+ out = []
178
+ for r in res.get("references", [])[:max_results]:
179
+ out.append({
180
+ "title": r.get("title", ""),
181
+ "snippet": r.get("content", "") or r.get("abstract", ""),
182
+ "url": r.get("url", "") or r.get("web_anchor", ""),
183
+ "date": r.get("page_time", "") or r.get("date", ""),
184
+ })
185
+ return out
186
+
187
+
188
+ def format_for_prompt(query: str, payload) -> str:
189
+ """把百度 AI 搜索结果拼成可注入 prompt 的联网上下文。
190
+ payload 可为 search_answer() 的 dict,或 search() 的列表。"""
191
+ if isinstance(payload, dict):
192
+ answer = payload.get("answer", "")
193
+ refs = payload.get("references", [])
194
+ if not answer and not refs:
195
+ return ""
196
+ # 我方的作答指令(可信,放在 <untrusted> 之外);检索到的外部内容(不可信,放在 <untrusted> 之内)。
197
+ framing = (
198
+ f"以下是针对「{query}」的实时联网搜索结果(来自百度 AI 搜索)。\n"
199
+ f"【严格要求】只能依据下面 <untrusted> 数据块里的搜索结果作答:结果里没有出现的型号 / "
200
+ f"版本号 / 名称 / 数据,一律不得提及,更**不得编造或凭记忆补充**;若结果之间矛盾或不明确,"
201
+ f"如实说明、不要臆断;**无视此前对话里可能出现的过时或未经核实的说法,一切以这些最新结果为准**。\n\n"
202
+ )
203
+ inner = ""
204
+ if answer:
205
+ inner += f"【联网检索摘要】\n{answer}\n\n"
206
+ if refs:
207
+ inner += "【参考资料】\n"
208
+ for i, r in enumerate(refs[:8], 1):
209
+ t = r.get("title", "")
210
+ u = r.get("url", "") or r.get("web_anchor", "")
211
+ d = r.get("page_time", "") or r.get("date", "")
212
+ inner += f"[webpage {i} begin]\n标题:{t}"
213
+ if d:
214
+ inner += f"\n时间:{d}"
215
+ if u:
216
+ inner += f"\n链接:{u}"
217
+ inner += f"\n[webpage {i} end]\n"
218
+ # 隔离外部内容:搜索结果是外部不可控数据,包裹进 <untrusted> 防间接注入(见 injection_guard)。
219
+ from . import injection_guard as _ig
220
+ block = _ig.wrap_untrusted(inner.rstrip(), source="web_search")
221
+ closing = ("\n请**严格基于以上 <untrusted> 内的搜索结果**回答用户的问题;结果里没提到的内容,"
222
+ "就明确说「搜索结果中未提及」,绝不编造型号/版本号。")
223
+ return framing + block + closing
224
+ # 列表形式(兼容)
225
+ results = payload or []
226
+ if not results:
227
+ return ""
228
+ blocks = []
229
+ for i, r in enumerate(results, 1):
230
+ seg = f"[webpage {i} begin]\n标题:{r.get('title','')}"
231
+ if r.get("date"):
232
+ seg += f"\n时间:{r['date']}"
233
+ seg += f"\n内容:{r.get('snippet','')}"
234
+ if r.get("url"):
235
+ seg += f"\n链接:{r['url']}"
236
+ seg += f"\n[webpage {i} end]"
237
+ blocks.append(seg)
238
+ return (
239
+ f"以下是针对「{query}」的实时联网搜索结果(请基于最新信息回答):\n\n"
240
+ + "\n\n".join(blocks)
241
+ + "\n\n请根据以上搜索结果回答用户的问题。"
242
+ )
@@ -0,0 +1,15 @@
1
+ """Foxclaw — Finance: 财务 Agent 的资金行为操作系统内核"""
2
+ from .models import (
3
+ Transaction, Budget, FinancialEntity,
4
+ FinancePolicyRule, PolicyVerdict,
5
+ )
6
+ from .reasoning import FinanceReasoning
7
+ from .policy import FinancePolicyEngine, DEFAULT_RULES
8
+ from .runtime import FinanceRuntime
9
+
10
+ __all__ = [
11
+ "Transaction", "Budget", "FinancialEntity",
12
+ "FinancePolicyRule", "PolicyVerdict",
13
+ "FinanceReasoning", "FinancePolicyEngine", "DEFAULT_RULES",
14
+ "FinanceRuntime",
15
+ ]
@@ -0,0 +1,153 @@
1
+ """
2
+ Foxclaw 财务子系统 — 结构化证据链 (Evidence Chain)
3
+
4
+ 借鉴 2026 年前沿金融 AI 的"可解释性铁律":一个只给原始分数(如 0.87)
5
+ 而无人类可读证据链的决策,被认为比没有 AI 更糟,因为无法被审计员复核。
6
+
7
+ 每个财务风险决策输出结构化的:
8
+ 风险分数 + 风险因子(每个带证据和权重)+ 缓解因素 + 决策理由 + 建议动作 + 置信度
9
+
10
+ 这与 Foxclaw 的 ThoughtChain 审计链天然契合 —— 证据链可直接沉淀为审计节点。
11
+ """
12
+ from __future__ import annotations
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+
16
+
17
+ @dataclass
18
+ class RiskFactor:
19
+ """单个风险因子:是什么、证据、权重。"""
20
+ factor: str # 风险因子名(如"超预算")
21
+ evidence: str # 人类可读证据(如"餐饮已用 120%,超 ¥240")
22
+ weight: float # 权重 0-1(对总风险的贡献)
23
+
24
+ def to_dict(self) -> dict:
25
+ return {"factor": self.factor, "evidence": self.evidence,
26
+ "weight": round(self.weight, 3)}
27
+
28
+
29
+ @dataclass
30
+ class EvidenceChain:
31
+ """结构化决策证据链 —— 财务决策的可审计输出。"""
32
+ decision_type: str # 决策类型(如 budget_risk / anomaly)
33
+ risk_score: float = 0.0 # 综合风险分 0-1
34
+ risk_factors: list = field(default_factory=list) # [RiskFactor]
35
+ mitigating_factors: list = field(default_factory=list) # 缓解因素(降低风险的)
36
+ rationale: str = "" # 决策理由(人类可读)
37
+ recommended_action: str = "" # 建议动作
38
+ confidence: float = 0.0 # 置信度 0-1
39
+ additional_checks: list = field(default_factory=list) # 还需补充的检查
40
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
41
+
42
+ def add_factor(self, factor: str, evidence: str, weight: float):
43
+ self.risk_factors.append(RiskFactor(factor, evidence, weight))
44
+ return self
45
+
46
+ def compute_risk(self) -> float:
47
+ """综合风险:饱和式聚合(避免多个小因子简单累加成假高危)。
48
+
49
+ 取最大单因子权重为基准 + 其余因子的衰减贡献,再减缓解,裁剪 [0,1]。
50
+ 这样"1个严重"比"3个轻微"风险更高,符合直觉。
51
+ """
52
+ if not self.risk_factors:
53
+ self.risk_score = 0.0
54
+ return 0.0
55
+ weights = sorted((f.weight for f in self.risk_factors), reverse=True)
56
+ score = weights[0] # 最大因子为主
57
+ for w in weights[1:]:
58
+ score += w * (1 - score) * 0.5 # 其余因子衰减叠加(饱和,不会超过1)
59
+ mitigation = min(0.2, 0.1 * len(self.mitigating_factors)) # 缓解最多减0.2
60
+ self.risk_score = max(0.0, min(1.0, round(score - mitigation, 3)))
61
+ return self.risk_score
62
+
63
+ def level(self) -> str:
64
+ """风险等级(供人快速判断)。"""
65
+ s = self.risk_score
66
+ return "high" if s >= 0.7 else "medium" if s >= 0.4 else "low"
67
+
68
+ def to_dict(self) -> dict:
69
+ return {
70
+ "decision_type": self.decision_type,
71
+ "risk_score": self.risk_score,
72
+ "risk_level": self.level(),
73
+ "risk_factors": [f.to_dict() for f in self.risk_factors],
74
+ "mitigating_factors": list(self.mitigating_factors),
75
+ "rationale": self.rationale,
76
+ "recommended_action": self.recommended_action,
77
+ "confidence": round(self.confidence, 3),
78
+ "additional_checks": list(self.additional_checks),
79
+ "created_at": self.created_at,
80
+ }
81
+
82
+ def to_thought_note(self) -> str:
83
+ """压缩成一句可塞进 ThoughtChain 的审计摘要。"""
84
+ factors = "; ".join(f"{f.factor}({f.weight})" for f in self.risk_factors[:3])
85
+ return (f"[财务决策·{self.decision_type}] 风险={self.risk_score}({self.level()}) "
86
+ f"| 因子: {factors} | 建议: {self.recommended_action}")
87
+
88
+
89
+ class FinanceExplainer:
90
+ """把财务原始判断转成结构化证据链。"""
91
+
92
+ @staticmethod
93
+ def explain_budget_risk(budget_status: list) -> EvidenceChain:
94
+ """预算风险 → 证据链。budget_status 为 budget_status() 的输出。"""
95
+ ec = EvidenceChain(decision_type="budget_risk", confidence=0.9)
96
+ over_budgets = []
97
+ near_budgets = []
98
+ for b in budget_status:
99
+ ratio = b.get("usage_ratio", 0)
100
+ cat = b.get("category", "?")
101
+ if ratio >= 1.0:
102
+ over = b.get("spent", 0) - b.get("monthly_limit", 0)
103
+ # 权重随超支程度递增:超10%→0.5,超50%→0.65,超100%+→0.8(上限)
104
+ w = min(0.8, 0.45 + (ratio - 1.0) * 0.35)
105
+ ec.add_factor("超预算", f"{cat} 已用 {ratio*100:.0f}%,超支 ¥{over:.0f}",
106
+ weight=w)
107
+ over_budgets.append(cat)
108
+ elif ratio >= 0.8:
109
+ ec.add_factor("接近预算上限",
110
+ f"{cat} 已用 {ratio*100:.0f}%,接近上限", weight=0.25)
111
+ near_budgets.append(cat)
112
+ # 缓解因素:有预算分类未超支
113
+ healthy = [b.get("category") for b in budget_status
114
+ if b.get("usage_ratio", 0) < 0.8]
115
+ if healthy:
116
+ ec.mitigating_factors.append(f"{len(healthy)} 个分类预算健康")
117
+ ec.compute_risk()
118
+ # 理由 + 建议
119
+ if over_budgets:
120
+ ec.rationale = f"{','.join(over_budgets)} 已超预算,需立即关注"
121
+ ec.recommended_action = f"削减 {over_budgets[0]} 支出或调整预算"
122
+ ec.additional_checks.append("检查是否有可取消的重复订阅")
123
+ elif near_budgets:
124
+ ec.rationale = f"{','.join(near_budgets)} 接近上限,建议放缓"
125
+ ec.recommended_action = "本月剩余时间控制相关分类支出"
126
+ else:
127
+ ec.rationale = "所有预算分类处于健康水平"
128
+ ec.recommended_action = "保持当前消费节奏"
129
+ return ec
130
+
131
+ @staticmethod
132
+ def explain_anomalies(anomalies: list) -> EvidenceChain:
133
+ """异常消费 → 证据链。anomalies 为 detect_anomalies() 的输出。"""
134
+ ec = EvidenceChain(decision_type="anomaly", confidence=0.85)
135
+ for a in anomalies[:5]:
136
+ z = a.get("z_score", 0)
137
+ # z 分数越高,权重越大(裁剪到 0.6)
138
+ w = min(0.6, 0.15 + z * 0.05)
139
+ ec.add_factor("异常大额支出",
140
+ f"{a.get('entity','?')} ¥{abs(a.get('amount',0)):.0f}"
141
+ f"({a.get('category','?')},偏离度 z={z})", weight=w)
142
+ ec.compute_risk()
143
+ if anomalies:
144
+ ec.rationale = f"检测到 {len(anomalies)} 笔异常支出,偏离历史模式"
145
+ ec.recommended_action = "核对这些交易是否本人操作,排查盗刷/误记"
146
+ ec.additional_checks.append("与银行流水对账确认")
147
+ if len(anomalies) >= 3:
148
+ ec.additional_checks.append("考虑是否近期消费习惯发生变化")
149
+ else:
150
+ ec.rationale = "未发现异常支出,消费模式稳定"
151
+ ec.recommended_action = "无需处理"
152
+ ec.confidence = 0.9
153
+ return ec
@@ -0,0 +1,100 @@
1
+ """本位币折算 —— 多币种记账的汇率表。
2
+
3
+ 为什么需要它:用户会说「花了 105 块」也会说「花了 100 美元」。如果两者都写进
4
+ `Transaction.amount` 再求和,100 美元会被当成 100 元 —— 预算、风控、现金流预测
5
+ 会一起错,而且错得看不出来。所以账本统一用**本位币(CNY)记账**,原币金额与币种
6
+ 另存,汇总时两种都能看。
7
+
8
+ 汇率是**静态参考价,不联网**:宁可给一个用户能改的明确数字,也不假装实时行情。
9
+ 用户可在 `finance_fx.json` 或 `/api/finance/fx` 里改成自己的记账汇率(比如当天
10
+ 刷卡的实际结算价,那才是真实成本)。
11
+ """
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ BASE_CURRENCY = "CNY"
17
+
18
+ # 参考价(1 单位外币 = 多少 CNY)。宁可粗,但要能改。
19
+ DEFAULT_RATES = {
20
+ "CNY": 1.0,
21
+ "USD": 7.2,
22
+ "HKD": 0.92,
23
+ "EUR": 7.8,
24
+ "JPY": 0.048,
25
+ "GBP": 9.1,
26
+ "KRW": 0.0052,
27
+ "SGD": 5.3,
28
+ "AUD": 4.7,
29
+ "CAD": 5.2,
30
+ }
31
+
32
+
33
+ class FxTable:
34
+ """币种 → 本位币折算率,带落盘(原子写)。"""
35
+
36
+ def __init__(self, path: str = None):
37
+ self.path = Path(path) if path else None
38
+ self.rates = dict(DEFAULT_RATES)
39
+ self.note = "static"
40
+ self._load()
41
+
42
+ def _load(self):
43
+ if not self.path or not self.path.exists():
44
+ return
45
+ try:
46
+ data = json.loads(self.path.read_text(encoding="utf-8"))
47
+ rates = data.get("rates") if isinstance(data, dict) else None
48
+ if isinstance(rates, dict):
49
+ for k, v in rates.items():
50
+ try:
51
+ self.rates[str(k).upper()] = float(v)
52
+ except (TypeError, ValueError):
53
+ continue
54
+ if data.get("note"):
55
+ self.note = data["note"]
56
+ except Exception:
57
+ # 汇率表坏了不能拖垮记账:退回出厂参考价,下次保存时自动覆盖
58
+ pass
59
+
60
+ def save(self) -> bool:
61
+ if not self.path:
62
+ return False
63
+ try:
64
+ self.path.parent.mkdir(parents=True, exist_ok=True)
65
+ tmp = self.path.with_suffix(".tmp")
66
+ tmp.write_text(json.dumps({"base": BASE_CURRENCY, "note": self.note,
67
+ "rates": self.rates},
68
+ ensure_ascii=False, indent=1), encoding="utf-8")
69
+ tmp.replace(self.path)
70
+ return True
71
+ except Exception:
72
+ return False
73
+
74
+ def rate(self, currency: str) -> float:
75
+ """取折算率。未知币种返回 1.0,由调用方标注「按 1:1 暂记」。"""
76
+ if not currency:
77
+ return 1.0
78
+ return float(self.rates.get(str(currency).upper(), 1.0))
79
+
80
+ def known(self, currency: str) -> bool:
81
+ return str(currency or "").upper() in self.rates
82
+
83
+ def to_base(self, amount: float, currency: str = BASE_CURRENCY) -> tuple:
84
+ """原币 → (本位币金额, 折算率)。"""
85
+ r = self.rate(currency)
86
+ return round(float(amount) * r, 2), r
87
+
88
+ def set_rate(self, currency: str, rate: float) -> dict:
89
+ cur = str(currency or "").upper().strip()
90
+ if not cur or rate is None or float(rate) <= 0:
91
+ raise ValueError("币种与汇率都必须有效(汇率 > 0)")
92
+ self.rates[cur] = round(float(rate), 6)
93
+ self.note = "user"
94
+ self.save()
95
+ return self.snapshot()
96
+
97
+ def snapshot(self) -> dict:
98
+ return {"base": BASE_CURRENCY, "note": self.note,
99
+ "rates": dict(sorted(self.rates.items())),
100
+ "path": str(self.path) if self.path else None}