mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-20 21:57:14 +08:00
fix: issues with empty tool calls and handling excessively long tool results
This commit is contained in:
@@ -336,7 +336,7 @@ class AgentStreamExecutor:
|
||||
# Build tool result block (Claude format)
|
||||
# Format content in a way that's easy for LLM to understand
|
||||
is_error = result.get("status") == "error"
|
||||
|
||||
|
||||
if is_error:
|
||||
# For errors, provide clear error message
|
||||
result_content = f"Error: {result.get('result', 'Unknown error')}"
|
||||
@@ -349,7 +349,16 @@ class AgentStreamExecutor:
|
||||
else:
|
||||
# Fallback to full JSON
|
||||
result_content = json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
# Truncate large tool results to prevent message bloat
|
||||
# Keep tool results under 50KB to avoid context window issues
|
||||
MAX_TOOL_RESULT_CHARS = 20000
|
||||
if len(result_content) > MAX_TOOL_RESULT_CHARS:
|
||||
truncated_len = len(result_content)
|
||||
result_content = result_content[:MAX_TOOL_RESULT_CHARS] + \
|
||||
f"\n\n[Output truncated: {truncated_len} chars total, showing first {MAX_TOOL_RESULT_CHARS} chars]"
|
||||
logger.info(f"📎 Truncated tool result for '{tool_call['name']}': {truncated_len} -> {MAX_TOOL_RESULT_CHARS} chars")
|
||||
|
||||
tool_result_block = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_call["id"],
|
||||
@@ -678,6 +687,14 @@ class AgentStreamExecutor:
|
||||
tool_calls = []
|
||||
for idx in sorted(tool_calls_buffer.keys()):
|
||||
tc = tool_calls_buffer[idx]
|
||||
|
||||
# Ensure tool call has a valid ID (some providers return empty/None IDs)
|
||||
tool_id = tc.get("id") or ""
|
||||
if not tool_id:
|
||||
import uuid
|
||||
tool_id = f"call_{uuid.uuid4().hex[:24]}"
|
||||
logger.debug(f"⚠️ Tool call missing ID for '{tc.get('name')}', generated fallback: {tool_id}")
|
||||
|
||||
try:
|
||||
# Safely get arguments, handle None case
|
||||
args_str = tc.get("arguments") or ""
|
||||
@@ -690,11 +707,11 @@ class AgentStreamExecutor:
|
||||
logger.error(f"Arguments length: {len(args_str)} chars")
|
||||
logger.error(f"Arguments preview: {args_preview}...")
|
||||
logger.error(f"JSON decode error: {e}")
|
||||
|
||||
|
||||
# Return a clear error message to the LLM instead of empty dict
|
||||
# This helps the LLM understand what went wrong
|
||||
tool_calls.append({
|
||||
"id": tc["id"],
|
||||
"id": tool_id,
|
||||
"name": tc["name"],
|
||||
"arguments": {},
|
||||
"_parse_error": f"Invalid JSON in tool arguments: {args_preview}... Error: {str(e)}. Tip: For large content, consider splitting into smaller chunks or using a different approach."
|
||||
@@ -702,7 +719,7 @@ class AgentStreamExecutor:
|
||||
continue
|
||||
|
||||
tool_calls.append({
|
||||
"id": tc["id"],
|
||||
"id": tool_id,
|
||||
"name": tc["name"],
|
||||
"arguments": arguments
|
||||
})
|
||||
|
||||
@@ -628,9 +628,28 @@ def _handle_linkai_stream_response(self, base_url, headers, body):
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(line)
|
||||
yield chunk
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Check for error responses within the stream
|
||||
# Some providers (e.g., MiniMax via LinkAI) return errors as:
|
||||
# {'type': 'error', 'error': {'type': '...', 'message': '...', 'http_code': '400'}}
|
||||
if chunk.get("type") == "error" or (
|
||||
isinstance(chunk.get("error"), dict) and "message" in chunk.get("error", {})
|
||||
):
|
||||
error_data = chunk.get("error", {})
|
||||
error_msg = error_data.get("message", "Unknown error") if isinstance(error_data, dict) else str(error_data)
|
||||
http_code = error_data.get("http_code", "") if isinstance(error_data, dict) else ""
|
||||
status_code = int(http_code) if http_code and str(http_code).isdigit() else 400
|
||||
logger.error(f"[LinkAI] stream error: {error_msg} (http_code={http_code})")
|
||||
yield {
|
||||
"error": True,
|
||||
"message": error_msg,
|
||||
"status_code": status_code
|
||||
}
|
||||
return
|
||||
|
||||
yield chunk
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[LinkAI] stream response error: {e}")
|
||||
|
||||
@@ -285,10 +285,16 @@ class MinimaxBot(Bot):
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif block.get("type") == "tool_result":
|
||||
# Tool result should be a separate message with role="tool"
|
||||
tool_call_id = block.get("tool_use_id") or ""
|
||||
if not tool_call_id:
|
||||
logger.warning(f"[MINIMAX] tool_result missing tool_use_id")
|
||||
result_content = block.get("content", "")
|
||||
if not isinstance(result_content, str):
|
||||
result_content = json.dumps(result_content, ensure_ascii=False)
|
||||
tool_results.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": block.get("tool_use_id"),
|
||||
"content": str(block.get("content", ""))
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": result_content
|
||||
})
|
||||
|
||||
if text_parts:
|
||||
|
||||
@@ -233,56 +233,66 @@ class OpenAICompatibleBot:
|
||||
# Separate text content and tool_result blocks
|
||||
text_parts = []
|
||||
tool_results = []
|
||||
|
||||
|
||||
for block in content:
|
||||
if block.get("type") == "text":
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif block.get("type") == "tool_result":
|
||||
tool_results.append(block)
|
||||
|
||||
|
||||
# First, add tool result messages (must come immediately after assistant with tool_calls)
|
||||
for block in tool_results:
|
||||
tool_call_id = block.get("tool_use_id") or ""
|
||||
if not tool_call_id:
|
||||
logger.warning(f"[OpenAICompatible] tool_result missing tool_use_id, using empty string")
|
||||
# Ensure content is a string (some providers require string content)
|
||||
result_content = block.get("content", "")
|
||||
if not isinstance(result_content, str):
|
||||
result_content = json.dumps(result_content, ensure_ascii=False)
|
||||
openai_messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": block.get("tool_use_id"),
|
||||
"content": block.get("content", "")
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": result_content
|
||||
})
|
||||
|
||||
|
||||
# Then, add text content as a separate user message if present
|
||||
if text_parts:
|
||||
openai_messages.append({
|
||||
"role": "user",
|
||||
"content": " ".join(text_parts)
|
||||
})
|
||||
|
||||
|
||||
# Check if this is an assistant message with tool_use blocks
|
||||
elif role == "assistant":
|
||||
# Separate text content and tool_use blocks
|
||||
text_parts = []
|
||||
tool_calls = []
|
||||
|
||||
|
||||
for block in content:
|
||||
if block.get("type") == "text":
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif block.get("type") == "tool_use":
|
||||
tool_id = block.get("id") or ""
|
||||
if not tool_id:
|
||||
logger.warning(f"[OpenAICompatible] tool_use missing id for '{block.get('name')}'")
|
||||
tool_calls.append({
|
||||
"id": block.get("id"),
|
||||
"id": tool_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": block.get("name"),
|
||||
"arguments": json.dumps(block.get("input", {}))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# Build OpenAI format assistant message
|
||||
openai_msg = {
|
||||
"role": "assistant",
|
||||
"content": " ".join(text_parts) if text_parts else None
|
||||
}
|
||||
|
||||
|
||||
if tool_calls:
|
||||
openai_msg["tool_calls"] = tool_calls
|
||||
|
||||
|
||||
openai_messages.append(openai_msg)
|
||||
else:
|
||||
# Other list content, keep as is
|
||||
|
||||
Reference in New Issue
Block a user