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,403 @@
|
|
|
1
|
+
"""领域推理评测框架 —— 让"推理深度"从感觉变成**数字**。
|
|
2
|
+
|
|
3
|
+
为什么需要它:守门人的价值不取决于"规则多不多",而取决于
|
|
4
|
+
· **漏报**(该抓没抓)→ 规则缺 → 补规则
|
|
5
|
+
· **误报**(拦了但没事)→ 规则太粗/阈值不合 → **必须能定位是哪条规则干的**,才改得动
|
|
6
|
+
误报率一高,用户就会把守门人关掉 —— 那它就形同虚设。所以误报是一等公民指标。
|
|
7
|
+
|
|
8
|
+
怎么用(给未来的你/我):
|
|
9
|
+
· 往 CASES 里加**你真实遇到的情形** + 期望裁决,跑一次就知道漏报/误报和**归因**。
|
|
10
|
+
· 加一条新规则后重跑:立刻看到它抓住了什么、误伤了什么。
|
|
11
|
+
· 与 test_evals_smoke.py(治理层拦截率/误报)同款方法,搬到领域推理层。
|
|
12
|
+
|
|
13
|
+
裁决语义:approve=放行 / confirm=要你确认 / reject=拒。
|
|
14
|
+
"标注"= expect_flag:True 表示"这笔该被拦下来(confirm 或 reject)"。
|
|
15
|
+
"""
|
|
16
|
+
import sys, tempfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
19
|
+
|
|
20
|
+
def test_domain_evals():
|
|
21
|
+
from foxclaw.finance import FinanceRuntime
|
|
22
|
+
from foxclaw.finance.models import Transaction
|
|
23
|
+
from foxclaw.finance.reasoner import FinanceGatekeeperReasoner
|
|
24
|
+
|
|
25
|
+
print("=" * 60); print("领域推理评测(财务守门人)"); print("=" * 60)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _policy():
|
|
29
|
+
# 用产品**默认**规则集评测 —— 评的就是"开箱默认够不够好"
|
|
30
|
+
# data_path 必须独立:默认落在 policy 同目录,而 mktemp 的父目录是**系统 Temp**,
|
|
31
|
+
# 不显式隔离就会和别的测试共用一个 finance_data.json(顺序一变就互相污染)。
|
|
32
|
+
return FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
33
|
+
data_path=tempfile.mktemp(suffix=".json")).policy
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 标注案例集:(说明, 金额, 类别, 余额, 该不该被拦) ───────────────────
|
|
37
|
+
# ↓↓↓ 往这里加你真实遇到的情形 ↓↓↓
|
|
38
|
+
CASES = [
|
|
39
|
+
# 日常小额:不该被拦(这是误报的主要来源——原 200 阈值下它们全会被拦)
|
|
40
|
+
("买咖啡", -35, "food", 8000, False),
|
|
41
|
+
("午饭", -60, "food", 8000, False),
|
|
42
|
+
("打车", -80, "transport", 8000, False),
|
|
43
|
+
("买菜", -150, "food", 8000, False),
|
|
44
|
+
("买书", -220, "book", 8000, False),
|
|
45
|
+
("聚餐 AA", -320, "food", 8000, False),
|
|
46
|
+
("买鞋", -450, "shopping", 8000, False),
|
|
47
|
+
# 大额:该被拦(>500)
|
|
48
|
+
("买手机", -4500, "digital", 8000, True),
|
|
49
|
+
("交房租", -3000, "housing", 8000, True),
|
|
50
|
+
("买笔记本", -8000, "digital", 20000, True),
|
|
51
|
+
# 余额保护:低余额该被拦(即便金额不大)
|
|
52
|
+
("低余额时消费", -100, "food", 300, True),
|
|
53
|
+
# 收入:不该被拦
|
|
54
|
+
("发工资", +15000, "salary", 8000, False),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def evaluate(cases, policy):
|
|
59
|
+
"""跑评测,返回 (漏报, 误报) —— 误报带**归因**(是哪条规则拦的)。"""
|
|
60
|
+
misses, false_pos = [], []
|
|
61
|
+
for name, amount, category, balance, expect_flag in cases:
|
|
62
|
+
txn = Transaction(amount=amount, category=category, entity=name, account="default")
|
|
63
|
+
rv = FinanceGatekeeperReasoner(policy).reason((txn, balance))
|
|
64
|
+
flagged = rv.decision in ("confirm", "reject")
|
|
65
|
+
if expect_flag and not flagged:
|
|
66
|
+
misses.append((name, amount, rv.decision))
|
|
67
|
+
elif not expect_flag and flagged:
|
|
68
|
+
# 归因:证据里 raw 非空的就是**触发裁决的规则**(informational 证据 raw=None)
|
|
69
|
+
rules = [e.factor for e in rv.evidence if e.raw is not None]
|
|
70
|
+
false_pos.append((name, amount, rv.decision, rules))
|
|
71
|
+
return misses, false_pos
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
pol = _policy()
|
|
75
|
+
misses, fps = evaluate(CASES, pol)
|
|
76
|
+
n = len(CASES)
|
|
77
|
+
n_pos = sum(1 for c in CASES if c[4])
|
|
78
|
+
n_neg = n - n_pos
|
|
79
|
+
miss_rate = len(misses) / n_pos if n_pos else 0.0
|
|
80
|
+
fp_rate = len(fps) / n_neg if n_neg else 0.0
|
|
81
|
+
|
|
82
|
+
print(f"\n案例 {n}(该拦 {n_pos} / 不该拦 {n_neg})")
|
|
83
|
+
print(f" 漏报率 = {miss_rate:.0%} ({len(misses)}/{n_pos})")
|
|
84
|
+
print(f" 误报率 = {fp_rate:.0%} ({len(fps)}/{n_neg})")
|
|
85
|
+
if misses:
|
|
86
|
+
print(" 【漏报】该抓没抓 → 缺规则:")
|
|
87
|
+
for nm, amt, d in misses:
|
|
88
|
+
print(f" {nm}({amt}) → {d}")
|
|
89
|
+
if fps:
|
|
90
|
+
print(" 【误报】拦了但没事 → 归因到规则(据此改阈值/规则):")
|
|
91
|
+
for nm, amt, d, rules in fps:
|
|
92
|
+
print(f" {nm}({amt}) → {d} ← 触发规则: {rules or '(未归因)'}")
|
|
93
|
+
|
|
94
|
+
# [1] 漏报:该抓的必须都抓住(>500 / 低余额)
|
|
95
|
+
assert not misses, f"存在漏报(该抓没抓): {misses}"
|
|
96
|
+
print("\n[1] 无漏报(>500 与低余额保护都抓住了)OK")
|
|
97
|
+
|
|
98
|
+
# [2] 误报:日常消费不该被拦。阈值 500 后,220/320/450 都该放行
|
|
99
|
+
# (原默认 200 时它们全是误报 → 用户会被烦到关掉守门人)
|
|
100
|
+
assert fp_rate <= 0.15, f"误报率过高({fp_rate:.0%}),用户会关掉守门人: {fps}"
|
|
101
|
+
print(f"[2] 误报率 {fp_rate:.0%} ≤ 15%(沿用 evals_smoke 同款红线)OK")
|
|
102
|
+
|
|
103
|
+
# [3] 误报**可归因**:这是"确定误报原因加以修改"的前提 ——
|
|
104
|
+
# 若无法定位是哪条规则拦的,就无从改起。这里验证归因机制本身有效:
|
|
105
|
+
# 故意把阈值压到 100 制造误报,必须能指出是 large_purchase_check 干的。
|
|
106
|
+
pol_bad = _policy()
|
|
107
|
+
for r in pol_bad.rules:
|
|
108
|
+
if r.id == "large_purchase_check":
|
|
109
|
+
r.threshold = 100.0
|
|
110
|
+
_m2, fps2 = evaluate(CASES, pol_bad)
|
|
111
|
+
assert fps2, "阈值压到 100 应制造出误报(用于验证归因)"
|
|
112
|
+
assert all("large_purchase_check" in rules for _, _, _, rules in fps2), \
|
|
113
|
+
f"误报必须能归因到具体规则,否则无从改起: {fps2}"
|
|
114
|
+
print(f"[3] 误报可归因(阈值压到100 → {len(fps2)} 条误报,全部指向 large_purchase_check)OK")
|
|
115
|
+
|
|
116
|
+
# [4] 规则实例隔离(本框架第一次跑就抓到的真 bug):
|
|
117
|
+
# 原 `self.rules = list(DEFAULT_RULES)` 只复制列表、不复制规则对象 → 所有 runtime
|
|
118
|
+
# 共享同一批**可变**规则,[3] 里把 pol_bad 阈值压到 100 竟连带把 pol 和模块级
|
|
119
|
+
# DEFAULT_RULES 一起改了(要重启才恢复)。现已 deepcopy。
|
|
120
|
+
from foxclaw.finance.policy import DEFAULT_RULES
|
|
121
|
+
p1, p2 = _policy(), _policy()
|
|
122
|
+
r1 = [r for r in p1.rules if r.id == "large_purchase_check"][0]
|
|
123
|
+
r2 = [r for r in p2.rules if r.id == "large_purchase_check"][0]
|
|
124
|
+
rd = [r for r in DEFAULT_RULES if r.id == "large_purchase_check"][0]
|
|
125
|
+
assert r1 is not r2, "不同 runtime 不得共享同一规则对象"
|
|
126
|
+
r1.threshold = 99.0
|
|
127
|
+
assert r2.threshold != 99.0, "改一个 runtime 的阈值污染了另一个(共享可变状态)"
|
|
128
|
+
assert rd.threshold != 99.0, "改 runtime 的阈值污染了模块级 DEFAULT_RULES"
|
|
129
|
+
print("[4] 规则实例隔离(改一处不污染其它 runtime / 模块级默认)OK")
|
|
130
|
+
|
|
131
|
+
# [5] 阈值是可调项(改 yaml 即可,不用动代码);默认 500
|
|
132
|
+
assert rd.threshold == 500.0, f"默认单笔阈值应为 500,实际 {rd.threshold}"
|
|
133
|
+
print("[5] 阈值为可调项(finance_policy.yaml 可改,默认 500)OK")
|
|
134
|
+
|
|
135
|
+
# ══════════════════════════════════════════════════════════
|
|
136
|
+
# 用户点名要的 4 条规则(2026-07-16)。前两条的检测早就实现,只是从不产生裁决。
|
|
137
|
+
# ══════════════════════════════════════════════════════════
|
|
138
|
+
from datetime import datetime, timedelta
|
|
139
|
+
from foxclaw.finance.models import Budget
|
|
140
|
+
from foxclaw.finance.reasoning import FinanceReasoning as FR
|
|
141
|
+
|
|
142
|
+
def _txn(amount, entity, days_ago=0, category="misc", tags=None):
|
|
143
|
+
return Transaction(amount=amount, category=category, entity=entity, account="default",
|
|
144
|
+
timestamp=(datetime.now() - timedelta(days=days_ago)).isoformat(),
|
|
145
|
+
tags=tags or [])
|
|
146
|
+
|
|
147
|
+
def _fire(**kw):
|
|
148
|
+
"""跑状态裁决,返回触发的 rule_id 集合。"""
|
|
149
|
+
return {v.rule_id for v in _policy().evaluate_state(**kw)}
|
|
150
|
+
|
|
151
|
+
# [6] 同一商家高频扣款 → 必须裁决(此前 detect_duplicate_charges 算了却不吭声)
|
|
152
|
+
dups = FR.detect_duplicate_charges([
|
|
153
|
+
_txn(-99, "SomeApp", days_ago=1), _txn(-99, "SomeApp", days_ago=1),
|
|
154
|
+
])
|
|
155
|
+
assert dups, "重复扣款应被检测到"
|
|
156
|
+
assert "duplicate_charge_alert" in _fire(duplicates=dups), "检测到却不裁决(老毛病)"
|
|
157
|
+
assert "duplicate_charge_alert" not in _fire(duplicates=[]), "无重复扣款不该报警"
|
|
158
|
+
print(f"[6] 同一商家高频扣款 → 触发裁决({dups[0]['reason']})OK")
|
|
159
|
+
|
|
160
|
+
# [7] 跨月预算超支**趋势**:还没超、但按速度月底会超 → 提醒;已超/不会超 → 不重复报
|
|
161
|
+
f_trend = {"category": "food", "spent_so_far": 800, "monthly_limit": 1000,
|
|
162
|
+
"projected_month_end": 1600, "projected_overage": 600, "will_overspend": True}
|
|
163
|
+
f_safe = {"category": "food", "spent_so_far": 200, "monthly_limit": 1000,
|
|
164
|
+
"projected_month_end": 400, "projected_overage": 0, "will_overspend": False}
|
|
165
|
+
assert "overspend_trend_alert" in _fire(forecasts=[f_trend]), "超支趋势应提醒"
|
|
166
|
+
assert "overspend_trend_alert" not in _fire(forecasts=[f_safe]), "不会超不该提醒"
|
|
167
|
+
print("[7] 跨月超支趋势(还没超但会超 → 提醒;不会超 → 不吵)OK")
|
|
168
|
+
|
|
169
|
+
# [8] 年费自动续订**前**提醒:从历史间隔推断周期,只在临近时提醒
|
|
170
|
+
# 上次扣款 362 天前、上上次 727 天前 → 间隔 365(年订)→ 下次在 3 天后
|
|
171
|
+
annual = [_txn(-598, "某会员", days_ago=362 + 365, tags=["recurring"]),
|
|
172
|
+
_txn(-598, "某会员", days_ago=362, tags=["recurring"])]
|
|
173
|
+
ren = FR.detect_upcoming_renewals(annual, horizon_days=7)
|
|
174
|
+
assert ren and ren[0]["cadence"] == "年订" and ren[0]["days_until"] <= 7, ren
|
|
175
|
+
assert "renewal_reminder" in _fire(renewals=ren)
|
|
176
|
+
# 间隔紊乱 → 不预测(宁可不提醒,也不为猜错的日期制造误报)
|
|
177
|
+
messy = [_txn(-50, "乱扣", days_ago=200, tags=["recurring"]),
|
|
178
|
+
_txn(-50, "乱扣", days_ago=3, tags=["recurring"])]
|
|
179
|
+
assert FR.detect_upcoming_renewals(messy) == [], "间隔紊乱不该预测续订日"
|
|
180
|
+
# 只扣过一次 → 无法判周期
|
|
181
|
+
assert FR.detect_upcoming_renewals([_txn(-50, "新订阅", days_ago=1, tags=["recurring"])]) == []
|
|
182
|
+
print(f"[8] 年费续订前提醒({ren[0]['entity']} {ren[0]['days_until']} 天后;紊乱/单次不预测)OK")
|
|
183
|
+
|
|
184
|
+
# [9] 退款未到账:**需用户标记** refund_expected;已到账则不报
|
|
185
|
+
overdue = FR.detect_pending_refunds([_txn(-300, "某电商", days_ago=20, tags=["refund_expected"])])
|
|
186
|
+
assert overdue and overdue[0]["days_waiting"] >= 14, overdue
|
|
187
|
+
assert "refund_overdue_alert" in _fire(pending_refunds=overdue)
|
|
188
|
+
# 等额退款已入账 → 不报
|
|
189
|
+
done = FR.detect_pending_refunds([_txn(-300, "某电商", days_ago=20, tags=["refund_expected"]),
|
|
190
|
+
_txn(+300, "某电商", days_ago=2)])
|
|
191
|
+
assert done == [], "退款已到账不该再报"
|
|
192
|
+
# 【关键】未标记的普通消费**绝不**报警 —— 否则每笔消费都报,是灾难性误报
|
|
193
|
+
assert FR.detect_pending_refunds([_txn(-300, "某电商", days_ago=20)]) == [], \
|
|
194
|
+
"未标记的普通消费不得触发退款报警(灾难性误报)"
|
|
195
|
+
print("[9] 退款未到账(需标记;已到账不报;未标记的普通消费不误报)OK")
|
|
196
|
+
|
|
197
|
+
# [10] 新规则不得把日常消费变成噪音 —— 无信号时一条都不该响
|
|
198
|
+
assert _fire() == set(), f"无信号时不该有任何裁决: {_fire()}"
|
|
199
|
+
print("[10] 无信号 → 零裁决(新规则不制造背景噪音)OK")
|
|
200
|
+
|
|
201
|
+
# ══════════════════════════════════════════════════════════
|
|
202
|
+
# 阈值自校准:阈值不再是我拍的,而是从用户自己的消费分布长出来的。
|
|
203
|
+
# 这一段验的是"没有真实数据时也不会瞎猜、有数据时自动贴合、且永不踩人的决定"。
|
|
204
|
+
# ══════════════════════════════════════════════════════════
|
|
205
|
+
def _stream(each, n=60, entity="日常"):
|
|
206
|
+
"""造 n 笔支出,跨**正好** n-1 天(满足校准的样本门槛)。
|
|
207
|
+
|
|
208
|
+
基准时刻只取一次:若每笔各调 datetime.now(),60 次调用间的微秒漂移会把跨度
|
|
209
|
+
啃掉不到一天,而 .days 向下取整 → span 变成 58,校准值随机器快慢漂移。
|
|
210
|
+
"""
|
|
211
|
+
base = datetime.now()
|
|
212
|
+
return [Transaction(amount=-each, category="misc", entity=entity, account="default",
|
|
213
|
+
timestamp=(base - timedelta(days=i)).isoformat())
|
|
214
|
+
for i in range(n)]
|
|
215
|
+
|
|
216
|
+
# [11] 样本不足 → 不校准(继续用出厂默认)
|
|
217
|
+
assert FR.calibrate_thresholds([]) == {}, "空数据不该校准"
|
|
218
|
+
assert FR.calibrate_thresholds(_stream(300, n=5)) == {}, "5 笔就校准 = 用垃圾数据冒充个性化"
|
|
219
|
+
assert FR.calibrate_thresholds(_stream(300, n=30)) == {}, \
|
|
220
|
+
"笔数够但跨度只有 29 天(差一天到门槛)→ 日均不可信,不该校准"
|
|
221
|
+
print("[11] 样本不足(空/笔数少/跨度短)→ 不校准,沿用出厂默认 OK")
|
|
222
|
+
|
|
223
|
+
# 有方差的真实感消费流:大量小额 + 可选一笔大额尖峰。均匀数据测不出"抗离群"。
|
|
224
|
+
_PAT = [30, 45, 60, 80, 120, 55, 90, 40, 150, 70] # median≈65 的日常波动
|
|
225
|
+
def _real(scale=1.0, n=80, spike=None):
|
|
226
|
+
base = datetime.now()
|
|
227
|
+
txns = [Transaction(amount=-_PAT[i % len(_PAT)] * scale, category="misc",
|
|
228
|
+
entity="日常", account="default",
|
|
229
|
+
timestamp=(base - timedelta(days=i)).isoformat())
|
|
230
|
+
for i in range(n)]
|
|
231
|
+
if spike: # 一笔大额(买电脑/交房租),落在窗口内,不改变跨度
|
|
232
|
+
txns.append(Transaction(amount=-spike, category="misc", entity="大额",
|
|
233
|
+
account="default", timestamp=(base - timedelta(days=1)).isoformat()))
|
|
234
|
+
return txns
|
|
235
|
+
|
|
236
|
+
# [12] 校准随消费水平伸缩 —— 这正是固定 500 做不到的事
|
|
237
|
+
rich = FR.calibrate_thresholds(_real(scale=2.0)) # 消费翻倍的人
|
|
238
|
+
poor = FR.calibrate_thresholds(_real(scale=0.5)) # 消费减半的人
|
|
239
|
+
lr, lp = rich["large_purchase_check"][0], poor["large_purchase_check"][0]
|
|
240
|
+
assert lr > lp, f"高消费者的大额门槛应更高: 富{lr} vs 穷{lp}"
|
|
241
|
+
assert rich["low_balance_protection"][0] > poor["low_balance_protection"][0]
|
|
242
|
+
print(f"[12] 阈值随消费水平伸缩(富→大额{lr:.0f} / 穷→大额{lp:.0f};固定 500 对两者都错)OK")
|
|
243
|
+
|
|
244
|
+
# [12b]【B 修复的直接证据】大额离群**不**抬高门槛 ——
|
|
245
|
+
# 这是把均值锚换成 median+MAD 的全部理由:一笔 8000 的电脑不该让此后的
|
|
246
|
+
# 大额检测变迟钝。均值锚在这里会让门槛暴涨(月均×10% 从 ~200 跳到 ~500)。
|
|
247
|
+
base_large = FR.calibrate_thresholds(_real())["large_purchase_check"][0]
|
|
248
|
+
spiked_large = FR.calibrate_thresholds(_real(spike=8000))["large_purchase_check"][0]
|
|
249
|
+
assert abs(spiked_large - base_large) <= 100, \
|
|
250
|
+
f"一笔大额离群把门槛从 {base_large} 顶到 {spiked_large}(均值锚的老毛病,MAD 应免疫)"
|
|
251
|
+
print(f"[12b] 抗离群:加一笔 8000 大额,门槛 {base_large:.0f}→{spiked_large:.0f} 几乎不动 OK")
|
|
252
|
+
|
|
253
|
+
# [13] 时间类阈值**不**校准(它们是语义常数,不是从消费分布学得出来的)
|
|
254
|
+
assert set(rich) == {"large_purchase_check", "low_balance_protection"}, \
|
|
255
|
+
f"只有金额类阈值有分布依据;时间类不得假装校准: {set(rich)}"
|
|
256
|
+
print("[13] 时间类阈值(48h/7d/14d)不参与校准(无分布依据,不给猜测套数学外衣)OK")
|
|
257
|
+
|
|
258
|
+
# [14] 落盘:builtin 且停在出厂值 → 校准生效,且留下可解释的依据
|
|
259
|
+
cal = FR.calibrate_thresholds(_real())
|
|
260
|
+
cal_large = cal["large_purchase_check"][0]
|
|
261
|
+
p14 = _policy()
|
|
262
|
+
ch = p14.apply_calibration(cal)
|
|
263
|
+
r14 = [r for r in p14.rules if r.id == "large_purchase_check"][0]
|
|
264
|
+
assert r14.threshold == cal_large and r14.source == "learned" and r14.calibrated_value == cal_large
|
|
265
|
+
assert "median" in (r14.calibrated_from or ""), "阈值必须可解释,用户才敢信"
|
|
266
|
+
# large 一定变了;low_balance 恰好也算出 500(=出厂)时不进 ch —— 那正是"值没变不写盘"
|
|
267
|
+
assert "large_purchase_check" in {c["rule_id"] for c in ch}, ch
|
|
268
|
+
# 幂等:同样的建议再来一次 → 无改动、不重复写盘(analyze 每次都会调到)
|
|
269
|
+
assert p14.apply_calibration(cal) == [], "值没变还写盘 = 每次 analyze 都刷一遍 yaml"
|
|
270
|
+
print(f"[14] 校准落盘 + 可解释({r14.calibrated_from})+ 幂等 OK")
|
|
271
|
+
|
|
272
|
+
# [15]【最重要】校准**绝不踩人的决定** —— 否则用户会发现自己改的设置莫名变回去,
|
|
273
|
+
# 从此不信任整个系统。三种"人碰过"的形态都必须让校准收手:
|
|
274
|
+
p_user = _policy() # (a) source=user 显式锁定
|
|
275
|
+
for r in p_user.rules:
|
|
276
|
+
if r.id == "large_purchase_check": r.source = "user"
|
|
277
|
+
p_user.apply_calibration(rich)
|
|
278
|
+
assert [r for r in p_user.rules if r.id == "large_purchase_check"][0].threshold == 500.0, \
|
|
279
|
+
"source=user 是显式锁定,校准不得覆盖"
|
|
280
|
+
|
|
281
|
+
p_edit = _policy() # (b) builtin 但用户改过 yaml 阈值
|
|
282
|
+
for r in p_edit.rules:
|
|
283
|
+
if r.id == "large_purchase_check": r.threshold = 1234.0
|
|
284
|
+
p_edit.apply_calibration(rich)
|
|
285
|
+
assert [r for r in p_edit.rules if r.id == "large_purchase_check"][0].threshold == 1234.0, \
|
|
286
|
+
"阈值偏离出厂值 = 有人有意改过,校准必须收手"
|
|
287
|
+
|
|
288
|
+
p_re = _policy() # (c) 我校准过,之后用户又手改
|
|
289
|
+
p_re.apply_calibration(rich)
|
|
290
|
+
for r in p_re.rules:
|
|
291
|
+
if r.id == "large_purchase_check": r.threshold = 777.0
|
|
292
|
+
p_re.apply_calibration(poor)
|
|
293
|
+
assert [r for r in p_re.rules if r.id == "large_purchase_check"][0].threshold == 777.0, \
|
|
294
|
+
"校准值被人改过 = 人的决定,不得再次覆盖"
|
|
295
|
+
print("[15] 校准不踩人的决定(user 锁定 / 手改出厂值 / 手改校准值 → 三种都收手)OK")
|
|
296
|
+
|
|
297
|
+
# [16] 校准不污染模块级出厂默认(自校准直接写 rule.threshold,最容易复发 [4] 那个 bug)
|
|
298
|
+
assert rd.threshold == 500.0, f"校准污染了模块级 DEFAULT_RULES(阈值变成 {rd.threshold})"
|
|
299
|
+
from foxclaw.finance.policy import FACTORY_THRESHOLDS
|
|
300
|
+
assert FACTORY_THRESHOLDS["large_purchase_check"] == 500.0, "出厂快照被污染 → 判定基准失效"
|
|
301
|
+
print("[16] 校准不污染模块级出厂默认/出厂快照 OK")
|
|
302
|
+
|
|
303
|
+
# [17] 校准必须**对用户可见**(阈值自己变了还不吭声 = 偷偷改用户的设置)
|
|
304
|
+
rt = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
305
|
+
data_path=tempfile.mktemp(suffix=".json"))
|
|
306
|
+
for t in _real():
|
|
307
|
+
rt.add_transaction(t, run_gatekeeper=False)
|
|
308
|
+
rep = rt.analyze()
|
|
309
|
+
assert rep["threshold_calibration"], "analyze 必须把阈值变更报出来"
|
|
310
|
+
assert any(c["rule_id"] == "large_purchase_check" and c["from"] == 500.0
|
|
311
|
+
and c["to"] == cal_large for c in rep["threshold_calibration"]), \
|
|
312
|
+
rep["threshold_calibration"]
|
|
313
|
+
assert rt.analyze()["threshold_calibration"] == [], "已校准后重复 analyze 不该再报变更"
|
|
314
|
+
print(f"[17] 阈值变更对用户可见(analyze 报出 500→{cal_large:.0f}),且不重复打扰 OK")
|
|
315
|
+
|
|
316
|
+
# ══════════════════════════════════════════════════════════
|
|
317
|
+
# 深审补丁(2026-07-16):A 月初误报闸 / C 续订阈值真正生效。
|
|
318
|
+
# 这两条是"审核后面做的"翻出来的真问题——不是新功能,是把已埋的雷拆掉。
|
|
319
|
+
# ══════════════════════════════════════════════════════════
|
|
320
|
+
|
|
321
|
+
# [18]【A】月初超支外推不可信 → 不报。overspend_forecast 用 spent/day,day=1 时
|
|
322
|
+
# 一笔正常消费被 ×30 放大成"月底将超支"。前 7 天标 reliable=False,裁决层闭嘴。
|
|
323
|
+
from foxclaw.finance.models import Transaction as _T
|
|
324
|
+
def _jul(day, amt): # 落在 2026-07 的一笔支出
|
|
325
|
+
return _T(amount=-amt, category="food", entity="午饭", account="default",
|
|
326
|
+
timestamp=f"2026-07-{day:02d}T12:00:00")
|
|
327
|
+
food_budget = Budget(category="food", monthly_limit=1000)
|
|
328
|
+
# 月初第 2 天,只花了 100:daily_rate=50 → 外推 1500 > 1000,老逻辑会喊"将超支"
|
|
329
|
+
early = FR.overspend_forecast([_jul(1, 100)], food_budget,
|
|
330
|
+
now=datetime(2026, 7, 2))
|
|
331
|
+
assert early["will_overspend"] and not early["reliable"], early
|
|
332
|
+
assert "overspend_trend_alert" not in _fire(forecasts=[early]), "月初噪音不得报警(A 修复)"
|
|
333
|
+
# 到第 20 天,同样趋势才可信
|
|
334
|
+
late = FR.overspend_forecast([_jul(d, 100) for d in range(1, 16)], food_budget,
|
|
335
|
+
now=datetime(2026, 7, 20))
|
|
336
|
+
assert late["reliable"], late
|
|
337
|
+
print("[18]【A】月初(day<7)超支外推 reliable=False → 不误报;7 天后才裁决 OK")
|
|
338
|
+
|
|
339
|
+
# [19]【C】续订提醒阈值真正生效。检测层窗口放宽到 35 天,"提前几天提醒"由规则
|
|
340
|
+
# threshold 精确控制。旧代码里 detect 硬封 7 天 → 用户改阈值到 25 也没反应。
|
|
341
|
+
monthly_sub = [_txn(-30, "月刊会员", days_ago=10 + 30, tags=["recurring"]),
|
|
342
|
+
_txn(-30, "月刊会员", days_ago=10, tags=["recurring"])]
|
|
343
|
+
ren = FR.detect_upcoming_renewals(monthly_sub) # 下次在约 20 天后
|
|
344
|
+
assert ren and ren[0]["cadence"] == "月订" and 18 <= ren[0]["days_until"] <= 22, ren
|
|
345
|
+
assert "renewal_reminder" not in _fire(renewals=ren), "默认提前 7 天 → 20 天后的续订还太远,不报"
|
|
346
|
+
p19 = _policy() # 用户想提前 25 天提醒
|
|
347
|
+
for r in p19.rules:
|
|
348
|
+
if r.id == "renewal_reminder": r.threshold = 25.0
|
|
349
|
+
v19 = {x.rule_id for x in p19.evaluate_state(renewals=ren)}
|
|
350
|
+
assert "renewal_reminder" in v19, "阈值改到 25 应生效(旧代码被检测层硬封 7 天 → 静默失效)"
|
|
351
|
+
print("[19]【C】续订阈值真正生效(默认7天不报20天后的;改25天则报)OK")
|
|
352
|
+
|
|
353
|
+
# [20]【D】退款消耗式匹配:两笔同额 refund_expected、只收到一笔退款 → 第二笔仍报。
|
|
354
|
+
# 旧的 any() 匹配让两笔支出都认领同一笔入账 → 第二笔漏报(以为退了,其实没到)。
|
|
355
|
+
two_expected = [
|
|
356
|
+
_txn(-300, "某电商", days_ago=40, tags=["refund_expected"]),
|
|
357
|
+
_txn(-300, "某电商", days_ago=30, tags=["refund_expected"]),
|
|
358
|
+
_txn(+300, "某电商", days_ago=5), # 只到账一笔退款
|
|
359
|
+
]
|
|
360
|
+
pend = FR.detect_pending_refunds(two_expected)
|
|
361
|
+
assert len(pend) == 1, f"两笔标记只退一笔 → 应恰好报 1 笔未到账,实得 {len(pend)}: {pend}"
|
|
362
|
+
# 两笔都到账 → 零报警(两笔入账各抵一笔)
|
|
363
|
+
both_back = two_expected[:2] + [_txn(+300, "某电商", days_ago=5),
|
|
364
|
+
_txn(+300, "某电商", days_ago=4)]
|
|
365
|
+
assert FR.detect_pending_refunds(both_back) == [], "两笔退款都到账不该报"
|
|
366
|
+
print("[20]【D】退款消耗式匹配(两笔同额退一笔→报第二笔;都退→不报)OK")
|
|
367
|
+
|
|
368
|
+
# [21]【E】并发校准无竞态、yaml 不写坏。finance_runtime 是单例,analyze() 会被请求
|
|
369
|
+
# 线程并发调 → apply_calibration 并发改规则+写盘。写锁 + 原子 rename 保证安全。
|
|
370
|
+
import threading as _th
|
|
371
|
+
from foxclaw.finance.policy import FinancePolicyEngine
|
|
372
|
+
rt21 = FinanceRuntime(policy_path=tempfile.mktemp(suffix=".yaml"),
|
|
373
|
+
data_path=tempfile.mktemp(suffix=".json"))
|
|
374
|
+
for t in _real():
|
|
375
|
+
rt21.add_transaction(t, run_gatekeeper=False)
|
|
376
|
+
errs21, seen_to = [], []
|
|
377
|
+
_lk = _th.Lock()
|
|
378
|
+
def _hammer():
|
|
379
|
+
try:
|
|
380
|
+
rep = rt21.analyze()
|
|
381
|
+
with _lk:
|
|
382
|
+
seen_to.append(tuple(sorted((c["rule_id"], c["to"])
|
|
383
|
+
for c in rep["threshold_calibration"])))
|
|
384
|
+
except Exception as e:
|
|
385
|
+
with _lk: errs21.append(repr(e))
|
|
386
|
+
_ts = [_th.Thread(target=_hammer) for _ in range(16)]
|
|
387
|
+
for t in _ts: t.start()
|
|
388
|
+
for t in _ts: t.join()
|
|
389
|
+
assert not errs21, f"并发 analyze/校准抛异常: {errs21[:2]}"
|
|
390
|
+
# 恰好一个线程做了首次校准(拿到非空 changed),其余读到已校准状态(空)
|
|
391
|
+
assert sum(1 for s in seen_to if s) == 1, \
|
|
392
|
+
f"首次校准应只发生一次(其余幂等空),实得 {sum(1 for s in seen_to if s)} 次非空"
|
|
393
|
+
# yaml 落盘完好可重载,且阈值 = 校准值(没被并发写坏)
|
|
394
|
+
reload21 = FinancePolicyEngine(str(rt21.policy.policy_path))
|
|
395
|
+
r21 = [r for r in reload21.rules if r.id == "large_purchase_check"][0]
|
|
396
|
+
assert r21.threshold == cal_large and r21.source == "learned", \
|
|
397
|
+
f"并发后 yaml 落盘损坏或值不对: threshold={r21.threshold}"
|
|
398
|
+
print(f"[21]【E】16 线程并发校准:无异常 · 首次只一次 · yaml 完好(large={r21.threshold:.0f})OK")
|
|
399
|
+
|
|
400
|
+
print("\n领域推理评测通过")
|
|
401
|
+
|
|
402
|
+
if __name__ == "__main__":
|
|
403
|
+
test_domain_evals()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""OS 领域导入超越 csv:三领域(finance/learning/itinerary)支持上传 csv / xlsx / json。
|
|
2
|
+
多格式统一解析成 rows → 桥接临时 CSV → 复用各运行时 import_csv(唯一转换真源)。"""
|
|
3
|
+
import os, sys, io, json, tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_domain_import():
|
|
9
|
+
os.environ["HKC_EMBEDDING"] = "stub"
|
|
10
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
11
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_import_")
|
|
12
|
+
|
|
13
|
+
import openpyxl
|
|
14
|
+
|
|
15
|
+
print("=" * 60); print("OS 领域导入(csv/xlsx/json)验证"); print("=" * 60)
|
|
16
|
+
|
|
17
|
+
# ── Part A:解析器单元 ─────────────────────────────────────
|
|
18
|
+
from foxclaw.api import _parse_import_rows
|
|
19
|
+
|
|
20
|
+
csv_bytes = b"amount,category,entity\n-50,food,cafe\n-20,transport,metro\n"
|
|
21
|
+
rows = _parse_import_rows("t.csv", csv_bytes)
|
|
22
|
+
assert len(rows) == 2 and rows[0]["amount"] == "-50" and rows[0]["category"] == "food", rows
|
|
23
|
+
json_bytes = json.dumps([{"amount": -30, "category": "book"}]).encode()
|
|
24
|
+
assert _parse_import_rows("t.json", json_bytes)[0]["amount"] == -30
|
|
25
|
+
assert _parse_import_rows("t.json", b'{"rows":[{"amount":1}]}')[0]["amount"] == 1
|
|
26
|
+
|
|
27
|
+
wb = openpyxl.Workbook(); ws = wb.active
|
|
28
|
+
ws.append(["amount", "category", "entity"]); ws.append([-15, "coffee", "cafe"]); ws.append([-8, "snack", "shop"])
|
|
29
|
+
buf = io.BytesIO(); wb.save(buf); xlsx_bytes = buf.getvalue()
|
|
30
|
+
xr = _parse_import_rows("t.xlsx", xlsx_bytes)
|
|
31
|
+
assert len(xr) == 2 and str(xr[0]["amount"]) == "-15" and xr[0]["category"] == "coffee", xr
|
|
32
|
+
try:
|
|
33
|
+
_parse_import_rows("t.pptx", b"x"); raise AssertionError("应对未支持格式报错")
|
|
34
|
+
except ValueError:
|
|
35
|
+
pass
|
|
36
|
+
print("[1] 解析器 csv/json/xlsx + 未支持格式报错 OK")
|
|
37
|
+
|
|
38
|
+
# ── Part B:三领域文件导入端点(TestClient)────────────────
|
|
39
|
+
from fastapi.testclient import TestClient
|
|
40
|
+
from foxclaw.api import app
|
|
41
|
+
|
|
42
|
+
with TestClient(app) as c:
|
|
43
|
+
# [2] finance:csv / xlsx / json 三格式各导入成功
|
|
44
|
+
r = c.post("/api/finance/import/file", files={"file": ("f.csv", csv_bytes, "text/csv")})
|
|
45
|
+
assert r.status_code == 200 and r.json()["imported"] == 2, r.text
|
|
46
|
+
r = c.post("/api/finance/import/file",
|
|
47
|
+
files={"file": ("f.xlsx", xlsx_bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")})
|
|
48
|
+
assert r.status_code == 200 and r.json()["imported"] == 2, r.text
|
|
49
|
+
r = c.post("/api/finance/import/file",
|
|
50
|
+
files={"file": ("f.json", json.dumps([{"amount": -30, "category": "book", "entity": "store"}]).encode(), "application/json")})
|
|
51
|
+
assert r.status_code == 200 and r.json()["imported"] == 1, r.text
|
|
52
|
+
print("[2] finance 导入 csv/xlsx/json(2+2+1)OK")
|
|
53
|
+
|
|
54
|
+
# [3] learning:csv(content,topic,tags)
|
|
55
|
+
lc = "content,topic,tags\n间隔重复提升记忆,学习方法,记忆|复习\n费曼技巧,学习方法,输出\n".encode()
|
|
56
|
+
r = c.post("/api/learning/import/file", files={"file": ("l.csv", lc, "text/csv")})
|
|
57
|
+
assert r.status_code == 200 and r.json()["imported"] == 2, r.text
|
|
58
|
+
print("[3] learning 导入 csv(含 tags 管道分隔)OK")
|
|
59
|
+
|
|
60
|
+
# [4] itinerary:json(name/city/duration)
|
|
61
|
+
ij = json.dumps([{"name": "故宫", "city": "北京", "duration_min": 120},
|
|
62
|
+
{"name": "颐和园", "city": "北京", "duration_min": 90}]).encode()
|
|
63
|
+
r = c.post("/api/itinerary/import/file", files={"file": ("i.json", ij, "application/json")})
|
|
64
|
+
assert r.status_code == 200 and r.json()["imported"] == 2, r.text
|
|
65
|
+
print("[4] itinerary 导入 json OK")
|
|
66
|
+
|
|
67
|
+
# [5] 未支持格式 → 400;空文件 → imported 0
|
|
68
|
+
r = c.post("/api/finance/import/file", files={"file": ("x.pptx", b"x", "application/octet-stream")})
|
|
69
|
+
assert r.status_code == 400, r.status_code
|
|
70
|
+
r = c.post("/api/finance/import/file", files={"file": ("e.csv", b"amount,category\n", "text/csv")})
|
|
71
|
+
assert r.status_code == 200 and r.json()["imported"] == 0, r.text
|
|
72
|
+
print("[5] 未支持格式→400;空数据→imported 0 OK")
|
|
73
|
+
|
|
74
|
+
print("\nOS 领域导入(csv/xlsx/json)验证通过")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
test_domain_import()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
领域认知化推广(v2 ③ Step4:itinerary / learning 复用 DomainReasoner)—— 全离线。
|
|
3
|
+
|
|
4
|
+
同款双层安全不变量(与 finance 一致):
|
|
5
|
+
· 规则永远权威:检出硬冲突/高遗忘风险 → confirm,**绝不被 LLM 推翻/隐藏**,且不问 LLM;
|
|
6
|
+
· 可选 LLM 层默认关,只对"无硬问题、但紧凑/负荷重"的微妙情形**升级** approve→confirm;
|
|
7
|
+
· LLM 结构上无从产生"推翻规则"结果(helper 只返回 escalate/keep);
|
|
8
|
+
· 顾问域 fail-safe:LLM 出错/stub/默认关 → 维持规则裁决(approve),不制造确认噪声;
|
|
9
|
+
· 每个裁决带证据链(可审计)。
|
|
10
|
+
"""
|
|
11
|
+
import os, sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
14
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
15
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _MockLLM:
|
|
20
|
+
"""可配置回答的假 LLM(is_stub=False 表示"有真实模型")。"""
|
|
21
|
+
def __init__(self, resp="OK", raises=False):
|
|
22
|
+
self.resp, self.raises, self.calls, self.is_stub = resp, raises, 0, False
|
|
23
|
+
def complete(self, system, messages, max_tokens=2048, agent_id=""):
|
|
24
|
+
self.calls += 1
|
|
25
|
+
if self.raises:
|
|
26
|
+
raise RuntimeError("llm down")
|
|
27
|
+
return self.resp
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ─────────────────────────── 行程冲突推理 ───────────────────────────
|
|
31
|
+
def _wp(name, start=None, dur=0, lng=None, lat=None):
|
|
32
|
+
from foxclaw.itinerary.models import Waypoint
|
|
33
|
+
return Waypoint(name=name, start_time=start, duration_min=dur, lng=lng, lat=lat)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_itinerary_reasoner():
|
|
37
|
+
print("\n【1】行程冲突推理:规则权威 + 可选 LLM 只升级")
|
|
38
|
+
from foxclaw.itinerary.reasoner import ItineraryConflictReasoner
|
|
39
|
+
# 时间重叠 → confirm,规则权威;即便 LLM 说 OK 也不被推翻,且不问 LLM
|
|
40
|
+
conflict = [_wp("会议A", "2026-07-08T10:00:00", 120), _wp("会议B", "2026-07-08T11:00:00", 60)]
|
|
41
|
+
llm = _MockLLM("OK")
|
|
42
|
+
rv = ItineraryConflictReasoner(llm=llm, llm_enabled=True).reason((conflict, "driving"))
|
|
43
|
+
assert rv.decision == "confirm" and rv.source == "rules" and llm.calls == 0
|
|
44
|
+
assert any(e.factor == "time_conflict" for e in rv.evidence)
|
|
45
|
+
|
|
46
|
+
# 无冲突、不紧凑(空档 60 分 >> 通勤 ~19 分)→ approve,LLM 不调
|
|
47
|
+
loose = [_wp("A", "2026-07-08T10:00:00", 60, 116.40, 39.90),
|
|
48
|
+
_wp("B", "2026-07-08T12:00:00", 60, 116.50, 39.90)]
|
|
49
|
+
llm = _MockLLM("CONFIRM")
|
|
50
|
+
rv = ItineraryConflictReasoner(llm=llm, llm_enabled=True).reason((loose, "driving"))
|
|
51
|
+
assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
|
|
52
|
+
|
|
53
|
+
# 紧凑(空档 25 分,通勤 ~19 分,可达但赶)+ LLM 说 CONFIRM → confirm,source=llm
|
|
54
|
+
tight = [_wp("A", "2026-07-08T10:00:00", 60, 116.40, 39.90),
|
|
55
|
+
_wp("B", "2026-07-08T11:25:00", 60, 116.50, 39.90)]
|
|
56
|
+
llm = _MockLLM("CONFIRM")
|
|
57
|
+
rv = ItineraryConflictReasoner(llm=llm, llm_enabled=True).reason((tight, "driving"))
|
|
58
|
+
assert rv.decision == "confirm" and rv.source == "llm" and llm.calls == 1
|
|
59
|
+
|
|
60
|
+
# 紧凑 + LLM 说 OK → 维持规则 approve
|
|
61
|
+
llm = _MockLLM("OK")
|
|
62
|
+
rv = ItineraryConflictReasoner(llm=llm, llm_enabled=True).reason((tight, "driving"))
|
|
63
|
+
assert rv.decision == "approve" and rv.source == "rules"
|
|
64
|
+
|
|
65
|
+
# 紧凑 + LLM 出错 → fail-safe 维持规则 approve(顾问域不因抖动制造确认)
|
|
66
|
+
llm = _MockLLM(raises=True)
|
|
67
|
+
rv = ItineraryConflictReasoner(llm=llm, llm_enabled=True).reason((tight, "driving"))
|
|
68
|
+
assert rv.decision == "approve" and rv.source == "rules"
|
|
69
|
+
|
|
70
|
+
# 默认关 → 紧凑也只走规则(behavior-preserving)
|
|
71
|
+
rv = ItineraryConflictReasoner().reason((tight, "driving"))
|
|
72
|
+
assert rv.decision == "approve" and rv.source == "rules"
|
|
73
|
+
print(" ✓ 冲突→confirm(规则,不问LLM);紧凑+LLM升级→confirm(llm);OK/出错/默认关→approve(规则)")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ─────────────────────────── 学习复习推理 ───────────────────────────
|
|
77
|
+
def _rs(item_id, rep, ease, due_days):
|
|
78
|
+
from datetime import datetime, timedelta
|
|
79
|
+
from foxclaw.learning.models import ReviewState
|
|
80
|
+
return ReviewState(item_id=item_id, repetitions=rep, ease_factor=ease,
|
|
81
|
+
due_date=(datetime.now() + timedelta(days=due_days)).isoformat())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_learning_reasoner():
|
|
85
|
+
print("\n【2】学习复习推理:规则权威 + 可选 LLM 只升级")
|
|
86
|
+
from foxclaw.learning.reasoner import LearningReviewReasoner
|
|
87
|
+
# 高遗忘风险(掌握度 0、已到期)→ confirm,规则权威,不问 LLM
|
|
88
|
+
risky = [_rs("weak", 0, 1.3, 0)]
|
|
89
|
+
llm = _MockLLM("OK")
|
|
90
|
+
rv = LearningReviewReasoner(llm=llm, llm_enabled=True).reason(risky)
|
|
91
|
+
assert rv.decision == "confirm" and rv.source == "rules" and llm.calls == 0
|
|
92
|
+
assert any(e.factor == "forgetting_risk" for e in rv.evidence)
|
|
93
|
+
|
|
94
|
+
# 健康(高掌握、远期到期)→ approve,LLM 不调
|
|
95
|
+
healthy = [_rs("strong", 8, 2.5, 30)]
|
|
96
|
+
llm = _MockLLM("CONFIRM")
|
|
97
|
+
rv = LearningReviewReasoner(llm=llm, llm_enabled=True).reason(healthy)
|
|
98
|
+
assert rv.decision == "approve" and rv.source == "rules" and llm.calls == 0
|
|
99
|
+
|
|
100
|
+
# 负荷偏重但无高风险(150 项高掌握、近期到期,日均≈21≥20)+ LLM CONFIRM → confirm,source=llm
|
|
101
|
+
heavy = [_rs(f"h{i}", 8, 2.5, 3) for i in range(150)]
|
|
102
|
+
llm = _MockLLM("CONFIRM")
|
|
103
|
+
rv = LearningReviewReasoner(llm=llm, llm_enabled=True).reason(heavy)
|
|
104
|
+
assert rv.decision == "confirm" and rv.source == "llm" and llm.calls == 1
|
|
105
|
+
|
|
106
|
+
# 负荷重 + LLM OK → approve;+ LLM 出错 → fail-safe approve
|
|
107
|
+
rv = LearningReviewReasoner(llm=_MockLLM("OK"), llm_enabled=True).reason(heavy)
|
|
108
|
+
assert rv.decision == "approve" and rv.source == "rules"
|
|
109
|
+
rv = LearningReviewReasoner(llm=_MockLLM(raises=True), llm_enabled=True).reason(heavy)
|
|
110
|
+
assert rv.decision == "approve" and rv.source == "rules"
|
|
111
|
+
|
|
112
|
+
# 默认关 + stub → 只走规则
|
|
113
|
+
rv = LearningReviewReasoner().reason(heavy)
|
|
114
|
+
assert rv.decision == "approve" and rv.source == "rules"
|
|
115
|
+
stub = _MockLLM("CONFIRM"); stub.is_stub = True
|
|
116
|
+
rv = LearningReviewReasoner(llm=stub, llm_enabled=True).reason(heavy)
|
|
117
|
+
assert rv.source == "rules" and stub.calls == 0
|
|
118
|
+
print(" ✓ 高风险→confirm(规则,不问LLM);负荷重+LLM升级→confirm(llm);OK/出错/默认/stub→approve(规则)")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_llm_cannot_hide_hard_problems():
|
|
122
|
+
print("\n【3】核心不变量:LLM 永远无法隐藏规则已检出的硬问题")
|
|
123
|
+
from foxclaw.itinerary.reasoner import ItineraryConflictReasoner
|
|
124
|
+
from foxclaw.learning.reasoner import LearningReviewReasoner
|
|
125
|
+
# 即使 LLM 疯狂回 "OK/APPROVE/一切正常",冲突/风险仍被规则裁为 confirm
|
|
126
|
+
for bad in ["OK", "APPROVE", "一切正常没问题", ""]:
|
|
127
|
+
conflict = [_wp("A", "2026-07-08T10:00:00", 120), _wp("B", "2026-07-08T11:00:00", 60)]
|
|
128
|
+
rv = ItineraryConflictReasoner(llm=_MockLLM(bad), llm_enabled=True).reason((conflict, "driving"))
|
|
129
|
+
assert rv.decision == "confirm" and rv.source == "rules", (bad, rv.decision)
|
|
130
|
+
rv = LearningReviewReasoner(llm=_MockLLM(bad), llm_enabled=True).reason([_rs("w", 0, 1.3, 0)])
|
|
131
|
+
assert rv.decision == "confirm" and rv.source == "rules", (bad, rv.decision)
|
|
132
|
+
print(" ✓ 任何 LLM 回答都无法把已检出的冲突/高风险降级为 approve")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
print("=" * 60)
|
|
137
|
+
print("领域认知化推广 · itinerary + learning(v2 ③ Step4)")
|
|
138
|
+
print("=" * 60)
|
|
139
|
+
test_itinerary_reasoner()
|
|
140
|
+
test_learning_reasoner()
|
|
141
|
+
test_llm_cannot_hide_hard_problems()
|
|
142
|
+
print("\n" + "=" * 60)
|
|
143
|
+
print("✓ Step4:两域复用 DomainReasoner;规则权威、LLM 只增强、证据链可审计")
|
|
144
|
+
print("=" * 60)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|