Add dynamic analyst runtime updates and deployment guides
This commit is contained in:
@@ -310,11 +310,12 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
|
||||
)
|
||||
logger.debug("Registered workspace watch hook")
|
||||
|
||||
async def _reasoning(self, **kwargs) -> Msg:
|
||||
async def _reasoning(self, tool_choice: Optional[str] = None, **kwargs) -> Msg:
|
||||
"""Override reasoning to execute pre-reasoning hooks.
|
||||
|
||||
Args:
|
||||
**kwargs: Arguments for reasoning
|
||||
tool_choice: Optional tool choice for structured output
|
||||
**kwargs: Additional arguments for reasoning
|
||||
|
||||
Returns:
|
||||
Response message
|
||||
@@ -327,7 +328,7 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
|
||||
)
|
||||
|
||||
# Call parent (which may be ToolGuardMixin's _reasoning)
|
||||
return await super()._reasoning(**kwargs)
|
||||
return await super()._reasoning(tool_choice=tool_choice, **kwargs)
|
||||
|
||||
def reload_runtime_assets(self, active_skill_dirs: Optional[List[Path]] = None) -> None:
|
||||
"""Reload toolkit and system prompt from current run assets.
|
||||
@@ -579,7 +580,7 @@ class EvoAgent(ToolGuardMixin, ReActAgent):
|
||||
return
|
||||
|
||||
try:
|
||||
self._messenger = AgentMessenger(agent_id=self.agent_id)
|
||||
self._messenger = AgentMessenger()
|
||||
self._task_delegator = TaskDelegator(agent=self)
|
||||
logger.debug(
|
||||
"Team infrastructure initialized for agent: %s",
|
||||
|
||||
@@ -312,12 +312,21 @@ class RunWorkspaceManager:
|
||||
"- 审阅分析以理解市场观点\n"
|
||||
"- 在做决策前先考虑风险警告\n"
|
||||
"- 评估当前投资组合持仓、现金与保证金占用\n"
|
||||
"- 在做最终决策前,先判断当前团队是否足以覆盖任务;如果覆盖不足,不要勉强给结论,先扩编团队\n"
|
||||
"- 当现有团队覆盖不足、观点分歧过大、或出现新的专业分析需求时,优先考虑动态创建合适的分析师,再继续讨论\n"
|
||||
"- 决策必须与整体投资目标和风险约束一致\n\n"
|
||||
"动态扩编触发条件:\n"
|
||||
"- 出现当前团队未覆盖的研究领域:期权、宏观、行业专项、事件驱动、监管冲击、加密资产、商品链、特殊市场结构\n"
|
||||
"- 关键 ticker 的结论依赖某种专业知识,但现有 analyst 无法提供直接证据链\n"
|
||||
"- 分析师之间存在明显冲突,且仅靠风险经理无法完成裁决\n"
|
||||
"- 你需要第二个同类型但不同风格的 analyst 来验证一个高风险假设\n\n"
|
||||
"决策类型:\n"
|
||||
'- `long`:看涨,建议买入\n'
|
||||
'- `short`:看跌,建议卖出或做空\n'
|
||||
'- `hold`:中性,维持当前持仓\n\n'
|
||||
"输出要求:\n"
|
||||
"- 触发扩编条件时,必须先使用动态团队工具创建分析师,并在继续决策前吸收其分析输入\n"
|
||||
"- 不允许口头声称“需要更多分析”但不实际调用创建工具\n"
|
||||
"- 使用 `make_decision` 工具记录每个股票的最终决策\n"
|
||||
"- 记录完成后给出投资逻辑总结\n"
|
||||
"- 最终总结必须使用简体中文\n"
|
||||
@@ -327,6 +336,10 @@ class RunWorkspaceManager:
|
||||
"- 在决定数量时考虑可用现金,不要超出现金允许范围\n"
|
||||
"- 考虑做空头寸的保证金要求\n"
|
||||
"- 仓位规模相对于组合总资产保持保守\n"
|
||||
"- 当任务涉及当前团队未覆盖的领域(如期权、宏观、行业专项、事件驱动、加密资产等)时,应优先创建或克隆对应分析师,而不是勉强用现有团队输出低质量结论\n"
|
||||
"- 当分析师之间长期存在高冲突且缺乏裁决信息时,应考虑增加一个补充视角的分析师\n"
|
||||
"- 如果你已经识别出覆盖缺口,却没有调用动态团队工具补齐团队,就不应直接输出高置信度交易决策\n"
|
||||
"- 对新创建分析师的输出必须纳入本轮决策依据,不能创建后忽略\n"
|
||||
"- 始终为决策提供清晰理由\n"
|
||||
"- 不要输出英文投资报告或英文结论\n"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -154,6 +155,7 @@ class RunContextResponse(BaseModel):
|
||||
|
||||
class RuntimeAgentState(BaseModel):
|
||||
agent_id: str
|
||||
display_name: Optional[str] = None
|
||||
status: str
|
||||
last_session: Optional[str] = None
|
||||
last_updated: str
|
||||
@@ -300,6 +302,70 @@ def _load_run_server_state(run_dir: Path) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
def _resolve_runtime_agent_display_name(run_id: str, agent_id: str) -> Optional[str]:
|
||||
"""Best-effort display name for one runtime agent.
|
||||
|
||||
Priority:
|
||||
1. PROFILE.md line like `角色定位:中文名`
|
||||
2. PROFILE.md YAML frontmatter field `name`
|
||||
"""
|
||||
asset_dir = PROJECT_ROOT / "runs" / run_id / "agents" / agent_id
|
||||
profile_path = asset_dir / "PROFILE.md"
|
||||
if not profile_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
raw = profile_path.read_text(encoding="utf-8").strip()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
frontmatter_name: Optional[str] = None
|
||||
if raw.startswith("---"):
|
||||
parts = raw.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
import yaml
|
||||
parsed = yaml.safe_load(parts[1].strip()) or {}
|
||||
if isinstance(parsed, dict):
|
||||
value = parsed.get("name")
|
||||
if isinstance(value, str) and value.strip():
|
||||
frontmatter_name = value.strip()
|
||||
except Exception:
|
||||
pass
|
||||
raw = parts[2].strip()
|
||||
|
||||
for line in raw.splitlines():
|
||||
normalized = line.strip()
|
||||
if normalized.startswith("角色定位:"):
|
||||
value = normalized.split(":", 1)[1].strip()
|
||||
if value:
|
||||
return value
|
||||
if normalized.lower().startswith("role:"):
|
||||
value = normalized.split(":", 1)[1].strip()
|
||||
if value:
|
||||
return value
|
||||
|
||||
return frontmatter_name
|
||||
|
||||
|
||||
def _enrich_runtime_agents(run_id: Optional[str], agents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
if not run_id:
|
||||
return agents
|
||||
|
||||
enriched: List[Dict[str, Any]] = []
|
||||
for item in agents:
|
||||
payload = dict(item)
|
||||
display_name = payload.get("display_name")
|
||||
agent_id = str(payload.get("agent_id") or "").strip()
|
||||
if agent_id and (not isinstance(display_name, str) or not display_name.strip()):
|
||||
payload["display_name"] = _resolve_runtime_agent_display_name(run_id, agent_id)
|
||||
enriched.append(payload)
|
||||
return enriched
|
||||
|
||||
|
||||
def _extract_history_metrics(run_dir: Path) -> tuple[int, Optional[float]]:
|
||||
"""Prefer runtime state files over dashboard exports for history summaries."""
|
||||
server_state = _load_run_server_state(run_dir)
|
||||
@@ -566,10 +632,11 @@ def _validate_gateway_config(bootstrap: Dict[str, Any]) -> List[str]:
|
||||
# Check LLM configuration
|
||||
model_name = os.getenv("MODEL_NAME")
|
||||
openai_key = os.getenv("OPENAI_API_KEY")
|
||||
dashscope_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
if not model_name:
|
||||
errors.append("MODEL_NAME environment variable is not set")
|
||||
if not openai_key:
|
||||
errors.append("OPENAI_API_KEY environment variable is not set")
|
||||
if not openai_key and not dashscope_key:
|
||||
errors.append("Either OPENAI_API_KEY or DASHSCOPE_API_KEY environment variable must be set")
|
||||
|
||||
# Validate tickers
|
||||
tickers = bootstrap.get("tickers", [])
|
||||
@@ -722,7 +789,8 @@ async def get_run_context() -> RunContextResponse:
|
||||
async def get_runtime_agents() -> RuntimeAgentsResponse:
|
||||
"""Return agent states from the active runtime, or latest persisted run."""
|
||||
snapshot = _get_active_runtime_snapshot() if _is_gateway_running() else _load_latest_runtime_snapshot()
|
||||
agents = snapshot.get("agents", [])
|
||||
run_id = snapshot.get("context", {}).get("config_name")
|
||||
agents = _enrich_runtime_agents(run_id, snapshot.get("agents", []))
|
||||
|
||||
return RuntimeAgentsResponse(
|
||||
agents=[RuntimeAgentState(**a) for a in agents]
|
||||
@@ -869,11 +937,24 @@ def _load_latest_runtime_snapshot() -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _get_active_runtime_snapshot() -> Dict[str, Any]:
|
||||
"""Return the active runtime snapshot, preferring in-memory manager state."""
|
||||
"""Return the active runtime snapshot.
|
||||
|
||||
For a running Gateway, the canonical runtime source of truth is the
|
||||
run-scoped snapshot file under `runs/<run_id>/state/runtime_state.json`,
|
||||
because the Gateway subprocess mutates it directly while the parent
|
||||
runtime_service process may still hold a stale in-memory manager snapshot.
|
||||
"""
|
||||
if not _is_gateway_running():
|
||||
raise HTTPException(status_code=404, detail="No runtime is currently running")
|
||||
|
||||
manager = _runtime_state.runtime_manager
|
||||
if manager is not None:
|
||||
run_id = str(getattr(manager, "config_name", "") or "").strip()
|
||||
if run_id:
|
||||
snapshot_path = _get_run_dir(run_id) / "state" / "runtime_state.json"
|
||||
if snapshot_path.exists():
|
||||
return json.loads(snapshot_path.read_text(encoding="utf-8"))
|
||||
|
||||
if manager is not None and hasattr(manager, "build_snapshot"):
|
||||
snapshot = manager.build_snapshot()
|
||||
context = snapshot.get("context") or {}
|
||||
@@ -900,11 +981,32 @@ def _read_log_tail(path: Path, max_chars: int = 120_000) -> str:
|
||||
if not path.exists() or not path.is_file():
|
||||
return ""
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
text = _sanitize_runtime_log_text(text)
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[-max_chars:]
|
||||
|
||||
|
||||
def _sanitize_runtime_log_text(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# Drop repetitive development-only warnings for unsandboxed skill execution.
|
||||
text = re.sub(
|
||||
r"(?:^|\n)=+\n"
|
||||
r"⚠️\s+\[安全警告\]\s+技能在无沙盒模式下运行\s+\(SKILL_SANDBOX_MODE=none\)\n"
|
||||
r"\s+技能脚本将直接在当前进程中执行,无隔离保护。\n"
|
||||
r"\s+建议:生产环境请设置\s+SKILL_SANDBOX_MODE=docker\n"
|
||||
r"=+\n?",
|
||||
"\n",
|
||||
text,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _get_current_runtime_context() -> Dict[str, Any]:
|
||||
"""Return the active runtime context from the latest snapshot."""
|
||||
if not _is_gateway_running():
|
||||
|
||||
@@ -27,8 +27,10 @@ valuation_analyst:
|
||||
portfolio_manager:
|
||||
skills:
|
||||
- portfolio_decisioning
|
||||
- dynamic_team_management
|
||||
active_tool_groups:
|
||||
- portfolio_ops
|
||||
- dynamic_team
|
||||
|
||||
risk_manager:
|
||||
skills:
|
||||
|
||||
@@ -6,11 +6,13 @@ Core Pipeline - Orchestrates multi-agent analysis and decision-making
|
||||
# flake8: noqa: E501
|
||||
# pylint: disable=W0613,C0301
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
||||
|
||||
@@ -32,7 +34,7 @@ from backend.agents.toolkit_factory import create_agent_toolkit
|
||||
from backend.agents.workspace_manager import WorkspaceManager
|
||||
from backend.agents.prompt_loader import get_prompt_loader
|
||||
from backend.llm.models import get_agent_formatter, get_agent_model
|
||||
from backend.config.constants import ANALYST_TYPES
|
||||
from backend.config.constants import ANALYST_TYPES, AGENT_CONFIG
|
||||
from backend.agents.dynamic_team_types import AnalystConfig
|
||||
from backend.tools.dynamic_team_tools import DynamicTeamController, set_controller
|
||||
|
||||
@@ -230,8 +232,25 @@ class TradingPipeline:
|
||||
"system",
|
||||
),
|
||||
):
|
||||
# Phase 1.1: Analysts
|
||||
# Phase 1.0: PM assesses team coverage and expands if needed
|
||||
if not last_phase or last_phase == "cleared":
|
||||
_log("Phase 1.0: Team gap assessment")
|
||||
await self._run_team_gap_assessment(
|
||||
tickers=tickers,
|
||||
date=date,
|
||||
prices=prices,
|
||||
)
|
||||
active_analysts = self._get_active_analysts()
|
||||
if self.runtime_manager:
|
||||
self._runtime_batch_status(active_analysts, "analysis_in_progress")
|
||||
self._save_checkpoint(session_key, "team_assessment", {
|
||||
"prices": prices,
|
||||
"close_prices": close_prices,
|
||||
})
|
||||
last_phase = "team_assessment"
|
||||
|
||||
# Phase 1.1: Analysts
|
||||
if last_phase == "team_assessment":
|
||||
_log("Phase 1.1: Analyst analysis (parallel)")
|
||||
analyst_results = await self._run_analysts_parallel(
|
||||
tickers,
|
||||
@@ -754,6 +773,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id="Daily Log",
|
||||
content=reflection_content,
|
||||
agent_name="每日记录",
|
||||
)
|
||||
|
||||
# Phase 6: APO (Autonomous Policy Optimization)
|
||||
@@ -1020,12 +1040,13 @@ class TradingPipeline:
|
||||
|
||||
pm_msg = Msg(name="system", content=pm_prompt, role="user")
|
||||
pm_response = await self.pm.reply(pm_msg)
|
||||
pm_content = self._extract_text_content(pm_response.content)
|
||||
|
||||
if self.state_sync:
|
||||
pm_content = self._extract_text_content(pm_response.content)
|
||||
await self.state_sync.on_conference_message(
|
||||
agent_id="portfolio_manager",
|
||||
content=pm_content,
|
||||
agent_name=self._resolve_agent_display_name("portfolio_manager"),
|
||||
)
|
||||
|
||||
# Analysts share perspectives (supports per-round active team updates)
|
||||
@@ -1050,6 +1071,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_conference_message(
|
||||
agent_id=analyst.name,
|
||||
content=analyst_content,
|
||||
agent_name=self._resolve_agent_display_name(analyst.name),
|
||||
)
|
||||
|
||||
if self.state_sync:
|
||||
@@ -1082,6 +1104,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_conference_message(
|
||||
agent_id="conference summary",
|
||||
content=conference_summary,
|
||||
agent_name="会议总结",
|
||||
)
|
||||
await self.state_sync.on_conference_end()
|
||||
|
||||
@@ -1139,6 +1162,116 @@ class TradingPipeline:
|
||||
f"and any remaining concerns about {', '.join(tickers)}."
|
||||
)
|
||||
|
||||
async def _run_team_gap_assessment(
|
||||
self,
|
||||
*,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
prices: Optional[Dict[str, float]],
|
||||
) -> str:
|
||||
active_analysts = self._get_active_analysts()
|
||||
team_summary = [
|
||||
{
|
||||
"agent_id": analyst.name,
|
||||
"display_name": self._resolve_agent_display_name(analyst.name),
|
||||
}
|
||||
for analyst in active_analysts
|
||||
]
|
||||
prompt = (
|
||||
f"As Portfolio Manager, perform a team coverage assessment before analysis for {date}.\n\n"
|
||||
f"Tickers: {', '.join(tickers)}\n"
|
||||
f"Current team: {json.dumps(team_summary, ensure_ascii=False, indent=2)}\n"
|
||||
f"Current prices snapshot: {json.dumps(prices, ensure_ascii=False, indent=2) if prices else 'N/A'}\n\n"
|
||||
"Your job in this phase is not to make investment decisions. "
|
||||
"First decide whether the current team has enough domain coverage. "
|
||||
"If the team is insufficient, immediately call dynamic team tools to create or clone the needed analysts now. "
|
||||
"Before creating any analyst, explicitly check whether an existing analyst already covers that role. "
|
||||
"Do not create duplicate roles with different IDs but the same responsibilities. "
|
||||
"If the current team is sufficient, explicitly say the current team is sufficient and explain why."
|
||||
)
|
||||
msg = Msg(name="system", content=prompt, role="user")
|
||||
response = await self.pm.reply(msg)
|
||||
pm_content = self._extract_text_content(response.content)
|
||||
enforced_pm_content = await self._enforce_pm_team_expansion_if_needed(
|
||||
tickers=tickers,
|
||||
date=date,
|
||||
pm_content=pm_content,
|
||||
)
|
||||
if enforced_pm_content:
|
||||
pm_content = enforced_pm_content
|
||||
|
||||
if self.state_sync:
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id="portfolio_manager",
|
||||
agent_name=self._resolve_agent_display_name("portfolio_manager"),
|
||||
content=pm_content,
|
||||
)
|
||||
|
||||
return pm_content
|
||||
|
||||
def _pm_requests_team_expansion(self, text: str) -> bool:
|
||||
normalized = (text or "").strip().lower()
|
||||
if not normalized:
|
||||
return False
|
||||
|
||||
phrases = [
|
||||
"创建",
|
||||
"新增分析师",
|
||||
"补充分析师",
|
||||
"扩编团队",
|
||||
"需要行业分析师",
|
||||
"需要量化分析师",
|
||||
"需要宏观分析师",
|
||||
"需要补充",
|
||||
"先扩编",
|
||||
"create analyst",
|
||||
"create a new analyst",
|
||||
"add analyst",
|
||||
"expand the team",
|
||||
"need a specialist",
|
||||
"need another analyst",
|
||||
]
|
||||
return any(phrase in normalized for phrase in phrases)
|
||||
|
||||
async def _enforce_pm_team_expansion_if_needed(
|
||||
self,
|
||||
*,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
pm_content: str,
|
||||
) -> Optional[str]:
|
||||
if not self._pm_requests_team_expansion(pm_content):
|
||||
return None
|
||||
|
||||
before_ids = {agent.name for agent in self._get_active_analysts()}
|
||||
|
||||
followup_prompt = (
|
||||
f"You identified a team coverage gap for {date} across {', '.join(tickers)}. "
|
||||
"This is still the pre-analysis team assessment phase. "
|
||||
"Do not merely recommend adding analysts. If additional analysts are needed, "
|
||||
"you must now call the dynamic team tools (`create_analyst` or `clone_analyst`) "
|
||||
"to add the required specialists before analyst analysis begins. "
|
||||
"Only after the tool call succeeds may you explain why the new analysts were added. "
|
||||
"If you truly believe the current team is sufficient, explicitly say the current team is sufficient."
|
||||
)
|
||||
followup_msg = Msg(name="system", content=followup_prompt, role="user")
|
||||
followup_response = await self.pm.reply(followup_msg)
|
||||
followup_content = self._extract_text_content(followup_response.content)
|
||||
after_ids = {agent.name for agent in self._get_active_analysts()}
|
||||
|
||||
if after_ids != before_ids:
|
||||
created = sorted(after_ids - before_ids)
|
||||
logger.info(
|
||||
"PM team expansion enforced successfully before analysis; added analysts=%s",
|
||||
created,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"PM mentioned expansion in team assessment but did not add analysts after enforcement prompt",
|
||||
)
|
||||
|
||||
return followup_content
|
||||
|
||||
def _build_analyst_discussion_prompt(
|
||||
self,
|
||||
cycle: int,
|
||||
@@ -1152,6 +1285,88 @@ class TradingPipeline:
|
||||
f"Do not use tools - focus on sharing your professional opinion."
|
||||
)
|
||||
|
||||
def _resolve_agent_display_name(self, agent_id: str) -> str:
|
||||
runtime_name = None
|
||||
if self.runtime_manager:
|
||||
state = self.runtime_manager.get_agent_state(agent_id)
|
||||
runtime_name = getattr(state, "display_name", None) if state else None
|
||||
if isinstance(runtime_name, str) and runtime_name.strip():
|
||||
return runtime_name.strip()
|
||||
|
||||
static_name = AGENT_CONFIG.get(agent_id, {}).get("name")
|
||||
if isinstance(static_name, str) and static_name.strip():
|
||||
return static_name.strip()
|
||||
|
||||
profile_path = Path(__file__).resolve().parents[2] / "runs" / self.runtime_manager.config_name / "agents" / agent_id / "PROFILE.md" if self.runtime_manager else None
|
||||
if profile_path and profile_path.exists():
|
||||
try:
|
||||
raw = profile_path.read_text(encoding="utf-8")
|
||||
for line in raw.splitlines():
|
||||
text = line.strip()
|
||||
if text.startswith("角色定位:"):
|
||||
value = text.split(":", 1)[1].strip()
|
||||
if value:
|
||||
return value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return agent_id
|
||||
|
||||
@staticmethod
|
||||
def _normalize_role_key(value: str) -> str:
|
||||
normalized = (value or "").strip().lower()
|
||||
normalized = normalized.replace("_", "")
|
||||
normalized = normalized.replace(" ", "")
|
||||
replacements = {
|
||||
"analyst": "分析师",
|
||||
"macro": "宏观",
|
||||
"technical": "技术",
|
||||
"tech": "技术",
|
||||
"sentiment": "情绪",
|
||||
"fundamentals": "基本面",
|
||||
"fundamental": "基本面",
|
||||
"valuation": "估值",
|
||||
"crypto": "加密",
|
||||
"cryptocurrency": "加密",
|
||||
"semiconductor": "半导体",
|
||||
"industry": "行业",
|
||||
"sector": "行业",
|
||||
"risk": "风险",
|
||||
}
|
||||
for src, target in replacements.items():
|
||||
normalized = normalized.replace(src, target)
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def _contains_cjk(value: str) -> bool:
|
||||
text = (value or "").strip()
|
||||
return any("\u4e00" <= ch <= "\u9fff" for ch in text)
|
||||
|
||||
def _find_similar_existing_analyst(
|
||||
self,
|
||||
*,
|
||||
agent_id: str,
|
||||
analyst_type: str,
|
||||
custom_config: Optional[AnalystConfig],
|
||||
) -> Optional[str]:
|
||||
requested_names = {self._normalize_role_key(agent_id)}
|
||||
if custom_config and custom_config.persona and custom_config.persona.name:
|
||||
requested_names.add(self._normalize_role_key(custom_config.persona.name))
|
||||
|
||||
for agent in self._all_analysts():
|
||||
existing_id = getattr(agent, "name", None) or getattr(agent, "agent_id", None)
|
||||
if not existing_id or existing_id == agent_id:
|
||||
continue
|
||||
|
||||
existing_names = {
|
||||
self._normalize_role_key(existing_id),
|
||||
self._normalize_role_key(self._resolve_agent_display_name(existing_id)),
|
||||
}
|
||||
if requested_names & existing_names:
|
||||
return existing_id
|
||||
|
||||
return None
|
||||
|
||||
async def _collect_final_predictions(
|
||||
self,
|
||||
tickers: List[str],
|
||||
@@ -1300,6 +1515,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id=analyst.name,
|
||||
content=text_content,
|
||||
agent_name=self._resolve_agent_display_name(analyst.name),
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -1375,6 +1591,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id=analyst.name,
|
||||
content=text_content,
|
||||
agent_name=self._resolve_agent_display_name(analyst.name),
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -1456,6 +1673,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id="risk_manager",
|
||||
content=text_content,
|
||||
agent_name=self._resolve_agent_display_name("risk_manager"),
|
||||
)
|
||||
|
||||
return extracted
|
||||
@@ -1542,6 +1760,7 @@ class TradingPipeline:
|
||||
await self.state_sync.on_agent_complete(
|
||||
agent_id="portfolio_manager",
|
||||
content=text_content,
|
||||
agent_name=self._resolve_agent_display_name("portfolio_manager"),
|
||||
)
|
||||
|
||||
return extracted
|
||||
@@ -1776,8 +1995,29 @@ class TradingPipeline:
|
||||
f"Available: {', '.join(ANALYST_TYPES.keys())}. "
|
||||
f"Or provide custom_config to create a custom analyst."
|
||||
)
|
||||
display_name = (
|
||||
custom_config.persona.name
|
||||
if custom_config and custom_config.persona and custom_config.persona.name
|
||||
else ""
|
||||
)
|
||||
if not self._contains_cjk(display_name):
|
||||
return (
|
||||
f"Analyst '{agent_id}' requires a Chinese display name. "
|
||||
"Please provide `name` in Chinese characters when creating dynamic analysts."
|
||||
)
|
||||
if agent_id in {agent.name for agent in self._all_analysts()}:
|
||||
return f"Analyst '{agent_id}' already exists."
|
||||
similar_existing = self._find_similar_existing_analyst(
|
||||
agent_id=agent_id,
|
||||
analyst_type=analyst_type,
|
||||
custom_config=custom_config,
|
||||
)
|
||||
if similar_existing:
|
||||
return (
|
||||
f"Analyst '{agent_id}' is too similar to existing analyst "
|
||||
f"'{similar_existing}'. Reuse or clone the existing analyst instead of "
|
||||
f"creating a duplicate role."
|
||||
)
|
||||
|
||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
@@ -1860,6 +2100,48 @@ class TradingPipeline:
|
||||
setattr(agent, "workspace_id", config_name)
|
||||
self._dynamic_analysts[agent_id] = agent
|
||||
|
||||
if self.runtime_manager:
|
||||
display_name = None
|
||||
if custom_config and custom_config.persona and custom_config.persona.name:
|
||||
display_name = custom_config.persona.name
|
||||
self.runtime_manager.register_agent(
|
||||
agent_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
self.runtime_manager.log_event(
|
||||
"agent:created",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"analyst_type": analyst_type,
|
||||
"display_name": display_name,
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"Dynamic analyst created: agent_id=%s analyst_type=%s custom=%s",
|
||||
agent_id,
|
||||
analyst_type,
|
||||
bool(custom_config),
|
||||
)
|
||||
if self.state_sync:
|
||||
try:
|
||||
asyncio.create_task(
|
||||
self.state_sync.emit(
|
||||
{
|
||||
"type": "runtime_agents_updated",
|
||||
"action": "created",
|
||||
"agentId": agent_id,
|
||||
"agentName": display_name or self._resolve_agent_display_name(agent_id),
|
||||
},
|
||||
persist=False,
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast runtime_agents_updated(create) for %s: %s",
|
||||
agent_id,
|
||||
exc,
|
||||
)
|
||||
|
||||
# Store custom config for future reference (e.g., cloning)
|
||||
if custom_config:
|
||||
self._dynamic_analyst_configs[agent_id] = custom_config
|
||||
@@ -1879,6 +2161,31 @@ class TradingPipeline:
|
||||
self._dynamic_analysts.pop(agent_id, None)
|
||||
# Also remove stored config if exists
|
||||
self._dynamic_analyst_configs.pop(agent_id, None)
|
||||
if self.runtime_manager:
|
||||
self.runtime_manager.unregister_agent(agent_id)
|
||||
self.runtime_manager.log_event(
|
||||
"agent:removed",
|
||||
{"agent_id": agent_id},
|
||||
)
|
||||
logger.info("Dynamic analyst removed: agent_id=%s", agent_id)
|
||||
if self.state_sync:
|
||||
try:
|
||||
asyncio.create_task(
|
||||
self.state_sync.emit(
|
||||
{
|
||||
"type": "runtime_agents_updated",
|
||||
"action": "removed",
|
||||
"agentId": agent_id,
|
||||
},
|
||||
persist=False,
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast runtime_agents_updated(remove) for %s: %s",
|
||||
agent_id,
|
||||
exc,
|
||||
)
|
||||
config_name = getattr(self.pm, "config", {}).get("config_name", "default")
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
update_active_analysts(
|
||||
|
||||
@@ -135,6 +135,7 @@ class StateSync:
|
||||
self,
|
||||
agent_id: str,
|
||||
content: str,
|
||||
agent_name: Optional[str] = None,
|
||||
**extra,
|
||||
):
|
||||
"""
|
||||
@@ -151,6 +152,7 @@ class StateSync:
|
||||
{
|
||||
"type": "agent_message",
|
||||
"agentId": agent_id,
|
||||
"agentName": agent_name,
|
||||
"content": content,
|
||||
"ts": ts_ms,
|
||||
**extra,
|
||||
@@ -212,7 +214,12 @@ class StateSync:
|
||||
persist=False,
|
||||
)
|
||||
|
||||
async def on_conference_message(self, agent_id: str, content: str):
|
||||
async def on_conference_message(
|
||||
self,
|
||||
agent_id: str,
|
||||
content: str,
|
||||
agent_name: Optional[str] = None,
|
||||
):
|
||||
"""Called when an agent speaks during conference"""
|
||||
ts_ms = self._get_timestamp_ms()
|
||||
|
||||
@@ -220,6 +227,7 @@ class StateSync:
|
||||
{
|
||||
"type": "conference_message",
|
||||
"agentId": agent_id,
|
||||
"agentName": agent_name,
|
||||
"content": content,
|
||||
"ts": ts_ms,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Dict
|
||||
@dataclass
|
||||
class AgentRuntimeState:
|
||||
agent_id: str
|
||||
display_name: str | None = None
|
||||
status: str = "idle"
|
||||
last_session: str | None = None
|
||||
last_updated: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||
@@ -20,6 +21,7 @@ class AgentRuntimeState:
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"display_name": self.display_name,
|
||||
"status": self.status,
|
||||
"last_session": self.last_session,
|
||||
"last_updated": self.last_updated.isoformat(),
|
||||
|
||||
@@ -102,12 +102,22 @@ class TradingRuntimeManager:
|
||||
self._persist_snapshot()
|
||||
return entry
|
||||
|
||||
def register_agent(self, agent_id: str) -> AgentRuntimeState:
|
||||
state = AgentRuntimeState(agent_id=agent_id)
|
||||
def register_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
display_name: Optional[str] = None,
|
||||
) -> AgentRuntimeState:
|
||||
state = AgentRuntimeState(agent_id=agent_id, display_name=display_name)
|
||||
self.registry.register(agent_id, state)
|
||||
self._persist_snapshot()
|
||||
return state
|
||||
|
||||
def unregister_agent(self, agent_id: str) -> Optional[AgentRuntimeState]:
|
||||
state = self.registry.unregister(agent_id)
|
||||
if state is not None:
|
||||
self._persist_snapshot()
|
||||
return state
|
||||
|
||||
def register_pending_approval(self, approval_id: str, payload: Dict[str, Any]) -> None:
|
||||
payload.setdefault("status", "pending")
|
||||
payload.setdefault("created_at", datetime.now(UTC).isoformat())
|
||||
|
||||
@@ -13,6 +13,9 @@ class RuntimeRegistry:
|
||||
def get(self, agent_id: str) -> Optional["AgentRuntimeState"]:
|
||||
return self._states.get(agent_id)
|
||||
|
||||
def unregister(self, agent_id: str) -> Optional["AgentRuntimeState"]:
|
||||
return self._states.pop(agent_id, None)
|
||||
|
||||
def list_agents(self) -> list[str]:
|
||||
return list(self._states.keys())
|
||||
|
||||
|
||||
@@ -23,15 +23,17 @@ version: 1.0.0
|
||||
## 3) Decision procedure
|
||||
|
||||
1. 汇总并比较 analyst 信号,识别共识与分歧。
|
||||
2. 将风险警示映射到仓位上限与禁开条件。
|
||||
3. 在资金与保证金约束下,为每个 ticker 生成候选动作与数量。
|
||||
4. 对冲突信号执行保守仲裁:降低仓位、提高触发门槛或改为 `hold`。
|
||||
5. 逐个 ticker 记录最终决策,并给出组合级理由。
|
||||
2. 先判断当前团队是否覆盖了本轮任务所需的专业能力;若未覆盖,优先扩编团队而不是直接仲裁。
|
||||
3. 将风险警示映射到仓位上限与禁开条件。
|
||||
4. 在资金与保证金约束下,为每个 ticker 生成候选动作与数量。
|
||||
5. 对冲突信号执行保守仲裁:降低仓位、提高触发门槛、补充 analyst,或改为 `hold`。
|
||||
6. 逐个 ticker 记录最终决策,并给出组合级理由。
|
||||
|
||||
## 4) Tool call policy
|
||||
|
||||
- 必须使用决策工具记录每个 ticker 的最终 `action/quantity`。
|
||||
- 在讨论阶段如发现当前团队能力不足,可使用团队工具动态创建或移除 analyst(再继续讨论)。
|
||||
- 在讨论阶段如发现当前团队能力不足、证据链断裂、或观点冲突无法裁决,必须优先使用团队工具动态创建或克隆 analyst(再继续讨论)。
|
||||
- 如果已经判断“需要更多专业分析”,但没有调用动态团队工具补齐团队,则不得输出高置信度最终决策。
|
||||
- 若风险工具提示阻断项,优先遵循阻断,不得绕过。
|
||||
- 工具调用失败时:重试一次;仍失败则输出结构化“未完成决策清单”和人工处理建议。
|
||||
|
||||
@@ -46,5 +48,6 @@ version: 1.0.0
|
||||
## 6) Failure fallback
|
||||
|
||||
- 当分析师信号与风险结论显著冲突时,默认采用更小仓位或 `hold`。
|
||||
- 当任务明显超出当前团队能力边界时,优先扩编团队;如果扩编失败,再降级为 `hold` 或条件决策草案。
|
||||
- 当约束校验失败(现金/保证金不足)时,自动下调数量,不输出不可执行指令。
|
||||
- 当任务要求完整清单时,不允许遗漏 ticker;无法决策时必须显式标记 `hold` 并说明原因。
|
||||
|
||||
@@ -10,12 +10,15 @@ description: 整合分析师观点与风险反馈,形成明确的组合层决
|
||||
## 工作流程
|
||||
|
||||
1. 行动前先阅读分析师结论和风险警示。
|
||||
2. 评估当前组合、现金和保证金约束。
|
||||
3. 使用决策工具为每个 ticker 记录一个明确决策。
|
||||
4. 在全部决策记录完成后,总结组合层面的整体理由。
|
||||
2. 先判断当前团队是否足以覆盖本轮任务;如果不够,先扩编团队再继续。
|
||||
3. 评估当前组合、现金和保证金约束。
|
||||
4. 使用决策工具为每个 ticker 记录一个明确决策。
|
||||
5. 在全部决策记录完成后,总结组合层面的整体理由。
|
||||
|
||||
## 约束
|
||||
|
||||
- 仓位大小必须遵守资金和保证金限制。
|
||||
- 当分析师信心与风险信号不一致时,优先采用更小仓位。
|
||||
- 当任务超出当前团队能力边界时,应优先使用动态团队工具创建或克隆分析师。
|
||||
- 如果已经识别出覆盖缺口,不应跳过扩编步骤直接给出高置信度结论。
|
||||
- 当任务要求完整决策清单时,不要让任何 ticker 处于未决状态。
|
||||
|
||||
@@ -13,8 +13,11 @@ as described in the Dynamic Team Architecture.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Callable
|
||||
from dataclasses import asdict
|
||||
|
||||
from agentscope.message import TextBlock
|
||||
from agentscope.tool import ToolResponse
|
||||
|
||||
from backend.agents.dynamic_team_types import (
|
||||
AnalystPersona,
|
||||
@@ -22,7 +25,7 @@ from backend.agents.dynamic_team_types import (
|
||||
CreateAnalystResult,
|
||||
AnalystTypeInfo,
|
||||
)
|
||||
from backend.config.constants import ANALYST_TYPES
|
||||
from backend.config.constants import ANALYST_TYPES, AGENT_CONFIG
|
||||
|
||||
|
||||
# Type alias for callbacks set by pipeline
|
||||
@@ -30,6 +33,14 @@ CreateAnalystCallback = Callable[[str, str, Optional[AnalystConfig]], str]
|
||||
RemoveAnalystCallback = Callable[[str], str]
|
||||
|
||||
|
||||
def _to_tool_response(payload: Any) -> ToolResponse:
|
||||
if isinstance(payload, str):
|
||||
text = payload
|
||||
else:
|
||||
text = json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
||||
return ToolResponse(content=[TextBlock(type="text", text=text)])
|
||||
|
||||
|
||||
class DynamicTeamController:
|
||||
"""Controller for dynamic analyst team management.
|
||||
|
||||
@@ -296,6 +307,23 @@ class DynamicTeamController:
|
||||
Dict with analyst configuration and status
|
||||
"""
|
||||
config = self._instance_configs.get(agent_id)
|
||||
current_analysts = self._get_analysts_callback() if self._get_analysts_callback else []
|
||||
analyst_map = {
|
||||
(getattr(agent, "name", None) or getattr(agent, "agent_id", None)): agent
|
||||
for agent in current_analysts
|
||||
}
|
||||
if agent_id in analyst_map and not config:
|
||||
builtin_meta = AGENT_CONFIG.get(agent_id, {})
|
||||
return {
|
||||
"found": True,
|
||||
"agent_id": agent_id,
|
||||
"name": builtin_meta.get("name") or agent_id,
|
||||
"type": agent_id,
|
||||
"is_custom": False,
|
||||
"is_clone": False,
|
||||
"is_builtin": True,
|
||||
"message": f"Built-in analyst '{agent_id}' is active",
|
||||
}
|
||||
if not config:
|
||||
return {
|
||||
"found": False,
|
||||
@@ -310,6 +338,7 @@ class DynamicTeamController:
|
||||
"is_custom": config.persona is not None,
|
||||
"is_clone": config.parent_id is not None,
|
||||
"parent_id": config.parent_id,
|
||||
"is_builtin": False,
|
||||
}
|
||||
|
||||
def register_analyst_type(
|
||||
@@ -372,13 +401,26 @@ class DynamicTeamController:
|
||||
Dict with team composition information
|
||||
"""
|
||||
analysts = []
|
||||
for agent_id, config in self._instance_configs.items():
|
||||
current_analysts = self._get_analysts_callback() if self._get_analysts_callback else []
|
||||
instance_configs = self._instance_configs
|
||||
|
||||
for agent in current_analysts:
|
||||
agent_id = getattr(agent, "name", None) or getattr(agent, "agent_id", None)
|
||||
if not agent_id:
|
||||
continue
|
||||
config = instance_configs.get(agent_id)
|
||||
builtin_meta = AGENT_CONFIG.get(agent_id, {})
|
||||
analysts.append({
|
||||
"agent_id": agent_id,
|
||||
"name": config.persona.name if config.persona else agent_id,
|
||||
"type": config.analyst_type,
|
||||
"is_custom": config.persona is not None,
|
||||
"is_clone": config.parent_id is not None,
|
||||
"name": (
|
||||
config.persona.name
|
||||
if config and config.persona and config.persona.name
|
||||
else builtin_meta.get("name") or agent_id
|
||||
),
|
||||
"type": config.analyst_type if config else agent_id,
|
||||
"is_custom": bool(config and config.persona is not None),
|
||||
"is_clone": bool(config and config.parent_id is not None),
|
||||
"is_builtin": config is None,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -418,91 +460,95 @@ def get_controller() -> Optional[DynamicTeamController]:
|
||||
def create_analyst(
|
||||
agent_id: str,
|
||||
analyst_type: str,
|
||||
name: Optional[str] = None,
|
||||
focus: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
soul_md: Optional[str] = None,
|
||||
agents_md: Optional[str] = None,
|
||||
model_name: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
name: str = "",
|
||||
focus: str = "",
|
||||
description: str = "",
|
||||
soul_md: str = "",
|
||||
agents_md: str = "",
|
||||
model_name: str = "",
|
||||
) -> ToolResponse:
|
||||
"""Tool wrapper for create_analyst.
|
||||
|
||||
Note: focus parameter accepts comma-separated string for tool compatibility.
|
||||
"""
|
||||
controller = get_controller()
|
||||
if not controller:
|
||||
return {"success": False, "error": "Dynamic team controller not available"}
|
||||
return _to_tool_response({"success": False, "error": "Dynamic team controller not available"})
|
||||
|
||||
focus_list = [f.strip() for f in focus.split(",")] if focus else None
|
||||
return controller.create_analyst(
|
||||
agent_id=agent_id,
|
||||
analyst_type=analyst_type,
|
||||
name=name,
|
||||
focus=focus_list,
|
||||
description=description,
|
||||
soul_md=soul_md,
|
||||
agents_md=agents_md,
|
||||
model_name=model_name,
|
||||
return _to_tool_response(
|
||||
controller.create_analyst(
|
||||
agent_id=agent_id,
|
||||
analyst_type=analyst_type,
|
||||
name=name,
|
||||
focus=focus_list,
|
||||
description=description,
|
||||
soul_md=soul_md,
|
||||
agents_md=agents_md,
|
||||
model_name=model_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def clone_analyst(
|
||||
source_id: str,
|
||||
new_id: str,
|
||||
name: Optional[str] = None,
|
||||
focus_additions: Optional[str] = None,
|
||||
description_override: Optional[str] = None,
|
||||
model_name: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
name: str = "",
|
||||
focus_additions: str = "",
|
||||
description_override: str = "",
|
||||
model_name: str = "",
|
||||
) -> ToolResponse:
|
||||
"""Tool wrapper for clone_analyst.
|
||||
|
||||
Note: focus_additions accepts comma-separated string.
|
||||
"""
|
||||
controller = get_controller()
|
||||
if not controller:
|
||||
return {"success": False, "error": "Dynamic team controller not available"}
|
||||
return _to_tool_response({"success": False, "error": "Dynamic team controller not available"})
|
||||
|
||||
additions_list = [f.strip() for f in focus_additions.split(",")] if focus_additions else None
|
||||
return controller.clone_analyst(
|
||||
source_id=source_id,
|
||||
new_id=new_id,
|
||||
name=name,
|
||||
focus_additions=additions_list,
|
||||
description_override=description_override,
|
||||
model_name=model_name,
|
||||
return _to_tool_response(
|
||||
controller.clone_analyst(
|
||||
source_id=source_id,
|
||||
new_id=new_id,
|
||||
name=name,
|
||||
focus_additions=additions_list,
|
||||
description_override=description_override,
|
||||
model_name=model_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def remove_analyst(agent_id: str) -> Dict[str, Any]:
|
||||
def remove_analyst(agent_id: str) -> ToolResponse:
|
||||
"""Tool wrapper for remove_analyst."""
|
||||
controller = get_controller()
|
||||
if not controller:
|
||||
return {"success": False, "error": "Dynamic team controller not available"}
|
||||
return controller.remove_analyst(agent_id)
|
||||
return _to_tool_response({"success": False, "error": "Dynamic team controller not available"})
|
||||
return _to_tool_response(controller.remove_analyst(agent_id))
|
||||
|
||||
|
||||
def list_analyst_types() -> List[Dict[str, Any]]:
|
||||
def list_analyst_types() -> ToolResponse:
|
||||
"""Tool wrapper for list_analyst_types."""
|
||||
controller = get_controller()
|
||||
if not controller:
|
||||
return []
|
||||
return controller.list_analyst_types()
|
||||
return _to_tool_response([])
|
||||
return _to_tool_response(controller.list_analyst_types())
|
||||
|
||||
|
||||
def get_analyst_info(agent_id: str) -> Dict[str, Any]:
|
||||
def get_analyst_info(agent_id: str) -> ToolResponse:
|
||||
"""Tool wrapper for get_analyst_info."""
|
||||
controller = get_controller()
|
||||
if not controller:
|
||||
return {"found": False, "error": "Controller not available"}
|
||||
return controller.get_analyst_info(agent_id)
|
||||
return _to_tool_response({"found": False, "error": "Controller not available"})
|
||||
return _to_tool_response(controller.get_analyst_info(agent_id))
|
||||
|
||||
|
||||
def get_team_summary() -> Dict[str, Any]:
|
||||
def get_team_summary() -> ToolResponse:
|
||||
"""Tool wrapper for get_team_summary."""
|
||||
controller = get_controller()
|
||||
if not controller:
|
||||
return {"error": "Controller not available"}
|
||||
return controller.get_team_summary()
|
||||
return _to_tool_response({"error": "Controller not available"})
|
||||
return _to_tool_response(controller.get_team_summary())
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
@@ -71,7 +70,6 @@ class NoSandboxBackend(SandboxBackend):
|
||||
|
||||
def __init__(self):
|
||||
self._module_cache = {}
|
||||
self._warning_shown = False
|
||||
|
||||
def _get_script_name(self, function_name: str) -> str:
|
||||
"""
|
||||
@@ -96,19 +94,6 @@ class NoSandboxBackend(SandboxBackend):
|
||||
) -> dict:
|
||||
"""直接导入模块并执行函数"""
|
||||
|
||||
# 首次使用时显示安全警告
|
||||
if not self._warning_shown:
|
||||
warnings.warn(
|
||||
"\n" + "=" * 60 + "\n"
|
||||
"⚠️ [安全警告] 技能在无沙盒模式下运行 (SKILL_SANDBOX_MODE=none)\n"
|
||||
" 技能脚本将直接在当前进程中执行,无隔离保护。\n"
|
||||
" 建议:生产环境请设置 SKILL_SANDBOX_MODE=docker\n"
|
||||
"=" * 60,
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self._warning_shown = True
|
||||
|
||||
logger.debug(f"[NoSandbox] 执行技能: {skill_name}.{function_name}")
|
||||
|
||||
try:
|
||||
@@ -345,13 +330,13 @@ class SkillSandbox:
|
||||
self._backend = self._create_backend()
|
||||
self._initialized = True
|
||||
|
||||
logger.info(f"SkillSandbox 初始化完成,模式: {self.mode}")
|
||||
logger.debug(f"SkillSandbox 初始化完成,模式: {self.mode}")
|
||||
|
||||
def _create_backend(self) -> SandboxBackend:
|
||||
"""根据模式创建对应后端"""
|
||||
|
||||
if self.mode == "none":
|
||||
logger.info("使用无沙盒模式(直接执行)")
|
||||
logger.debug("使用无沙盒模式(直接执行)")
|
||||
return NoSandboxBackend()
|
||||
|
||||
elif self.mode == "docker":
|
||||
|
||||
Reference in New Issue
Block a user