fix: 修复子代理环境隔离、统计时间戳问题,提取子代理配置
This commit is contained in:
parent
57e998f896
commit
24e8497230
|
|
@ -36,6 +36,13 @@ frontend_port: 4000
|
|||
# Max agentic loop iterations (tool call rounds)
|
||||
max_iterations: 15
|
||||
|
||||
# Sub-agent settings (multi_agent tool)
|
||||
sub_agent:
|
||||
max_iterations: 3 # Max tool-call rounds per sub-agent
|
||||
max_tokens: 4096 # Max tokens per LLM call inside a sub-agent
|
||||
max_agents: 5 # Max number of concurrent sub-agents per request
|
||||
max_concurrency: 3 # ThreadPoolExecutor max workers
|
||||
|
||||
# Available models
|
||||
# Each model must have its own id, name, api_url, api_key
|
||||
models:
|
||||
|
|
@ -117,6 +124,7 @@ backend/
|
|||
│ ├── data.py # 计算器、文本、JSON 处理
|
||||
│ ├── weather.py # 天气查询(模拟)
|
||||
│ ├── file_ops.py # 文件操作(6 个工具,project_id 自动注入)
|
||||
│ ├── agent.py # 多智能体(子 Agent 并发执行,工具权限隔离)
|
||||
│ └── code.py # Python 代码执行(沙箱)
|
||||
└── utils/ # 辅助函数
|
||||
├── helpers.py # 通用函数(ok/err/build_messages 等)
|
||||
|
|
@ -207,6 +215,7 @@ frontend/
|
|||
| **代码执行** | execute_python | 沙箱环境执行 Python |
|
||||
| **文件操作** | file_read, file_write, file_delete, file_list, file_exists, file_mkdir | project_id 自动注入 |
|
||||
| **天气** | get_weather | 天气查询(模拟) |
|
||||
| **智能体** | multi_agent | 派生子 Agent 并发执行(禁止递归,工具权限与主 Agent 一致) |
|
||||
|
||||
## 文档
|
||||
|
||||
|
|
|
|||
|
|
@ -39,3 +39,10 @@ TOOL_MAX_WORKERS = _cfg.get("tool_max_workers", 4)
|
|||
|
||||
# Max character length for a single tool result content (truncated if exceeded)
|
||||
TOOL_RESULT_MAX_LENGTH = _cfg.get("tool_result_max_length", 4096)
|
||||
|
||||
# Sub-agent settings (multi_agent tool)
|
||||
_sa = _cfg.get("sub_agent", {})
|
||||
SUB_AGENT_MAX_ITERATIONS = _sa.get("max_iterations", 3)
|
||||
SUB_AGENT_MAX_TOKENS = _sa.get("max_tokens", 4096)
|
||||
SUB_AGENT_MAX_AGENTS = _sa.get("max_agents", 5)
|
||||
SUB_AGENT_MAX_CONCURRENCY = _sa.get("max_concurrency", 3)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,25 @@ Provides:
|
|||
- multi_agent: Spawn sub-agents with independent LLM conversation loops
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from backend.tools.factory import tool
|
||||
from backend.tools.core import registry
|
||||
from backend.tools.executor import ToolExecutor
|
||||
from backend.config import (
|
||||
DEFAULT_MODEL,
|
||||
SUB_AGENT_MAX_ITERATIONS,
|
||||
SUB_AGENT_MAX_TOKENS,
|
||||
SUB_AGENT_MAX_AGENTS,
|
||||
SUB_AGENT_MAX_CONCURRENCY,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Sub-agents are forbidden from using multi_agent to prevent infinite recursion
|
||||
BLOCKED_TOOLS = {"multi_agent"}
|
||||
|
||||
|
||||
def _to_executor_calls(tool_calls: list, id_prefix: str = "tc") -> list:
|
||||
|
|
@ -68,13 +81,16 @@ def _run_sub_agent(
|
|||
"error": "LLM client not available",
|
||||
}
|
||||
|
||||
# Build tool list – filter to requested tools or use all
|
||||
# Build tool list – filter to requested tools, then remove blocked
|
||||
all_tools = registry.list_all()
|
||||
if tool_names:
|
||||
allowed = set(tool_names)
|
||||
tools = [t for t in all_tools if t["function"]["name"] in allowed]
|
||||
else:
|
||||
tools = all_tools
|
||||
tools = list(all_tools)
|
||||
|
||||
# Remove blocked tools to prevent recursion
|
||||
tools = [t for t in tools if t["function"]["name"] not in BLOCKED_TOOLS]
|
||||
|
||||
executor = ToolExecutor(registry=registry)
|
||||
context = {"model": model}
|
||||
|
|
@ -128,7 +144,17 @@ def _run_sub_agent(
|
|||
})
|
||||
tc_list = message["tool_calls"]
|
||||
executor_calls = _to_executor_calls(tc_list)
|
||||
tool_results = executor.process_tool_calls(executor_calls, context)
|
||||
# Execute tools inside app_context – file ops and other DB-
|
||||
# dependent tools require an active Flask context and session.
|
||||
with app.app_context():
|
||||
if len(executor_calls) > 1:
|
||||
tool_results = executor.process_tool_calls_parallel(
|
||||
executor_calls, context
|
||||
)
|
||||
else:
|
||||
tool_results = executor.process_tool_calls(
|
||||
executor_calls, context
|
||||
)
|
||||
messages.extend(tool_results)
|
||||
else:
|
||||
# Final text response
|
||||
|
|
@ -159,7 +185,7 @@ def _run_sub_agent(
|
|||
"Spawn multiple sub-agents to work on tasks concurrently. "
|
||||
"Each agent runs its own independent conversation with the LLM and can use tools. "
|
||||
"Useful for parallel research, multi-file analysis, or dividing complex tasks into sub-tasks. "
|
||||
"Each agent is limited to 3 iterations and 4096 tokens to control cost."
|
||||
"Resource limits (iterations, tokens, concurrency) are configured in config.yml -> sub_agent."
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
|
|
@ -221,19 +247,18 @@ def multi_agent(arguments: dict) -> dict:
|
|||
|
||||
tasks = arguments["tasks"]
|
||||
|
||||
if len(tasks) > 5:
|
||||
return {"success": False, "error": "Maximum 5 concurrent agents allowed"}
|
||||
if len(tasks) > SUB_AGENT_MAX_AGENTS:
|
||||
return {"success": False, "error": f"Maximum {SUB_AGENT_MAX_AGENTS} concurrent agents allowed"}
|
||||
|
||||
# Get current conversation context for model/project info
|
||||
app = current_app._get_current_object()
|
||||
|
||||
# Use injected model/project_id from executor context, fall back to defaults
|
||||
from backend.config import DEFAULT_MODEL
|
||||
model = arguments.get("_model") or DEFAULT_MODEL
|
||||
project_id = arguments.get("_project_id")
|
||||
|
||||
# Execute agents concurrently (max 3 at a time)
|
||||
concurrency = min(len(tasks), 3)
|
||||
# Execute agents concurrently
|
||||
concurrency = min(len(tasks), SUB_AGENT_MAX_CONCURRENCY)
|
||||
results = [None] * len(tasks)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=concurrency) as pool:
|
||||
|
|
@ -244,9 +269,10 @@ def multi_agent(arguments: dict) -> dict:
|
|||
task["instruction"],
|
||||
task.get("tools"),
|
||||
model,
|
||||
4096,
|
||||
SUB_AGENT_MAX_TOKENS,
|
||||
project_id,
|
||||
app,
|
||||
SUB_AGENT_MAX_ITERATIONS,
|
||||
): i
|
||||
for i, task in enumerate(tasks)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Common helper functions"""
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Any
|
||||
from flask import jsonify
|
||||
from backend import db
|
||||
|
|
@ -97,7 +97,7 @@ def message_to_dict(msg: Message) -> dict:
|
|||
|
||||
def record_token_usage(user_id, model, prompt_tokens, completion_tokens):
|
||||
"""Record token usage"""
|
||||
today = date.today()
|
||||
today = datetime.now(timezone.utc).date()
|
||||
usage = TokenUsage.query.filter_by(
|
||||
user_id=user_id, date=today, model=model
|
||||
).first()
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ backend/
|
|||
│ ├── data.py # 计算器、文本、JSON
|
||||
│ ├── weather.py # 天气查询
|
||||
│ ├── file_ops.py # 文件操作(project_id 自动注入)
|
||||
│ ├── agent.py # 多智能体(子 Agent 并发执行,工具权限隔离)
|
||||
│ └── code.py # 代码执行
|
||||
│
|
||||
├── utils/ # 辅助函数
|
||||
|
|
@ -1020,6 +1021,13 @@ frontend_port: 4000
|
|||
# 智能体循环最大迭代次数(工具调用轮次上限,默认 5)
|
||||
max_iterations: 15
|
||||
|
||||
# 子代理资源配置(multi_agent 工具)
|
||||
sub_agent:
|
||||
max_iterations: 3 # 每个子代理的最大工具调用轮数
|
||||
max_tokens: 4096 # 每次调用的最大 token 数
|
||||
max_agents: 5 # 每次请求最多派生的子代理数
|
||||
max_concurrency: 3 # 并发线程数
|
||||
|
||||
# 可用模型列表(每个模型必须指定 api_url 和 api_key)
|
||||
# 支持任何 OpenAI 兼容 API(DeepSeek、GLM、OpenAI、Moonshot、Qwen 等)
|
||||
models:
|
||||
|
|
|
|||
|
|
@ -249,14 +249,25 @@ file_read({"path": "src/main.py", "project_id": "xxx"})
|
|||
|
||||
| 工具名称 | 描述 | 参数 |
|
||||
|---------|------|------|
|
||||
| `multi_agent` | 派生子 Agent 并发执行任务(最多 5 个) | `tasks`: 任务数组(name, instruction, tools)<br>`_model`: 模型名称(自动注入)<br>`_project_id`: 项目 ID(自动注入) |
|
||||
| `multi_agent` | 派生子 Agent 并发执行任务 | `tasks`: 任务数组(name, instruction, tools)<br>`_model`: 模型名称(自动注入)<br>`_project_id`: 项目 ID(自动注入) |
|
||||
|
||||
**`multi_agent` 工作原理:**
|
||||
1. 接收任务数组,每个任务指定 name、instruction 和可选的 tools 列表
|
||||
2. 为每个子 Agent 创建独立线程,各自拥有 LLM 对话循环(最多 3 轮迭代,4096 tokens)
|
||||
3. 通过 Service Locator 获取 `llm_client` 实例
|
||||
4. 子 Agent 在 `app.app_context()` 中运行,可独立调用所有注册工具
|
||||
5. 返回 `{success, results: [{task_name, success, response/error}], total}`
|
||||
2. 子 Agent **禁止使用 `multi_agent` 工具**(`BLOCKED_TOOLS`),防止无限递归
|
||||
3. 子 Agent 工具权限与主 Agent 一致(除 multi_agent 外的所有已注册工具),支持并行工具执行
|
||||
4. 为每个子 Agent 创建独立线程,各自拥有 LLM 对话循环
|
||||
5. 子 Agent 在 `app.app_context()` 中运行 LLM 调用和工具执行,确保数据库等依赖正常工作
|
||||
6. 通过 Service Locator 获取 `llm_client` 实例
|
||||
7. 返回 `{success, results: [{task_name, success, response/error}], total}`
|
||||
|
||||
**资源配置**(`config.yml` → `sub_agent`):
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `max_iterations` | 3 | 每个子代理的最大工具调用轮数 |
|
||||
| `max_tokens` | 4096 | 每次调用的最大 token 数 |
|
||||
| `max_agents` | 5 | 每次请求最多派生的子代理数 |
|
||||
| `max_concurrency` | 3 | ThreadPoolExecutor 并发线程数 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -132,8 +132,15 @@ const sortedDaily = computed(() => {
|
|||
const chartData = computed(() => {
|
||||
if (period.value === 'daily' && stats.value?.hourly) {
|
||||
const hourly = stats.value.hourly
|
||||
// Backend returns UTC hours — convert to local timezone for display.
|
||||
const offset = -new Date().getTimezoneOffset() / 60 // e.g. +8 for UTC+8
|
||||
const localHourly = {}
|
||||
for (const [utcH, val] of Object.entries(hourly)) {
|
||||
const localH = ((parseInt(utcH) + offset) % 24 + 24) % 24
|
||||
localHourly[localH] = val
|
||||
}
|
||||
let minH = 24, maxH = -1
|
||||
for (const h of Object.keys(hourly)) {
|
||||
for (const h of Object.keys(localHourly)) {
|
||||
const hour = parseInt(h)
|
||||
if (hour < minH) minH = hour
|
||||
if (hour > maxH) maxH = hour
|
||||
|
|
@ -145,16 +152,19 @@ const chartData = computed(() => {
|
|||
const h = start + i
|
||||
return {
|
||||
label: `${h}:00`,
|
||||
value: hourly[String(h)]?.total || 0,
|
||||
value: localHourly[String(h)]?.total || 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const data = sortedDaily.value
|
||||
return Object.entries(data).map(([date, val]) => {
|
||||
const d = new Date(date)
|
||||
// date is "YYYY-MM-DD" from backend — parse directly to avoid
|
||||
// new Date() timezone shift (parsed as UTC midnight then
|
||||
// getMonth/getDate applies local offset, potentially off by one day).
|
||||
const [year, month, day] = date.split('-')
|
||||
return {
|
||||
label: `${d.getMonth() + 1}/${d.getDate()}`,
|
||||
label: `${parseInt(month)}/${parseInt(day)}`,
|
||||
value: val.total,
|
||||
prompt: val.prompt || 0,
|
||||
completion: val.completion || 0,
|
||||
|
|
|
|||
Loading…
Reference in New Issue