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,177 @@
|
|
|
1
|
+
"""P1-1 · 主管 pipeline 化回归。
|
|
2
|
+
|
|
3
|
+
主管不再重写 run_task,路由分发拆成 8 个 pipeline stage
|
|
4
|
+
(recall→meta_check→schedule→route→plan→dispatch→replan→aggregate),走 base 统一模板
|
|
5
|
+
`_run_task_core`。本文件锁定:
|
|
6
|
+
|
|
7
|
+
[1] 结构:SupervisorAgent.run_task 即基类模板;主管 pipeline 是 SUPERVISOR_STAGES;
|
|
8
|
+
[2] 元问题短路:meta_check 自答,后续 stage 跳过,内部标记不外泄;
|
|
9
|
+
[3] 单 Agent 分发:返回体顶层键与旧实现逐字一致(result/schedule/plan/process_id);
|
|
10
|
+
[4] stub 兜底:无关键词命中 + stub → 兜底派发 assistant(不经自答);
|
|
11
|
+
[5] 统一 execution.completed:多步并行任务也只发**一条**任务级事件、
|
|
12
|
+
agent_id=supervisor(文稿 Step 4:删手动 bus.publish);blocked 子任务 →
|
|
13
|
+
顶层 status=blocked 且执行事件 blocked=True(未完成不沉淀真值);
|
|
14
|
+
[6] 流式:run_task_stream 逐 stage 出 8 个进度事件;子 Agent 的 act token 随
|
|
15
|
+
contextvar 传播流出(delta 非空);stop_event 预置 → interrupted;
|
|
16
|
+
[7] 恢复记账:经 base 生命周期钩子,started/finished 配对、异常路径记 failed。
|
|
17
|
+
|
|
18
|
+
全离线:ws.needs_search 关掉联网;router 强制 stub(共享实例,五个 Agent 同源)。
|
|
19
|
+
"""
|
|
20
|
+
import asyncio
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import threading
|
|
24
|
+
import types
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
27
|
+
|
|
28
|
+
import foxclaw.core.web_search as ws
|
|
29
|
+
ws.needs_search = lambda q: False # 自答/兜底路径不触发联网,保持离线
|
|
30
|
+
|
|
31
|
+
from foxclaw import Foxclaw
|
|
32
|
+
from foxclaw.agents.base import CognitiveAgent, HarnessBlockedError
|
|
33
|
+
from foxclaw.agents.agents import SupervisorAgent
|
|
34
|
+
from foxclaw.pipeline import SUPERVISOR_STAGES
|
|
35
|
+
|
|
36
|
+
EXPECTED_STAGES = ["recall", "meta_check", "schedule", "route", "plan", "dispatch", "replan", "aggregate"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check(cond, msg):
|
|
40
|
+
if not cond:
|
|
41
|
+
raise AssertionError(f"✗ {msg}")
|
|
42
|
+
print(f" ✓ {msg}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _names():
|
|
46
|
+
return [getattr(s, "stage_name", getattr(s, "__name__", "?")) for s in SUPERVISOR_STAGES]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
print("=" * 60)
|
|
51
|
+
print("P1-1 · 主管 pipeline 化回归")
|
|
52
|
+
print("=" * 60)
|
|
53
|
+
|
|
54
|
+
print("\n【1】结构:主管走统一模板,不再重写 run_task")
|
|
55
|
+
check(SupervisorAgent.run_task is CognitiveAgent.run_task,
|
|
56
|
+
"SupervisorAgent.run_task 即基类模板(P1-1 达成)")
|
|
57
|
+
check(SupervisorAgent._cognitive_pipeline is not CognitiveAgent._cognitive_pipeline,
|
|
58
|
+
"主管覆盖 _cognitive_pipeline(路由分发走 SUPERVISOR_STAGES)")
|
|
59
|
+
check(_names() == EXPECTED_STAGES, f"主管 stage 序列={EXPECTED_STAGES}(实为 {_names()})")
|
|
60
|
+
|
|
61
|
+
os_ = Foxclaw(data_dir=tempfile.mkdtemp(prefix="foxclaw_svpipe_"))
|
|
62
|
+
os_.router.provider = "stub" # 共享 router,五 Agent 同源 → 全离线确定性
|
|
63
|
+
os_.router._client = None
|
|
64
|
+
sup = os_.supervisor
|
|
65
|
+
|
|
66
|
+
print("\n【2】元问题短路:meta_check 自答,内部标记不外泄")
|
|
67
|
+
r = asyncio.run(sup.run_task("你是谁,能做什么"))
|
|
68
|
+
res = r["result"]
|
|
69
|
+
check(res.get("dispatched_to") == "supervisor" and res.get("self_answered") is True,
|
|
70
|
+
"元问题 → 主管自答(dispatched_to=supervisor, self_answered=True)")
|
|
71
|
+
check("_meta_answered" not in r and "_self_answer" not in r,
|
|
72
|
+
"内部短路标记不外泄到返回体")
|
|
73
|
+
check(r.get("agent") == "supervisor" and "session_id" in r and "thought_summary" in r,
|
|
74
|
+
"返回体由 base 模板统一组装(agent/session_id/thought_summary)")
|
|
75
|
+
|
|
76
|
+
print("\n【3】单 Agent 分发:返回体顶层键与旧实现逐字一致")
|
|
77
|
+
r = asyncio.run(sup.run_task("帮我做个预算分析")) # "预算" 命中 finance
|
|
78
|
+
res = r["result"]
|
|
79
|
+
check(res.get("dispatched_to") == "finance", f"关键词命中 → finance(实为 {res.get('dispatched_to')})")
|
|
80
|
+
for k in ("schedule", "plan", "process_id"):
|
|
81
|
+
check(k in r, f"顶层键 {k} 保留(与旧主管返回体一致)")
|
|
82
|
+
check(r["plan"].get("multi_step") is False, "单步任务 plan.multi_step=False")
|
|
83
|
+
check(res.get("blocked") is False, "正常分发 blocked=False")
|
|
84
|
+
|
|
85
|
+
print("\n【4】stub 兜底:无关键词命中 + stub → 主管自处理(助手已下放为技能)")
|
|
86
|
+
r = asyncio.run(sup.run_task("解释一下什么是航天器"))
|
|
87
|
+
res = r["result"]
|
|
88
|
+
check(res.get("dispatched_to") == "supervisor" and not res.get("self_answered"),
|
|
89
|
+
f"stub 下未命中 → 主管自身处理(实为 {res.get('dispatched_to')})")
|
|
90
|
+
|
|
91
|
+
print("\n【5】统一 execution.completed(文稿 Step 4)")
|
|
92
|
+
spine = os_.event_spine
|
|
93
|
+
before = len(spine.history("execution.completed"))
|
|
94
|
+
asyncio.run(sup.run_task("同时查今天的日程、分析本月理财、总结这篇论文")) # 并行 3 子任务
|
|
95
|
+
evts = spine.history("execution.completed")
|
|
96
|
+
new_evts = evts[before:]
|
|
97
|
+
check(len(new_evts) == 1,
|
|
98
|
+
f"并行 3 子任务只发 1 条任务级 execution.completed(实为 {len(new_evts)} 条)"
|
|
99
|
+
"——逐子任务手动 publish 已删")
|
|
100
|
+
check(new_evts and new_evts[0]["payload"].get("agent_id") == "supervisor",
|
|
101
|
+
"任务级事件归因 supervisor(子 Agent 带 _no_emit 不双发)")
|
|
102
|
+
|
|
103
|
+
print("\n【5b】blocked 子任务 → 顶层 status=blocked + 事件 blocked=True(不沉淀真值)")
|
|
104
|
+
async def _boom(self, task, context, session_id, memories):
|
|
105
|
+
raise HarnessBlockedError(types.SimpleNamespace(reason="模拟越界"), None)
|
|
106
|
+
os_.finance._execute = types.MethodType(_boom, os_.finance)
|
|
107
|
+
before = len(spine.history("execution.completed"))
|
|
108
|
+
r = asyncio.run(sup.run_task("帮我做个预算分析"))
|
|
109
|
+
check(r["result"].get("blocked") is True, "子 Agent 被拦 → result.blocked=True")
|
|
110
|
+
check(r.get("status") == "blocked",
|
|
111
|
+
"被拦提升为顶层 status=blocked(与 base 模板 skill_status 机制一致)")
|
|
112
|
+
new_evts = spine.history("execution.completed")[before:]
|
|
113
|
+
check(len(new_evts) == 1 and new_evts[0]["payload"].get("blocked") is True,
|
|
114
|
+
"被拦任务的执行事件 blocked=True → 真值链不沉淀")
|
|
115
|
+
# 还原 finance,别污染后续用例
|
|
116
|
+
del os_.finance._execute
|
|
117
|
+
|
|
118
|
+
print("\n【6】流式:run_task_stream 八 stage + 子 Agent token 传播 + 中断")
|
|
119
|
+
events = list(sup.run_task_stream("帮我做个预算分析"))
|
|
120
|
+
stages = [e["stage"] for e in events if "stage" in e]
|
|
121
|
+
deltas = [e["delta"] for e in events if "delta" in e]
|
|
122
|
+
results = [e["result"] for e in events if "result" in e]
|
|
123
|
+
check(stages == EXPECTED_STAGES, f"流式逐 stage 进度=主管八 stage(实为 {stages})")
|
|
124
|
+
check(len(results) == 1 and results[0].get("agent") == "supervisor",
|
|
125
|
+
"恰一个最终 result,agent=supervisor")
|
|
126
|
+
check(results[0]["result"].get("dispatched_to") == "finance", "流式与非流式路由结论一致")
|
|
127
|
+
check(len(deltas) > 0,
|
|
128
|
+
"delta 非空:子 Agent 的 act token 随 _act_stream contextvar 传播流出(文稿坑5实测反转)")
|
|
129
|
+
check("".join(deltas) == str(results[0]["result"].get("sub_result")),
|
|
130
|
+
"流式 token 拼接 == 子 Agent 最终结果(透传忠实)")
|
|
131
|
+
|
|
132
|
+
ev = threading.Event(); ev.set()
|
|
133
|
+
ev_events = list(sup.run_task_stream("这个任务会被中断", stop_event=ev))
|
|
134
|
+
ev_results = [e["result"] for e in ev_events if "result" in e]
|
|
135
|
+
check(ev_results and ev_results[0].get("interrupted") is True,
|
|
136
|
+
"stop_event 预置 → result.interrupted=True(stage 边界优雅停)")
|
|
137
|
+
check(not [e for e in ev_events if "stage" in e], "起步即停:无 stage 进度事件")
|
|
138
|
+
|
|
139
|
+
print("\n【7】恢复记账:base 生命周期钩子,started/finished 配对")
|
|
140
|
+
calls = []
|
|
141
|
+
|
|
142
|
+
class _Spy:
|
|
143
|
+
def task_started(self, task_id, description, session_id, channel=""):
|
|
144
|
+
calls.append(("started", task_id))
|
|
145
|
+
def task_finished(self, sid, status="done"):
|
|
146
|
+
calls.append(("finished", sid, status))
|
|
147
|
+
|
|
148
|
+
sup._recovery = _Spy()
|
|
149
|
+
asyncio.run(sup.run_task("帮我做个预算分析", context={"channel": "test"}))
|
|
150
|
+
check(calls[0][0] == "started" and calls[1][0] == "finished" and calls[1][2] == "done",
|
|
151
|
+
f"正常任务:started→finished(done) 配对(实为 {calls})")
|
|
152
|
+
check(calls[0][1] == calls[1][1], "started/finished 同一 task_id(可配对)")
|
|
153
|
+
|
|
154
|
+
calls.clear()
|
|
155
|
+
async def _raise(self, task, context=None, **kw):
|
|
156
|
+
raise RuntimeError("子 Agent 崩溃")
|
|
157
|
+
sup._subagents["finance"].run_task = _raise
|
|
158
|
+
try:
|
|
159
|
+
asyncio.run(sup.run_task("帮我做个预算分析"))
|
|
160
|
+
raised = False
|
|
161
|
+
except RuntimeError:
|
|
162
|
+
raised = True
|
|
163
|
+
check(raised, "子 Agent 未兜底异常向上传播(不吞)")
|
|
164
|
+
check(calls[0][0] == "started" and calls[1] == ("finished", calls[0][1], "failed"),
|
|
165
|
+
f"异常任务记 failed(区别于正常完成)(实为 {calls})")
|
|
166
|
+
|
|
167
|
+
print("\n" + "=" * 60)
|
|
168
|
+
print("P1-1 主管 pipeline 化回归通过")
|
|
169
|
+
print("=" * 60)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_supervisor_pipeline():
|
|
173
|
+
main()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""主管分诊与自答策略回归。
|
|
2
|
+
|
|
3
|
+
覆盖主管在「是否命中领域关键词」×「模型是否可用」下的派发决策:
|
|
4
|
+
· 关键词命中 → 直接路由到领域 Agent(快、确定),不额外调用一次 LLM 分诊;
|
|
5
|
+
· 未命中 + 模型可用 → 主管**直接自答**(dispatched_to=supervisor, self_answered=True);
|
|
6
|
+
· 未命中 + stub → 走派发兜底(assistant),保持离线确定性。
|
|
7
|
+
|
|
8
|
+
⚠ 历史说明(改名前为 test_llm_routing.py):本文件曾断言「关键词未命中 → LLM 分诊精修到
|
|
9
|
+
finance」。那次精修实现在 `agents.py:_llm_classify`,但因每次要多一次约 5s 的串行 LLM
|
|
10
|
+
往返,产品上被「主管直接自答」取代而退化为**死代码**(全仓库零调用点),已随本轮删除。
|
|
11
|
+
原断言随之永远无法满足,长期为红,却被归因为「环境问题」——这是**误判**:它不是环境问题,
|
|
12
|
+
是**测试过期**。现按真实行为重写,并把三种组合都锁住。
|
|
13
|
+
|
|
14
|
+
用"假 router"模拟真机(is_stub=False),避免依赖真实 API key,全离线。
|
|
15
|
+
"""
|
|
16
|
+
import asyncio
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
|
+
|
|
22
|
+
import foxclaw.core.web_search as ws
|
|
23
|
+
ws.needs_search = lambda q: False # 自答路径不触发联网,保持离线
|
|
24
|
+
|
|
25
|
+
from foxclaw import Foxclaw
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check(cond, msg):
|
|
29
|
+
if not cond:
|
|
30
|
+
raise AssertionError(f"✗ {msg}")
|
|
31
|
+
print(f" ✓ {msg}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 分诊 prompt 的特征串。历史上 _llm_classify 用它问模型;现已无此路径,
|
|
35
|
+
# 故断言它**从不出现**,即「不再为分诊多花一次 LLM 往返」。
|
|
36
|
+
CLASSIFY_MARK = "只输出一个词"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
print("=" * 56)
|
|
41
|
+
print("主管分诊与自答策略回归")
|
|
42
|
+
print("=" * 56)
|
|
43
|
+
os_ = Foxclaw(data_dir=tempfile.mkdtemp())
|
|
44
|
+
|
|
45
|
+
calls = []
|
|
46
|
+
|
|
47
|
+
def fake(system, messages, max_tokens=2048, agent_id=""):
|
|
48
|
+
content = str(messages[-1]["content"]) if messages else ""
|
|
49
|
+
calls.append(content)
|
|
50
|
+
if CLASSIFY_MARK in content: # 若真有人恢复分诊,这里会暴露
|
|
51
|
+
return "finance"
|
|
52
|
+
return "已处理"
|
|
53
|
+
|
|
54
|
+
# 让路由认为模型可用(非 stub)
|
|
55
|
+
os_.router.provider = "deepseek"
|
|
56
|
+
os_.router._client = object()
|
|
57
|
+
os_.router.complete = fake
|
|
58
|
+
check(os_.router.is_stub is False, "router 处于'可用'状态(非 stub)")
|
|
59
|
+
|
|
60
|
+
print("\n【1】关键词未命中 + 模型可用 → 主管直接自答(不再二次分诊)")
|
|
61
|
+
# 这句不含任何领域关键词,也不像"花了钱"的报备(没有完成态动词),
|
|
62
|
+
# 所以仍然走自答 —— 花费报备现在已算作财务命中,见【1b】。
|
|
63
|
+
no_kw = "讲个笑话听听"
|
|
64
|
+
check(os_.supervisor._has_keyword_hit(no_kw) is False, f"「{no_kw[:12]}…」确无关键词命中")
|
|
65
|
+
r = asyncio.run(os_.supervisor.run_task(no_kw))
|
|
66
|
+
d1 = r["result"].get("dispatched_to")
|
|
67
|
+
check(d1 == "supervisor", f"未命中 → 主管自答(dispatched_to={d1},预期 supervisor)")
|
|
68
|
+
check(r["result"].get("self_answered") is True, "标记 self_answered=True(确为自答而非派发)")
|
|
69
|
+
check(bool(str(r["result"].get("sub_result") or "").strip()), "自答产出非空答复")
|
|
70
|
+
check(not any(CLASSIFY_MARK in c for c in calls), "全程未发出分诊 prompt(不再多花一次 LLM 往返)")
|
|
71
|
+
|
|
72
|
+
print("\n【1b】零星花费报备(无财务关键词)→ 认作财务命中,派给财务")
|
|
73
|
+
for line in ("今天买了一件衣服花了105块", "后天花了100洗车"):
|
|
74
|
+
check(os_.supervisor._has_keyword_hit(line), f"「{line[:12]}…」算作领域命中")
|
|
75
|
+
check(os_.supervisor._route(line) == "finance", f"「{line[:12]}…」→ finance")
|
|
76
|
+
# 讨论额度/比例不算报备,但仍属于财务问题 → 也交财务,而不是主管自答
|
|
77
|
+
check(os_.supervisor._route("每月房租3000占收入10000这样合理吗") == "finance",
|
|
78
|
+
"房租占比咨询 → finance")
|
|
79
|
+
|
|
80
|
+
print("\n【2】关键词命中 → 直接路由,不问 LLM 分诊(省成本)")
|
|
81
|
+
calls.clear()
|
|
82
|
+
r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析")) # "预算" 命中 finance
|
|
83
|
+
d2 = r2["result"].get("dispatched_to")
|
|
84
|
+
check(d2 == "finance", f"命中'预算' → 直接 finance(实为 {d2})")
|
|
85
|
+
check(not any(CLASSIFY_MARK in c for c in calls), "关键词命中时不发出分诊 prompt")
|
|
86
|
+
|
|
87
|
+
print("\n【3】stub + 关键词未命中 → 主管自处理(离线确定性)")
|
|
88
|
+
os_.router.provider = "stub"
|
|
89
|
+
os_.router._client = None
|
|
90
|
+
calls.clear()
|
|
91
|
+
r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器")) # 无关键词
|
|
92
|
+
check(os_.router.is_stub is True, "router 回到 stub")
|
|
93
|
+
d3 = r3["result"].get("dispatched_to")
|
|
94
|
+
check(d3 == "supervisor", f"stub 下未命中 → 主管自身处理(实为 {d3})")
|
|
95
|
+
check(not r3["result"].get("self_answered"), "stub 下走派发路径,而非主管自答")
|
|
96
|
+
check(not any(CLASSIFY_MARK in c for c in calls), "stub 下不发出分诊 prompt")
|
|
97
|
+
|
|
98
|
+
print("\n" + "=" * 56)
|
|
99
|
+
print("主管分诊与自答策略回归通过")
|
|
100
|
+
print("=" * 56)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_supervisor_triage():
|
|
104
|
+
main()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据污点追踪 + 动态审批门槛测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
对齐 Meta Muse「eBPF 污点追踪」的软件版:会话一旦取回用户/敏感数据就打污点,
|
|
5
|
+
之后外发/副作用动作的审批门槛动态提高。覆盖:
|
|
6
|
+
1. injection_guard.taint_level:来源 → 敏感度分级。
|
|
7
|
+
2. permission.evaluate 污点门槛:默认关闭(行为不变)/ 达标+外发 → TAINT_CONFIRM /
|
|
8
|
+
达标+只读 → 不触发 / 未达标 → 不触发。
|
|
9
|
+
3. base 污点传播:_dispatch_tool 取回 memory/web_search 后会话污点单调上升;
|
|
10
|
+
会话开始 _taint_reset 清零。
|
|
11
|
+
"""
|
|
12
|
+
import os, sys, types
|
|
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.core import injection_guard as IG
|
|
20
|
+
from foxclaw.skills import permission as PERM
|
|
21
|
+
from foxclaw.agents.base import (
|
|
22
|
+
CognitiveAgent, _taint_get, _taint_raise, _taint_reset,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _skill(side_effect="read", read_only=True, external=False, destructive=False):
|
|
27
|
+
"""构造一个 permission.evaluate 可用的最小 skill-spec(duck-typed)。"""
|
|
28
|
+
return types.SimpleNamespace(
|
|
29
|
+
id=f"tool:test_{side_effect}",
|
|
30
|
+
params={},
|
|
31
|
+
side_effect=side_effect,
|
|
32
|
+
read_only=read_only,
|
|
33
|
+
external=external,
|
|
34
|
+
destructive=destructive,
|
|
35
|
+
trust_level=lambda: "high",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── 1. taint_level 分级 ───────────────────────────────────────────────
|
|
40
|
+
def test_taint_level_grading():
|
|
41
|
+
assert IG.taint_level("web_search") == IG.TAINT_EXTERNAL == 1
|
|
42
|
+
assert IG.taint_level("knowledge") == IG.TAINT_PERSONAL == 2
|
|
43
|
+
assert IG.taint_level("file") == IG.TAINT_PERSONAL == 2
|
|
44
|
+
assert IG.taint_level("memory") == IG.TAINT_SENSITIVE == 3
|
|
45
|
+
assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL # 未知来源不低估
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── 2. permission 污点门槛 ─────────────────────────────────────────────
|
|
49
|
+
def test_permission_taint_default_off_behavior_unchanged():
|
|
50
|
+
# 不传污点参数(默认关闭)→ 外发工具走原有 Stage 4 副作用确认
|
|
51
|
+
s = _skill(side_effect="send", read_only=False)
|
|
52
|
+
d = PERM.evaluate(s, {})
|
|
53
|
+
assert d.decision == "ask" and d.reason_code == PERM.SIDE_EFFECT_CONFIRM
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_permission_taint_blocks_send_when_sensitive():
|
|
57
|
+
s = _skill(side_effect="send", read_only=False)
|
|
58
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_SENSITIVE,
|
|
59
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
60
|
+
assert d.decision == "ask" and d.reason_code == PERM.TAINT_CONFIRM
|
|
61
|
+
assert d.requires_confirmation
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_permission_taint_blocks_external_when_personal():
|
|
65
|
+
s = _skill(side_effect="read", read_only=False, external=True)
|
|
66
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_PERSONAL,
|
|
67
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
68
|
+
assert d.decision == "ask" and d.reason_code == PERM.TAINT_CONFIRM
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_permission_taint_not_trigger_on_readonly():
|
|
72
|
+
s = _skill(side_effect="read", read_only=True)
|
|
73
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_SENSITIVE,
|
|
74
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
75
|
+
assert d.decision == "allow"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_permission_taint_not_trigger_below_threshold():
|
|
79
|
+
s = _skill(side_effect="send", read_only=False)
|
|
80
|
+
# 污点只有 EXTERNAL(1) < 阈值 PERSONAL(2)→ 不因污点拦,走副作用确认
|
|
81
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_EXTERNAL,
|
|
82
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
83
|
+
assert d.reason_code == PERM.SIDE_EFFECT_CONFIRM
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── 3. base 污点传播 ───────────────────────────────────────────────────
|
|
87
|
+
def test_taint_helpers_on_fake_agent():
|
|
88
|
+
a = types.SimpleNamespace(_think=lambda **k: None)
|
|
89
|
+
assert _taint_get(a) == 0
|
|
90
|
+
_taint_raise(a, IG.TAINT_EXTERNAL)
|
|
91
|
+
_taint_raise(a, IG.TAINT_SENSITIVE)
|
|
92
|
+
assert _taint_get(a) == IG.TAINT_SENSITIVE # 单调不减
|
|
93
|
+
_taint_reset(a)
|
|
94
|
+
assert _taint_get(a) == 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_dispatch_recall_memory_raises_taint():
|
|
98
|
+
a = types.SimpleNamespace(
|
|
99
|
+
_think=lambda **k: None,
|
|
100
|
+
_recall_memory=lambda q, top_k=5: [{"content": "用户偏好:喜欢简洁"}])
|
|
101
|
+
out = CognitiveAgent._dispatch_tool(a, "recall_memory", {"query": "偏好"})
|
|
102
|
+
assert "喜欢简洁" in out
|
|
103
|
+
assert _taint_get(a) == IG.TAINT_SENSITIVE
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_dispatch_web_search_raises_taint(monkeypatch):
|
|
107
|
+
from foxclaw.core import web_search as WS
|
|
108
|
+
monkeypatch.setattr(WS, "search_answer", lambda q: {"answer": "答案ABC", "references": []})
|
|
109
|
+
a = types.SimpleNamespace(_think=lambda **k: None)
|
|
110
|
+
out = CognitiveAgent._dispatch_tool(a, "web_search", {"query": "问题"})
|
|
111
|
+
assert "答案ABC" in out
|
|
112
|
+
assert _taint_get(a) == IG.TAINT_EXTERNAL
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
import pytest
|
|
117
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""D1:/api/task/stream SSE —— 任务执行流式化(逐 stage 进度 + act 逐 token + 最终 result)。
|
|
2
|
+
与 test_streaming.py 的 /api/chat/stream 端点测试对称:那条测对话直答流式,这条测**走完整认知
|
|
3
|
+
管线**的任务流式。核心逻辑(run_task_stream)在 test_agent_lifecycle 已直测,这里补端点级 SSE 打通。"""
|
|
4
|
+
import os, sys, tempfile, json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_task_stream():
|
|
10
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
11
|
+
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
12
|
+
os.environ["FOXCLAW_STREAM_RATE"] = "60" # 放开限流(限流由 test_streaming 覆盖),这里只验通路
|
|
13
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_taskstream_")
|
|
14
|
+
# 强制 stub(离线/确定性/免费):关掉 .env,使进程环境变量成为唯一来源。
|
|
15
|
+
# (旧写法是逐个把 key 置空以依赖 load_dotenv(override=False)——但那挡不住「先 pop 再
|
|
16
|
+
# import」的路径,也挡不住 .env 里的 FOXCLAW_MCP_SERVER 等非 key 变量。见 api.py 说明。)
|
|
17
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
18
|
+
for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
|
19
|
+
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
|
|
20
|
+
os.environ[_k] = ""
|
|
21
|
+
|
|
22
|
+
print("=" * 60); print("D1 · /api/task/stream 任务流式化验证"); print("=" * 60)
|
|
23
|
+
|
|
24
|
+
from fastapi.testclient import TestClient
|
|
25
|
+
import foxclaw.api as _api
|
|
26
|
+
from foxclaw.api import app
|
|
27
|
+
# pytest 同进程下,test_streaming.py 可能已把模块级流式限流桶设为 rate=1(并耗尽),
|
|
28
|
+
# 而本测试要放开限流跑两条流式通路 —— 复位桶容量/速率/余量(限流语义由 test_streaming 覆盖)。
|
|
29
|
+
_api._STREAM_LIMITER.capacity = 60
|
|
30
|
+
_api._STREAM_LIMITER.rate = 1.0
|
|
31
|
+
_api._STREAM_LIMITER.tokens = 60.0
|
|
32
|
+
|
|
33
|
+
with TestClient(app) as c:
|
|
34
|
+
# 1) 正常流式:SSE 收到 stage 进度 + act delta + 恰一个 result,无 error
|
|
35
|
+
# 拓扑 5 → 3:领域 Agent 才走 DEFAULT_STAGES(含 act),主管走路由管线
|
|
36
|
+
# (recall→route→dispatch→aggregate),不会产出 act —— 故此处用 office。
|
|
37
|
+
with c.stream("POST", "/api/task/stream",
|
|
38
|
+
json={"task": "帮我分析一下本月开销", "agent_id": "office"}) as resp:
|
|
39
|
+
assert resp.status_code == 200, resp.status_code
|
|
40
|
+
stages, deltas, results, errors = [], [], [], []
|
|
41
|
+
for line in resp.iter_lines():
|
|
42
|
+
if not line or not line.startswith("data: "):
|
|
43
|
+
continue
|
|
44
|
+
ev = json.loads(line[6:])
|
|
45
|
+
if "stage" in ev: stages.append(ev["stage"])
|
|
46
|
+
if "delta" in ev: deltas.append(ev["delta"])
|
|
47
|
+
if "result" in ev: results.append(ev["result"])
|
|
48
|
+
if "error" in ev: errors.append(ev["error"])
|
|
49
|
+
assert not errors, f"不应有 error 事件: {errors}"
|
|
50
|
+
assert "act" in stages, f"应有 stage 进度事件(含 act): {stages}"
|
|
51
|
+
assert len(deltas) > 0, "应有 act 逐 token delta 事件(证明流式透传经端点打通)"
|
|
52
|
+
assert len(results) == 1, f"应恰有一个最终 result 事件,实得 {len(results)}"
|
|
53
|
+
assert results[0].get("agent") == "office" and "result" in results[0], results[0]
|
|
54
|
+
print(f"[1] /api/task/stream SSE(stage={len(stages)} delta={len(deltas)} result=1) OK")
|
|
55
|
+
|
|
56
|
+
# 2) 【P3-3】主管不再回落领域 Agent:P1-1 pipeline 化后主管自身支持流式,
|
|
57
|
+
# stage 事件是主管管线七阶段,result.agent=supervisor,路由结论照常(stub 无关键词
|
|
58
|
+
# → 主管自身处理)。
|
|
59
|
+
with c.stream("POST", "/api/task/stream",
|
|
60
|
+
json={"task": "随便问问", "agent_id": "supervisor"}) as resp:
|
|
61
|
+
assert resp.status_code == 200, resp.status_code
|
|
62
|
+
stages, results, errors = [], [], []
|
|
63
|
+
for line in resp.iter_lines():
|
|
64
|
+
if not line or not line.startswith("data: "):
|
|
65
|
+
continue
|
|
66
|
+
ev = json.loads(line[6:])
|
|
67
|
+
if "stage" in ev: stages.append(ev["stage"])
|
|
68
|
+
if "result" in ev: results.append(ev["result"])
|
|
69
|
+
if "error" in ev: errors.append(ev["error"])
|
|
70
|
+
assert not errors, f"不应有 error 事件: {errors}"
|
|
71
|
+
assert len(results) == 1, f"应恰有一个最终 result 事件,实得 {len(results)}"
|
|
72
|
+
r0 = results[0]
|
|
73
|
+
assert r0.get("agent") == "supervisor", \
|
|
74
|
+
f"P3-3:主管走自身 pipeline,不再回落 assistant(实为 {r0.get('agent')})"
|
|
75
|
+
assert "dispatch" in stages and "route" in stages, \
|
|
76
|
+
f"应见主管管线 stage(route/dispatch): {stages}"
|
|
77
|
+
assert r0["result"].get("dispatched_to") == "supervisor", \
|
|
78
|
+
f"stub 无关键词 → 主管自身处理(实为 {r0['result'].get('dispatched_to')})"
|
|
79
|
+
print("[2] agent_id=supervisor → 主管自身流式管线(P3-3,不再回落) OK")
|
|
80
|
+
|
|
81
|
+
print("\nD1 /api/task/stream 验证通过")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
test_task_stream()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""/api/chat/stream 的命名 SSE 事件:`event: thinking` 与正文 delta 分离。
|
|
2
|
+
|
|
3
|
+
前端按事件名折叠展示推理;正文 delta 里不得出现推理文本(否则复制/落库会被污染)。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_chat_stream_emits_named_thinking_event():
|
|
18
|
+
# ISOLATION_GUARD:依赖 import 期固化的单例(路由配置/限流器),须独立进程跑。
|
|
19
|
+
if "foxclaw.api" in sys.modules:
|
|
20
|
+
import pytest
|
|
21
|
+
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
22
|
+
os.environ.setdefault("FOXCLAW_DATA_DIR", tempfile.mkdtemp(prefix="foxclaw_think_"))
|
|
23
|
+
|
|
24
|
+
from fastapi.testclient import TestClient
|
|
25
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
26
|
+
from foxclaw.api import app
|
|
27
|
+
|
|
28
|
+
def _fake_stream(self, task, stop_event=None, images=None, history=None,
|
|
29
|
+
attachments=None, on_thinking=None, on_evidence=None):
|
|
30
|
+
if on_thinking is not None:
|
|
31
|
+
on_thinking("先分析一下用户意图")
|
|
32
|
+
yield "好的"
|
|
33
|
+
|
|
34
|
+
original = CognitiveAgent.stream_reply
|
|
35
|
+
CognitiveAgent.stream_reply = _fake_stream
|
|
36
|
+
try:
|
|
37
|
+
with TestClient(app) as c:
|
|
38
|
+
resp = c.post("/api/chat/stream", json={"task": "hi", "agent_id": "supervisor"})
|
|
39
|
+
assert resp.status_code == 200, resp.status_code
|
|
40
|
+
body = resp.text
|
|
41
|
+
finally:
|
|
42
|
+
CognitiveAgent.stream_reply = original
|
|
43
|
+
|
|
44
|
+
assert "event: thinking" in body, "推理必须以命名事件送出"
|
|
45
|
+
blocks = [b for b in body.split("\n\n") if b.strip()]
|
|
46
|
+
think_payloads, delta_payloads = [], []
|
|
47
|
+
for b in blocks:
|
|
48
|
+
name = "message"
|
|
49
|
+
data = None
|
|
50
|
+
for line in b.splitlines():
|
|
51
|
+
if line.startswith("event: "):
|
|
52
|
+
name = line[7:].strip()
|
|
53
|
+
elif line.startswith("data: "):
|
|
54
|
+
data = line[6:].strip()
|
|
55
|
+
if data is None:
|
|
56
|
+
continue
|
|
57
|
+
ev = json.loads(data)
|
|
58
|
+
if name == "thinking":
|
|
59
|
+
think_payloads.append(ev)
|
|
60
|
+
elif "delta" in ev:
|
|
61
|
+
delta_payloads.append(ev["delta"])
|
|
62
|
+
|
|
63
|
+
assert think_payloads, "应有 thinking 事件块"
|
|
64
|
+
assert "先分析一下用户意图" == "".join(p.get("thinking", "") for p in think_payloads)
|
|
65
|
+
joined = "".join(delta_payloads)
|
|
66
|
+
assert joined == "好的"
|
|
67
|
+
assert "先分析一下" not in joined, "推理不得混入正文 delta"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""推理过程可见:`_ThinkStripper` 不再丢弃思考,原生 reasoning 走独立 thinking 通道。
|
|
2
|
+
|
|
3
|
+
背景:`core/model_router.py` 的 `_ThinkStripper.feed` 只返回可见正文,think 块内容被整段
|
|
4
|
+
淘汰;openai-compatible 的 `delta.reasoning_content`(deepseek-reasoner 等)更是完全没读,
|
|
5
|
+
用户只看到结论,看不到推理。现在两者都转成 `on_thinking` 回调 → SSE `event: thinking`。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
|
|
12
|
+
from foxclaw.core.model_router import ModelRouter, _ThinkStripper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeCompletions:
|
|
16
|
+
def __init__(self, chunks):
|
|
17
|
+
self.chunks = chunks
|
|
18
|
+
self.kwargs: dict = {}
|
|
19
|
+
|
|
20
|
+
def create(self, **kwargs):
|
|
21
|
+
self.kwargs = kwargs
|
|
22
|
+
return iter(self.chunks)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _FakeClient:
|
|
26
|
+
def __init__(self, chunks):
|
|
27
|
+
self.chat = SimpleNamespace(completions=_FakeCompletions(chunks))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _chunk(content=None, reasoning=None):
|
|
31
|
+
delta = SimpleNamespace(content=content, reasoning_content=reasoning)
|
|
32
|
+
return SimpleNamespace(choices=[SimpleNamespace(delta=delta)])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _router(chunks) -> ModelRouter:
|
|
36
|
+
r = ModelRouter(provider="openai", model="gpt-4o-mini")
|
|
37
|
+
r._client = _FakeClient(chunks)
|
|
38
|
+
return r
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_stripper_returns_think_text_separately() -> None:
|
|
42
|
+
s = _ThinkStripper()
|
|
43
|
+
vis1, th1 = s.feed_events("答案前<thi")
|
|
44
|
+
assert (vis1, th1) == ("答案前", ""), "半个标签不能当正文吐出"
|
|
45
|
+
vis2, th2 = s.feed_events("nk>内部推理</think>答案")
|
|
46
|
+
assert vis2 == "答案"
|
|
47
|
+
assert th2 == "内部推理"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_stripper_feed_still_visible_only() -> None:
|
|
51
|
+
s = _ThinkStripper()
|
|
52
|
+
assert s.feed("a<think>b</think>c") == "ac"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_reasoning_content_goes_to_thinking_only() -> None:
|
|
56
|
+
r = _router([_chunk(None, "先想一下"), _chunk("你好"), _chunk("世界")])
|
|
57
|
+
think: list[str] = []
|
|
58
|
+
text = "".join(r.stream("sys", [{"role": "user", "content": "hi"}],
|
|
59
|
+
on_thinking=think.append))
|
|
60
|
+
assert "".join(think) == "先想一下", "原生 reasoning_content 必须进推理通道"
|
|
61
|
+
assert text == "你好世界", "正文不得混入推理"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_embedded_think_routed_to_callback() -> None:
|
|
65
|
+
r = _router([_chunk("<think>内嵌思考</think>答案是 42<think>补一句")])
|
|
66
|
+
think: list[str] = []
|
|
67
|
+
text = "".join(r.stream("sys", [{"role": "user", "content": "hi"}],
|
|
68
|
+
on_thinking=think.append))
|
|
69
|
+
assert text == "答案是 42"
|
|
70
|
+
assert "内嵌思考" in "".join(think)
|
|
71
|
+
assert "补一句" not in text, "未闭合的 think 尾巴不得漏进正文"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_no_callback_keeps_old_behavior() -> None:
|
|
75
|
+
r = _router([_chunk("<think>x</think>ok")])
|
|
76
|
+
assert "".join(r.stream("sys", [{"role": "user", "content": "hi"}])) == "ok"
|