refactor: enhance error handling and response validation in tool execution and output processing for multi-modal functioanlity

pull/904/head
harshalmore31 2 weeks ago
parent 64ac3125b0
commit fb494b9ce2

@ -2800,13 +2800,18 @@ class Agent:
) )
def execute_tools(self, response: any, loop_count: int): def execute_tools(self, response: any, loop_count: int):
try:
output = ( output = (
self.tool_struct.execute_function_calls_from_api_response( self.tool_struct.execute_function_calls_from_api_response(
response response
) )
) )
# Handle empty or None output
if not output:
logger.info("No tool function calls found in response")
return
self.short_memory.add( self.short_memory.add(
role="Tool Executor", role="Tool Executor",
content=format_data_structure(output), content=format_data_structure(output),
@ -2820,6 +2825,7 @@ class Agent:
# Now run the LLM again without tools - create a temporary LLM instance # Now run the LLM again without tools - create a temporary LLM instance
# instead of modifying the cached one # instead of modifying the cached one
# Create a temporary LLM instance without tools for the follow-up call # Create a temporary LLM instance without tools for the follow-up call
try:
temp_llm = self.temp_llm_instance_for_tool_summary() temp_llm = self.temp_llm_instance_for_tool_summary()
tool_response = temp_llm.run( tool_response = temp_llm.run(
@ -2842,6 +2848,24 @@ class Agent:
f"{tool_response}", f"{tool_response}",
loop_count, loop_count,
) )
except Exception as e:
logger.error(f"Error in tool summary generation: {e}")
# Add a fallback summary
fallback_summary = f"Tool execution completed. Output: {format_data_structure(output)}"
self.short_memory.add(
role=self.agent_name,
content=fallback_summary,
)
self.pretty_print(fallback_summary, loop_count)
except Exception as e:
logger.error(f"Error in tool execution: {e}")
error_message = f"Tool execution failed: {str(e)}"
self.short_memory.add(
role="Tool Executor",
content=error_message,
)
self.pretty_print(error_message, loop_count)
def list_output_types(self): def list_output_types(self):
return OutputType return OutputType

@ -2253,17 +2253,31 @@ class BaseTool(BaseModel):
else: else:
# Convert string to dict if needed # Convert string to dict if needed
if isinstance(api_response, str): if isinstance(api_response, str):
# Handle empty or whitespace-only strings
api_response = api_response.strip()
if not api_response:
self._log_if_verbose(
"warning",
"Empty API response string received, returning empty list"
)
return []
try: try:
api_response = json.loads(api_response) api_response = json.loads(api_response)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ToolValidationError( self._log_if_verbose(
f"Invalid JSON in API response: {e}" "error",
) from e f"Failed to parse JSON from API response: {e}. Response: '{api_response[:100]}...'"
)
# If JSON parsing fails, try to continue without function calls
return []
if not isinstance(api_response, dict): if not isinstance(api_response, dict):
raise ToolValidationError( self._log_if_verbose(
"API response must be a dictionary, JSON string, BaseModel, or list of tool calls" "warning",
f"API response is not a dictionary (type: {type(api_response)}), returning empty list"
) )
return []
# Extract function calls from dictionary response # Extract function calls from dictionary response
function_calls = ( function_calls = (
@ -2387,11 +2401,15 @@ class BaseTool(BaseModel):
if name: if name:
try: try:
# Parse arguments JSON string # Parse arguments JSON string
arguments = ( if isinstance(arguments_str, str):
json.loads(arguments_str) # Handle empty or whitespace-only arguments
if isinstance(arguments_str, str) arguments_str = arguments_str.strip()
else arguments_str if not arguments_str:
) arguments = {}
else:
arguments = json.loads(arguments_str)
else:
arguments = arguments_str if arguments_str is not None else {}
function_calls.append( function_calls.append(
{ {
@ -2404,7 +2422,16 @@ class BaseTool(BaseModel):
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
self._log_if_verbose( self._log_if_verbose(
"error", "error",
f"Failed to parse arguments for {name}: {e}", f"Failed to parse arguments for {name}: {e}. Using empty dict instead.",
)
# Use empty dict as fallback
function_calls.append(
{
"name": name,
"arguments": {},
"id": response.get("id"),
"type": "openai",
}
) )
# Check for choices[].message.tool_calls format # Check for choices[].message.tool_calls format
@ -2885,12 +2912,15 @@ class BaseTool(BaseModel):
if name: if name:
try: try:
# Parse arguments JSON string # Parse arguments JSON string with better error handling
arguments = ( if isinstance(arguments_str, str):
json.loads(arguments_str) arguments_str = arguments_str.strip()
if isinstance(arguments_str, str) if not arguments_str:
else arguments_str arguments = {}
) else:
arguments = json.loads(arguments_str)
else:
arguments = arguments_str if arguments_str is not None else {}
function_calls.append( function_calls.append(
{ {
@ -2905,7 +2935,18 @@ class BaseTool(BaseModel):
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
self._log_if_verbose( self._log_if_verbose(
"error", "error",
f"Failed to parse arguments for {name}: {e}", f"Failed to parse arguments for {name}: {e}. Using empty dict instead.",
)
# Use empty dict as fallback
function_calls.append(
{
"name": name,
"arguments": {},
"id": getattr(
tool_call, "id", None
),
"type": "openai",
}
) )
# Handle dictionary representations of tool calls # Handle dictionary representations of tool calls

@ -152,7 +152,16 @@ class LiteLLM:
) )
def output_for_tools(self, response: any): def output_for_tools(self, response: any):
try:
if self.mcp_call is True: if self.mcp_call is True:
# Validate response structure for MCP calls
if (hasattr(response, 'choices') and
len(response.choices) > 0 and
hasattr(response.choices[0], 'message') and
hasattr(response.choices[0].message, 'tool_calls') and
response.choices[0].message.tool_calls and
len(response.choices[0].message.tool_calls) > 0):
out = response.choices[0].message.tool_calls[0].function out = response.choices[0].message.tool_calls[0].function
output = { output = {
"function": { "function": {
@ -162,11 +171,26 @@ class LiteLLM:
} }
return output return output
else: else:
logger.warning("Invalid MCP response structure, returning empty dict")
return {}
else:
# Validate response structure for regular tool calls
if (hasattr(response, 'choices') and
len(response.choices) > 0 and
hasattr(response.choices[0], 'message') and
hasattr(response.choices[0].message, 'tool_calls')):
out = response.choices[0].message.tool_calls out = response.choices[0].message.tool_calls
if isinstance(out, BaseModel): if isinstance(out, BaseModel):
out = out.model_dump() out = out.model_dump()
return out return out
else:
logger.warning("Invalid tool response structure, returning empty list")
return []
except Exception as e:
logger.error(f"Error processing tool response: {e}")
return {} if self.mcp_call else []
def _prepare_messages( def _prepare_messages(
self, self,
@ -449,14 +473,29 @@ class LiteLLM:
# Make the completion call # Make the completion call
response = completion(**completion_params) response = completion(**completion_params)
# Validate response structure before processing
if not hasattr(response, 'choices') or not response.choices:
logger.error("Invalid response: no choices found")
return "Error: Invalid response from API"
if not hasattr(response.choices[0], 'message'):
logger.error("Invalid response: no message found in first choice")
return "Error: Invalid response structure"
# Handle tool-based response # Handle tool-based response
if self.tools_list_dictionary is not None: if (self.tools_list_dictionary is not None and
hasattr(response.choices[0].message, 'tool_calls') and
response.choices[0].message.tool_calls is not None):
return self.output_for_tools(response) return self.output_for_tools(response)
elif self.return_all is True: elif self.return_all is True:
return response.model_dump() return response.model_dump()
else: else:
# Return standard response content # Return standard response content
return response.choices[0].message.content content = response.choices[0].message.content
if content is None:
logger.warning("Response content is None, returning empty string")
return ""
return content
except LiteLLMException as error: except LiteLLMException as error:
logger.error( logger.error(

Loading…
Cancel
Save