feat: 增强工具调用代理功能,支持多工具调用和消息历史转换

主要改进:
- 新增 convert_tool_calls_to_content 函数,将消息历史中的 tool_calls 转换为 LLM 可理解的 XML 格式
- 修复 response_parser 支持同时解析多个 tool_calls
- 优化响应解析逻辑,支持 content 和 tool_calls 同时存在
- 添加完整的测试覆盖,包括多工具调用、消息转换和混合响应

技术细节:
- services.py: 实现工具调用历史到 content 的转换
- response_parser.py: 使用非贪婪匹配支持多个 tool_calls 解析
- main.py: 集成消息转换功能,确保消息历史正确传递给 LLM

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Vertex-AI-Step-Builder
2025-12-31 13:33:25 +00:00
parent f7508d915b
commit 5c2904e010
6 changed files with 624 additions and 31 deletions

View File

@@ -39,6 +39,70 @@ def _parse_sse_data(chunk: bytes) -> Optional[Dict[str, Any]]:
# --- End Helper ---
def convert_tool_calls_to_content(messages: List[ChatMessage]) -> List[ChatMessage]:
"""
Converts assistant messages with tool_calls into content format using XML tags.
This function processes the message history and converts any assistant messages
that have tool_calls into a format that LLMs can understand. The tool_calls
are converted to <invoke>...</invoke> tags in the content field.
Args:
messages: List of ChatMessage objects from the client
Returns:
Processed list of ChatMessage objects with tool_calls converted to content
Example:
Input: [{"role": "assistant", "tool_calls": [...]}]
Output: [{"role": "assistant", "content": "<invoke>{...}</invoke>"}]
"""
from .response_parser import TOOL_CALL_START_TAG, TOOL_CALL_END_TAG
processed_messages = []
for msg in messages:
# Check if this is an assistant message with tool_calls
if msg.role == "assistant" and msg.tool_calls and len(msg.tool_calls) > 0:
# Convert each tool call to XML tag format
tool_call_contents = []
for tc in msg.tool_calls:
tc_data = tc.get("function", {})
name = tc_data.get("name", "")
arguments_str = tc_data.get("arguments", "{}")
# Parse arguments JSON to ensure it's valid
try:
arguments = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str
except json.JSONDecodeError:
arguments = {}
# Build the tool call JSON
tool_call_json = {"name": name, "arguments": arguments}
# Wrap in XML tags
tool_call_content = f'{TOOL_CALL_START_TAG}{json.dumps(tool_call_json, ensure_ascii=False)}{TOOL_CALL_END_TAG}'
tool_call_contents.append(tool_call_content)
# Create new message with tool calls in content
# Preserve original content if it exists
content_parts = []
if msg.content:
content_parts.append(msg.content)
content_parts.extend(tool_call_contents)
new_content = "\n".join(content_parts)
processed_messages.append(
ChatMessage(role=msg.role, content=new_content)
)
else:
# Keep other messages as-is
processed_messages.append(msg)
return processed_messages
def inject_tools_into_prompt(messages: List[ChatMessage], tools: List[Tool]) -> List[ChatMessage]:
"""
Injects a system prompt with tool definitions at the beginning of the message list.