mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 06:07:13 +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)
|
# Build tool result block (Claude format)
|
||||||
# Format content in a way that's easy for LLM to understand
|
# Format content in a way that's easy for LLM to understand
|
||||||
is_error = result.get("status") == "error"
|
is_error = result.get("status") == "error"
|
||||||
|
|
||||||
if is_error:
|
if is_error:
|
||||||
# For errors, provide clear error message
|
# For errors, provide clear error message
|
||||||
result_content = f"Error: {result.get('result', 'Unknown error')}"
|
result_content = f"Error: {result.get('result', 'Unknown error')}"
|
||||||
@@ -349,7 +349,16 @@ class AgentStreamExecutor:
|
|||||||
else:
|
else:
|
||||||
# Fallback to full JSON
|
# Fallback to full JSON
|
||||||
result_content = json.dumps(result, ensure_ascii=False)
|
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 = {
|
tool_result_block = {
|
||||||
"type": "tool_result",
|
"type": "tool_result",
|
||||||
"tool_use_id": tool_call["id"],
|
"tool_use_id": tool_call["id"],
|
||||||
@@ -678,6 +687,14 @@ class AgentStreamExecutor:
|
|||||||
tool_calls = []
|
tool_calls = []
|
||||||
for idx in sorted(tool_calls_buffer.keys()):
|
for idx in sorted(tool_calls_buffer.keys()):
|
||||||
tc = tool_calls_buffer[idx]
|
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:
|
try:
|
||||||
# Safely get arguments, handle None case
|
# Safely get arguments, handle None case
|
||||||
args_str = tc.get("arguments") or ""
|
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 length: {len(args_str)} chars")
|
||||||
logger.error(f"Arguments preview: {args_preview}...")
|
logger.error(f"Arguments preview: {args_preview}...")
|
||||||
logger.error(f"JSON decode error: {e}")
|
logger.error(f"JSON decode error: {e}")
|
||||||
|
|
||||||
# Return a clear error message to the LLM instead of empty dict
|
# Return a clear error message to the LLM instead of empty dict
|
||||||
# This helps the LLM understand what went wrong
|
# This helps the LLM understand what went wrong
|
||||||
tool_calls.append({
|
tool_calls.append({
|
||||||
"id": tc["id"],
|
"id": tool_id,
|
||||||
"name": tc["name"],
|
"name": tc["name"],
|
||||||
"arguments": {},
|
"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."
|
"_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
|
continue
|
||||||
|
|
||||||
tool_calls.append({
|
tool_calls.append({
|
||||||
"id": tc["id"],
|
"id": tool_id,
|
||||||
"name": tc["name"],
|
"name": tc["name"],
|
||||||
"arguments": arguments
|
"arguments": arguments
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -628,9 +628,28 @@ def _handle_linkai_stream_response(self, base_url, headers, body):
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
chunk = json.loads(line)
|
chunk = json.loads(line)
|
||||||
yield chunk
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
continue
|
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:
|
except Exception as e:
|
||||||
logger.error(f"[LinkAI] stream response error: {e}")
|
logger.error(f"[LinkAI] stream response error: {e}")
|
||||||
|
|||||||
@@ -285,10 +285,16 @@ class MinimaxBot(Bot):
|
|||||||
text_parts.append(block.get("text", ""))
|
text_parts.append(block.get("text", ""))
|
||||||
elif block.get("type") == "tool_result":
|
elif block.get("type") == "tool_result":
|
||||||
# Tool result should be a separate message with role="tool"
|
# 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({
|
tool_results.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": block.get("tool_use_id"),
|
"tool_call_id": tool_call_id,
|
||||||
"content": str(block.get("content", ""))
|
"content": result_content
|
||||||
})
|
})
|
||||||
|
|
||||||
if text_parts:
|
if text_parts:
|
||||||
|
|||||||
@@ -233,56 +233,66 @@ class OpenAICompatibleBot:
|
|||||||
# Separate text content and tool_result blocks
|
# Separate text content and tool_result blocks
|
||||||
text_parts = []
|
text_parts = []
|
||||||
tool_results = []
|
tool_results = []
|
||||||
|
|
||||||
for block in content:
|
for block in content:
|
||||||
if block.get("type") == "text":
|
if block.get("type") == "text":
|
||||||
text_parts.append(block.get("text", ""))
|
text_parts.append(block.get("text", ""))
|
||||||
elif block.get("type") == "tool_result":
|
elif block.get("type") == "tool_result":
|
||||||
tool_results.append(block)
|
tool_results.append(block)
|
||||||
|
|
||||||
# First, add tool result messages (must come immediately after assistant with tool_calls)
|
# First, add tool result messages (must come immediately after assistant with tool_calls)
|
||||||
for block in tool_results:
|
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({
|
openai_messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": block.get("tool_use_id"),
|
"tool_call_id": tool_call_id,
|
||||||
"content": block.get("content", "")
|
"content": result_content
|
||||||
})
|
})
|
||||||
|
|
||||||
# Then, add text content as a separate user message if present
|
# Then, add text content as a separate user message if present
|
||||||
if text_parts:
|
if text_parts:
|
||||||
openai_messages.append({
|
openai_messages.append({
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": " ".join(text_parts)
|
"content": " ".join(text_parts)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Check if this is an assistant message with tool_use blocks
|
# Check if this is an assistant message with tool_use blocks
|
||||||
elif role == "assistant":
|
elif role == "assistant":
|
||||||
# Separate text content and tool_use blocks
|
# Separate text content and tool_use blocks
|
||||||
text_parts = []
|
text_parts = []
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
|
|
||||||
for block in content:
|
for block in content:
|
||||||
if block.get("type") == "text":
|
if block.get("type") == "text":
|
||||||
text_parts.append(block.get("text", ""))
|
text_parts.append(block.get("text", ""))
|
||||||
elif block.get("type") == "tool_use":
|
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({
|
tool_calls.append({
|
||||||
"id": block.get("id"),
|
"id": tool_id,
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": block.get("name"),
|
"name": block.get("name"),
|
||||||
"arguments": json.dumps(block.get("input", {}))
|
"arguments": json.dumps(block.get("input", {}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Build OpenAI format assistant message
|
# Build OpenAI format assistant message
|
||||||
openai_msg = {
|
openai_msg = {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": " ".join(text_parts) if text_parts else None
|
"content": " ".join(text_parts) if text_parts else None
|
||||||
}
|
}
|
||||||
|
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
openai_msg["tool_calls"] = tool_calls
|
openai_msg["tool_calls"] = tool_calls
|
||||||
|
|
||||||
openai_messages.append(openai_msg)
|
openai_messages.append(openai_msg)
|
||||||
else:
|
else:
|
||||||
# Other list content, keep as is
|
# Other list content, keep as is
|
||||||
|
|||||||
Reference in New Issue
Block a user