foxclaw 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -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 +115 -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 +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -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 +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -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 +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -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 +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -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 +144 -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 +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -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 +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -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 +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -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 +1436 -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-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -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 +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -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 +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -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 +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -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 +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -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,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一事件脊柱(EventSpine, P0 第二项)测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. EventSpine:发布/订阅(前缀匹配) + 投影(收到每条) + 有界历史
|
|
6
|
+
2. SystemBus 门面:退化为脊柱门面后,既有消费者零改;系统事件汇入脊柱
|
|
7
|
+
3. Harness → 脊柱转发:harness 事件汇入脊柱;高频内部信号(goal_registered)不上脊柱
|
|
8
|
+
4. Foxclaw 统一投影:harness 事件与系统事件都被**同一个投影**观测(核心统一验证)
|
|
9
|
+
|
|
10
|
+
为避免加载 7s 重模型,harness embedding 用不存在的模型名 → 降级 hash(不影响事件验证)。
|
|
11
|
+
"""
|
|
12
|
+
import os, sys, asyncio, tempfile, shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
15
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
16
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
18
|
+
|
|
19
|
+
from foxclaw.kernel import EventSpine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_spine_pubsub_prefix_projection():
|
|
23
|
+
print("\n【1】EventSpine:前缀订阅 + 投影 + 历史")
|
|
24
|
+
spine = EventSpine()
|
|
25
|
+
got = []
|
|
26
|
+
spine.subscribe("harness", lambda e: got.append(e.topic))
|
|
27
|
+
allseen = []
|
|
28
|
+
spine.project(lambda e: allseen.append(e.topic))
|
|
29
|
+
spine.publish("harness.drift.blocked", {"x": 1})
|
|
30
|
+
spine.publish("memory.ingested", {"y": 2})
|
|
31
|
+
assert got == ["harness.drift.blocked"], "前缀 'harness' 应只命中 harness.*"
|
|
32
|
+
assert allseen == ["harness.drift.blocked", "memory.ingested"], "投影应收到每一条"
|
|
33
|
+
assert len(spine.history()) == 2 and len(spine.history("harness")) == 1
|
|
34
|
+
print(" ✓ 前缀匹配正确;投影全量;历史可按前缀过滤")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_systembus_facade():
|
|
38
|
+
print("\n【2】SystemBus 门面:消费者零改 + 汇入脊柱")
|
|
39
|
+
from foxclaw.core.system_bus import SystemBus
|
|
40
|
+
spine = EventSpine()
|
|
41
|
+
seen = []
|
|
42
|
+
spine.project(lambda e: seen.append((e.topic, e.source)))
|
|
43
|
+
bus = SystemBus(spine=spine)
|
|
44
|
+
got = []
|
|
45
|
+
bus.subscribe("execution.completed", lambda m: got.append(m.payload))
|
|
46
|
+
bus.publish("execution.completed", {"task": "t"})
|
|
47
|
+
assert got == [{"task": "t"}], "旧消费者 handler(msg) 仍收到 .payload"
|
|
48
|
+
assert ("execution.completed", "system_bus") in seen, "系统事件应汇入同一脊柱"
|
|
49
|
+
assert bus.history("execution.completed"), "history 门面可用"
|
|
50
|
+
print(" ✓ handler(msg) 兼容;系统事件经脊柱;history 正常")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_harness_forwards_to_spine():
|
|
54
|
+
print("\n【3】Harness → 脊柱转发(goal_registered 不上脊柱)")
|
|
55
|
+
from foxclaw.harness.harness import Harness
|
|
56
|
+
from foxclaw.core.thought_chain import ThoughtChain
|
|
57
|
+
d = tempfile.mkdtemp(prefix="spine_h_")
|
|
58
|
+
try:
|
|
59
|
+
spine = EventSpine()
|
|
60
|
+
seen = []
|
|
61
|
+
spine.project(lambda e: seen.append(e.topic))
|
|
62
|
+
h = Harness("tester", ThoughtChain(str(Path(d) / "tc.db")),
|
|
63
|
+
policy_path=str(Path(d) / "p.yaml"), spine=spine)
|
|
64
|
+
local = []
|
|
65
|
+
h.bus.on("harness_warn", lambda data: local.append(data))
|
|
66
|
+
h.bus.emit("harness_warn", {"a": 1})
|
|
67
|
+
h.bus.emit("goal_registered", {"g": 1})
|
|
68
|
+
assert local == [{"a": 1}], "本地订阅(向后兼容)仍收到"
|
|
69
|
+
assert "harness_warn" in seen, "harness 事件应汇入脊柱"
|
|
70
|
+
assert "goal_registered" not in seen, "高频内部信号不应上脊柱(降噪)"
|
|
71
|
+
finally:
|
|
72
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
73
|
+
print(" ✓ 本地订阅兼容 + harness 汇入脊柱 + goal_registered 被过滤")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_hkc_events_bridge_to_spine():
|
|
77
|
+
print("\n【3b】HKC EventBus → 脊柱桥接(知识事件不再是孤岛;memory.* 刻意不桥)")
|
|
78
|
+
from foxclaw.knowledge import bridge_hkc_events
|
|
79
|
+
|
|
80
|
+
class _FakeHKCBus:
|
|
81
|
+
def __init__(self): self._h = {}
|
|
82
|
+
def subscribe(self, ev, h): self._h.setdefault(ev, []).append(h)
|
|
83
|
+
def publish(self, d):
|
|
84
|
+
for h in self._h.get(d.get("event"), []): h(d)
|
|
85
|
+
|
|
86
|
+
spine = EventSpine()
|
|
87
|
+
seen = []
|
|
88
|
+
spine.project(lambda e: seen.append((e.topic, e.source, e.payload)))
|
|
89
|
+
bus = _FakeHKCBus()
|
|
90
|
+
assert bridge_hkc_events(bus, spine) is True
|
|
91
|
+
bus.publish({"event": "knowledge.merged", "payload": {"ku_id": "K1"}, "event_id": "EVT_1"})
|
|
92
|
+
bus.publish({"event": "conflict.detected", "payload": {"card": "C1"}})
|
|
93
|
+
bus.publish({"event": "ability.created", "payload": {"key": "A1"}})
|
|
94
|
+
assert ("knowledge.merged", "hkc", {"ku_id": "K1"}) in seen, seen
|
|
95
|
+
assert any(t == "conflict.detected" and s == "hkc" for t, s, _ in seen)
|
|
96
|
+
assert any(t == "ability.created" for t, _, _ in seen)
|
|
97
|
+
bus.publish({"event": "memory.created", "payload": {}})
|
|
98
|
+
assert not any(t == "memory.created" for t, _, _ in seen), "memory.* 不应被桥接"
|
|
99
|
+
assert bridge_hkc_events(None, spine) is False
|
|
100
|
+
print(" ✓ knowledge.merged/conflict/ability 桥到脊柱(source=hkc);memory.* 不桥;None 优雅降级")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_unified_projection_via_foxclawos():
|
|
104
|
+
print("\n【4】Foxclaw:harness 事件与系统事件被同一投影统一观测")
|
|
105
|
+
d = tempfile.mkdtemp(prefix="spine_os_")
|
|
106
|
+
try:
|
|
107
|
+
from foxclaw import Foxclaw
|
|
108
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
109
|
+
seen = []
|
|
110
|
+
os_.event_spine.project(lambda e: seen.append(e.topic))
|
|
111
|
+
os_.finance.harness.bus.emit("harness_warn", {"agent": "finance"})
|
|
112
|
+
os_.system_bus.publish("execution.completed", {"task": "demo"})
|
|
113
|
+
assert "harness_warn" in seen, "harness 事件应到统一投影"
|
|
114
|
+
assert "execution.completed" in seen, "系统事件应到统一投影(修复:此前到不了)"
|
|
115
|
+
assert os_.system_bus.spine is os_.event_spine
|
|
116
|
+
asyncio.run(os_.aclose())
|
|
117
|
+
finally:
|
|
118
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
119
|
+
print(" ✓ harness + 系统事件汇入同一脊柱、被同一投影观测(4→1 统一成立)")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_ws_projection_safe_and_bounded():
|
|
123
|
+
print("\n【5】WS 投影净化:脏/大 payload 不丢连接、不泛洪(深审修复)")
|
|
124
|
+
import json as _json
|
|
125
|
+
import foxclaw.api as api
|
|
126
|
+
class Weird:
|
|
127
|
+
def __repr__(self): return "<Weird>"
|
|
128
|
+
safe = api._ws_safe_payload({"obj": Weird(), "n": 1})
|
|
129
|
+
_json.dumps(safe)
|
|
130
|
+
big = api._ws_safe_payload({"blob": "x" * 10000})
|
|
131
|
+
assert big.get("_truncated") is True, "超大 payload 应被截断"
|
|
132
|
+
assert api._ws_safe_payload({"a": 1, "b": "ok"}) == {"a": 1, "b": "ok"}, "正常 payload 透传"
|
|
133
|
+
print(" ✓ 非 JSON 对象被 str 化;超大被截断;正常透传")
|
|
134
|
+
|
|
135
|
+
d = tempfile.mkdtemp(prefix="spine_ws_")
|
|
136
|
+
try:
|
|
137
|
+
from foxclaw import Foxclaw
|
|
138
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
139
|
+
while not api._event_queue.empty():
|
|
140
|
+
api._event_queue.get_nowait()
|
|
141
|
+
api._register_spine_projection(os_)
|
|
142
|
+
os_.system_bus.publish("execution.completed",
|
|
143
|
+
{"agent_id": "x", "task": "t", "result": Weird()})
|
|
144
|
+
items = []
|
|
145
|
+
while not api._event_queue.empty():
|
|
146
|
+
items.append(api._event_queue.get_nowait())
|
|
147
|
+
assert items, "投影应把事件放进队列"
|
|
148
|
+
for it in items:
|
|
149
|
+
_json.dumps(it)
|
|
150
|
+
topics = {it["event"] for it in items}
|
|
151
|
+
assert "execution.completed" in topics, f"应含 execution.completed,实得 {topics}"
|
|
152
|
+
asyncio.run(os_.aclose())
|
|
153
|
+
finally:
|
|
154
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
155
|
+
print(" ✓ 脏 result 事件经投影后队列项 JSON 安全(WS 不会静默丢连接)")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
print("=" * 60)
|
|
160
|
+
print("统一事件脊柱 · EventSpine(P0 第二项)")
|
|
161
|
+
print("=" * 60)
|
|
162
|
+
test_spine_pubsub_prefix_projection()
|
|
163
|
+
test_systembus_facade()
|
|
164
|
+
test_harness_forwards_to_spine()
|
|
165
|
+
test_hkc_events_bridge_to_spine()
|
|
166
|
+
test_unified_projection_via_foxclawos()
|
|
167
|
+
test_ws_projection_safe_and_bounded()
|
|
168
|
+
print("\n" + "=" * 60)
|
|
169
|
+
print("✓ P0-2 验证完成:脊柱 + SystemBus门面 + Harness转发 + 统一投影")
|
|
170
|
+
print("=" * 60)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""证据融合语义化 —— 原始复盘点名的 `search/evidence.py "规则占位"`(此前一直漏修)。
|
|
2
|
+
|
|
3
|
+
修掉两个真问题:
|
|
4
|
+
① 否定词**子串**误命中:原 ("不是","并非","no longer","not") 按子串匹配 →
|
|
5
|
+
"another"/"notebook" 里的 "not" 被命中 → 无关证据被判**矛盾**;而 confidence()
|
|
6
|
+
按矛盾数扣分(0.1/条)→ 搜索置信被**幻影矛盾**无端拉低。
|
|
7
|
+
这与 hkc_kee/kee.py 早已修过的 not⊂nothing / no⊂know 是**同一 bug 家族**,证据层漏了同步。
|
|
8
|
+
② 相似度是**字符级**(SequenceMatcher)而非语义 → 语义等价的证据识别不出互相支持。
|
|
9
|
+
|
|
10
|
+
无 ST 模型时优雅降级回字符级(不崩),故本测试对语义部分做"可用才断言"。
|
|
11
|
+
"""
|
|
12
|
+
import os, sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "vendor" / "hkc"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_evidence_semantic():
|
|
19
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
20
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
21
|
+
os.environ["FOXCLAW_EVIDENCE_MODEL"] = "all-MiniLM-L6-v2"
|
|
22
|
+
|
|
23
|
+
from foxclaw.search.evidence import EvidenceFusion, _has_neg, _semantic_sim
|
|
24
|
+
from foxclaw.search.models import RawEvidence
|
|
25
|
+
|
|
26
|
+
print("=" * 60); print("证据融合语义化验证"); print("=" * 60)
|
|
27
|
+
f = EvidenceFusion()
|
|
28
|
+
|
|
29
|
+
assert _has_neg("this is another way") is False, "another 里的 not 不该命中"
|
|
30
|
+
assert _has_neg("the notebook is useful") is False, "notebook 里的 not 不该命中"
|
|
31
|
+
assert _has_neg("I know this") is False, "know 里的 no 不该命中"
|
|
32
|
+
assert _has_neg("cannot proceed") is True
|
|
33
|
+
assert _has_neg("this is not good") is True
|
|
34
|
+
assert _has_neg("这个方案并非最优") is True
|
|
35
|
+
print("[1] 否定词词边界(another/notebook/know 不误命中)OK")
|
|
36
|
+
|
|
37
|
+
assert f._contradicts("this is another way", "this is a way") is False, "幻影矛盾"
|
|
38
|
+
assert f._contradicts("the notebook is useful", "the book is useful") is False, "幻影矛盾"
|
|
39
|
+
print("[2] 幻影矛盾消失(another/notebook 不再被判矛盾)OK")
|
|
40
|
+
|
|
41
|
+
assert f._contradicts("cannot proceed", "can proceed") is True
|
|
42
|
+
assert f._contradicts("这个方案并非最优", "这个方案是最优") is True
|
|
43
|
+
print("[3] 真矛盾仍识别(cannot/并非)OK")
|
|
44
|
+
|
|
45
|
+
g = f.build([
|
|
46
|
+
RawEvidence(id="e1", fact="this is another way", source="s1", origin="internal", confidence=0.9),
|
|
47
|
+
RawEvidence(id="e2", fact="this is a way", source="s2", origin="internal", confidence=0.9),
|
|
48
|
+
])
|
|
49
|
+
assert len(g.contradictions()) == 0, f"不该有矛盾边: {g.contradictions()}"
|
|
50
|
+
assert f.confidence(g) >= 0.85, f"置信不该被幻影矛盾扣分: {f.confidence(g)}"
|
|
51
|
+
print(f"[4] 端到端:无幻影矛盾边,置信未被误扣({f.confidence(g):.2f})OK")
|
|
52
|
+
|
|
53
|
+
if _semantic_sim("a", "a") is None:
|
|
54
|
+
print("[skip] ST 模型未缓存 → 语义部分跳过(已回退字符级,不崩)")
|
|
55
|
+
print("\n证据融合语义化验证通过(降级路径)")
|
|
56
|
+
if __name__ != "__main__":
|
|
57
|
+
import pytest
|
|
58
|
+
pytest.skip("ST 模型未缓存 → 语义部分跳过(降级路径已由 [1]-[4] 覆盖)",
|
|
59
|
+
allow_module_level=True)
|
|
60
|
+
import sys as _sys
|
|
61
|
+
_sys.exit(0)
|
|
62
|
+
|
|
63
|
+
close = f._similarity("the plan is good", "the plan is nice")
|
|
64
|
+
far = f._similarity("the plan is good", "the stock market crashed")
|
|
65
|
+
assert close > 0.6 > far, f"近义应>0.6>无关: {close:.3f} vs {far:.3f}"
|
|
66
|
+
assert f._similarity("hello world", "hello world") >= 0.99
|
|
67
|
+
print(f"[5] 语义相似(近义 {close:.2f} > 0.6 > 无关 {far:.2f})OK")
|
|
68
|
+
|
|
69
|
+
g2 = f.build([
|
|
70
|
+
RawEvidence(id="a", fact="it will rain today", source="s1", origin="internal", confidence=0.8),
|
|
71
|
+
RawEvidence(id="b", fact="rain is expected today", source="s2", origin="external", confidence=0.8),
|
|
72
|
+
])
|
|
73
|
+
sup = [e for e in g2.edges if e.relation == "supports"]
|
|
74
|
+
assert sup, "语义等价的证据应互相支持(字符级识别不出)"
|
|
75
|
+
print("[6] 语义等价证据建立 supports 边 OK")
|
|
76
|
+
|
|
77
|
+
g3 = f.build([
|
|
78
|
+
RawEvidence(id="x", fact="hello world", source="s1", origin="internal", confidence=0.8),
|
|
79
|
+
RawEvidence(id="y", fact="hello world", source="s2", origin="internal", confidence=0.8),
|
|
80
|
+
])
|
|
81
|
+
assert len(g3.nodes) == 1, f"完全相同应去重: {list(g3.nodes)}"
|
|
82
|
+
print("[7] 阈值在余弦下仍成立 + 相同证据去重 OK")
|
|
83
|
+
|
|
84
|
+
print("\n证据融合语义化验证全部通过")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
test_evidence_semantic()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""/api/chat/stream 的命名 SSE 事件:`event: evidence`(执行凭据)与正文 delta 分离。
|
|
2
|
+
|
|
3
|
+
锁住的契约:
|
|
4
|
+
· 工具轮每步的核验结论 + 凭据以**命名事件**整块送出,不拼进正文(否则复制/落库被污染);
|
|
5
|
+
· 没有工具步的纯对话不产生 evidence 事件(不能凭空生成"凭据",前端也不闪空面板);
|
|
6
|
+
· 前端拿到的是一个数组:`[{tool,status,reason,evidence:[{kind,ref,label,...}]}]`。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
18
|
+
|
|
19
|
+
EVIDENCE = [
|
|
20
|
+
{"tool": "web_search", "kind": "ok", "status": "verified", "reason": "",
|
|
21
|
+
"evidence": [{"kind": "url", "ref": "https://example.com/a", "label": "example.com",
|
|
22
|
+
"source": "web_search", "at": 0}]},
|
|
23
|
+
{"tool": "send_mail", "kind": "ok", "status": "unverified",
|
|
24
|
+
"reason": "该工具未声明可观察的后置条件", "evidence": []},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse(body: str) -> tuple[list, str]:
|
|
29
|
+
"""拆 SSE:返回 (evidence 事件载荷列表, 正文 delta 拼接)。"""
|
|
30
|
+
ev_payloads, delta_text = [], ""
|
|
31
|
+
for b in [x for x in body.split("\n\n") if x.strip()]:
|
|
32
|
+
name, data = "message", None
|
|
33
|
+
for line in b.splitlines():
|
|
34
|
+
if line.startswith("event: "):
|
|
35
|
+
name = line[7:].strip()
|
|
36
|
+
elif line.startswith("data: "):
|
|
37
|
+
data = line[6:].strip()
|
|
38
|
+
if data is None:
|
|
39
|
+
continue
|
|
40
|
+
ev = json.loads(data)
|
|
41
|
+
if name == "evidence":
|
|
42
|
+
ev_payloads.append(ev)
|
|
43
|
+
elif "delta" in ev:
|
|
44
|
+
delta_text += ev["delta"]
|
|
45
|
+
return ev_payloads, delta_text
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_chat_stream_evidence_event_contract():
|
|
49
|
+
if "foxclaw.api" in sys.modules:
|
|
50
|
+
import pytest
|
|
51
|
+
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
52
|
+
os.environ.setdefault("FOXCLAW_DATA_DIR", tempfile.mkdtemp(prefix="foxclaw_ev_"))
|
|
53
|
+
|
|
54
|
+
from fastapi.testclient import TestClient
|
|
55
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
56
|
+
from foxclaw.api import app
|
|
57
|
+
|
|
58
|
+
seen: dict = {}
|
|
59
|
+
|
|
60
|
+
def _fake_stream(self, task, stop_event=None, images=None, history=None,
|
|
61
|
+
attachments=None, on_thinking=None, on_evidence=None):
|
|
62
|
+
seen["has_cb"] = on_evidence is not None
|
|
63
|
+
if task == "plain":
|
|
64
|
+
yield "纯对话,没有动用工具。"
|
|
65
|
+
return
|
|
66
|
+
yield "已完成。"
|
|
67
|
+
if on_evidence is not None:
|
|
68
|
+
on_evidence(EVIDENCE)
|
|
69
|
+
|
|
70
|
+
original = CognitiveAgent.stream_reply
|
|
71
|
+
CognitiveAgent.stream_reply = _fake_stream
|
|
72
|
+
try:
|
|
73
|
+
with TestClient(app) as c:
|
|
74
|
+
with_tools = c.post("/api/chat/stream", json={"task": "use-tools", "agent_id": "supervisor"})
|
|
75
|
+
plain = c.post("/api/chat/stream", json={"task": "plain", "agent_id": "supervisor"})
|
|
76
|
+
assert with_tools.status_code == 200 and plain.status_code == 200
|
|
77
|
+
body, plain_body = with_tools.text, plain.text
|
|
78
|
+
finally:
|
|
79
|
+
CognitiveAgent.stream_reply = original
|
|
80
|
+
|
|
81
|
+
assert seen.get("has_cb") is True, "端点必须把 on_evidence 传给 Agent"
|
|
82
|
+
|
|
83
|
+
ev_payloads, delta_text = _parse(body)
|
|
84
|
+
assert "event: evidence" in body, "证据必须以命名事件送出"
|
|
85
|
+
assert len(ev_payloads) == 1, "一轮只上报一次"
|
|
86
|
+
items = ev_payloads[0]["evidence"]
|
|
87
|
+
assert [i["tool"] for i in items] == ["web_search", "send_mail"]
|
|
88
|
+
assert [i["status"] for i in items] == ["verified", "unverified"]
|
|
89
|
+
assert items[0]["evidence"][0]["ref"] == "https://example.com/a"
|
|
90
|
+
assert "example.com" not in delta_text
|
|
91
|
+
assert delta_text.strip() == "已完成。"
|
|
92
|
+
|
|
93
|
+
assert "event: evidence" not in plain_body
|
|
94
|
+
assert "纯对话" in plain_body
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件摄入打通交互层:docx/xlsx 加载器 + KDE 后缀分派 + 上传路由格式门。
|
|
3
|
+
|
|
4
|
+
离线可跑:加载器是确定性纯解析(无 LLM);KDE 分派用假抽取器验证
|
|
5
|
+
「新后缀 → loader → chunker → packager → KU」链路,不调真实模型。
|
|
6
|
+
真实 LLM 抽取质量不在离线回归内(与 test_hkc 一致哲学)。
|
|
7
|
+
"""
|
|
8
|
+
import os, sys, tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "vendor" / "hkc"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_file_ingest():
|
|
15
|
+
for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY"):
|
|
16
|
+
os.environ.pop(k, None)
|
|
17
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
18
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
19
|
+
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
20
|
+
|
|
21
|
+
from docx import Document
|
|
22
|
+
from openpyxl import Workbook
|
|
23
|
+
from hkc_kde.loaders.office_loader import DocxLoader, XlsxLoader
|
|
24
|
+
from hkc_kde.chunker import Chunker
|
|
25
|
+
from hkc_kde.models import ExtractionResult, RawExtraction
|
|
26
|
+
|
|
27
|
+
print("=" * 60); print("文件摄入(docx/xlsx)打通验证"); print("=" * 60)
|
|
28
|
+
|
|
29
|
+
TMP = Path(tempfile.mkdtemp(prefix="foxclaw_fileingest_"))
|
|
30
|
+
|
|
31
|
+
def _make_docx() -> Path:
|
|
32
|
+
p = TMP / "sample.docx"
|
|
33
|
+
doc = Document()
|
|
34
|
+
doc.add_heading("第一章 引言", level=1)
|
|
35
|
+
doc.add_paragraph("Foxclaw 是一个认知代理操作系统,由五个 Agent 组成。")
|
|
36
|
+
t = doc.add_table(rows=2, cols=2)
|
|
37
|
+
t.rows[0].cells[0].text = "字段"; t.rows[0].cells[1].text = "取值"
|
|
38
|
+
t.rows[1].cells[0].text = "版本"; t.rows[1].cells[1].text = "v1.6"
|
|
39
|
+
doc.add_heading("第二章 架构", level=1)
|
|
40
|
+
doc.add_paragraph("知识核心负责长期记忆的沉淀与演化。")
|
|
41
|
+
doc.save(str(p))
|
|
42
|
+
return p
|
|
43
|
+
|
|
44
|
+
def _make_xlsx() -> Path:
|
|
45
|
+
p = TMP / "sample.xlsx"
|
|
46
|
+
wb = Workbook()
|
|
47
|
+
ws = wb.active; ws.title = "财务"
|
|
48
|
+
ws.append(["科目", "金额"]); ws.append(["收入", 100]); ws.append(["支出", 40])
|
|
49
|
+
ws2 = wb.create_sheet("备注")
|
|
50
|
+
ws2.append(["说明", "季度汇总"])
|
|
51
|
+
wb.save(str(p)); wb.close()
|
|
52
|
+
return p
|
|
53
|
+
|
|
54
|
+
pd = DocxLoader().load(str(_make_docx()))
|
|
55
|
+
assert pd.doc_type == "DOCX", pd.doc_type
|
|
56
|
+
titles = [c.title for c in pd.chapters]
|
|
57
|
+
assert "第一章 引言" in titles and "第二章 架构" in titles, titles
|
|
58
|
+
alltext = "\n".join(s.content for c in pd.chapters for s in c.sections)
|
|
59
|
+
assert "五个 Agent" in alltext, "正文段落丢失"
|
|
60
|
+
assert "版本 | v1.6" in alltext, "表格行未转文本"
|
|
61
|
+
ch1 = [c for c in pd.chapters if c.title == "第一章 引言"][0]
|
|
62
|
+
ch2 = [c for c in pd.chapters if c.title == "第二章 架构"][0]
|
|
63
|
+
assert any("版本 | v1.6" in s.content for s in ch1.sections), "表格未归到出现位置的第一章"
|
|
64
|
+
assert not any("版本" in s.content for s in ch2.sections), "表格错误归到末章第二章"
|
|
65
|
+
print(f"[1] DocxLoader({len(pd.chapters)} 章,标题切分 + 表格按序归章)OK")
|
|
66
|
+
|
|
67
|
+
px = XlsxLoader().load(str(_make_xlsx()))
|
|
68
|
+
assert px.doc_type == "XLSX", px.doc_type
|
|
69
|
+
sheet_titles = [c.title for c in px.chapters]
|
|
70
|
+
assert "财务" in sheet_titles and "备注" in sheet_titles, sheet_titles
|
|
71
|
+
xtext = "\n".join(s.content for c in px.chapters for s in c.sections)
|
|
72
|
+
assert "收入 | 100" in xtext and "季度汇总" in xtext, xtext
|
|
73
|
+
print(f"[2] XlsxLoader({len(px.chapters)} 表→章,行合并)OK")
|
|
74
|
+
|
|
75
|
+
chunks = Chunker().chunk(pd)
|
|
76
|
+
assert chunks and any("Agent" in ch.content for ch in chunks), "docx 未切出有效 chunk"
|
|
77
|
+
print(f"[3] Chunker 消费 docx({len(chunks)} chunk)OK")
|
|
78
|
+
|
|
79
|
+
def _build_kde():
|
|
80
|
+
from hkc_core.utils.id_gen import IDGenerator
|
|
81
|
+
from hkc_core.graph.sqlite_store import SQLiteGraphStore
|
|
82
|
+
from hkc_kep.event_bus import EventBus
|
|
83
|
+
from hkc_kde.kde import KnowledgeDigestEngine
|
|
84
|
+
d = Path(tempfile.mkdtemp(prefix="foxclaw_kde_"))
|
|
85
|
+
store = SQLiteGraphStore(str(d / "hkc.db"))
|
|
86
|
+
bus = EventBus(str(d / "events.db"))
|
|
87
|
+
idg = IDGenerator(str(d / "id.db"))
|
|
88
|
+
return KnowledgeDigestEngine(store, bus, idg, provider="anthropic")
|
|
89
|
+
|
|
90
|
+
class _FakeExtractor:
|
|
91
|
+
"""每个 chunk 抽出一个实体,捕获收到的 chunk 供断言。"""
|
|
92
|
+
def __init__(self): self.seen = []
|
|
93
|
+
def extract(self, chunks, progress_cb=None):
|
|
94
|
+
self.seen = list(chunks)
|
|
95
|
+
doc_id = chunks[0].doc_id if chunks else ""
|
|
96
|
+
raw = RawExtraction(entities=[{"name": "Foxclaw", "type": "Person"}])
|
|
97
|
+
return ExtractionResult(doc_id=doc_id, source=doc_id, items=[raw])
|
|
98
|
+
|
|
99
|
+
for suffix, maker in ((".docx", _make_docx), (".xlsx", _make_xlsx)):
|
|
100
|
+
kde = _build_kde()
|
|
101
|
+
fake = _FakeExtractor(); kde.extractor = fake
|
|
102
|
+
kus = kde.ingest_file(str(maker()), source=f"user{suffix}")
|
|
103
|
+
assert fake.seen, f"{suffix}: 抽取器未收到 chunk(分派断裂)"
|
|
104
|
+
assert kus, f"{suffix}: 未写入任何 KU"
|
|
105
|
+
print("[4] KDE 分派 .docx/.xlsx → loader → chunker → KU 全链路 OK")
|
|
106
|
+
|
|
107
|
+
kde = _build_kde()
|
|
108
|
+
bad = TMP / "x.pptx"; bad.write_bytes(b"PK\x03\x04not really")
|
|
109
|
+
try:
|
|
110
|
+
kde.ingest_file(str(bad))
|
|
111
|
+
raise AssertionError("应对 .pptx 抛不支持错误")
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
assert ".pptx" in str(e) or "不支持" in str(e), str(e)
|
|
114
|
+
print("[5] 不支持格式(.pptx)明确 ValueError OK")
|
|
115
|
+
|
|
116
|
+
from fastapi.testclient import TestClient
|
|
117
|
+
from foxclaw.api import app
|
|
118
|
+
with TestClient(app) as c:
|
|
119
|
+
r = c.post("/knowledge/ingest/file",
|
|
120
|
+
files={"file": ("bad.pptx", b"x", "application/octet-stream")})
|
|
121
|
+
assert r.status_code == 415, (r.status_code, r.text)
|
|
122
|
+
assert ".docx" in r.text and ".xlsx" in r.text, "415 提示未含新格式"
|
|
123
|
+
with open(_make_docx(), "rb") as fh:
|
|
124
|
+
r2 = c.post("/knowledge/ingest/file",
|
|
125
|
+
files={"file": ("sample.docx", fh.read(),
|
|
126
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document")},
|
|
127
|
+
data={"domain": "test"})
|
|
128
|
+
assert r2.status_code == 200, (r2.status_code, r2.text)
|
|
129
|
+
print("[6] 上传路由格式门(.pptx→415;.docx→200)OK")
|
|
130
|
+
|
|
131
|
+
print("\n文件摄入(docx/xlsx)验证全部通过")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
test_file_ingest()
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
fr = FinanceRuntime(policy_path=str(Path(tempfile.mkdtemp()) / "b_finance_policy.yaml"))
|
|
29
|
+
n = fr.import_csv(CSV)
|
|
30
|
+
fr.set_budget("food", 400)
|
|
31
|
+
fr.set_budget("subscription", 100)
|
|
32
|
+
fr.set_balance("debit", 11800)
|
|
33
|
+
fr.set_balance("credit_card_1", -700)
|
|
34
|
+
print(f"\n导入 {n} 笔交易")
|
|
35
|
+
check(n == 13, f"CSV 导入 13 笔交易(实为 {n})")
|
|
36
|
+
|
|
37
|
+
r = fr.analyze(period="2026-06", current_balance=11100)
|
|
38
|
+
|
|
39
|
+
print("\n【预算推理】")
|
|
40
|
+
for b in r["budget_status"]:
|
|
41
|
+
flag = "⚠ 超支" if b["is_over"] else "OK"
|
|
42
|
+
print(f" {b['category']:<14} {b['spent']:.0f}/{b['monthly_limit']:.0f} "
|
|
43
|
+
f"({b['usage_ratio']*100:.0f}%) {flag}")
|
|
44
|
+
over = {b["category"]: b["is_over"] for b in r["budget_status"]}
|
|
45
|
+
check(over.get("food") is True and over.get("subscription") is True,
|
|
46
|
+
f"两大超支分类被正确标记(food={over.get('food')}, subscription={over.get('subscription')})")
|
|
47
|
+
|
|
48
|
+
print("\n【超支预测】")
|
|
49
|
+
for f in r["overspend_forecast"]:
|
|
50
|
+
print(f" {f['category']:<14} 月底预计 {f['projected_month_end']:.0f} / "
|
|
51
|
+
f"限额 {f['monthly_limit']:.0f} · 将超支={f['will_overspend']}")
|
|
52
|
+
check(len(r["overspend_forecast"]) >= 2
|
|
53
|
+
and all(f["will_overspend"] for f in r["overspend_forecast"]),
|
|
54
|
+
f"超支预测命中({len(r['overspend_forecast'])} 项,全部 will_overspend=True)")
|
|
55
|
+
|
|
56
|
+
print("\n【异常消费(MAD 检测)】")
|
|
57
|
+
for a in r["anomalies"]:
|
|
58
|
+
print(f" {a['entity']} {a['amount']} (修正Z={a['z_score']}) — {a['reason']}")
|
|
59
|
+
check(any(a["entity"] == "高级餐厅" for a in r["anomalies"]),
|
|
60
|
+
f"MAD 检测出「高级餐厅」离群消费({len(r['anomalies'])} 条异常)")
|
|
61
|
+
|
|
62
|
+
print("\n【重复扣款】")
|
|
63
|
+
for d in r["duplicate_charges"]:
|
|
64
|
+
print(f" {d['reason']}")
|
|
65
|
+
if not r["duplicate_charges"]:
|
|
66
|
+
print(" 无")
|
|
67
|
+
check(isinstance(r["duplicate_charges"], list), "重复扣款检测返回列表结构")
|
|
68
|
+
|
|
69
|
+
print("\n【订阅膨胀】")
|
|
70
|
+
sc = r["subscription_creep"]
|
|
71
|
+
print(f" {sc['subscription_count']} 个订阅,月总 {sc['monthly_total']:.0f},"
|
|
72
|
+
f"预警={sc['creep_warning']}")
|
|
73
|
+
check(sc["subscription_count"] == 6 and sc["creep_warning"] is True,
|
|
74
|
+
f"订阅膨胀预警触发(6 个订阅 / 月总 {sc['monthly_total']:.0f})")
|
|
75
|
+
|
|
76
|
+
print("\n【烧钱速度 / 现金流】")
|
|
77
|
+
br = r["burn_rate"]
|
|
78
|
+
print(f" 日均支出 {br['daily_burn']} · 日均净流 {br['daily_net']}")
|
|
79
|
+
cf = r["cashflow_projection"]
|
|
80
|
+
if cf:
|
|
81
|
+
print(f" 现金流趋势: {cf['trend']} · 月净流 {cf['monthly_net']}")
|
|
82
|
+
for t in cf["trajectory"]:
|
|
83
|
+
print(f" 第 {t['month']} 月预计余额 {t['projected_balance']}")
|
|
84
|
+
check(len(cf["trajectory"]) == 3, "现金流预测给出 3 个月轨迹")
|
|
85
|
+
|
|
86
|
+
_cutoff = datetime.now() - timedelta(days=30)
|
|
87
|
+
in_window = [t for t in fr.transactions if t.dt >= _cutoff]
|
|
88
|
+
expect_expense = round(sum(abs(t.amount) for t in in_window if t.is_expense), 2)
|
|
89
|
+
check(br["total_expense"] == expect_expense,
|
|
90
|
+
f"burn_rate 严格按 now-30天 窗口统计(窗口内 {len(in_window)} 笔;样例日期已过期,故为 {expect_expense})")
|
|
91
|
+
check(isinstance(br["daily_burn"], (int, float)) and br["daily_burn"] == round(expect_expense / 30, 2),
|
|
92
|
+
f"日均支出 = 窗口内支出 / 30(daily_burn={br['daily_burn']})")
|
|
93
|
+
|
|
94
|
+
fresh = [
|
|
95
|
+
Transaction(amount=-300, category="food", entity="测试超市", account="debit"),
|
|
96
|
+
Transaction(amount=-150, category="transport", entity="测试打车", account="debit"),
|
|
97
|
+
Transaction(amount=2000, category="salary", entity="某公司", account="debit"),
|
|
98
|
+
]
|
|
99
|
+
br2 = FinanceReasoning.burn_rate(fresh, days=30)
|
|
100
|
+
check(br2["total_expense"] == 450.0 and br2["total_income"] == 2000.0,
|
|
101
|
+
f"当日交易用例:支出 450 / 收入 2000(实为 {br2['total_expense']}/{br2['total_income']})")
|
|
102
|
+
check(br2["daily_burn"] == 15.0, f"日均支出 = 450/30 = 15.0(实为 {br2['daily_burn']})")
|
|
103
|
+
check(br2["daily_net"] == round(1550 / 30, 2),
|
|
104
|
+
f"日均净流 = (2000-450)/30(实为 {br2['daily_net']})")
|
|
105
|
+
cf2 = FinanceReasoning.cashflow_projection(fresh, current_balance=1000.0, months=3)
|
|
106
|
+
check(cf2["trend"] == "上升" and cf2["monthly_net"] > 0, "净流入 → 趋势判定为上升")
|
|
107
|
+
check(cf2["trajectory"][-1]["projected_balance"] > 1000.0, "预测余额随净流入增长")
|
|
108
|
+
|
|
109
|
+
print("\n【守门人裁决(整体状态)】")
|
|
110
|
+
for v in r["gatekeeper_verdicts"]:
|
|
111
|
+
print(f" [{v['action'].upper()}] {v['message']}")
|
|
112
|
+
acts = {v["action"].lower() for v in r["gatekeeper_verdicts"]}
|
|
113
|
+
check("veto" in acts and "alert" in acts,
|
|
114
|
+
f"整体裁决含 VETO(异常消费否决)与 ALERT(订阅超标)(实为 {sorted(acts)})")
|
|
115
|
+
|
|
116
|
+
print("\n【守门人裁决(单笔大额交易)】")
|
|
117
|
+
big = Transaction(amount=-680, category="electronics", entity="Apple Store",
|
|
118
|
+
account="credit_card_1")
|
|
119
|
+
big_acts = {v.action.lower() for v in fr.policy.evaluate_transaction(big, account_balance=300)}
|
|
120
|
+
for v in fr.policy.evaluate_transaction(big, account_balance=300):
|
|
121
|
+
print(f" [{v.action.upper()}] {v.message}")
|
|
122
|
+
check("require_confirmation" in big_acts,
|
|
123
|
+
"大额支出(680>500)触发 REQUIRE_CONFIRMATION")
|
|
124
|
+
check("freeze" in big_acts,
|
|
125
|
+
"余额不足(300<500)触发 FREEZE 可选支出")
|
|
126
|
+
|
|
127
|
+
print("\n【执行层:审批流(不自动动钱)】")
|
|
128
|
+
req = fr.request_action("cancel_subscription", {"entity": "YouTube", "save": 12.0})
|
|
129
|
+
print(f" 请求: {req['action']} · 状态: {req['status']}")
|
|
130
|
+
print(f" 说明: {req['note']}")
|
|
131
|
+
check("PENDING" in str(req["status"]).upper(),
|
|
132
|
+
f"执行层只产出待批状态,不自动动钱(status={req['status']})")
|
|
133
|
+
|
|
134
|
+
print("\n" + "=" * 60)
|
|
135
|
+
print("✓ B 阶段验证完成:财务内核(记忆+推理+Policy 守门人)成立")
|
|
136
|
+
print("=" * 60)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_finance():
|
|
140
|
+
main()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|