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,48 +2800,72 @@ class Agent:
) )
def execute_tools(self, response: any, loop_count: int): def execute_tools(self, response: any, loop_count: int):
try:
output = (
self.tool_struct.execute_function_calls_from_api_response(
response
)
)
# Handle empty or None output
if not output:
logger.info("No tool function calls found in response")
return
output = ( self.short_memory.add(
self.tool_struct.execute_function_calls_from_api_response( role="Tool Executor",
response content=format_data_structure(output),
) )
)
self.short_memory.add( self.pretty_print(
role="Tool Executor", f"{format_data_structure(output)}",
content=format_data_structure(output), loop_count,
) )
self.pretty_print( # Now run the LLM again without tools - create a temporary LLM instance
f"{format_data_structure(output)}", # instead of modifying the cached one
loop_count, # Create a temporary LLM instance without tools for the follow-up call
) try:
temp_llm = self.temp_llm_instance_for_tool_summary()
# Now run the LLM again without tools - create a temporary LLM instance tool_response = temp_llm.run(
# instead of modifying the cached one f"""
# Create a temporary LLM instance without tools for the follow-up call Please analyze and summarize the following tool execution output in a clear and concise way.
temp_llm = self.temp_llm_instance_for_tool_summary() Focus on the key information and insights that would be most relevant to the user's original request.
If there are any errors or issues, highlight them prominently.
tool_response = temp_llm.run( Tool Output:
f""" {output}
Please analyze and summarize the following tool execution output in a clear and concise way. """
Focus on the key information and insights that would be most relevant to the user's original request. )
If there are any errors or issues, highlight them prominently.
Tool Output: self.short_memory.add(
{output} role=self.agent_name,
""" content=tool_response,
) )
self.short_memory.add( self.pretty_print(
role=self.agent_name, f"{tool_response}",
content=tool_response, 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)
self.pretty_print( except Exception as e:
f"{tool_response}", logger.error(f"Error in tool execution: {e}")
loop_count, 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,21 +152,45 @@ class LiteLLM:
) )
def output_for_tools(self, response: any): def output_for_tools(self, response: any):
if self.mcp_call is True: try:
out = response.choices[0].message.tool_calls[0].function if self.mcp_call is True:
output = { # Validate response structure for MCP calls
"function": { if (hasattr(response, 'choices') and
"name": out.name, len(response.choices) > 0 and
"arguments": out.arguments, hasattr(response.choices[0], 'message') and
} hasattr(response.choices[0].message, 'tool_calls') and
} response.choices[0].message.tool_calls and
return output len(response.choices[0].message.tool_calls) > 0):
else:
out = response.choices[0].message.tool_calls out = response.choices[0].message.tool_calls[0].function
output = {
"function": {
"name": out.name,
"arguments": out.arguments,
}
}
return output
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')):
if isinstance(out, BaseModel): out = response.choices[0].message.tool_calls
out = out.model_dump()
return out if isinstance(out, BaseModel):
out = out.model_dump()
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