AI Blog Writer - Implement modular architecture with research, outline, and core services
This commit is contained in:
Binary file not shown.
@@ -9,6 +9,8 @@ Based on Google AI's official grounding documentation.
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
@@ -104,6 +106,29 @@ class GeminiGroundedProvider:
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception(f"Gemini API request timed out after {self.timeout} seconds")
|
||||
except Exception as api_error:
|
||||
# Handle specific Google API errors with retry logic
|
||||
error_str = str(api_error)
|
||||
if "503" in error_str and "overloaded" in error_str:
|
||||
# Conservative retry for overloaded service (expensive API calls)
|
||||
response = await self._retry_with_backoff(
|
||||
lambda: self._make_api_request(grounded_prompt, config),
|
||||
max_retries=1, # Only 1 retry to avoid excessive costs
|
||||
base_delay=5 # Longer delay
|
||||
)
|
||||
elif "429" in error_str:
|
||||
# Conservative retry for rate limits
|
||||
response = await self._retry_with_backoff(
|
||||
lambda: self._make_api_request(grounded_prompt, config),
|
||||
max_retries=1, # Only 1 retry
|
||||
base_delay=10 # Much longer delay for rate limits
|
||||
)
|
||||
elif "401" in error_str or "403" in error_str:
|
||||
raise Exception("Authentication failed. Please check your API credentials.")
|
||||
elif "400" in error_str:
|
||||
raise Exception("Invalid request. Please check your input parameters.")
|
||||
else:
|
||||
raise Exception(f"Google AI service error: {error_str}")
|
||||
|
||||
# Process the grounded response
|
||||
result = self._process_grounded_response(response, content_type)
|
||||
@@ -112,9 +137,47 @@ class GeminiGroundedProvider:
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error generating grounded content: {str(e)}")
|
||||
# Log error without causing secondary exceptions
|
||||
try:
|
||||
logger.error(f"❌ Error generating grounded content: {str(e)}")
|
||||
except:
|
||||
# Fallback to print if logging fails
|
||||
print(f"Error generating grounded content: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _make_api_request(self, grounded_prompt: str, config: Any):
|
||||
"""Make the actual API request to Gemini."""
|
||||
import concurrent.futures
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
return await asyncio.wait_for(
|
||||
loop.run_in_executor(
|
||||
executor,
|
||||
lambda: self.client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents=grounded_prompt,
|
||||
config=config,
|
||||
)
|
||||
),
|
||||
timeout=self.timeout
|
||||
)
|
||||
|
||||
async def _retry_with_backoff(self, func, max_retries: int = 3, base_delay: float = 1.0):
|
||||
"""Retry a function with exponential backoff."""
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return await func()
|
||||
except Exception as e:
|
||||
if attempt == max_retries:
|
||||
# Last attempt failed, raise the error
|
||||
raise e
|
||||
|
||||
# Calculate delay with exponential backoff
|
||||
delay = base_delay * (2 ** attempt)
|
||||
logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay} seconds: {str(e)}")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
def _build_grounded_prompt(self, prompt: str, content_type: str) -> str:
|
||||
"""
|
||||
Build a prompt optimized for grounded content generation.
|
||||
|
||||
@@ -389,11 +389,37 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
|
||||
config=generation_config,
|
||||
)
|
||||
|
||||
# According to the documentation, we should use response.parsed for structured output
|
||||
# Check for parsed content first (primary method for structured output)
|
||||
if hasattr(response, 'parsed') and response.parsed is not None:
|
||||
logger.info("Using response.parsed for structured output")
|
||||
return response.parsed
|
||||
|
||||
# Check for text content as fallback
|
||||
if hasattr(response, 'text') and response.text:
|
||||
logger.info("No parsed content, trying to parse text response")
|
||||
try:
|
||||
import json
|
||||
parsed_text = json.loads(response.text)
|
||||
logger.info("Successfully parsed text as JSON")
|
||||
return parsed_text
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse text as JSON: {e}")
|
||||
|
||||
# Check candidates for content (fallback for edge cases)
|
||||
if hasattr(response, 'candidates') and response.candidates:
|
||||
candidate = response.candidates[0]
|
||||
if hasattr(candidate, 'content') and candidate.content:
|
||||
if hasattr(candidate.content, 'parts') and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if hasattr(part, 'text') and part.text:
|
||||
try:
|
||||
import json
|
||||
parsed_text = json.loads(part.text)
|
||||
logger.info("Successfully parsed candidate text as JSON")
|
||||
return parsed_text
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse candidate text as JSON: {e}")
|
||||
|
||||
logger.error("No valid structured response content found")
|
||||
return {"error": "No valid structured response content found"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user