feat: 为智能体操作实现人工审批机制

- 新增 HumanApprovalModal 组件,用于处理用户对关键操作的审批。
- 引入 useApproval 可组合项,用于管理审批状态和逻辑。
- 更新 AgentChatComponent 以显示审批模态框并处理审批操作。
- 增强消息处理功能,支持工具调用合并并改进对 AI 消息块的处理。
- 重构各种组件和 API,以整合新的审批流程,确保代理交互期间的流畅用户体验。
This commit is contained in:
Wenjie Zhang 2025-11-01 21:34:16 +08:00
parent 127c73e180
commit 181341db48
19 changed files with 989 additions and 173 deletions

1
CLAUDE.md Normal file
View File

@ -0,0 +1 @@
See AGENTS.md

View File

@ -314,7 +314,10 @@ def upload(
directory: pathlib.Path = typer.Option(
..., help="The directory containing files to upload.", exists=True, file_okay=False
),
pattern: list[str] = typer.Option(["*.md"], help="The glob patterns for files to upload (e.g., '*.pdf', '**/*.txt'). Can be specified multiple times."),
pattern: list[str] = typer.Option(
["*.md"],
help="The glob patterns for files to upload (e.g., '*.pdf', '**/*.txt'). Can be specified multiple times.",
),
base_url: str = typer.Option("http://127.0.0.1:5050/api", help="The base URL of the API server."),
username: str = typer.Option(..., help="Admin username for login."),
password: str = typer.Option(..., help="Admin password for login."),
@ -354,7 +357,9 @@ def upload(
if not all_files:
patterns_str = "', '".join(pattern)
console.print(f"[bold yellow]No files found in '{directory}' matching patterns: '{patterns_str}'. Aborting.[/bold yellow]")
console.print(
f"[bold yellow]No files found in '{directory}' matching patterns: '{patterns_str}'. Aborting.[/bold yellow]"
)
raise typer.Exit()
# 过滤掉macos的隐藏文件
@ -398,11 +403,13 @@ def upload(
# Split all files into batches
for batch_num in range(0, len(files_to_upload), batch_size):
batch_files = files_to_upload[batch_num:batch_num + batch_size]
batch_files = files_to_upload[batch_num : batch_num + batch_size]
batch_start = batch_num + 1
batch_end = min(batch_num + batch_size, len(files_to_upload))
console.print(f"\n[bold yellow]=== Batch {batch_start}-{batch_end} of {len(files_to_upload)} ===[/bold yellow]")
console.print(
f"\n[bold yellow]=== Batch {batch_start}-{batch_end} of {len(files_to_upload)} ===[/bold yellow]"
)
# Step 1: Upload this batch of files sequentially
console.print(f"[blue]Step 1: Uploading {len(batch_files)} files...[/blue]")
@ -420,7 +427,9 @@ def upload(
console=console,
transient=True,
) as progress:
upload_task_id = progress.add_task(f"Uploading batch {batch_start}-{batch_end}...", total=len(batch_files), postfix="")
upload_task_id = progress.add_task(
f"Uploading batch {batch_start}-{batch_end}...", total=len(batch_files), postfix=""
)
for file_path, file_hash in batch_files:
server_file_path = await upload_single_file(
@ -458,7 +467,9 @@ def upload(
# Step 3: Wait for this batch to complete
if wait_for_completion and task_id:
console.print(f"[cyan]Step 3: Waiting for batch {batch_start}-{batch_end} to complete...[/cyan]")
console.print(
f"[cyan]Step 3: Waiting for batch {batch_start}-{batch_end} to complete...[/cyan]"
)
await wait_for_tasks_completion(client, base_url, [task_id], poll_interval)
console.print(f"[green]Batch {batch_start}-{batch_end} completed![/green]")
else:

View File

@ -8,6 +8,7 @@ from pathlib import Path
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.responses import StreamingResponse
from langchain.messages import AIMessageChunk, HumanMessage
from langgraph.types import Command
from pydantic import BaseModel
from sqlalchemy.orm import Session
@ -81,6 +82,200 @@ async def set_default_agent(request_data: dict = Body(...), current_user=Depends
# =============================================================================
async def _get_langgraph_messages(agent_instance, config_dict):
"""获取LangGraph中的消息"""
graph = await agent_instance.get_graph()
state = await graph.aget_state(config_dict)
if not state or not state.values:
logger.warning("No state found in LangGraph")
return None
return state.values.get("messages", [])
def _get_existing_message_ids(conv_mgr, thread_id):
"""获取已保存的消息ID集合"""
existing_messages = conv_mgr.get_messages_by_thread_id(thread_id)
return {msg.extra_metadata["id"] for msg in existing_messages if msg.extra_metadata and "id" in msg.extra_metadata}
async def _save_ai_message(conv_mgr, thread_id, msg_dict):
"""保存AI消息和相关的工具调用"""
content = msg_dict.get("content", "")
tool_calls_data = msg_dict.get("tool_calls", [])
# 保存AI消息
ai_msg = conv_mgr.add_message_by_thread_id(
thread_id=thread_id,
role="assistant",
content=content,
message_type="text",
extra_metadata=msg_dict,
)
# 保存工具调用
if tool_calls_data:
logger.debug(f"Saving {len(tool_calls_data)} tool calls from AI message")
for tc in tool_calls_data:
conv_mgr.add_tool_call(
message_id=ai_msg.id,
tool_name=tc.get("name", "unknown"),
tool_input=tc.get("args", {}),
status="pending",
langgraph_tool_call_id=tc.get("id"),
)
logger.debug(f"Saved AI message {ai_msg.id} with {len(tool_calls_data)} tool calls")
def _save_tool_message(conv_mgr, msg_dict):
"""保存工具执行结果"""
tool_call_id = msg_dict.get("tool_call_id")
content = msg_dict.get("content", "")
name = msg_dict.get("name", "")
if not tool_call_id:
return
# 确保tool_output是字符串类型
if isinstance(content, list):
tool_output = json.dumps(content) if content else ""
else:
tool_output = str(content)
# 更新工具调用结果
updated_tc = conv_mgr.update_tool_call_output(
langgraph_tool_call_id=tool_call_id,
tool_output=tool_output,
status="success",
)
if updated_tc:
logger.debug(f"Updated tool_call {tool_call_id} ({name}) with output")
else:
logger.warning(f"Tool call {tool_call_id} not found for update")
async def save_messages_from_langgraph_state(
agent_instance,
thread_id,
conv_mgr,
config_dict,
):
"""
LangGraph state 中读取完整消息并保存到数据库
这样可以获得完整的 tool_calls 参数
"""
try:
messages = await _get_langgraph_messages(agent_instance, config_dict)
if messages is None:
return
logger.debug(f"Retrieved {len(messages)} messages from LangGraph state")
existing_ids = _get_existing_message_ids(conv_mgr, thread_id)
for msg in messages:
msg_dict = msg.model_dump() if hasattr(msg, "model_dump") else {}
msg_type = msg_dict.get("type", "unknown")
if msg_type == "human" or msg.id in existing_ids:
continue
if msg_type == "ai":
await _save_ai_message(conv_mgr, thread_id, msg_dict)
elif msg_type == "tool":
_save_tool_message(conv_mgr, msg_dict)
else:
logger.warning(f"Unknown message type: {msg_type}, skipping")
continue
logger.debug(f"Processed message type={msg_type}")
logger.info("Saved messages from LangGraph state")
except Exception as e:
logger.error(f"Error saving messages from LangGraph state: {e}")
logger.error(traceback.format_exc())
async def check_and_handle_interrupts(agent, langgraph_config, make_chunk, meta, thread_id):
"""检查并处理 LangGraph 中断状态,发送人工审批请求到前端"""
try:
# 获取 agent 的 graph 对象
graph = await agent.get_graph()
# 获取当前状态,检查是否有中断
state = await graph.aget_state(langgraph_config)
if not state or not state.values:
logger.debug("No state found when checking for interrupts")
return
# 检查是否有中断信息
# LangGraph 中断信息通常在 state.tasks 或 __interrupt__ 字段中
interrupt_info = None
# 方法1: 检查 state.tasks 中的中断
if hasattr(state, "tasks") and state.tasks:
for task in state.tasks:
if hasattr(task, "interrupts") and task.interrupts:
interrupt_info = task.interrupts[0] # 取第一个中断
break
# 方法2: 检查 state.values 中的 __interrupt__ 字段
if not interrupt_info and state.values:
interrupt_data = state.values.get("__interrupt__")
if interrupt_data and isinstance(interrupt_data, list) and len(interrupt_data) > 0:
interrupt_info = interrupt_data[0]
# 方法3: 检查 state.next 字段,如果指向中断节点
if not interrupt_info and hasattr(state, "next") and state.next:
# 如果 next 指向某个需要审批的节点,可能需要额外处理
logger.debug(f"State next nodes: {state.next}")
if interrupt_info:
logger.info(f"Human approval interrupt detected: {interrupt_info}")
# 提取中断信息
question = "是否批准以下操作?"
operation = "需要人工审批的操作"
if isinstance(interrupt_info, dict):
question = interrupt_info.get("question", question)
operation = interrupt_info.get("operation", operation)
elif isinstance(interrupt_info, (list, tuple)) and len(interrupt_info) > 0:
# 有些情况下中断信息可能是元组形式
first_interrupt = interrupt_info[0]
if isinstance(first_interrupt, dict):
question = first_interrupt.get("question", question)
operation = first_interrupt.get("operation", operation)
else:
operation = str(first_interrupt)
else:
operation = str(interrupt_info)
# 发送人工审批请求到前端
logger.info(f"Sending human approval request - question: {question}, operation: {operation}")
yield make_chunk(
status="human_approval_required",
thread_id=thread_id,
interrupt_info={"question": question, "operation": operation},
)
else:
logger.debug("No human approval interrupt detected")
except Exception as e:
logger.error(f"Error checking for interrupts: {e}")
logger.error(traceback.format_exc())
# 不抛出异常,避免影响主流程
# =============================================================================
@chat.post("/call")
async def call(query: str = Body(...), meta: dict = Body(None), current_user: User = Depends(get_required_user)):
"""调用模型进行简单问答(需要登录)"""
@ -147,114 +342,6 @@ async def chat_agent(
+ b"\n"
)
async def save_messages_from_langgraph_state(
agent_instance,
thread_id,
conv_mgr,
config_dict,
):
"""
LangGraph state 中读取完整消息并保存到数据库
这样可以获得完整的 tool_calls 参数
"""
try:
graph = await agent_instance.get_graph()
state = await graph.aget_state(config_dict)
if not state or not state.values:
logger.warning("No state found in LangGraph")
return
messages = state.values.get("messages", [])
logger.debug(f"Retrieved {len(messages)} messages from LangGraph state")
# 获取已保存的消息数量,避免重复保存
existing_messages = conv_mgr.get_messages_by_thread_id(thread_id)
existing_ids = {
msg.extra_metadata["id"]
for msg in existing_messages
if msg.extra_metadata and "id" in msg.extra_metadata
}
for msg in messages:
msg_dict = msg.model_dump() if hasattr(msg, "model_dump") else {}
msg_type = msg_dict.get("type", "unknown")
if msg_type == "human" or msg.id in existing_ids:
continue
elif msg_type == "ai":
# AI 消息
content = msg_dict.get("content", "")
tool_calls_data = msg_dict.get("tool_calls", [])
# 格式清洗
if finish_reason := msg_dict.get("response_metadata", {}).get("finish_reason"):
if "tool_call" in finish_reason and len(finish_reason) > len("tool_call"):
model_name = msg_dict.get("response_metadata", {}).get("model_name", "")
repeat_count = len(finish_reason) // len("tool_call")
msg_dict["response_metadata"]["finish_reason"] = "tool_call"
msg_dict["response_metadata"]["model_name"] = model_name[: len(model_name) // repeat_count]
# 保存 AI 消息
ai_msg = conv_mgr.add_message_by_thread_id(
thread_id=thread_id,
role="assistant",
content=content,
message_type="text",
extra_metadata=msg_dict, # 保存原始 model_dump
)
# 保存 tool_calls如果有- 使用 LangGraph 的 tool_call_id
if tool_calls_data:
logger.debug(f"Saving {len(tool_calls_data)} tool calls from AI message")
for tc in tool_calls_data:
conv_mgr.add_tool_call(
message_id=ai_msg.id,
tool_name=tc.get("name", "unknown"),
tool_input=tc.get("args", {}), # 完整的参数
status="pending", # 工具还未执行
langgraph_tool_call_id=tc.get("id"), # 保存 LangGraph tool_call_id
)
logger.debug(f"Saved AI message {ai_msg.id} with {len(tool_calls_data)} tool calls")
elif msg_type == "tool":
# 工具执行结果消息 - 使用 tool_call_id 精确匹配
tool_call_id = msg_dict.get("tool_call_id")
content = msg_dict.get("content", "")
name = msg_dict.get("name", "")
if tool_call_id:
# 确保tool_output是字符串类型避免SQLite不支持列表类型
if isinstance(content, list):
tool_output = json.dumps(content) if content else ""
else:
tool_output = str(content)
# 通过 LangGraph tool_call_id 精确匹配并更新
updated_tc = conv_mgr.update_tool_call_output(
langgraph_tool_call_id=tool_call_id,
tool_output=tool_output,
status="success",
)
if updated_tc:
logger.debug(f"Updated tool_call {tool_call_id} ({name}) with output")
else:
logger.warning(f"Tool call {tool_call_id} not found for update")
else:
logger.warning(f"Unknown message type: {msg_type}, skipping")
continue
logger.debug(f"Processed message type={msg_type}")
logger.info("Saved messages from LangGraph state")
except Exception as e:
logger.error(f"Error saving messages from LangGraph state: {e}")
logger.error(traceback.format_exc())
# TODO:[功能建议]针对需要人工审批后再执行的工具,
# 可以使用langgraph的interrupt方法中断对话等待用户输入后再使用command跳转回去
async def stream_messages():
@ -323,11 +410,17 @@ async def chat_agent(
yield make_chunk(message="检测到敏感内容,已中断输出", status="error")
return
# After streaming finished, check for interrupts and save messages
langgraph_config = {"configurable": input_context}
# Check for human approval interrupts
async for chunk in check_and_handle_interrupts(agent, langgraph_config, make_chunk, meta, thread_id):
yield chunk
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
yield make_chunk(status="finished", meta=meta)
# After streaming finished, save all messages from LangGraph state
langgraph_config = {"configurable": input_context}
# Save all messages from LangGraph state
await save_messages_from_langgraph_state(
agent_instance=agent,
thread_id=thread_id,
@ -336,8 +429,17 @@ async def chat_agent(
)
except (asyncio.CancelledError, ConnectionError) as e:
# 客户端主动中断连接,尝试保存已生成的部分内容
# 客户端主动中断连接,检查中断并保存已生成的部分内容
logger.warning(f"Client disconnected, cancelling stream: {e}")
# 即使在断开连接时也检查中断,确保状态一致性
langgraph_config = {"configurable": input_context}
try:
async for chunk in check_and_handle_interrupts(agent, langgraph_config, make_chunk, meta, thread_id):
yield chunk
except Exception as interrupt_error:
logger.error(f"Error checking interrupts during disconnect: {interrupt_error}")
if full_msg:
# 创建新的 db session因为原 session 可能已关闭
new_db = db_manager.get_session()
@ -360,6 +462,15 @@ async def chat_agent(
except Exception as e:
logger.error(f"Error streaming messages: {e}, {traceback.format_exc()}")
# 即使在异常情况下也检查中断,确保状态一致性
langgraph_config = {"configurable": input_context}
try:
async for chunk in check_and_handle_interrupts(agent, langgraph_config, make_chunk, meta, thread_id):
yield chunk
except Exception as interrupt_error:
logger.error(f"Error checking interrupts during exception: {interrupt_error}")
if full_msg:
# 创建新的 db session因为原 session 可能已关闭
new_db = db_manager.get_session()
@ -420,6 +531,91 @@ async def get_tools(agent_id: str, current_user: User = Depends(get_required_use
return {"tools": {tool["id"]: tool for tool in tools_info}}
@chat.post("/agent/{agent_id}/resume")
async def resume_agent_chat(
agent_id: str,
thread_id: str = Body(...),
approved: bool = Body(...),
current_user: User = Depends(get_required_user),
db: Session = Depends(get_db),
):
"""恢复被人工审批中断的对话(需要登录)"""
start_time = asyncio.get_event_loop().time()
logger.info(f"Resuming agent_id: {agent_id}, thread_id: {thread_id}, approved: {approved}")
meta = {
"agent_id": agent_id,
"thread_id": thread_id,
"user_id": current_user.id,
"approved": approved,
}
if "request_id" not in meta or not meta.get("request_id"):
meta["request_id"] = str(uuid.uuid4())
async def stream_resume():
# 定义resume专用的make_chunk函数与主聊天端点保持一致
def make_resume_chunk(content=None, **kwargs):
return (
json.dumps(
{"request_id": meta.get("request_id"), "response": content, **kwargs}, ensure_ascii=False
).encode("utf-8")
+ b"\n"
)
try:
agent = agent_manager.get_agent(agent_id)
except Exception as e:
logger.error(f"Error getting agent {agent_id}: {e}, {traceback.format_exc()}")
yield (
f'{{"request_id": "{meta.get("request_id")}", "message": '
f'"Error getting agent {agent_id}: {e}", "status": "error"}}\n'
)
return
# 发送init状态块与主聊天端点保持一致
init_msg = {"type": "system", "content": f"Resume with approved: {approved}"}
yield make_resume_chunk(status="init", meta=meta, msg=init_msg)
# 使用 Command(resume=approved) 恢复执行
resume_command = Command(resume=approved)
graph = await agent.get_graph()
# 加载 context包含 tools, model 等配置)
input_context = {"user_id": str(current_user.id), "thread_id": thread_id}
context = agent.context_schema.from_file(module_name=agent.module_name, input_context=input_context)
logger.debug(f"Resume with context: {context}")
# 创建流式数据源
stream_source = graph.astream(
resume_command, context=context, config={"configurable": input_context}, stream_mode="messages"
)
async for msg, metadata in stream_source:
# 确保msg有正确的ID结构
msg_dict = msg.model_dump()
if "id" not in msg_dict:
msg_dict["id"] = str(uuid.uuid4())
yield make_resume_chunk(
content=getattr(msg, "content", ""), msg=msg_dict, metadata=metadata, status="loading"
)
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
yield make_resume_chunk(status="finished", meta=meta)
# 保存消息到数据库
langgraph_config = {"configurable": input_context}
conv_manager = ConversationManager(db)
await save_messages_from_langgraph_state(
agent_instance=agent,
thread_id=thread_id,
conv_mgr=conv_manager,
config_dict=langgraph_config,
)
return StreamingResponse(stream_resume(), media_type="application/json")
@chat.post("/agent/{agent_id}/config")
async def save_agent_config(agent_id: str, config: dict = Body(...), current_user: User = Depends(get_required_user)):
"""保存智能体配置到YAML文件需要登录"""

View File

@ -88,6 +88,7 @@ async def get_mcp_tools(server_name: str, additional_servers: dict[str, dict] =
logger.error(f"Failed to load tools from MCP server '{server_name}': {e}")
return []
async def get_all_mcp_tools() -> list[Callable[..., Any]]:
"""Get all tools from all configured MCP servers."""
all_tools = []

View File

@ -10,8 +10,9 @@ from src.agents.common.mcp import get_mcp_tools
from src.agents.common.models import load_chat_model
from src.utils import logger
from .state import BaseState
from .context import BaseContext
from .state import BaseState
class ToolAgent(BaseAgent):
name = "ToolAgent"
@ -24,7 +25,6 @@ class ToolAgent(BaseAgent):
self.context_schema = BaseContext
self.agent_tools = None
# TODO:[修改建议] _get_invoke_tools,llm_call,dynamic_tools_node这类针对工具调用的功能大多数Agent都能用得到
# 可以通过一个ToolAgent类继承BaseAgent,通过重写抽象方法获取tools,通过继承BaseState和BaseContext获取配置
# 必要时可通过重写以下方法实现其他逻辑
@ -33,7 +33,6 @@ class ToolAgent(BaseAgent):
logger.error(f"get_tools() is not implemented in {self.__class__.__name__}")
return []
async def _get_invoke_tools(self, selected_tools: list[str], selected_mcps: list[str]):
"""根据配置获取工具。
默认不使用任何工具

View File

@ -11,6 +11,7 @@ from pydantic import BaseModel, Field
from src import config, graph_base, knowledge_base
from src.utils import logger
# TODO[修改建议]:前端需要通过interrupt进行交互点击是或否来批准执行
# 返回中断点:
# is_approved : bool = True 或者 False
@ -20,7 +21,7 @@ from src.utils import logger
@tool(name_or_callable="人工审批工具", description="请求人工审批工具,用于在执行重要操作前获得人类确认。")
def get_approved_user_goal(
operation_description: str,
)->dict:
) -> dict:
"""
请求人工审批在执行重要操作前获得人类确认
@ -54,6 +55,7 @@ def get_approved_user_goal(
return result
@tool(name_or_callable="查询知识图谱", description="使用这个工具可以查询知识图谱中包含的三元组信息。")
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
"""Use this to query knowledge graph, which include some food domain knowledge."""
@ -72,10 +74,7 @@ def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge
def get_static_tools() -> list:
"""注册静态工具"""
static_tools = [
query_knowledge_graph,
get_approved_user_goal
]
static_tools = [query_knowledge_graph, get_approved_user_goal]
# 检查是否启用网页搜索
if config.enable_web_search:

View File

@ -1,4 +1,3 @@
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, ModelResponse, dynamic_prompt, wrap_model_call

View File

@ -13,12 +13,12 @@ class SampleMultiAgent(ToolAgent):
description = "Supervisor智能体具有调用其他子智能体的能力(在工具中添加)"
# TODO[已完成]: 通过将其他agent封装为工具的方式添加了多智能体调度
'''
"""
你是一个多智能体核心通过多智能体调用的方式帮助用户完成一系列任务
1.当你需要知识库问答功能时请调用对话聊天智能体实现
2.当你需要加密计算的时候请调用加密计算智能体实现
'''
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)

View File

@ -7,6 +7,7 @@ from src.agents import agent_manager
from src.agents.common.tools import get_buildin_tools
from src.utils import logger
# TODO[修改建议]:能不能通过前端直接指定子智能体?
# 调用子智能体后的日志是输出到tool_calls的
@tool(name_or_callable="对话聊天智能体", description="调用指定智能体进行对话聊天的功能")
@ -23,20 +24,21 @@ async def call_chatbot(query: str, config: RunnableConfig) -> str:
try:
input = [{"role": "user", "content": query}]
chatbot = agent_manager.get_agent("ChatbotAgent")
configurable = config.get("configurable",{})
configurable = config.get("configurable", {})
input_context = {
"thread_id":configurable.get("thread_id"),
"thread_id": configurable.get("thread_id"),
"user_id": configurable.get("user_id"),
}
message = await chatbot.invoke_messages(input,input_context=input_context)
message = await chatbot.invoke_messages(input, input_context=input_context)
# 直接获取最后一个消息的内容
final_answer = message.get('messages', [])[-1].content
final_answer = message.get("messages", [])[-1].content
logger.info(f"ChatbotAgent: {final_answer}")
return final_answer
except Exception as e:
logger.error(f"CallAgent error: {e}")
raise
@tool(name_or_callable="加密计算智能体", description="调用指定智能体进行加密计算的功能")
async def call_react_agent(query: str, config: RunnableConfig) -> str:
"""
@ -51,14 +53,14 @@ async def call_react_agent(query: str, config: RunnableConfig) -> str:
try:
input = [{"role": "user", "content": query}]
chatbot = agent_manager.get_agent("ReActAgent")
configurable = config.get("configurable",{})
configurable = config.get("configurable", {})
input_context = {
"thread_id":configurable.get("thread_id"),
"thread_id": configurable.get("thread_id"),
"user_id": configurable.get("user_id"),
}
message = await chatbot.invoke_messages(input,input_context=input_context)
message = await chatbot.invoke_messages(input, input_context=input_context)
# 直接获取最后一个消息的内容
final_answer = message.get('messages', [])[-1].content
final_answer = message.get("messages", [])[-1].content
logger.info(f"ReActAgent: {final_answer}")
return final_answer
except Exception as e:

View File

@ -3,10 +3,11 @@ from langgraph.graph import StateGraph
from src.agents.common.toolagent import ToolAgent
from .state import State
from .context import Context
from .state import State
from .tools import get_tools
def tools_branch_continue(state: State):
messages = state.messages
last_message = messages[-1]
@ -21,14 +22,14 @@ class ReActAgent(ToolAgent):
name = "ReActAgent"
description = "符合 ReAct 范式的智能体,可以通过调用工具来完成复杂任务。"
'''
"""
提示词示例
你是一个智能体助手
你的所有需要计算的工作不能自己计算只能通过调用加密计算机工具进行,你只能通过调用该工具的方式进行计算才能得到正确答案计算顺序是正常的
例如 2+3+2你必须调用两次加密计算机工具进行运算最终结果为9
'''
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -64,4 +65,3 @@ class ReActAgent(ToolAgent):
graph = builder.compile(checkpointer=self.checkpointer, name=self.name)
self.graph = graph
return graph

View File

@ -6,6 +6,7 @@ from src.agents.common.toolkits.mysql import get_mysql_tools
from src.agents.common.tools import get_buildin_tools
from src.utils import logger
@tool(name_or_callable="加密计算器", description="可以对给定的2个数字选择进行加减乘除四种加密计算")
def calculator(a: float, b: float, operation: str) -> float:
"""

View File

@ -4,8 +4,8 @@ from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, ModelResponse, dynamic_prompt, wrap_model_call
from src.agents.common.base import BaseAgent
from src.agents.common.models import load_chat_model
from src.agents.common.mcp import get_mcp_tools
from src.agents.common.models import load_chat_model
from src.agents.common.toolkits.mysql import get_mysql_tools
from src.utils import logger
@ -16,6 +16,7 @@ _mcp_servers = {
},
}
@dynamic_prompt
def context_aware_prompt(request: ModelRequest) -> str:
user_prompt = request.runtime.context.system_prompt

View File

@ -136,9 +136,8 @@ class KnowledgeBase(ABC):
"""
from src.utils import hashstr
# 从 kwargs 中获取 is_private 配置
is_private = kwargs.get('is_private', False)
is_private = kwargs.get("is_private", False)
prefix = "kb_private_" if is_private else "kb_"
db_id = f"{prefix}{hashstr(database_name, with_salt=True)}"

View File

@ -137,7 +137,33 @@ export const agentApi = {
* 获取所有可用工具的信息
* @returns {Promise} - 工具信息列表
*/
getTools: (agentId) => apiGet(`/api/chat/tools?agent_id=${agentId}`)
getTools: (agentId) => apiGet(`/api/chat/tools?agent_id=${agentId}`),
/**
* 恢复被人工审批中断的对话流式响应
* @param {string} agentId - 智能体ID
* @param {Object} data - 恢复数据 { thread_id, approved }
* @param {Object} options - 可选参数signal, headers等
* @returns {Promise} - 恢复响应流
*/
resumeAgentChat: (agentId, data, options = {}) => {
const { signal, headers: extraHeaders, ...restOptions } = options || {};
const baseHeaders = {
'Content-Type': 'application/json',
...useUserStore().getAuthHeaders()
};
return fetch(`/api/chat/agent/${agentId}/resume`, {
method: 'POST',
body: JSON.stringify(data),
signal,
headers: {
...baseHeaders,
...(extraHeaders || {})
},
...restOptions
})
}
}

View File

@ -29,7 +29,7 @@
<div type="button" class="agent-nav-btn" v-if="!uiState.isSidebarOpen" @click="toggleSidebar">
<PanelLeftOpen class="nav-btn-icon" size="18"/>
</div>
<div type="button" class="agent-nav-btn" v-if="!uiState.isSidebarOpen" @click="createNewChat" :disabled="isProcessing">
<div type="button" class="agent-nav-btn" v-if="!uiState.isSidebarOpen" @click="createNewChat" :disabled="chatState.creatingNewChat">
<MessageCirclePlus class="nav-btn-icon" size="18"/>
<span class="text" :class="{'hide-text': isMediumContainer}">新对话</span>
</div>
@ -97,14 +97,14 @@
</AgentMessageComponent>
<!-- 显示对话最后一个消息使用的模型 -->
<RefsComponent
v-if="getLastMessage(conv) && conv.status !== 'streaming'"
v-if="shouldShowRefs(conv)"
:message="getLastMessage(conv)"
:show-refs="['model', 'copy']"
:is-latest-message="false"
/>
</div>
<!-- 生成中的加载状态 -->
<!-- 生成中的加载状态 - 增强条件支持主聊天和resume流程 -->
<div class="generating-status" v-if="isProcessing && conversations.length > 0">
<div class="generating-indicator">
<div class="loading-dots">
@ -117,6 +117,15 @@
</div>
</div>
<div class="bottom">
<!-- 人工审批弹窗 - 放在输入框上方 -->
<HumanApprovalModal
:visible="approvalState.showModal"
:question="approvalState.question"
:operation="approvalState.operation"
@approve="handleApprove"
@reject="handleReject"
/>
<div class="message-input-wrapper" v-if="conversations.length > 0">
<MessageInputComponent
v-model="userInput"
@ -152,6 +161,8 @@ import { useAgentStore } from '@/stores/agent';
import { storeToRefs } from 'pinia';
import { MessageProcessor } from '@/utils/messageProcessor';
import { agentApi, threadApi } from '@/apis';
import HumanApprovalModal from '@/components/HumanApprovalModal.vue';
import { useApproval } from '@/composables/useApproval';
// ==================== PROPS & EMITS ====================
const props = defineProps({
@ -181,6 +192,14 @@ const exampleQuestions = computed(() => {
}));
});
// Keep per-thread streaming scratch data in a consistent shape.
const createOnGoingConvState = () => ({
msgChunks: {},
currentRequestKey: null,
currentAssistantKey: null,
toolCallBuffers: {}
});
const chatState = reactive({
currentThreadId: null,
isLoadingThreads: false,
@ -227,6 +246,18 @@ const currentThread = computed(() => {
const currentThreadMessages = computed(() => threadMessages.value[currentChatId.value] || []);
// Refs
const shouldShowRefs = computed(() => {
return (conv) => {
return getLastMessage(conv) &&
conv.status !== 'streaming' &&
!approvalState.showModal &&
!(approvalState.threadId &&
chatState.currentThreadId === approvalState.threadId &&
isProcessing.value);
};
});
// 线computed
const currentThreadState = computed(() => {
return getThreadState(currentChatId.value);
@ -316,7 +347,7 @@ const getThreadState = (threadId) => {
chatState.threadStates[threadId] = {
isStreaming: false,
streamAbortController: null,
onGoingConv: { msgChunks: {} }
onGoingConv: createOnGoingConvState()
};
}
return chatState.threadStates[threadId];
@ -349,11 +380,11 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
//
setTimeout(() => {
if (threadState.onGoingConv) {
threadState.onGoingConv = { msgChunks: {} };
threadState.onGoingConv = createOnGoingConvState();
}
}, 100);
} else {
threadState.onGoingConv = { msgChunks: {} };
threadState.onGoingConv = createOnGoingConvState();
}
}
} else {
@ -369,11 +400,11 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
if (preserveMessages) {
setTimeout(() => {
if (threadState.onGoingConv) {
threadState.onGoingConv = { msgChunks: {} };
threadState.onGoingConv = createOnGoingConvState();
}
}, 100);
} else {
threadState.onGoingConv = { msgChunks: {} };
threadState.onGoingConv = createOnGoingConvState();
}
}
} else {
@ -388,6 +419,7 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
const _processStreamChunk = (chunk, threadId) => {
const { status, msg, request_id, message } = chunk;
const threadState = getThreadState(threadId);
// console.log('Processing stream chunk:', chunk, 'for thread:', threadId);
if (!threadState) return false;
@ -399,10 +431,10 @@ const _processStreamChunk = (chunk, threadId) => {
if (msg.id) {
if (!threadState.onGoingConv.msgChunks[msg.id]) {
threadState.onGoingConv.msgChunks[msg.id] = [];
}
}
threadState.onGoingConv.msgChunks[msg.id].push(msg);
}
return false;
return false;
case 'error':
handleChatError({ message }, 'stream');
// Stop the loading indicator
@ -420,6 +452,9 @@ const _processStreamChunk = (chunk, threadId) => {
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId });
resetOnGoingConv(threadId);
return true;
case 'human_approval_required':
// 使 composable
return processApprovalInStream(chunk, threadId, currentAgentId.value);
case 'finished':
//
if (threadState) {
@ -542,6 +577,13 @@ const fetchThreadMessages = async ({ agentId, threadId }) => {
}
};
// ==================== ====================
const { approvalState, handleApproval, processApprovalInStream } = useApproval({
getThreadState,
resetOnGoingConv,
fetchThreadMessages
});
//
const sendMessage = async ({ agentId, threadId, text, signal = undefined }) => {
if (!agentId || !threadId || !text) {
@ -590,7 +632,7 @@ const switchToFirstChatIfEmpty = async () => {
};
const createNewChat = async () => {
if (!AgentValidator.validateAgentId(currentAgentId.value, '创建对话') || isProcessing.value) return;
if (!AgentValidator.validateAgentId(currentAgentId.value, '创建对话') || chatState.creatingNewChat) return;
//
if (await switchToFirstChatIfEmpty()) return;
@ -603,6 +645,17 @@ const createNewChat = async () => {
try {
const newThread = await createThread(currentAgentId.value, '新的对话');
if (newThread) {
// 线
const previousThreadId = chatState.currentThreadId;
if (previousThreadId) {
const previousThreadState = getThreadState(previousThreadId);
if (previousThreadState?.isStreaming && previousThreadState.streamAbortController) {
previousThreadState.streamAbortController.abort();
previousThreadState.isStreaming = false;
previousThreadState.streamAbortController = null;
}
}
chatState.currentThreadId = newThread.id;
}
} catch (error) {
@ -615,8 +668,17 @@ const createNewChat = async () => {
const selectChat = async (chatId) => {
if (!AgentValidator.validateAgentIdWithError(currentAgentId.value, '选择对话', handleValidationError)) return;
// 线线
// resetOnGoingConv(chatState.currentThreadId);
// 线
const previousThreadId = chatState.currentThreadId;
if (previousThreadId && previousThreadId !== chatId) {
const previousThreadState = getThreadState(previousThreadId);
if (previousThreadState?.isStreaming && previousThreadState.streamAbortController) {
previousThreadState.streamAbortController.abort();
previousThreadState.isStreaming = false;
previousThreadState.streamAbortController = null;
}
}
chatState.currentThreadId = chatId;
chatState.isLoadingMessages = true;
try {
@ -763,6 +825,106 @@ const handleSendOrStop = async () => {
await handleSendMessage();
};
// ==================== ====================
const handleApprovalWithStream = async (approved) => {
console.log('🔄 [STREAM] Starting resume stream processing');
const threadId = approvalState.threadId;
if (!threadId) {
message.error('无效的审批请求');
approvalState.showModal = false;
return;
}
const threadState = getThreadState(threadId);
if (!threadState) {
message.error('无法找到对应的对话线程');
approvalState.showModal = false;
return;
}
try {
// 使 composable
const response = await handleApproval(approved, currentAgentId.value);
if (!response) return; // handleApproval
console.log('🔄 [STREAM] Processing resume streaming response');
//
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let stopReading = false;
while (!stopReading) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine) {
try {
const chunk = JSON.parse(trimmedLine);
console.log('🔄 [STREAM] Processing chunk:', chunk);
// chunk - _processStreamChunk
if (_processStreamChunk(chunk, threadId)) {
stopReading = true;
break;
}
} catch (e) {
console.warn('Failed to parse stream chunk JSON:', e, 'Line:', trimmedLine);
}
}
}
}
if (!stopReading && buffer.trim()) {
try {
const chunk = JSON.parse(buffer.trim());
console.log('🔄 [STREAM] Processing final chunk:', chunk);
// chunk - _processStreamChunk
if (_processStreamChunk(chunk, threadId)) {
stopReading = true;
}
} catch (e) {
console.warn('Failed to parse final stream chunk JSON:', e);
}
}
console.log('🔄 [STREAM] Resume stream processing completed');
} catch (error) {
console.error('❌ [STREAM] Resume stream failed:', error);
if (error.name !== 'AbortError') {
console.error('Resume approval error:', error);
// handleChatError useApproval
}
} finally {
console.log('🔄 [STREAM] Cleaning up streaming state');
if (threadState) {
threadState.isStreaming = false;
threadState.streamAbortController = null;
}
}
};
const handleApprove = () => {
handleApprovalWithStream(true);
};
const handleReject = () => {
handleApprovalWithStream(false);
};
// ==================== UI HANDLERS ====================
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@ -795,7 +957,6 @@ defineExpose({
getExportPayload: buildExportPayload
});
const retryMessage = (msg) => { /* TODO */ };
const toggleSidebar = () => {
uiState.isSidebarOpen = !uiState.isSidebarOpen;
localStorage.setItem('chat_sidebar_open', uiState.isSidebarOpen);
@ -812,7 +973,24 @@ const getLastMessage = (conv) => {
};
const showMsgRefs = (msg) => {
if (msg.isLast) return ['copy'];
// refs
if (approvalState.showModal) {
return false;
}
// 线ID线ID
// refs
if (approvalState.threadId &&
chatState.currentThreadId === approvalState.threadId &&
!approvalState.showModal &&
isProcessing) {
return false;
}
// refs
if (msg.isLast && msg.status === 'finished') {
return ['copy'];
}
return false;
};
@ -875,6 +1053,7 @@ watch(currentAgentId, async (newAgentId, oldAgentId) => {
}
}, { immediate: true });
watch(conversations, () => {
if (isProcessing.value) {
scrollController.scrollToBottom();

View File

@ -3,6 +3,8 @@
<!-- 用户消息 -->
<p v-if="message.type === 'human'" class="message-text">{{ message.content }}</p>
<p v-else-if="message.type === 'system'" class="message-text-system">{{ message.content }}</p>
<!-- 助手消息 -->
<div v-else-if="message.type === 'ai'" class="assistant-message">
<div v-if="parsedData.reasoning_content" class="reasoning-box">
@ -232,6 +234,19 @@ const toggleToolCall = (toolCallId) => {
white-space: pre-line;
}
.message-text-system {
max-width: 100%;
margin-bottom: 0;
white-space: pre-line;
color: var(--gray-600);
font-style: italic;
font-size: 14px;
padding: 8px 12px;
background-color: var(--gray-50);
border-left: 3px solid var(--gray-300);
border-radius: 4px;
}
.err-msg {
color: #d15252;
border: 1px solid #f19999;

View File

@ -0,0 +1,238 @@
<template>
<transition name="slide-up">
<div v-if="visible" class="approval-modal">
<div class="approval-content">
<div class="approval-header">
<h4>{{ question }}</h4>
</div>
<div class="approval-operation">
<span class="label">操作</span>
<span class="operation-text">{{ operation }}</span>
</div>
</div>
<div class="approval-actions">
<button class="btn btn-reject" @click="handleReject" :disabled="isProcessing">
拒绝
</button>
<button class="btn btn-approve" @click="handleApprove" :disabled="isProcessing">
批准
</button>
</div>
<div v-if="isProcessing" class="approval-processing">
<span class="processing-spinner"></span>
处理中...
</div>
</div>
</transition>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
question: {
type: String,
default: '是否批准此操作?'
},
operation: {
type: String,
default: ''
}
});
const emit = defineEmits(['approve', 'reject']);
const isProcessing = ref(false);
//
watch(() => props.visible, (newVal) => {
if (!newVal) {
isProcessing.value = false;
}
});
const handleApprove = () => {
if (isProcessing.value) return;
isProcessing.value = true;
emit('approve');
};
const handleReject = () => {
if (isProcessing.value) return;
isProcessing.value = true;
emit('reject');
};
</script>
<style scoped>
.approval-modal {
background: white;
border-radius: 12px 12px;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12);
margin: 0 auto 8px;
max-width: 800px;
width: 100%;
border: 1px solid var(--gray-200);
}
.approval-content {
padding: 16px 20px;
}
.approval-header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
}
.approval-header h4 {
margin: 0;
font-size: 15px;
font-weight: 500;
color: var(--gray-800);
text-align: center;
}
.approval-operation {
background: var(--gray-50);
padding: 10px 12px;
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
display: flex;
gap: 6px;
}
.approval-operation .label {
color: var(--gray-600);
font-weight: 500;
flex-shrink: 0;
}
.approval-operation .operation-text {
color: var(--gray-800);
word-break: break-word;
}
.approval-actions {
display: flex;
gap: 10px;
padding: 12px 20px 16px;
}
.btn {
flex: 1;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-reject {
background: var(--gray-100);
color: var(--gray-700);
}
.btn-reject:hover:not(:disabled) {
background: var(--gray-200);
}
.btn-approve {
background: var(--main-color);
color: white;
}
.btn-approve:hover:not(:disabled) {
background: var(--main-700);
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.25);
}
.approval-processing {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
color: var(--gray-600);
font-size: 13px;
background: var(--gray-25);
border-top: 1px solid var(--gray-100);
}
.processing-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--gray-300);
border-top-color: var(--main-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 滑入滑出动画 */
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.25s ease;
}
.slide-up-enter-from {
opacity: 0;
transform: translateY(20px);
}
.slide-up-leave-to {
opacity: 0;
transform: translateY(20px);
}
@media (max-width: 520px) {
.approval-content {
padding: 12px 16px;
}
.approval-header h4 {
font-size: 14px;
}
.approval-operation {
font-size: 12px;
padding: 8px 10px;
}
.approval-actions {
padding: 10px 16px 12px;
gap: 8px;
}
.btn {
padding: 8px 16px;
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,128 @@
import { reactive } from 'vue';
import { message } from 'ant-design-vue';
import { handleChatError } from '@/utils/errorHandler';
import { agentApi } from '@/apis';
export function useApproval({ getThreadState, resetOnGoingConv, fetchThreadMessages }) {
// 审批状态
const approvalState = reactive({
showModal: false,
question: '',
operation: '',
threadId: null,
interruptInfo: null
});
// 处理审批逻辑
const handleApproval = async (approved, currentAgentId) => {
const threadId = approvalState.threadId;
if (!threadId) {
message.error('无效的审批请求');
approvalState.showModal = false;
return;
}
const threadState = getThreadState(threadId);
if (!threadState) {
message.error('无法找到对应的对话线程');
approvalState.showModal = false;
return;
}
// 关闭弹窗
approvalState.showModal = false;
// 清理旧的流式控制器(如果存在)
if (threadState.streamAbortController) {
threadState.streamAbortController.abort();
threadState.streamAbortController = null;
}
// 标记为处理中
threadState.isStreaming = true;
resetOnGoingConv(threadId);
threadState.streamAbortController = new AbortController();
console.log('🔄 [APPROVAL] Starting resume process:', { approved, threadId, currentAgentId });
try {
// 调用恢复接口
const response = await agentApi.resumeAgentChat(
currentAgentId,
{
thread_id: threadId,
approved: approved
},
{
signal: threadState.streamAbortController?.signal
}
);
console.log('🔄 [APPROVAL] Resume API response received');
if (!response.ok) {
const errorText = await response.text();
console.error('Resume API error:', response.status, errorText);
throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`);
}
console.log('🔄 [APPROVAL] Resume API successful, returning response for stream processing');
return response; // 返回响应供调用方处理流式数据
} catch (error) {
console.error('❌ [APPROVAL] Resume failed:', error);
if (error.name !== 'AbortError') {
handleChatError(error, 'resume');
message.error(`恢复对话失败: ${error.message || '未知错误'}`);
}
// 重置状态 - 只在错误时重置
threadState.isStreaming = false;
threadState.streamAbortController = null;
throw error; // 重新抛出错误让调用方处理
}
// 移除 finally 块 - 让组件管理流式状态的生命周期
};
// 在流式处理中处理审批请求
const processApprovalInStream = (chunk, threadId, currentAgentId) => {
if (chunk.status !== 'human_approval_required') {
return false;
}
const { interrupt_info } = chunk;
const threadState = getThreadState(threadId);
if (!threadState) return false;
// 停止显示"处理中"状态,让用户可以看到并操作审批弹窗
threadState.isStreaming = false;
// 显示审批弹窗
approvalState.showModal = true;
approvalState.question = interrupt_info?.question || '是否批准以下操作?';
approvalState.operation = interrupt_info?.operation || '未知操作';
approvalState.threadId = chunk.thread_id || threadId;
approvalState.interruptInfo = interrupt_info;
// 刷新消息历史显示已执行的部分
fetchThreadMessages({ agentId: currentAgentId, threadId: threadId });
return true; // 表示已处理审批请求,应停止流式处理
};
// 重置审批状态
const resetApprovalState = () => {
approvalState.showModal = false;
approvalState.question = '';
approvalState.operation = '';
approvalState.threadId = null;
approvalState.interruptInfo = null;
};
return {
approvalState,
handleApproval,
processApprovalInStream,
resetApprovalState
};
}

View File

@ -134,9 +134,6 @@ export class MessageProcessor {
// 处理AIMessageChunk类型
if (result.type === 'AIMessageChunk') {
result.type = 'ai';
if (result.additional_kwargs?.tool_calls) {
result.tool_calls = result.additional_kwargs.tool_calls;
}
}
return result;
@ -149,23 +146,47 @@ export class MessageProcessor {
* @param {Object} chunk - 当前块
*/
static _mergeToolCalls(result, chunk) {
if (chunk.additional_kwargs?.tool_calls) {
if (!result.additional_kwargs) result.additional_kwargs = {};
if (!result.additional_kwargs.tool_calls) result.additional_kwargs.tool_calls = [];
if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
// 确保 result 有 tool_calls 数组
if (!result.tool_calls) result.tool_calls = [];
for (const toolCall of chunk.additional_kwargs.tool_calls) {
const existingToolCall = result.additional_kwargs.tool_calls.find(
t => (t.id === toolCall.id || t.index === toolCall.index)
for (const toolCallChunk of chunk.tool_call_chunks) {
// 使用 index 来标识工具调用(因为可能有多个工具调用)
const existingToolCallIndex = result.tool_calls.findIndex(
t => t.index === toolCallChunk.index
);
if (existingToolCall) {
// 合并相同ID的tool call
if (existingToolCall.function && toolCall.function) {
existingToolCall.function.arguments += toolCall.function.arguments;
if (existingToolCallIndex !== -1) {
// 合并相同index的tool call
const existingToolCall = result.tool_calls[existingToolCallIndex];
// 更新名称和ID如果存在
if (toolCallChunk.name && !existingToolCall.function?.name) {
if (!existingToolCall.function) existingToolCall.function = {};
existingToolCall.function.name = toolCallChunk.name;
}
if (toolCallChunk.id && !existingToolCall.id) {
existingToolCall.id = toolCallChunk.id;
}
// 合并参数
if (toolCallChunk.args) {
if (!existingToolCall.function) existingToolCall.function = {};
if (!existingToolCall.function.arguments) existingToolCall.function.arguments = '';
existingToolCall.function.arguments += toolCallChunk.args;
}
} else {
// 添加新的tool call
result.additional_kwargs.tool_calls.push(JSON.parse(JSON.stringify(toolCall)));
const newToolCall = {
index: toolCallChunk.index,
id: toolCallChunk.id,
function: {
name: toolCallChunk.name || null,
arguments: toolCallChunk.args || ''
}
};
result.tool_calls.push(newToolCall);
}
}
}