mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
Fix: multi-agent AGENT_MAX_STEPS not respecting user override & skill agent graceful degradation (#1031)
* Fix multi-agent AGENT_MAX_STEPS not respecting user override and skill agent failure handling - Fix AGENT_MAX_STEPS ceiling-only logic: when user raises value above default (10), all sub-agents now adopt the global value instead of being capped at their per-agent defaults (fixes #1026) - Fix skill agent failure aborting entire pipeline: specialist/skill agents now degrade gracefully like intel/risk stages - Improve max-steps-exceeded error message with AGENT_MAX_STEPS guidance - Sync config descriptions in README, .env.example, and config_registry
This commit is contained in:
@@ -160,7 +160,7 @@ SEARXNG_PUBLIC_INSTANCES_ENABLED=true
|
||||
# AGENT_MODE=true
|
||||
# Agent 主模型(可选):留空时继承主模型;无 provider 前缀会按 openai/<model> 解析
|
||||
# AGENT_LITELLM_MODEL=
|
||||
# Agent 最大推理步数上限(多 Agent orchestrator 模式下作为各子 Agent ceiling,不会抬高内建较低默认值)
|
||||
# Agent 最大推理步数上限(默认 10 时各子 Agent 按自身预设运行;高于默认 10 时所有子 Agent 统一采用该值;低于某子 Agent 默认值时会作为上限进行封顶)
|
||||
# AGENT_MAX_STEPS=10
|
||||
# 默认启用策略(逗号分隔),不配置时使用以下内置默认值
|
||||
#
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
| `AGENT_MODE` | 开启 Agent 策略问股模式(内部统一命名为 skill,`true`/`false`,默认 false) | 可选 |
|
||||
| `AGENT_LITELLM_MODEL` | Agent 主模型(可选);留空继承主模型,无前缀会按 `openai/<model>` 解析 | 可选 |
|
||||
| `AGENT_SKILLS` | 激活的策略技能 id(逗号分隔),`all` 启用全部策略技能;留空时使用主默认策略 skill(内置默认是 `bull_trend`),详见 `.env.example` | 可选 |
|
||||
| `AGENT_MAX_STEPS` | Agent 最大推理步数上限(默认 10);多 Agent orchestrator 模式下按 `min(子 Agent 默认值, AGENT_MAX_STEPS)` 生效,不会抬高低默认值 Agent 的步数 | 可选 |
|
||||
| `AGENT_MAX_STEPS` | Agent 最大推理步数上限(默认 `10`);保持默认时各子 Agent 按自身预设步数运行;用户主动调高到高于默认值时,所有子 Agent 统一采用该值;若设置值低于某子 Agent 的默认步数,则仍按该值作为上限进行限制 | 可选 |
|
||||
| `AGENT_SKILL_DIR` | 自定义策略技能目录(默认沿用内置 `strategies/` 兼容路径) | 可选 |
|
||||
| `TRADING_DAY_CHECK_ENABLED` | 交易日检查(默认 `true`):非交易日跳过执行;设为 `false` 或使用 `--force-run` 强制执行 | 可选 |
|
||||
| `ENABLE_CHIP_DISTRIBUTION` | 启用筹码分布(Actions 默认 false;需筹码数据时在 Variables 中设为 true,接口可能不稳定) | 可选 |
|
||||
|
||||
@@ -129,7 +129,7 @@ const fieldDescriptionMap: Record<string, string> = {
|
||||
LOG_LEVEL: '设置日志输出级别。',
|
||||
WEBUI_PORT: 'Web 页面服务监听端口。',
|
||||
AGENT_MODE: '是否启用 ReAct Agent 策略问股。对外文案仍叫“策略”,内部配置字段统一使用 skill。',
|
||||
AGENT_MAX_STEPS: 'Agent 思考和调用工具的最大步数。',
|
||||
AGENT_MAX_STEPS: 'Agent 最大推理步数上限。保持默认 10 时,各子 Agent 按自身预设步数运行;调高到高于默认值时,所有子 Agent 统一采用该值;调低到低于某子 Agent 默认值时,该 Agent 会被封顶。',
|
||||
AGENT_SKILLS: '逗号分隔的交易策略列表。留空时使用 metadata 里声明的主默认策略 skill(内置默认是 bull_trend);也可填写 all 启用全部策略。',
|
||||
AGENT_SKILL_DIR: '存放 Agent 策略定义文件的目录路径,支持 YAML 与 SKILL.md bundle。',
|
||||
AGENT_ARCH: "选择 Agent 执行架构。single 为经典单 Agent;multi 为多 Agent 编排(实验性)。",
|
||||
|
||||
@@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore)-->
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
|
||||
- [修复] `AGENT_MAX_STEPS` 在 orchestrator 多 Agent 模式下改为作为各子 Agent 的步数上限而非硬覆盖;TechnicalAgent 等高默认值 Agent 会被封顶,低默认值 Agent 保持原值,减少不必要的 LLM 调用膨胀与配额消耗。
|
||||
- [修复] 大盘复盘链路接入 `REPORT_LANGUAGE`:`REPORT_LANGUAGE=en` 时,A 股/合并复盘的 Prompt、章节标题、模板兜底文案与通知包装标题统一改为英文,避免出现英文正文外包中文标题的问题。
|
||||
- [修复] `AGENT_MAX_STEPS` 在 orchestrator 多 Agent 模式下统一明确为“默认作为各子 Agent 的步数上限而非硬覆盖;TechnicalAgent 等高默认值 Agent 会被封顶、低默认值 Agent 保持原值;当用户主动调高(>10)时,再统一覆盖所有子 Agent 采用全局值”,同时修复用户设置 12 但 TechnicalAgent 仍以默认 6 步运行并报 "Agent exceeded max steps" 的问题(fixes #1026)
|
||||
- [修复] Specialist(Skill)Agent 失败不再中断整个分析管线,改为与 intel/risk 相同的优雅降级策略
|
||||
- [改进] Agent 超步数错误信息增加 AGENT_MAX_STEPS 调整提示,帮助用户自助排查
|
||||
- [修复] **MiniMax-M2.7 模型连接测试支持** — 修复 LLM 通道连接测试在 MiniMax-M2.7 模型下返回 "Empty response" 的问题;增加了 `max_tokens` 上限(8→256)以容纳 MiniMax 思考过程,并添加 `content_blocks` 格式解析逻辑统一处理 MiniMax 响应格式差异。
|
||||
- [修复] 移除 `HistoryItem` 与 `ReportSummary` 响应 Schema 中 `sentiment_score` 的 `ge=0/le=100` 约束(fixes #942)——历史库中存储的超范围负值或大于 100 的情绪评分不再触发 Pydantic ValidationError,历史列表与详情接口恢复正常返回。
|
||||
- [改进] 后端股票名称解析改为优先复用前端 `stocks.index.json` 全量索引并懒加载缓存;纯后端/缺失静态资源场景静默降级回 `STOCK_NAME_MAP` 与原有数据源回退链路。
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
| `LONGBRIDGE_PRINT_QUOTE_PACKAGES` | 連線時是否列印行情包(未設定時預設 `false`;設為 `1`/`true`/`yes` 開啟) | 可選 |
|
||||
| `AGENT_MODE` | 啟用 Agent 策略問股模式(內部統一命名為 skill,`true`/`false`,預設 `false`) | 可選 |
|
||||
| `AGENT_LITELLM_MODEL` | Agent 專用主模型(可選);留空時繼承主模型,無 provider 前綴時按 `openai/<model>` 解析 | 可選 |
|
||||
| `AGENT_MAX_STEPS` | Agent 最大推理步數上限(預設 `10`);多 Agent orchestrator 模式下按 `min(子 Agent 預設值, AGENT_MAX_STEPS)` 生效,不會抬高低預設值 Agent 的步數 | 可選 |
|
||||
| `AGENT_MAX_STEPS` | Agent 最大推理步數上限(預設 `10`);保持預設時各子 Agent 依自身預設步數運行;主動調高到高於預設值時,所有子 Agent 統一採用該值;若設定值低於某子 Agent 的預設步數,則仍按該值作為上限進行限制 | 可選 |
|
||||
| `AGENT_SKILLS` | 逗號分隔的策略技能 id。留空時使用 metadata 宣告的主預設策略 skill(內建預設為 `bull_trend`);使用 `all` 可啟用所有已載入策略技能。 | 可選 |
|
||||
| `AGENT_SKILL_DIR` | 自訂策略技能目錄(預設沿用內建 `strategies/` 相容路徑) | 可選 |
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|
||||
| `WECHAT_MSG_TYPE` | WeChat Work message type, default `markdown`, set to `text` for plain markdown text | Optional |
|
||||
| `AGENT_MODE` | Enable Agent strategy chat mode (internally normalized as `skill`, `true`/`false`, default `false`) | Optional |
|
||||
| `AGENT_LITELLM_MODEL` | Optional Agent-only primary model; when empty it inherits the primary model, and bare names are normalized to `openai/<model>` | Optional |
|
||||
| `AGENT_MAX_STEPS` | Max reasoning-step ceiling for Agent mode (default `10`); in orchestrator mode each sub-agent uses `min(its default, AGENT_MAX_STEPS)` instead of a hard override | Optional |
|
||||
| `AGENT_MAX_STEPS` | Max reasoning-step limit for Agent mode (default `10`); at the default each sub-agent keeps its own preset, when raised above the default all sub-agents adopt this value, and when lowered below a sub-agent's preset that sub-agent is capped at this value | Optional |
|
||||
| `AGENT_SKILLS` | Comma-separated active strategy-skill ids. Leave empty to use the primary default strategy skill declared in metadata (built-in default: `bull_trend`); use `all` to activate every loaded strategy skill. | Optional |
|
||||
| `AGENT_SKILL_DIR` | Custom strategy-skill directory (default built-in `strategies/` compatibility path) | Optional |
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from src.config import AGENT_MAX_STEPS_DEFAULT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -323,7 +325,7 @@ def build_agent_executor(config=None, skills: Optional[List[str]] = None):
|
||||
skill_instructions=prompt_state.skill_instructions,
|
||||
default_skill_policy=prompt_state.default_skill_policy,
|
||||
use_legacy_default_prompt=prompt_state.use_legacy_default_prompt,
|
||||
max_steps=getattr(config, "agent_max_steps", 10),
|
||||
max_steps=getattr(config, "agent_max_steps", AGENT_MAX_STEPS_DEFAULT),
|
||||
timeout_seconds=getattr(config, "agent_orchestrator_timeout_s", 0),
|
||||
)
|
||||
|
||||
@@ -344,7 +346,7 @@ def _build_orchestrator(config, registry, llm_adapter, skill_manager, *, technic
|
||||
llm_adapter=llm_adapter,
|
||||
skill_instructions=skill_manager.get_skill_instructions(),
|
||||
technical_skill_policy=technical_skill_policy,
|
||||
max_steps=getattr(config, "agent_max_steps", 10),
|
||||
max_steps=getattr(config, "agent_max_steps", AGENT_MAX_STEPS_DEFAULT),
|
||||
mode=mode,
|
||||
skill_manager=skill_manager,
|
||||
config=config,
|
||||
|
||||
@@ -42,6 +42,7 @@ from src.agent.protocols import (
|
||||
)
|
||||
from src.agent.runner import parse_dashboard_json
|
||||
from src.agent.tools.registry import ToolRegistry
|
||||
from src.config import AGENT_MAX_STEPS_DEFAULT
|
||||
from src.report_language import normalize_report_language
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -83,7 +84,7 @@ class AgentOrchestrator:
|
||||
llm_adapter: LLMToolAdapter,
|
||||
skill_instructions: str = "",
|
||||
technical_skill_policy: str = "",
|
||||
max_steps: int = 10,
|
||||
max_steps: int = AGENT_MAX_STEPS_DEFAULT,
|
||||
mode: str = "standard",
|
||||
skill_manager=None,
|
||||
config=None,
|
||||
@@ -200,15 +201,25 @@ class AgentOrchestrator:
|
||||
def _prepare_agent(self, agent: Any) -> Any:
|
||||
"""Apply orchestrator-level runtime settings to a child agent.
|
||||
|
||||
The orchestrator-level ``max_steps`` acts as a **ceiling** — it will
|
||||
never *increase* the per-agent limit that each specialised agent
|
||||
already defines. This prevents a global ``AGENT_MAX_STEPS=10``
|
||||
from inflating a decision agent (designed for 3 steps) to 10 steps,
|
||||
which is the primary cause of excessive LLM calls and quota
|
||||
exhaustion in multi-agent pipelines.
|
||||
When the orchestrator-level ``max_steps`` equals the default
|
||||
(``AGENT_MAX_STEPS_DEFAULT``),
|
||||
each agent keeps its own per-agent limit — this prevents inflating
|
||||
a decision agent (designed for 3 steps) to 10 steps.
|
||||
|
||||
When the user **explicitly** raises the global limit above the
|
||||
default, all agents adopt the global value so the user's intent to
|
||||
allow more steps is respected.
|
||||
|
||||
When the user **lowers** the global limit below an agent's default,
|
||||
the agent is capped at the global value.
|
||||
"""
|
||||
if hasattr(agent, "max_steps"):
|
||||
agent.max_steps = min(agent.max_steps, self.max_steps)
|
||||
if self.max_steps > AGENT_MAX_STEPS_DEFAULT:
|
||||
# User explicitly raised the limit — apply to all agents.
|
||||
agent.max_steps = self.max_steps
|
||||
else:
|
||||
# Default or lowered — keep per-agent limit as ceiling.
|
||||
agent.max_steps = min(agent.max_steps, self.max_steps)
|
||||
return agent
|
||||
|
||||
def _callable_accepts_timeout_kwarg(self, func: Any) -> Optional[bool]:
|
||||
@@ -507,9 +518,16 @@ class AgentOrchestrator:
|
||||
if result.success and agent.agent_name == "decision":
|
||||
self._apply_risk_override(ctx)
|
||||
|
||||
# Abort pipeline on critical failure (except intel/risk — degrade gracefully)
|
||||
# Abort pipeline on critical failure.
|
||||
# Non-critical stages that degrade gracefully:
|
||||
# - intel / risk (standard support stages)
|
||||
# - skill agents (specialist evaluation, optional)
|
||||
if result.status == StageStatus.FAILED:
|
||||
if agent.agent_name not in ("intel", "risk"):
|
||||
non_critical = (
|
||||
agent.agent_name in ("intel", "risk")
|
||||
or agent.agent_name in getattr(self, "_skill_agent_names", set())
|
||||
)
|
||||
if not non_critical:
|
||||
logger.error("[Orchestrator] critical stage '%s' failed: %s", agent.agent_name, result.error)
|
||||
return OrchestratorResult(
|
||||
success=False,
|
||||
|
||||
@@ -595,7 +595,7 @@ def run_agent_loop(
|
||||
total_tokens=total_tokens,
|
||||
provider=provider_used,
|
||||
models_used=models_used,
|
||||
error=f"Agent exceeded max steps ({max_steps})",
|
||||
error=f"Agent exceeded max steps ({max_steps}). Try increasing AGENT_MAX_STEPS if analysis tasks are complex.",
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ class ConfigIssue:
|
||||
_MANAGED_LITELLM_KEY_PROVIDERS = {"gemini", "vertex_ai", "anthropic", "openai", "deepseek"}
|
||||
SUPPORTED_LLM_CHANNEL_PROTOCOLS = ("openai", "anthropic", "gemini", "vertex_ai", "deepseek", "ollama")
|
||||
_FALSEY_ENV_VALUES = {"0", "false", "no", "off"}
|
||||
AGENT_MAX_STEPS_DEFAULT = 10
|
||||
NEWS_STRATEGY_WINDOWS: Dict[str, int] = {
|
||||
"ultra_short": 1,
|
||||
"short": 3,
|
||||
@@ -520,7 +521,7 @@ class Config:
|
||||
agent_litellm_model: str = "" # Optional Agent-only primary model; empty inherits LITELLM_MODEL
|
||||
agent_mode: bool = False
|
||||
_agent_mode_explicit: bool = False # True when AGENT_MODE was explicitly set in env
|
||||
agent_max_steps: int = 10
|
||||
agent_max_steps: int = AGENT_MAX_STEPS_DEFAULT
|
||||
agent_skills: List[str] = field(default_factory=list)
|
||||
agent_skill_dir: Optional[str] = None
|
||||
agent_nl_routing: bool = False # Enable natural language routing in bot dispatcher
|
||||
@@ -1174,7 +1175,12 @@ class Config:
|
||||
agent_litellm_model=agent_litellm_model,
|
||||
agent_mode=os.getenv('AGENT_MODE', 'false').lower() == 'true',
|
||||
_agent_mode_explicit=os.getenv('AGENT_MODE') is not None,
|
||||
agent_max_steps=parse_env_int(os.getenv('AGENT_MAX_STEPS'), 10, field_name='AGENT_MAX_STEPS', minimum=1),
|
||||
agent_max_steps=parse_env_int(
|
||||
os.getenv('AGENT_MAX_STEPS'),
|
||||
AGENT_MAX_STEPS_DEFAULT,
|
||||
field_name='AGENT_MAX_STEPS',
|
||||
minimum=1,
|
||||
),
|
||||
agent_skills=[s.strip() for s in os.getenv('AGENT_SKILLS', '').split(',') if s.strip()],
|
||||
agent_skill_dir=os.getenv('AGENT_SKILL_DIR') or os.getenv('AGENT_STRATEGY_DIR'),
|
||||
agent_nl_routing=os.getenv('AGENT_NL_ROUTING', 'false').lower() == 'true',
|
||||
|
||||
@@ -10,6 +10,8 @@ from __future__ import annotations
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from src.config import AGENT_MAX_STEPS_DEFAULT
|
||||
|
||||
SCHEMA_VERSION = "2026-03-29"
|
||||
|
||||
_CATEGORY_DEFINITIONS: List[Dict[str, Any]] = [
|
||||
@@ -1563,14 +1565,14 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||
},
|
||||
"AGENT_MAX_STEPS": {
|
||||
"title": "Agent Max Steps",
|
||||
"description": "Maximum reasoning-step ceiling for Agent mode. In orchestrator mode, each sub-agent keeps min(its default, this limit) so lower-default specialists are not inflated.",
|
||||
"description": f"Maximum reasoning-step limit for Agent mode. At the default ({AGENT_MAX_STEPS_DEFAULT}), each sub-agent keeps its own preset. When raised above {AGENT_MAX_STEPS_DEFAULT}, all sub-agents adopt this value. When lowered below a sub-agent's preset, that sub-agent is capped at this value.",
|
||||
"category": "agent",
|
||||
"data_type": "integer",
|
||||
"ui_control": "number",
|
||||
"is_sensitive": False,
|
||||
"is_required": False,
|
||||
"is_editable": True,
|
||||
"default_value": "10",
|
||||
"default_value": str(AGENT_MAX_STEPS_DEFAULT),
|
||||
"options": [],
|
||||
"validation": {"min": 1, "max": 50},
|
||||
"display_order": 20,
|
||||
|
||||
@@ -40,12 +40,12 @@ class TestAgentConfig(unittest.TestCase):
|
||||
@patch('src.config.load_dotenv')
|
||||
def test_default_agent_config(self, _mock_dotenv):
|
||||
"""Agent mode should be disabled by default."""
|
||||
from src.config import Config
|
||||
from src.config import AGENT_MAX_STEPS_DEFAULT, Config
|
||||
Config._instance = None
|
||||
config = Config._load_from_env()
|
||||
self.assertEqual(config.agent_litellm_model, "")
|
||||
self.assertFalse(config.agent_mode)
|
||||
self.assertEqual(config.agent_max_steps, 10)
|
||||
self.assertEqual(config.agent_max_steps, AGENT_MAX_STEPS_DEFAULT)
|
||||
self.assertEqual(config.agent_skills, [])
|
||||
|
||||
@patch.dict(os.environ, {
|
||||
@@ -334,6 +334,7 @@ class TestAgentResultConversion(unittest.TestCase):
|
||||
mock_cfg.max_workers = 2
|
||||
mock_cfg.agent_mode = True
|
||||
mock_cfg.agent_max_steps = 10
|
||||
mock_cfg.agent_orchestrator_timeout_s = 0
|
||||
mock_cfg.agent_skills = []
|
||||
mock_cfg.bocha_api_keys = []
|
||||
mock_cfg.tavily_api_keys = []
|
||||
@@ -628,7 +629,8 @@ class TestAnalyzeWithAgentStockName(unittest.TestCase):
|
||||
patch('src.core.pipeline.GeminiAnalyzer'), \
|
||||
patch('src.core.pipeline.NotificationService'), \
|
||||
patch('src.core.pipeline.SearchService'), \
|
||||
patch('src.agent.factory.build_agent_executor') as mock_build_executor:
|
||||
patch('src.agent.factory.build_agent_executor') as mock_build_executor, \
|
||||
patch('src.agent.executor.AgentExecutor.run') as mock_agent_run:
|
||||
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.max_workers = 2
|
||||
@@ -668,6 +670,7 @@ class TestAnalyzeWithAgentStockName(unittest.TestCase):
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run.return_value = agent_result
|
||||
mock_build_executor.return_value = mock_executor
|
||||
mock_agent_run.return_value = agent_result
|
||||
|
||||
news_response = MagicMock()
|
||||
news_response.success = True
|
||||
|
||||
@@ -35,6 +35,7 @@ from src.agent.protocols import (
|
||||
StageResult,
|
||||
StageStatus,
|
||||
)
|
||||
from src.config import AGENT_MAX_STEPS_DEFAULT
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -500,13 +501,9 @@ class TestOrchestratorModes(unittest.TestCase):
|
||||
self.assertEqual(orch.mode, "standard")
|
||||
|
||||
def test_chain_agents_inherit_orchestrator_max_steps(self):
|
||||
"""Orchestrator max_steps acts as a *ceiling*, not a hard override.
|
||||
|
||||
Each agent keeps ``min(own_default, orchestrator_limit)`` so that
|
||||
specialised agents with lower defaults are not inflated.
|
||||
"""
|
||||
"""Default/lowered limits cap agents; raised limits hard-override all agents."""
|
||||
orch = self._make_orchestrator("full")
|
||||
orch.max_steps = 9
|
||||
orch.max_steps = AGENT_MAX_STEPS_DEFAULT
|
||||
high_limit_chain = orch._build_agent_chain(AgentContext(query="test", stock_code="600519"))
|
||||
self.assertEqual(
|
||||
{agent.agent_name: agent.max_steps for agent in high_limit_chain},
|
||||
@@ -520,6 +517,23 @@ class TestOrchestratorModes(unittest.TestCase):
|
||||
{"technical": 5, "intel": 4, "risk": 4, "decision": 3},
|
||||
)
|
||||
|
||||
orch.max_steps = AGENT_MAX_STEPS_DEFAULT + 2
|
||||
raised_limit_chain = orch._build_agent_chain(AgentContext(query="test", stock_code="600519"))
|
||||
self.assertEqual(
|
||||
{agent.agent_name: agent.max_steps for agent in raised_limit_chain},
|
||||
{"technical": AGENT_MAX_STEPS_DEFAULT + 2, "intel": AGENT_MAX_STEPS_DEFAULT + 2, "risk": AGENT_MAX_STEPS_DEFAULT + 2, "decision": AGENT_MAX_STEPS_DEFAULT + 2},
|
||||
)
|
||||
|
||||
def test_prepare_agent_raised_limit_overrides_low_default_agent(self):
|
||||
orch = self._make_orchestrator("full")
|
||||
orch.max_steps = AGENT_MAX_STEPS_DEFAULT + 2
|
||||
decision = MagicMock(agent_name="decision", max_steps=3)
|
||||
|
||||
prepared = orch._prepare_agent(decision)
|
||||
|
||||
self.assertIs(prepared, decision)
|
||||
self.assertEqual(prepared.max_steps, AGENT_MAX_STEPS_DEFAULT + 2)
|
||||
|
||||
def test_build_context_from_dict(self):
|
||||
orch = self._make_orchestrator()
|
||||
ctx = orch._build_context(
|
||||
@@ -565,6 +579,24 @@ class TestOrchestratorExecution(unittest.TestCase):
|
||||
result.meta["models_used"] = ["test/model"]
|
||||
return result
|
||||
|
||||
def test_prepare_agent_uses_default_constant_as_raise_threshold(self):
|
||||
orch = self._make_orchestrator()
|
||||
agent = MagicMock(agent_name="technical", max_steps=6)
|
||||
|
||||
prepared = orch._prepare_agent(agent)
|
||||
self.assertIs(prepared, agent)
|
||||
self.assertEqual(agent.max_steps, 6)
|
||||
|
||||
orch.max_steps = 12
|
||||
agent.max_steps = 6
|
||||
orch._prepare_agent(agent)
|
||||
self.assertEqual(agent.max_steps, 12)
|
||||
|
||||
orch.max_steps = 5
|
||||
agent.max_steps = 6
|
||||
orch._prepare_agent(agent)
|
||||
self.assertEqual(agent.max_steps, 5)
|
||||
|
||||
def test_execute_pipeline_stops_on_critical_failure(self):
|
||||
orch = self._make_orchestrator()
|
||||
technical = MagicMock(agent_name="technical")
|
||||
@@ -593,6 +625,32 @@ class TestOrchestratorExecution(unittest.TestCase):
|
||||
self.assertTrue(result.success)
|
||||
self.assertIn("Analysis Summary", result.content)
|
||||
|
||||
def test_execute_pipeline_degrades_on_skill_agent_failure_and_continues_to_decision(self):
|
||||
orch = self._make_orchestrator()
|
||||
orch.mode = "specialist"
|
||||
ctx = AgentContext(query="test", stock_code="600519")
|
||||
ctx.add_opinion(AgentOpinion(agent_name="technical", signal="buy", confidence=0.8, reasoning="Strong trend"))
|
||||
|
||||
technical = MagicMock(agent_name="technical")
|
||||
technical.run.return_value = self._stage_result("technical")
|
||||
intel = MagicMock(agent_name="intel")
|
||||
intel.run.return_value = self._stage_result("intel")
|
||||
risk = MagicMock(agent_name="risk")
|
||||
risk.run.return_value = self._stage_result("risk")
|
||||
skill = MagicMock(agent_name="strategy_bull_trend")
|
||||
skill.run.return_value = self._stage_result("strategy_bull_trend", StageStatus.FAILED, error="skill boom")
|
||||
decision = MagicMock(agent_name="decision")
|
||||
decision.run.return_value = self._stage_result("decision")
|
||||
|
||||
with patch.object(orch, "_build_agent_chain", return_value=[technical, intel, risk, decision]):
|
||||
with patch.object(orch, "_build_specialist_agents", return_value=[skill]):
|
||||
result = orch._execute_pipeline(ctx, parse_dashboard=False)
|
||||
|
||||
self.assertTrue(result.success)
|
||||
self.assertIn("Analysis Summary", result.content)
|
||||
skill.run.assert_called_once()
|
||||
decision.run.assert_called_once()
|
||||
|
||||
def test_execute_pipeline_skips_stage_when_remaining_budget_below_minimum(self):
|
||||
orch = self._make_orchestrator(config=SimpleNamespace(agent_orchestrator_timeout_s=20))
|
||||
ctx = AgentContext(query="test", stock_code="600519", stock_name="贵州茅台")
|
||||
|
||||
Reference in New Issue
Block a user