Refactor Google and OpenAI provider response handling and tool utilities

- Improved error handling and logging in Google response processing.
- Simplified streaming content extraction and error detection in Google provider.
- Enhanced content extraction logic in OpenAI provider to handle edge cases.
- Streamlined tool conversion functions for both Google and OpenAI providers.
- Removed redundant comments and improved code readability across multiple files.
- Updated context window retrieval and message truncation logic for better performance.
- Ensured consistent handling of tool calls and arguments in OpenAI responses.
This commit is contained in:
2025-03-28 04:20:39 +00:00
parent 51e3058961
commit 247835e595
27 changed files with 265 additions and 564 deletions

View File

@@ -1,4 +1,3 @@
# src/providers/google_provider/response.py
"""
Response handling utilities specific to the Google Generative AI provider.
@@ -32,50 +31,36 @@ def get_streaming_content(response: Any) -> Generator[str, None, None]:
logger.debug("Processing Google stream...")
full_delta = ""
try:
# Check if the response itself is an error indicator (e.g., from create_chat_completion error handling)
if isinstance(response, dict) and "error" in response:
yield json.dumps(response)
logger.error(f"Stream processing stopped due to initial error: {response['error']}")
return
# Check if response is already an error iterator
if hasattr(response, "__iter__") and not hasattr(response, "candidates"):
# If it looks like an error iterator from create_chat_completion
first_item = next(response, None)
if first_item and isinstance(first_item, str):
try:
error_data = json.loads(first_item)
if "error" in error_data:
yield first_item # Yield the error JSON
yield first_item
yield from response
logger.error(f"Stream processing stopped due to yielded error: {error_data['error']}")
return
except json.JSONDecodeError:
# Not a JSON error, yield it as is and continue? Or stop?
# Assuming it might be valid content if not JSON error.
yield first_item
elif first_item: # Put the first item back if it wasn't an error
# This requires a way to chain iterators, simple yield doesn't work well here.
# For simplicity, we assume error iterators yield JSON strings.
# If the stream is valid, the loop below will handle it.
# Re-assigning response might be complex. Let the main loop handle valid streams.
pass # Let the main loop handle the original response iterator
elif first_item:
pass
# Process the stream chunk by chunk
for chunk in response:
# Check for errors embedded within the stream chunks (less common for Google?)
if isinstance(chunk, dict) and "error" in chunk:
yield json.dumps(chunk)
logger.error(f"Error encountered during Google stream: {chunk['error']}")
continue # Continue processing stream or stop? Continuing for now.
continue
# Extract text content
delta = ""
try:
if hasattr(chunk, "text"):
delta = chunk.text
elif hasattr(chunk, "candidates") and chunk.candidates:
# Sometimes content might be nested under candidates even in stream?
# Check the first candidate's first part for text.
first_candidate = chunk.candidates[0]
if hasattr(first_candidate, "content") and hasattr(first_candidate.content, "parts") and first_candidate.content.parts:
first_part = first_candidate.content.parts[0]
@@ -83,32 +68,27 @@ def get_streaming_content(response: Any) -> Generator[str, None, None]:
delta = first_part.text
except Exception as e:
logger.warning(f"Could not extract text from stream chunk: {chunk}. Error: {e}", exc_info=True)
delta = "" # Ensure delta is a string
delta = ""
if delta:
full_delta += delta
yield delta
# Detect function calls during stream (optional, for logging/early detection)
try:
if hasattr(chunk, "candidates") and chunk.candidates:
for part in chunk.candidates[0].content.parts:
if hasattr(part, "function_call") and part.function_call:
logger.debug(f"Function call detected during stream: {part.function_call.name}")
# Note: We don't yield the function call itself here, just the text.
# Function calls are typically processed after the stream completes.
break # Found a function call in this chunk
break
except Exception:
# Ignore errors during optional function call detection in stream
pass
logger.debug(f"Google stream finished. Total delta length: {len(full_delta)}")
except StopIteration:
logger.debug("Google stream finished (StopIteration).") # Normal end of iteration
logger.debug("Google stream finished (StopIteration).")
except Exception as e:
logger.error(f"Error processing Google stream: {e}", exc_info=True)
# Yield a final error message
yield json.dumps({"error": f"Stream processing error: {str(e)}"})
@@ -124,27 +104,21 @@ def get_content(response: GenerateContentResponse | dict[str, Any]) -> str:
The concatenated text content, or an error message string.
"""
try:
# Check if it's an error dictionary passed from upstream (e.g., completion helper)
if isinstance(response, dict) and "error" in response:
logger.error(f"Cannot get content from error dict: {response['error']}")
return f"[Error: {response['error']}]"
# Ensure it's a GenerateContentResponse object before accessing attributes
if not isinstance(response, GenerateContentResponse):
logger.error(f"Cannot get content: Expected GenerateContentResponse or error dict, got {type(response)}")
return f"[Error: Unexpected response type {type(response)}]"
# --- Access GenerateContentResponse attributes ---
# Prioritize response.text if available and not empty
if hasattr(response, "text") and response.text:
content = response.text
logger.debug(f"Extracted content (length {len(content)}) from response.text.")
return content
# Fallback: manually concatenate text from parts if .text is missing/empty
if hasattr(response, "candidates") and response.candidates:
first_candidate = response.candidates[0]
# Check candidate content and parts carefully
if hasattr(first_candidate, "content") and first_candidate.content and hasattr(first_candidate.content, "parts") and first_candidate.content.parts:
text_parts = [part.text for part in first_candidate.content.parts if hasattr(part, "text")]
if text_parts:
@@ -153,14 +127,13 @@ def get_content(response: GenerateContentResponse | dict[str, Any]) -> str:
return content
else:
logger.warning("Google response candidate parts contained no text.")
return "" # Return empty if parts exist but have no text
return ""
else:
logger.warning("Google response candidate has no valid content or parts.")
return "" # Return empty string if no valid content/parts
return ""
else:
# If neither .text nor valid candidates are found
logger.warning(f"Could not extract content from Google response: No .text or valid candidates found. Response: {response}")
return "" # Return empty string if no text found
return ""
except AttributeError as ae:
logger.error(f"Attribute error extracting content from Google response: {ae}. Response type: {type(response)}", exc_info=True)
@@ -182,20 +155,16 @@ def get_usage(response: GenerateContentResponse | dict[str, Any]) -> dict[str, i
usage information is unavailable or an error occurred.
"""
try:
# Check if it's an error dictionary passed from upstream
if isinstance(response, dict) and "error" in response:
logger.warning(f"Cannot get usage from error dict: {response['error']}")
return None
# Ensure it's a GenerateContentResponse object before accessing attributes
if not isinstance(response, GenerateContentResponse):
logger.warning(f"Cannot get usage: Expected GenerateContentResponse or error dict, got {type(response)}")
return None
# Safely access usage metadata
metadata = getattr(response, "usage_metadata", None)
if metadata:
# Google uses prompt_token_count and candidates_token_count
prompt_tokens = getattr(metadata, "prompt_token_count", 0)
completion_tokens = getattr(metadata, "candidates_token_count", 0)
usage = {