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.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
B 阶段测试 — 财务 Agent 内核(资金行为操作系统)
|
|
3
|
+
验证:CSV 导入 → 推理(预算/异常/订阅/烧钱) → Policy 守门人裁决
|
|
4
|
+
不真实动钱:执行层只产出审批请求。
|
|
5
|
+
"""
|
|
6
|
+
import sys, tempfile
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
10
|
+
|
|
11
|
+
from foxclaw.finance import FinanceRuntime, Transaction
|
|
12
|
+
from foxclaw.finance.reasoning import FinanceReasoning
|
|
13
|
+
|
|
14
|
+
CSV = str(Path(__file__).parent.parent / "examples" / "sample_transactions.csv")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check(cond, msg):
|
|
18
|
+
if not cond:
|
|
19
|
+
raise AssertionError(f"✗ {msg}")
|
|
20
|
+
print(f" ✓ {msg}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
print("=" * 60)
|
|
25
|
+
print("B 阶段 · 财务 Agent 内核(资金行为操作系统)")
|
|
26
|
+
print("=" * 60)
|
|
27
|
+
|
|
28
|
+
# policy 落临时目录:原先写 ./b_finance_policy.yaml 再 os.remove —— 一旦中途抛异常
|
|
29
|
+
# 就会在仓库根留下垃圾文件。改临时目录后无需清理,也不会污染工作区。
|
|
30
|
+
fr = FinanceRuntime(policy_path=str(Path(tempfile.mkdtemp()) / "b_finance_policy.yaml"))
|
|
31
|
+
n = fr.import_csv(CSV)
|
|
32
|
+
fr.set_budget("food", 400)
|
|
33
|
+
fr.set_budget("subscription", 100)
|
|
34
|
+
fr.set_balance("debit", 11800)
|
|
35
|
+
fr.set_balance("credit_card_1", -700)
|
|
36
|
+
print(f"\n导入 {n} 笔交易")
|
|
37
|
+
check(n == 13, f"CSV 导入 13 笔交易(实为 {n})")
|
|
38
|
+
|
|
39
|
+
r = fr.analyze(period="2026-06", current_balance=11100)
|
|
40
|
+
|
|
41
|
+
print("\n【预算推理】")
|
|
42
|
+
for b in r["budget_status"]:
|
|
43
|
+
flag = "⚠ 超支" if b["is_over"] else "OK"
|
|
44
|
+
print(f" {b['category']:<14} {b['spent']:.0f}/{b['monthly_limit']:.0f} "
|
|
45
|
+
f"({b['usage_ratio']*100:.0f}%) {flag}")
|
|
46
|
+
over = {b["category"]: b["is_over"] for b in r["budget_status"]}
|
|
47
|
+
check(over.get("food") is True and over.get("subscription") is True,
|
|
48
|
+
f"两大超支分类被正确标记(food={over.get('food')}, subscription={over.get('subscription')})")
|
|
49
|
+
|
|
50
|
+
print("\n【超支预测】")
|
|
51
|
+
for f in r["overspend_forecast"]:
|
|
52
|
+
print(f" {f['category']:<14} 月底预计 {f['projected_month_end']:.0f} / "
|
|
53
|
+
f"限额 {f['monthly_limit']:.0f} · 将超支={f['will_overspend']}")
|
|
54
|
+
check(len(r["overspend_forecast"]) >= 2
|
|
55
|
+
and all(f["will_overspend"] for f in r["overspend_forecast"]),
|
|
56
|
+
f"超支预测命中({len(r['overspend_forecast'])} 项,全部 will_overspend=True)")
|
|
57
|
+
|
|
58
|
+
print("\n【异常消费(MAD 检测)】")
|
|
59
|
+
for a in r["anomalies"]:
|
|
60
|
+
print(f" {a['entity']} {a['amount']} (修正Z={a['z_score']}) — {a['reason']}")
|
|
61
|
+
check(any(a["entity"] == "高级餐厅" for a in r["anomalies"]),
|
|
62
|
+
f"MAD 检测出「高级餐厅」离群消费({len(r['anomalies'])} 条异常)")
|
|
63
|
+
|
|
64
|
+
print("\n【重复扣款】")
|
|
65
|
+
for d in r["duplicate_charges"]:
|
|
66
|
+
print(f" {d['reason']}")
|
|
67
|
+
if not r["duplicate_charges"]:
|
|
68
|
+
print(" 无")
|
|
69
|
+
check(isinstance(r["duplicate_charges"], list), "重复扣款检测返回列表结构")
|
|
70
|
+
|
|
71
|
+
print("\n【订阅膨胀】")
|
|
72
|
+
sc = r["subscription_creep"]
|
|
73
|
+
print(f" {sc['subscription_count']} 个订阅,月总 {sc['monthly_total']:.0f},"
|
|
74
|
+
f"预警={sc['creep_warning']}")
|
|
75
|
+
check(sc["subscription_count"] == 6 and sc["creep_warning"] is True,
|
|
76
|
+
f"订阅膨胀预警触发(6 个订阅 / 月总 {sc['monthly_total']:.0f})")
|
|
77
|
+
|
|
78
|
+
print("\n【烧钱速度 / 现金流】")
|
|
79
|
+
br = r["burn_rate"]
|
|
80
|
+
print(f" 日均支出 {br['daily_burn']} · 日均净流 {br['daily_net']}")
|
|
81
|
+
cf = r["cashflow_projection"]
|
|
82
|
+
if cf:
|
|
83
|
+
print(f" 现金流趋势: {cf['trend']} · 月净流 {cf['monthly_net']}")
|
|
84
|
+
for t in cf["trajectory"]:
|
|
85
|
+
print(f" 第 {t['month']} 月预计余额 {t['projected_balance']}")
|
|
86
|
+
check(len(cf["trajectory"]) == 3, "现金流预测给出 3 个月轨迹")
|
|
87
|
+
|
|
88
|
+
# ⚠ burn_rate 是**相对 now 的 30 天窗口**,而 examples/sample_transactions.csv 的交易
|
|
89
|
+
# 日期固定在 2026-06 —— 随真实时间推移,样例必然滑出窗口,于是上面那段会**静默变成 0
|
|
90
|
+
# 却毫无报错**(本测试此前不断言,故退化无人发现)。这里做两件事:
|
|
91
|
+
# (a) 用"当下日期"的交易构造可控用例,把 burn/cashflow 的算术真正锁住;
|
|
92
|
+
# (b) 断言其与 now-30天 窗口严格一致,把这个耦合显式写出来(而非假装它是稳定值)。
|
|
93
|
+
_cutoff = datetime.now() - timedelta(days=30)
|
|
94
|
+
in_window = [t for t in fr.transactions if t.dt >= _cutoff]
|
|
95
|
+
expect_expense = round(sum(abs(t.amount) for t in in_window if t.is_expense), 2)
|
|
96
|
+
check(br["total_expense"] == expect_expense,
|
|
97
|
+
f"burn_rate 严格按 now-30天 窗口统计(窗口内 {len(in_window)} 笔;样例日期已过期,故为 {expect_expense})")
|
|
98
|
+
check(isinstance(br["daily_burn"], (int, float)) and br["daily_burn"] == round(expect_expense / 30, 2),
|
|
99
|
+
f"日均支出 = 窗口内支出 / 30(daily_burn={br['daily_burn']})")
|
|
100
|
+
|
|
101
|
+
fresh = [
|
|
102
|
+
Transaction(amount=-300, category="food", entity="测试超市", account="debit"),
|
|
103
|
+
Transaction(amount=-150, category="transport", entity="测试打车", account="debit"),
|
|
104
|
+
Transaction(amount=2000, category="salary", entity="某公司", account="debit"),
|
|
105
|
+
]
|
|
106
|
+
br2 = FinanceReasoning.burn_rate(fresh, days=30)
|
|
107
|
+
check(br2["total_expense"] == 450.0 and br2["total_income"] == 2000.0,
|
|
108
|
+
f"当日交易用例:支出 450 / 收入 2000(实为 {br2['total_expense']}/{br2['total_income']})")
|
|
109
|
+
check(br2["daily_burn"] == 15.0, f"日均支出 = 450/30 = 15.0(实为 {br2['daily_burn']})")
|
|
110
|
+
check(br2["daily_net"] == round(1550 / 30, 2),
|
|
111
|
+
f"日均净流 = (2000-450)/30(实为 {br2['daily_net']})")
|
|
112
|
+
cf2 = FinanceReasoning.cashflow_projection(fresh, current_balance=1000.0, months=3)
|
|
113
|
+
check(cf2["trend"] == "上升" and cf2["monthly_net"] > 0, "净流入 → 趋势判定为上升")
|
|
114
|
+
check(cf2["trajectory"][-1]["projected_balance"] > 1000.0, "预测余额随净流入增长")
|
|
115
|
+
|
|
116
|
+
print("\n【守门人裁决(整体状态)】")
|
|
117
|
+
for v in r["gatekeeper_verdicts"]:
|
|
118
|
+
print(f" [{v['action'].upper()}] {v['message']}")
|
|
119
|
+
acts = {v["action"].lower() for v in r["gatekeeper_verdicts"]}
|
|
120
|
+
check("veto" in acts and "alert" in acts,
|
|
121
|
+
f"整体裁决含 VETO(异常消费否决)与 ALERT(订阅超标)(实为 {sorted(acts)})")
|
|
122
|
+
|
|
123
|
+
print("\n【守门人裁决(单笔大额交易)】")
|
|
124
|
+
big = Transaction(amount=-680, category="electronics", entity="Apple Store",
|
|
125
|
+
account="credit_card_1")
|
|
126
|
+
big_acts = {v.action.lower() for v in fr.policy.evaluate_transaction(big, account_balance=300)}
|
|
127
|
+
for v in fr.policy.evaluate_transaction(big, account_balance=300):
|
|
128
|
+
print(f" [{v.action.upper()}] {v.message}")
|
|
129
|
+
check("require_confirmation" in big_acts,
|
|
130
|
+
"大额支出(680>500)触发 REQUIRE_CONFIRMATION")
|
|
131
|
+
check("freeze" in big_acts,
|
|
132
|
+
"余额不足(300<500)触发 FREEZE 可选支出")
|
|
133
|
+
|
|
134
|
+
print("\n【执行层:审批流(不自动动钱)】")
|
|
135
|
+
req = fr.request_action("cancel_subscription", {"entity": "YouTube", "save": 12.0})
|
|
136
|
+
print(f" 请求: {req['action']} · 状态: {req['status']}")
|
|
137
|
+
print(f" 说明: {req['note']}")
|
|
138
|
+
# 本文件 docstring 的承诺是"不真实动钱":执行层必须只产出**待批**状态。
|
|
139
|
+
# 若哪天有人把它改成自动执行(EXECUTED/DONE),这里必须红。
|
|
140
|
+
check("PENDING" in str(req["status"]).upper(),
|
|
141
|
+
f"执行层只产出待批状态,不自动动钱(status={req['status']})")
|
|
142
|
+
|
|
143
|
+
print("\n" + "=" * 60)
|
|
144
|
+
print("✓ B 阶段验证完成:财务内核(记忆+推理+Policy 守门人)成立")
|
|
145
|
+
print("=" * 60)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_finance():
|
|
149
|
+
main()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
main()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""自然语言记账 —— 用户随口报备的一句花费,要能自己识别、记账、汇总。
|
|
2
|
+
|
|
3
|
+
这是财务应用第一线的功能,输入是「今天买了一件衣服花了 105 块」这种大白话。
|
|
4
|
+
测试固定四件事:
|
|
5
|
+
1. 金额/币种/时间/分类识别对(含中文数字、美元/美金/块/块钱);
|
|
6
|
+
2. 过去/今天 → 已发生,明天/下个月 → 计划支出,且计划不污染"本月已花";
|
|
7
|
+
3. 一段话多笔要拆开,时间可继承;
|
|
8
|
+
4. 记账本汇总(按天/按分类/按币种)与汇率折算正确。
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
from foxclaw.finance import FinanceRuntime
|
|
17
|
+
from foxclaw.finance.nlparse import (cn_to_number, has_expense_signal,
|
|
18
|
+
has_payment_verb, parse)
|
|
19
|
+
from foxclaw.agents.impl.finance import _should_autorecord
|
|
20
|
+
from foxclaw.agents.impl.supervisor import SupervisorAgent
|
|
21
|
+
|
|
22
|
+
NOW = datetime(2026, 11, 7, 20, 0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _rt(tmp_path):
|
|
26
|
+
return FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
|
|
27
|
+
data_path=str(tmp_path / "finance_data.json"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _one(text, now=NOW):
|
|
31
|
+
ps = parse(text, now=now)
|
|
32
|
+
assert len(ps) == 1, f"{text!r} 解析出 {len(ps)} 笔"
|
|
33
|
+
return ps[0]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 1. 用户原话 ────────────────────────────────────────────
|
|
37
|
+
def test_今天买衣服105块():
|
|
38
|
+
p = _one("今天买了一件衣服花了105块")
|
|
39
|
+
assert (p.amount, p.currency, p.direction) == (105.0, "CNY", "expense")
|
|
40
|
+
assert p.when.date() == NOW.date() and not p.planned
|
|
41
|
+
assert p.category == "clothing"
|
|
42
|
+
assert p.entity == "衣服"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_明天买大米45():
|
|
46
|
+
p = _one("明天买一袋大米花了45")
|
|
47
|
+
assert p.amount == 45.0 and p.planned
|
|
48
|
+
assert p.when.date() == datetime(2026, 11, 8).date()
|
|
49
|
+
assert p.category == "food"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_后天洗车100():
|
|
53
|
+
p = _one("后天花了100洗车")
|
|
54
|
+
assert p.amount == 100.0 and p.planned
|
|
55
|
+
assert p.category == "transport"
|
|
56
|
+
assert p.when.date() == datetime(2026, 11, 9).date()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── 2. 币种 ───────────────────────────────────────────────
|
|
60
|
+
def test_货币单位识别():
|
|
61
|
+
assert _one("花了105块").currency == "CNY"
|
|
62
|
+
assert _one("花了105块钱").currency == "CNY"
|
|
63
|
+
assert _one("花了105元").currency == "CNY"
|
|
64
|
+
assert _one("花了105人民币").currency == "CNY"
|
|
65
|
+
assert _one("花了105rmb").currency == "CNY"
|
|
66
|
+
assert _one("花了105美元").currency == "USD"
|
|
67
|
+
assert _one("花了105美金").currency == "USD"
|
|
68
|
+
assert _one("花了$105").currency == "USD"
|
|
69
|
+
assert _one("花了105港币").currency == "HKD"
|
|
70
|
+
assert _one("花了105欧元").currency == "EUR"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_符号与前置币种():
|
|
74
|
+
assert _one("¥105").currency == "CNY"
|
|
75
|
+
assert _one("美元105").currency == "USD"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_量级后缀():
|
|
79
|
+
assert _one("买了个包1.5k").amount == 1500.0
|
|
80
|
+
assert _one("买了个手机2万").amount == 20000.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_中文数字():
|
|
84
|
+
assert cn_to_number("一百零五") == 105
|
|
85
|
+
assert cn_to_number("两千三") == 2300
|
|
86
|
+
assert cn_to_number("三千五") == 3500
|
|
87
|
+
assert cn_to_number("十五") == 15
|
|
88
|
+
assert cn_to_number("十万") == 100000
|
|
89
|
+
assert _one("买了一百零五块的衣服").amount == 105.0
|
|
90
|
+
assert _one("两千三买了个手机").amount == 2300.0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── 3. 时间关键词 ──────────────────────────────────────────
|
|
94
|
+
def test_过去时间词():
|
|
95
|
+
assert _one("昨天买了个杯子30").when.date() == datetime(2026, 11, 6).date()
|
|
96
|
+
assert _one("前天买了个杯子30").when.date() == datetime(2026, 11, 5).date()
|
|
97
|
+
assert _one("大前天买了个杯子30").when.date() == datetime(2026, 11, 4).date()
|
|
98
|
+
assert _one("3天前买了个杯子30").when.date() == datetime(2026, 11, 4).date()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_未来时间词():
|
|
102
|
+
assert _one("大后天买了个杯子30").when.date() == datetime(2026, 11, 10).date()
|
|
103
|
+
assert _one("3天后买了个杯子30").when.date() == datetime(2026, 11, 10).date()
|
|
104
|
+
assert _one("下个月5号交房租3000").when.date() == datetime(2026, 12, 5).date()
|
|
105
|
+
assert _one("下个月5号交房租3000").planned
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_上个月与上月():
|
|
109
|
+
p = _one("上个月花了2000")
|
|
110
|
+
assert p.when.strftime("%Y-%m") == "2026-10" and not p.planned
|
|
111
|
+
assert _one("上月5号买衣服300").when.date() == datetime(2026, 10, 5).date()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_几月与几号():
|
|
115
|
+
assert _one("9月买衣服105").when.strftime("%Y-%m") == "2026-09"
|
|
116
|
+
# 本月已过的 5 号 → 本月;本月还没到的 28 号 → 上个月
|
|
117
|
+
assert _one("5号买衣服105", now=datetime(2026, 11, 7)).when.strftime("%Y-%m") == "2026-11"
|
|
118
|
+
assert _one("28号买衣服105", now=datetime(2026, 11, 7)).when.strftime("%Y-%m") == "2026-10"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_时间词不会被当成金额():
|
|
122
|
+
p = _one("上个月5号买了双鞋300")
|
|
123
|
+
assert p.amount == 300.0
|
|
124
|
+
assert p.when.date() == datetime(2026, 10, 5).date()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── 4. 多笔与分类 ──────────────────────────────────────────
|
|
128
|
+
def test_一句话多笔拆分():
|
|
129
|
+
ps = parse("今天买衣服105,还买了大米45", now=NOW)
|
|
130
|
+
assert [p.amount for p in ps] == [105.0, 45.0]
|
|
131
|
+
assert {p.category for p in ps} == {"clothing", "food"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_时间可继承():
|
|
135
|
+
ps = parse("今天买衣服105,洗车100", now=NOW)
|
|
136
|
+
assert len(ps) == 2
|
|
137
|
+
assert all(p.when.date() == NOW.date() for p in ps)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_单笔带逗号不拆坏():
|
|
141
|
+
p = _one("今天,我买了一件衣服,花了105块")
|
|
142
|
+
assert p.amount == 105.0 and p.category == "clothing"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_收入方向():
|
|
146
|
+
p = _one("工资发了12000")
|
|
147
|
+
assert p.direction == "income" and p.amount == 12000.0
|
|
148
|
+
assert p.category == "income"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── 5. 不该记账的句子 ──────────────────────────────────────
|
|
152
|
+
def test_无金额不解析():
|
|
153
|
+
assert parse("今天几号", now=NOW) == []
|
|
154
|
+
assert parse("本月预算怎么分配", now=NOW) == []
|
|
155
|
+
assert parse("今天买了几件衣服", now=NOW) == []
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_花费信号闸门():
|
|
159
|
+
assert has_expense_signal("今天买了一件衣服花了105块")
|
|
160
|
+
assert has_expense_signal("洗车100")
|
|
161
|
+
assert not has_expense_signal("今天几号")
|
|
162
|
+
assert not has_expense_signal("3个月计划")
|
|
163
|
+
assert has_payment_verb("我买了105块的衣服")
|
|
164
|
+
assert not has_payment_verb("衣服预算800")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_预算句不自动记账():
|
|
168
|
+
assert not _should_autorecord("衣服预算800")
|
|
169
|
+
assert not _should_autorecord("给我定个800的餐饮预算")
|
|
170
|
+
assert _should_autorecord("今天买了一件衣服花了105块")
|
|
171
|
+
assert _should_autorecord("洗车100")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_主管把花费报备路由给财务():
|
|
175
|
+
assert SupervisorAgent._is_expense_report("今天买了一件衣服花了105块")
|
|
176
|
+
assert SupervisorAgent._is_expense_report("后天花了100洗车")
|
|
177
|
+
assert not SupervisorAgent._is_expense_report("帮我总结这篇文档")
|
|
178
|
+
assert not SupervisorAgent._is_expense_report("今天几号")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── 6. 记账与汇总 ──────────────────────────────────────────
|
|
182
|
+
def test_记账回执与账本(tmp_path):
|
|
183
|
+
rt = _rt(tmp_path)
|
|
184
|
+
rec = rt.record_text("今天买了一件衣服花了105块")
|
|
185
|
+
assert len(rec["recorded"]) == 1
|
|
186
|
+
assert "已记账 1 笔" in rec["ack"] and "105.00" in rec["ack"]
|
|
187
|
+
|
|
188
|
+
led = rt.ledger("2026-11")
|
|
189
|
+
assert led["count"] == 1 and led["expense"] == 105.0
|
|
190
|
+
assert led["by_category"][0]["category"] == "clothing"
|
|
191
|
+
assert led["planned"]["count"] == 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_计划支出不进本月已花(tmp_path):
|
|
195
|
+
rt = _rt(tmp_path)
|
|
196
|
+
rt.record_text("今天买了一件衣服花了105块")
|
|
197
|
+
rt.record_text("明天买一袋大米花了45")
|
|
198
|
+
|
|
199
|
+
led = rt.ledger()
|
|
200
|
+
assert led["expense"] == 105.0 # 45 还没发生
|
|
201
|
+
assert led["planned"]["count"] == 1
|
|
202
|
+
assert led["planned"]["expense"] == 45.0
|
|
203
|
+
assert rt.analyze()["transaction_count"] == 1 # 分析也不能把未来算进来
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_多币种折成本位币(tmp_path):
|
|
207
|
+
rt = _rt(tmp_path)
|
|
208
|
+
rt.record_text("花了100美元") # 内置汇率 7.2
|
|
209
|
+
led = rt.ledger()
|
|
210
|
+
assert led["expense"] == 720.0
|
|
211
|
+
usd = [c for c in led["by_currency"] if c["currency"] == "USD"][0]
|
|
212
|
+
assert usd["original_total"] == -100.0 and usd["base_total"] == -720.0
|
|
213
|
+
assert rt.transactions[0].amount_original == -100.0
|
|
214
|
+
assert rt.transactions[0].fx_rate == 7.2
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_汇率可改(tmp_path):
|
|
218
|
+
rt = _rt(tmp_path)
|
|
219
|
+
rt.fx.set_rate("USD", 7.0)
|
|
220
|
+
rt.record_text("花了100美金")
|
|
221
|
+
assert rt.ledger()["expense"] == 700.0
|
|
222
|
+
assert _rt(tmp_path).fx.rate("USD") == 7.0 # 汇率也落盘
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_按天与分类汇总(tmp_path):
|
|
226
|
+
rt = _rt(tmp_path)
|
|
227
|
+
rt.record_text("今天买了一件衣服花了105块")
|
|
228
|
+
rt.record_text("今天在星巴克花了32块钱")
|
|
229
|
+
led = rt.ledger()
|
|
230
|
+
assert led["count"] == 2
|
|
231
|
+
day = led["by_day"][0]
|
|
232
|
+
assert day["count"] == 2 and day["expense"] == -137.0
|
|
233
|
+
cats = {c["category"]: c["total"] for c in led["by_category"]}
|
|
234
|
+
assert cats["clothing"] == 105.0 and cats["food"] == 32.0
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_记账后账本可持久化(tmp_path):
|
|
238
|
+
rt = _rt(tmp_path)
|
|
239
|
+
rt.record_text("今天买了一件衣服花了105块")
|
|
240
|
+
rt.record_text("上个月5号买了双鞋300")
|
|
241
|
+
again = _rt(tmp_path)
|
|
242
|
+
assert len(again.transactions) == 2
|
|
243
|
+
assert again.ledger("2026-10")["expense"] == 300.0
|
|
244
|
+
assert again.ledger("2026-11")["expense"] == 105.0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_未配置汇率按1比1并提示(tmp_path):
|
|
248
|
+
rt = _rt(tmp_path)
|
|
249
|
+
rt.fx.rates.pop("JPY", None) # 模拟"币种认识但没配汇率"
|
|
250
|
+
rec = rt.record_text("花了100日元")
|
|
251
|
+
assert any("汇率" in w for w in rec["recorded"][0]["warnings"])
|
|
252
|
+
assert rt.ledger()["expense"] == 100.0
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""财务内核持久化 — 重启不丢账、坏文件不阻断启动、批量导入不逐笔写盘。
|
|
2
|
+
|
|
3
|
+
背景:交易/预算/余额原先只在内存,后端一重启就清零,财务分析永远是"0 笔交易"。
|
|
4
|
+
这里固定住三件事:
|
|
5
|
+
1. 写进去的状态能被**新实例**读回来(真正的重启等价);
|
|
6
|
+
2. 文件损坏 / 单条记录坏 / 落盘失败时,一律降级并把错误暴露出来,不抛、不阻断启动;
|
|
7
|
+
3. 批量导入只做一次磁盘 IO。
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
from foxclaw.finance import FinanceRuntime, Transaction
|
|
16
|
+
|
|
17
|
+
CSV = str(Path(__file__).parent.parent / "examples" / "sample_transactions.csv")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _rt(tmp_path, data_name="finance_data.json"):
|
|
21
|
+
"""临时目录里的 runtime:policy 与数据都写在 tmp_path,不污染工作区。"""
|
|
22
|
+
return FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
|
|
23
|
+
data_path=str(tmp_path / data_name))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_落盘点默认与policy同目录(tmp_path):
|
|
27
|
+
rt = FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"))
|
|
28
|
+
assert rt._data_path == tmp_path / "finance_data.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_交易预算余额重启后可恢复(tmp_path):
|
|
32
|
+
rt = _rt(tmp_path)
|
|
33
|
+
rt.add_transaction(Transaction(amount=-88.5, category="food",
|
|
34
|
+
entity="星巴克", account="debit",
|
|
35
|
+
tags=["recurring"], note="早餐"))
|
|
36
|
+
rt.add_transaction(Transaction(amount=12000, category="salary",
|
|
37
|
+
entity="公司", account="debit"))
|
|
38
|
+
rt.set_budget("food", 400)
|
|
39
|
+
rt.set_balance("debit", 11800)
|
|
40
|
+
|
|
41
|
+
assert rt.save() is True
|
|
42
|
+
assert (tmp_path / "finance_data.json").exists()
|
|
43
|
+
|
|
44
|
+
# 新实例 = 进程重启后的状态
|
|
45
|
+
rt2 = _rt(tmp_path)
|
|
46
|
+
assert len(rt2.transactions) == 2
|
|
47
|
+
assert rt2.transactions[0].amount == -88.5
|
|
48
|
+
assert rt2.transactions[0].entity == "星巴克"
|
|
49
|
+
assert rt2.transactions[0].note == "早餐"
|
|
50
|
+
assert rt2.transactions[0].tags == ["recurring"]
|
|
51
|
+
assert rt2.transactions[1].amount == 12000
|
|
52
|
+
assert len(rt2.budgets) == 1 and rt2.budgets[0].monthly_limit == 400
|
|
53
|
+
assert rt2.account_balances == {"debit": 11800}
|
|
54
|
+
# 实体统计随交易一起恢复(重启后 trace_category / status 不该突然归零)
|
|
55
|
+
assert rt2.entities["星巴克"].txn_count == 1
|
|
56
|
+
assert rt2.entities["星巴克"].entity_type == "subscription"
|
|
57
|
+
assert rt2.entities["公司"].total_amount == 12000
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_写入即落盘下次实例自动加载(tmp_path):
|
|
61
|
+
rt = _rt(tmp_path)
|
|
62
|
+
rt.add_transaction(Transaction(amount=-20, category="transport",
|
|
63
|
+
entity="地铁", account="debit"))
|
|
64
|
+
rt.set_balance("debit", 100)
|
|
65
|
+
|
|
66
|
+
loaded = _rt(tmp_path)
|
|
67
|
+
assert loaded.load() # 已显式 load 过,再次 load 幂等
|
|
68
|
+
assert len(loaded.transactions) == 1
|
|
69
|
+
assert loaded.status()["persist"]["load_error"] == ""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_文件格式合法且带版本(tmp_path):
|
|
73
|
+
rt = _rt(tmp_path)
|
|
74
|
+
rt.add_transaction(Transaction(amount=-1, category="misc",
|
|
75
|
+
entity="x", account="debit"))
|
|
76
|
+
data = json.loads((tmp_path / "finance_data.json").read_text(encoding="utf-8"))
|
|
77
|
+
assert data["version"] == 1
|
|
78
|
+
assert data["saved_at"]
|
|
79
|
+
assert len(data["transactions"]) == 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_损坏文件不阻断启动且可自愈(tmp_path):
|
|
83
|
+
p = tmp_path / "finance_data.json"
|
|
84
|
+
p.write_text("{ 这不是 json ", encoding="utf-8")
|
|
85
|
+
|
|
86
|
+
rt = _rt(tmp_path) # 构造本身不得抛
|
|
87
|
+
assert rt.transactions == [] and rt.budgets == []
|
|
88
|
+
assert rt.load_error # 错误必须可见,不能静默吞掉
|
|
89
|
+
|
|
90
|
+
rt.add_transaction(Transaction(amount=-5, category="misc",
|
|
91
|
+
entity="y", account="debit"))
|
|
92
|
+
assert rt.save() is True
|
|
93
|
+
assert rt.load_error == "" # 覆盖写成功后重新可读
|
|
94
|
+
assert len(_rt(tmp_path).transactions) == 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_顶层非对象也降级(tmp_path):
|
|
98
|
+
(tmp_path / "finance_data.json").write_text("[1, 2, 3]", encoding="utf-8")
|
|
99
|
+
rt = _rt(tmp_path)
|
|
100
|
+
assert rt.transactions == []
|
|
101
|
+
assert "JSON 对象" in rt.load_error
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_单条坏记录只跳过自己不废掉整份(tmp_path):
|
|
105
|
+
(tmp_path / "finance_data.json").write_text(json.dumps({
|
|
106
|
+
"version": 1,
|
|
107
|
+
"transactions": [
|
|
108
|
+
{"amount": -10, "category": "food", "entity": "a", "account": "debit"},
|
|
109
|
+
{"category": "food"}, # 缺 amount/entity/account → 跳过
|
|
110
|
+
"not-a-dict", # 非对象 → 跳过
|
|
111
|
+
{"amount": -20, "category": "food", "entity": "b", "account": "debit"},
|
|
112
|
+
],
|
|
113
|
+
"budgets": [{"category": "food", "monthly_limit": 300}],
|
|
114
|
+
"account_balances": {"debit": "not-a-number", "cash": 50},
|
|
115
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
rt = _rt(tmp_path)
|
|
118
|
+
assert [t.entity for t in rt.transactions] == ["a", "b"]
|
|
119
|
+
assert len(rt.budgets) == 1
|
|
120
|
+
assert rt.account_balances == {"cash": 50.0} # 坏值单独跳过
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_实体段缺失时按交易重建(tmp_path):
|
|
124
|
+
(tmp_path / "finance_data.json").write_text(json.dumps({
|
|
125
|
+
"transactions": [
|
|
126
|
+
{"amount": -30, "category": "subscription", "entity": "Netflix",
|
|
127
|
+
"account": "card", "tags": ["recurring"]},
|
|
128
|
+
{"amount": -10, "category": "food", "entity": "Netflix", "account": "card"},
|
|
129
|
+
],
|
|
130
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
131
|
+
|
|
132
|
+
rt = _rt(tmp_path)
|
|
133
|
+
assert rt.entities["Netflix"].txn_count == 2
|
|
134
|
+
assert rt.entities["Netflix"].total_amount == -40
|
|
135
|
+
assert rt.entities["Netflix"].entity_type == "subscription"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_批量导入只写一次盘(tmp_path, monkeypatch):
|
|
139
|
+
rt = _rt(tmp_path)
|
|
140
|
+
calls = []
|
|
141
|
+
orig = rt.save
|
|
142
|
+
|
|
143
|
+
def _counting_save():
|
|
144
|
+
calls.append(1)
|
|
145
|
+
return orig()
|
|
146
|
+
|
|
147
|
+
monkeypatch.setattr(rt, "save", _counting_save)
|
|
148
|
+
n = rt.import_csv(CSV)
|
|
149
|
+
assert n > 0
|
|
150
|
+
assert len(calls) == 1, f"导入 {n} 笔应只落盘 1 次,实为 {len(calls)} 次"
|
|
151
|
+
assert len(_rt(tmp_path).transactions) == n
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_落盘失败只回报不抛异常(tmp_path):
|
|
155
|
+
blocker = tmp_path / "blocker"
|
|
156
|
+
blocker.write_text("我是个文件,不是目录", encoding="utf-8")
|
|
157
|
+
# 父目录被一个文件占住 → mkdir/写入必失败
|
|
158
|
+
rt = FinanceRuntime(policy_path=str(tmp_path / "finance_policy.yaml"),
|
|
159
|
+
data_path=str(blocker / "finance_data.json"))
|
|
160
|
+
assert rt.save() is False
|
|
161
|
+
assert rt.save_error # 失败原因必须留痕
|
|
162
|
+
# 内存里的数据仍在,用户这次会话不至于丢东西
|
|
163
|
+
rt.add_transaction(Transaction(amount=-1, category="misc",
|
|
164
|
+
entity="z", account="debit"))
|
|
165
|
+
assert len(rt.transactions) == 1
|
|
166
|
+
assert rt.status()["persist"]["save_error"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_分析使用持久化后的数据(tmp_path):
|
|
170
|
+
rt = _rt(tmp_path)
|
|
171
|
+
rt.set_budget("food", 100)
|
|
172
|
+
rt.add_transaction(Transaction(amount=-150, category="food",
|
|
173
|
+
entity="餐厅", account="debit"))
|
|
174
|
+
|
|
175
|
+
report = _rt(tmp_path).analyze(period="2026-11")
|
|
176
|
+
assert report["transaction_count"] == 1
|
|
177
|
+
food = [b for b in report["budget_status"] if b["category"] == "food"]
|
|
178
|
+
assert food and food[0]["is_over"] is True
|