AI Blog Writer - Implement modular architecture with research, outline, and core services

This commit is contained in:
ajaysi
2025-09-12 16:53:16 +05:30
parent c0a366269d
commit 2ae0c4a8b9
29 changed files with 3210 additions and 907 deletions

View File

@@ -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.

View File

@@ -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"}