ALwrity Version 0.5.0 (Fastapi + React )

This commit is contained in:
ajaysi
2025-08-06 12:48:02 +05:30
parent f28a919caa
commit 32f97fa6b3
476 changed files with 115544 additions and 28747 deletions

View File

@@ -0,0 +1,22 @@
"""LLM Providers Service for ALwrity Backend.
This service handles all LLM (Language Model) provider integrations,
migrated from the legacy lib/gpt_providers functionality.
"""
from .main_text_generation import llm_text_gen
from .openai_provider import openai_chatgpt, test_openai_api_key
from .gemini_provider import gemini_text_response, gemini_structured_json_response, test_gemini_api_key
from .anthropic_provider import anthropic_text_response
from .deepseek_provider import deepseek_text_response
__all__ = [
"llm_text_gen",
"openai_chatgpt",
"test_openai_api_key",
"gemini_text_response",
"gemini_structured_json_response",
"test_gemini_api_key",
"anthropic_text_response",
"deepseek_text_response"
]

View File

@@ -0,0 +1,109 @@
"""Anthropic Provider Service for ALwrity Backend.
This service handles Anthropic Claude API integrations,
migrated from the legacy lib/gpt_providers/text_generation/anthropic_text_gen.py
"""
import os
import time
import anthropic
from typing import Tuple
from loguru import logger
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def anthropic_text_response(prompt: str, model: str = "claude-3-5-sonnet-20241022",
temperature: float = 0.7, max_tokens: int = 4000,
system_prompt: str = None) -> str:
"""
Generate text using Anthropic's Claude model.
Args:
prompt (str): The input text to generate completion for.
model (str, optional): Model to be used for the completion. Defaults to "claude-3-5-sonnet-20241022".
temperature (float, optional): Controls randomness. Lower values make responses more deterministic. Defaults to 0.7.
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 4000.
system_prompt (str, optional): System prompt for the conversation. Defaults to None.
Returns:
str: The generated text completion.
Raises:
SystemExit: If an API error, connection error, or rate limit error occurs.
"""
# Wait for 5 seconds to comply with rate limits
for _ in range(5):
time.sleep(1)
try:
# Get API key from environment
api_key = os.getenv('ANTHROPIC_API_KEY')
if not api_key:
raise ValueError("Anthropic API key not found in environment variables")
client = anthropic.Anthropic(api_key=api_key)
# Prepare messages
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
response = client.messages.create(
model=model,
messages=messages,
max_tokens=max_tokens,
temperature=temperature
)
logger.info(f"[anthropic_text_response] Generated response with {len(response.content[0].text)} characters")
return response.content[0].text
except anthropic.AuthenticationError as e:
logger.error(f"Anthropic Authentication Error: {e}")
raise SystemExit from e
except anthropic.RateLimitError as e:
logger.error(f"Anthropic Rate Limit Error: {e}")
raise SystemExit from e
except anthropic.APIConnectionError as e:
logger.error(f"Anthropic API Connection Error: {e}")
raise SystemExit from e
except Exception as e:
logger.error(f"Unexpected error in Anthropic API call: {e}")
raise SystemExit from e
async def test_anthropic_api_key(api_key: str) -> Tuple[bool, str]:
"""
Test if the provided Anthropic API key is valid.
Args:
api_key (str): The Anthropic API key to test
Returns:
tuple[bool, str]: A tuple containing (is_valid, message)
"""
try:
# Create Anthropic client with the provided key
client = anthropic.Anthropic(api_key=api_key)
# Try to generate a simple response as a test
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
messages=[{"role": "user", "content": "Hello"}],
max_tokens=10,
temperature=0.1
)
# If we get here, the key is valid
return True, "Anthropic API key is valid"
except anthropic.AuthenticationError:
return False, "Invalid Anthropic API key"
except anthropic.RateLimitError:
return False, "Rate limit exceeded. Please try again later."
except Exception as e:
return False, f"Error testing Anthropic API key: {str(e)}"

View File

@@ -0,0 +1,135 @@
"""DeepSeek Provider Service for ALwrity Backend.
This service handles DeepSeek API integrations,
migrated from the legacy lib/gpt_providers/text_generation/deepseek_text_gen.py
"""
import os
import time
import requests
from typing import Tuple
from loguru import logger
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def deepseek_text_response(prompt: str, model: str = "deepseek-chat",
temperature: float = 0.7, max_tokens: int = 4000,
system_prompt: str = None) -> str:
"""
Generate text using DeepSeek's API.
Args:
prompt (str): The input text to generate completion for.
model (str, optional): Model to be used for the completion. Defaults to "deepseek-chat".
temperature (float, optional): Controls randomness. Lower values make responses more deterministic. Defaults to 0.7.
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 4000.
system_prompt (str, optional): System prompt for the conversation. Defaults to None.
Returns:
str: The generated text completion.
Raises:
SystemExit: If an API error, connection error, or rate limit error occurs.
"""
# Wait for 5 seconds to comply with rate limits
for _ in range(5):
time.sleep(1)
try:
# Get API key from environment
api_key = os.getenv('DEEPSEEK_API_KEY')
if not api_key:
raise ValueError("DeepSeek API key not found in environment variables")
# Prepare messages
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
# Make API request
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
data = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
"stream": False
}
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers=headers,
json=data,
timeout=60
)
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
logger.info(f"[deepseek_text_response] Generated response with {len(content)} characters")
return content
else:
error_msg = f"DeepSeek API Error: {response.status_code} - {response.text}"
logger.error(error_msg)
raise SystemExit(error_msg)
except requests.exceptions.RequestException as e:
logger.error(f"DeepSeek API Connection Error: {e}")
raise SystemExit from e
except Exception as e:
logger.error(f"Unexpected error in DeepSeek API call: {e}")
raise SystemExit from e
async def test_deepseek_api_key(api_key: str) -> Tuple[bool, str]:
"""
Test if the provided DeepSeek API key is valid.
Args:
api_key (str): The DeepSeek API key to test
Returns:
tuple[bool, str]: A tuple containing (is_valid, message)
"""
try:
# Make a simple API test request
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
data = {
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 10,
"temperature": 0.1
}
response = requests.post(
"https://api.deepseek.com/v1/chat/completions",
headers=headers,
json=data,
timeout=30
)
if response.status_code == 200:
return True, "DeepSeek API key is valid"
elif response.status_code == 401:
return False, "Invalid DeepSeek API key"
elif response.status_code == 429:
return False, "Rate limit exceeded. Please try again later."
else:
return False, f"Error testing DeepSeek API key: {response.status_code} - {response.text}"
except requests.exceptions.RequestException as e:
return False, f"Connection error testing DeepSeek API key: {str(e)}"
except Exception as e:
return False, f"Error testing DeepSeek API key: {str(e)}"

View File

@@ -0,0 +1,339 @@
# Using Gemini Pro LLM model
import os
import sys
from pathlib import Path
from typing import Dict, Any
import time
import google.genai as genai
from google.genai import types
from dotenv import load_dotenv
load_dotenv(Path('../../../.env'))
from loguru import logger
logger.remove()
logger.add(sys.stdout,
colorize=True,
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
)
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
import asyncio
import json
import re
# Configure standard logging
import logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s-%(levelname)s-%(module)s-%(lineno)d]- %(message)s')
logger = logging.getLogger(__name__)
def get_gemini_api_key():
"""Get Gemini API key from API key manager or environment."""
try:
# Try to get from API key manager first
from services.api_key_manager import get_api_key_manager
api_key_manager = get_api_key_manager()
api_key = api_key_manager.get_api_key("gemini")
if api_key:
return api_key
except Exception as e:
logger.warning(f"Could not get API key from manager: {e}")
# Fallback to environment variable
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
raise ValueError("Gemini API key not found in environment variables or API key manager")
return api_key
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def gemini_text_response(prompt, temperature=0.7, top_p=0.9, n=40, max_tokens=2048, system_prompt=None):
"""Get response from Gemini Pro Text using official SDK pattern."""
try:
# Get API key
api_key = get_gemini_api_key()
logger.info(f"Temp: {temperature}, MaxTokens: {max_tokens}, TopP: {top_p}, N: {n}")
# Create the client with API key (official SDK pattern)
client = genai.Client(api_key=api_key)
# Prepare content with system instruction if provided
if system_prompt:
# Use system instruction in generation config (official SDK pattern)
generation_config = types.GenerateContentConfig(
temperature=temperature,
top_p=top_p,
top_k=n,
max_output_tokens=max_tokens,
system_instruction=system_prompt
)
response = client.models.generate_content(
model="gemini-2.0-flash-001", # Using the recommended model from docs
contents=prompt,
config=generation_config
)
else:
# Standard generation without system instruction (official SDK pattern)
generation_config = types.GenerateContentConfig(
temperature=temperature,
top_p=top_p,
top_k=n,
max_output_tokens=max_tokens,
)
response = client.models.generate_content(
model="gemini-2.0-flash-001", # Using the recommended model from docs
contents=prompt,
config=generation_config
)
logger.info(f"[gemini_text_response] Generated response with {len(response.text)} characters")
return response.text
except Exception as err:
logger.error(f"Failed to get response from Gemini: {err}. Retrying.")
raise
def _clean_schema_for_gemini(schema):
"""Clean schema to remove unsupported properties for Gemini API."""
if isinstance(schema, dict):
# Remove unsupported properties
unsupported_props = ['additionalProperties', 'pattern', 'format', 'minLength', 'maxLength']
cleaned = {}
for key, value in schema.items():
if key not in unsupported_props:
if isinstance(value, dict):
cleaned_value = _clean_schema_for_gemini(value)
# Skip empty objects or objects with empty properties
if key == "properties" and not cleaned_value:
continue
if key == "properties" and isinstance(cleaned_value, dict):
# Remove any properties that have empty object definitions
non_empty_props = {}
for prop_key, prop_value in cleaned_value.items():
if isinstance(prop_value, dict):
if prop_value.get("type") == "object":
# If it's an object type, ensure it has properties or change to string
if not prop_value.get("properties"):
non_empty_props[prop_key] = {"type": "string"}
else:
non_empty_props[prop_key] = prop_value
else:
non_empty_props[prop_key] = prop_value
else:
non_empty_props[prop_key] = prop_value
cleaned[key] = non_empty_props
else:
cleaned[key] = cleaned_value
elif isinstance(value, list):
cleaned[key] = [_clean_schema_for_gemini(item) if isinstance(item, dict) else item for item in value]
else:
cleaned[key] = value
return cleaned
elif isinstance(schema, list):
return [_clean_schema_for_gemini(item) if isinstance(item, dict) else item for item in schema]
else:
return schema
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def gemini_structured_json_response(prompt: str, schema: Dict[str, Any], model_name: str = "gemini-2.0-flash-001") -> str:
"""
Generate structured JSON response using Gemini API according to official SDK
"""
try:
api_key = get_gemini_api_key()
if not api_key:
logger.error("Gemini API key not found")
return json.dumps({"error": "API key not found"})
# Clean and validate schema
cleaned_schema = _clean_schema_for_gemini(schema)
validated_schema = _validate_and_fix_schema(cleaned_schema)
logger.info(f"🤖 Making Gemini API call to {model_name}")
logger.info(f"📝 Prompt: {prompt[:200]}...")
logger.info(f"🔧 Schema: {json.dumps(validated_schema, indent=2)}")
# Create the client with API key (official SDK pattern)
client = genai.Client(api_key=api_key)
generation_config = types.GenerateContentConfig(
temperature=0.7,
top_p=0.8,
top_k=40,
max_output_tokens=8192,
)
# Create the prompt with schema
full_prompt = f"""
{prompt}
Please respond with a valid JSON object that matches this schema:
{json.dumps(validated_schema, indent=2)}
Ensure the response is valid JSON and matches the schema exactly.
"""
logger.info(f"🚀 Sending request to Gemini API...")
start_time = time.time()
# Generate content using official SDK pattern
response = client.models.generate_content(
model=model_name,
contents=full_prompt,
config=generation_config
)
end_time = time.time()
logger.info(f"⏱️ Gemini API response received in {end_time - start_time:.2f} seconds")
logger.info(f"📄 Raw response: {response.text[:500]}...")
# Try to parse the response as JSON
try:
# First, try to extract JSON from the response
json_text = response.text.strip()
# Remove markdown code blocks if present
if json_text.startswith("```json"):
json_text = json_text[7:]
if json_text.endswith("```"):
json_text = json_text[:-3]
json_text = json_text.strip()
# Try to parse as JSON
parsed = json.loads(json_text)
logger.info(f"✅ Successfully parsed JSON response: {json.dumps(parsed, indent=2)}")
return json.dumps(parsed)
except json.JSONDecodeError as e:
logger.warning(f"❌ JSON parsing failed: {e}")
logger.warning(f"📄 Attempted to parse: {json_text}")
# Try to find JSON-like content in the response
import re
json_match = re.search(r'\{.*\}', response.text, re.DOTALL)
if json_match:
try:
parsed = json.loads(json_match.group())
logger.info(f"✅ Found and parsed JSON in response: {json.dumps(parsed, indent=2)}")
return json.dumps(parsed)
except json.JSONDecodeError:
logger.warning("❌ Failed to parse extracted JSON")
logger.warning("❌ No valid JSON found in response, returning full text")
return json.dumps({"error": "Invalid JSON response", "raw_text": response.text})
except Exception as e:
logger.error(f"❌ Gemini API error: {str(e)}")
return json.dumps({"error": f"Gemini API error: {str(e)}"})
def _validate_and_fix_schema(schema):
"""Validate and fix schema to ensure it's compatible with Gemini API."""
if isinstance(schema, dict):
# Check for empty object properties
if "properties" in schema and isinstance(schema["properties"], dict):
fixed_properties = {}
for key, value in schema["properties"].items():
if isinstance(value, dict):
if value.get("type") == "object":
# If object has no properties or empty properties, change to string
if not value.get("properties") or not value["properties"]:
fixed_properties[key] = {"type": "string"}
else:
# Recursively fix nested objects
fixed_properties[key] = _validate_and_fix_schema(value)
else:
fixed_properties[key] = value
else:
fixed_properties[key] = value
schema["properties"] = fixed_properties
# Recursively fix nested objects
for key, value in schema.items():
if isinstance(value, dict):
schema[key] = _validate_and_fix_schema(value)
return schema
async def test_gemini_api_key(api_key: str) -> tuple[bool, str]:
"""
Test if the provided Gemini API key is valid using official SDK pattern.
Args:
api_key (str): The Gemini API key to test
Returns:
tuple[bool, str]: A tuple containing (is_valid, message)
"""
try:
# Try to generate a simple response as a test using official SDK pattern
test_prompt = "Hello"
client = genai.Client(api_key=api_key)
response = client.models.generate_content(
model="gemini-2.0-flash-001", # Using the recommended model from docs
contents=test_prompt,
config=types.GenerateContentConfig(
temperature=0.1,
max_output_tokens=50
)
)
# If we get here, the key is valid
return True, "Gemini API key is valid"
except Exception as e:
error_msg = str(e)
if "API_KEY_INVALID" in error_msg or "authentication" in error_msg.lower():
return False, "Invalid Gemini API key"
elif "quota" in error_msg.lower() or "rate" in error_msg.lower():
return False, "Rate limit exceeded. Please try again later."
else:
return False, f"Error testing Gemini API key: {error_msg}"
def gemini_pro_text_gen(prompt, temperature=0.7, top_p=0.9, top_k=40, max_tokens=2048):
"""
Generate text using Google's Gemini Pro model according to official SDK.
Args:
prompt (str): The input text to generate completion for
temperature (float, optional): Controls randomness. Defaults to 0.7
top_p (float, optional): Controls diversity. Defaults to 0.9
top_k (int, optional): Controls vocabulary size. Defaults to 40
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 2048
Returns:
str: The generated text completion
"""
try:
# Get API key
api_key = get_gemini_api_key()
# Create the client with API key (official SDK pattern)
client = genai.Client(api_key=api_key)
# Generate content using the official SDK pattern
response = client.models.generate_content(
model='gemini-2.0-flash-001', # Using the recommended model from docs
contents=prompt,
config=types.GenerateContentConfig(
temperature=temperature,
top_p=top_p,
top_k=top_k,
max_output_tokens=max_tokens,
)
)
# Return the generated text
return response.text
except Exception as e:
logger.error(f"Error in Gemini Pro text generation: {e}")
return str(e)

View File

@@ -0,0 +1,234 @@
"""Main Text Generation Service for ALwrity Backend.
This service provides the main LLM text generation functionality,
migrated from the legacy lib/gpt_providers/text_generation/main_text_generation.py
"""
import os
import json
from typing import Optional, Dict, Any
from loguru import logger
from services.api_key_manager import APIKeyManager
from .openai_provider import openai_chatgpt
from .gemini_provider import gemini_text_response, gemini_structured_json_response
from .anthropic_provider import anthropic_text_response
from .deepseek_provider import deepseek_text_response
def llm_text_gen(prompt: str, system_prompt: Optional[str] = None, json_struct: Optional[Dict[str, Any]] = None) -> str:
"""
Generate text using Language Model (LLM) based on the provided prompt.
Args:
prompt (str): The prompt to generate text from.
system_prompt (str, optional): Custom system prompt to use instead of the default one.
json_struct (dict, optional): JSON schema structure for structured responses.
Returns:
str: Generated text based on the prompt.
"""
try:
logger.info("[llm_text_gen] Starting text generation")
logger.debug(f"[llm_text_gen] Prompt length: {len(prompt)} characters")
# Initialize API key manager
api_key_manager = APIKeyManager()
# Set default values for LLM parameters
gpt_provider = "google" # Default to Google Gemini
model = "models/gemini-2.0-flash"
temperature = 0.7
max_tokens = 4000
top_p = 0.9
n = 1
fp = 16
frequency_penalty = 0.0
presence_penalty = 0.0
# Default blog characteristics
blog_tone = "Professional"
blog_demographic = "Professional"
blog_type = "Informational"
blog_language = "English"
blog_output_format = "markdown"
blog_length = 2000
# Try to get provider from environment or config
try:
# Check which providers have API keys available
available_providers = []
if api_key_manager.get_api_key("openai"):
available_providers.append("openai")
if api_key_manager.get_api_key("gemini"):
available_providers.append("google")
if api_key_manager.get_api_key("anthropic"):
available_providers.append("anthropic")
if api_key_manager.get_api_key("deepseek"):
available_providers.append("deepseek")
# Prefer Google Gemini if available, otherwise use first available
if "google" in available_providers:
gpt_provider = "google"
model = "models/gemini-2.0-flash"
elif available_providers:
gpt_provider = available_providers[0]
if gpt_provider == "openai":
model = "gpt-4o"
elif gpt_provider == "anthropic":
model = "claude-3-5-sonnet-20241022"
elif gpt_provider == "deepseek":
model = "deepseek-chat"
else:
logger.warning("[llm_text_gen] No API keys found, using mock response")
return _get_mock_response(prompt)
logger.debug(f"[llm_text_gen] Using provider: {gpt_provider}, model: {model}")
except Exception as err:
logger.warning(f"[llm_text_gen] Error determining provider, using defaults: {err}")
gpt_provider = "google"
model = "models/gemini-2.0-flash"
# Construct the system prompt if not provided
if system_prompt is None:
system_instructions = f"""You are a highly skilled content writer with a knack for creating engaging and informative content.
Your expertise spans various writing styles and formats.
Writing Style Guidelines:
- Tone: {blog_tone}
- Target Audience: {blog_demographic}
- Content Type: {blog_type}
- Language: {blog_language}
- Output Format: {blog_output_format}
- Target Length: {blog_length} words
Please provide responses that are:
- Well-structured and easy to read
- Engaging and informative
- Tailored to the specified tone and audience
- Professional yet accessible
- Optimized for the target content type
"""
else:
system_instructions = system_prompt
# Generate response based on provider
if gpt_provider == "openai":
return openai_chatgpt(
prompt=prompt,
model=model,
temperature=temperature,
max_tokens=max_tokens,
top_p=top_p,
n=n,
fp=fp,
system_prompt=system_instructions
)
elif gpt_provider == "google":
if json_struct:
return gemini_structured_json_response(
prompt=prompt,
schema=json_struct,
temperature=temperature,
top_p=top_p,
top_k=n,
max_tokens=max_tokens,
system_prompt=system_instructions
)
else:
return gemini_text_response(
prompt=prompt,
temperature=temperature,
top_p=top_p,
n=n,
max_tokens=max_tokens,
system_prompt=system_instructions
)
elif gpt_provider == "anthropic":
return anthropic_text_response(
prompt=prompt,
model=model,
temperature=temperature,
max_tokens=max_tokens,
system_prompt=system_instructions
)
elif gpt_provider == "deepseek":
return deepseek_text_response(
prompt=prompt,
model=model,
temperature=temperature,
max_tokens=max_tokens,
system_prompt=system_instructions
)
else:
logger.error(f"[llm_text_gen] Unknown provider: {gpt_provider}")
return _get_mock_response(prompt)
except Exception as e:
logger.error(f"[llm_text_gen] Error during text generation: {str(e)}")
return _get_mock_response(prompt)
def _get_mock_response(prompt: str) -> str:
"""Get a mock response when no API keys are available."""
logger.warning("[llm_text_gen] Using mock response - no API keys configured")
# Return a structured mock response for style detection
if "style analysis" in prompt.lower() or "writing style" in prompt.lower():
return json.dumps({
"writing_style": {
"tone": "professional",
"voice": "active",
"complexity": "moderate",
"engagement_level": "high"
},
"content_characteristics": {
"sentence_structure": "well-structured",
"vocabulary_level": "intermediate",
"paragraph_organization": "logical flow",
"content_flow": "smooth transitions"
},
"target_audience": {
"demographics": ["professionals", "business users"],
"expertise_level": "intermediate",
"industry_focus": "technology",
"geographic_focus": "global"
},
"content_type": {
"primary_type": "blog",
"secondary_types": ["article", "guide"],
"purpose": "inform",
"call_to_action": "moderate"
},
"recommended_settings": {
"writing_tone": "professional",
"target_audience": "business professionals",
"content_type": "blog",
"creativity_level": "medium",
"geographic_location": "global"
}
})
# Generic mock response
return "This is a mock response. Please configure API keys for real content generation."
def check_gpt_provider(gpt_provider: str) -> bool:
"""Check if the specified GPT provider is supported."""
supported_providers = ["openai", "google", "anthropic", "deepseek"]
return gpt_provider in supported_providers
def get_api_key(gpt_provider: str) -> Optional[str]:
"""Get API key for the specified provider."""
try:
api_key_manager = APIKeyManager()
provider_mapping = {
"openai": "openai",
"google": "gemini",
"anthropic": "anthropic",
"deepseek": "deepseek"
}
mapped_provider = provider_mapping.get(gpt_provider, gpt_provider)
return api_key_manager.get_api_key(mapped_provider)
except Exception as e:
logger.error(f"[get_api_key] Error getting API key for {gpt_provider}: {str(e)}")
return None

View File

@@ -0,0 +1,128 @@
"""OpenAI Provider Service for ALwrity Backend.
This service handles OpenAI API integrations,
migrated from the legacy lib/gpt_providers/text_generation/openai_text_gen.py
"""
import os
import time
import openai
import asyncio
from typing import Tuple
from loguru import logger
from tenacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
async def test_openai_api_key(api_key: str) -> Tuple[bool, str]:
"""
Test if the provided OpenAI API key is valid.
Args:
api_key (str): The OpenAI API key to test
Returns:
tuple[bool, str]: A tuple containing (is_valid, message)
"""
try:
# Create OpenAI client with the provided key
client = openai.OpenAI(api_key=api_key)
# Try to list models as a simple API test
models = client.models.list()
# If we get here, the key is valid
return True, "OpenAI API key is valid"
except openai.AuthenticationError:
return False, "Invalid OpenAI API key"
except openai.RateLimitError:
return False, "Rate limit exceeded. Please try again later."
except Exception as e:
return False, f"Error testing OpenAI API key: {str(e)}"
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def openai_chatgpt(prompt: str, model: str = "gpt-4o", temperature: float = 0.7,
max_tokens: int = 4000, top_p: float = 0.9, n: int = 1,
fp: int = 16, system_prompt: str = None) -> str:
"""
Wrapper function for OpenAI's ChatGPT completion.
Args:
prompt (str): The input text to generate completion for.
model (str, optional): Model to be used for the completion. Defaults to "gpt-4o".
temperature (float, optional): Controls randomness. Lower values make responses more deterministic. Defaults to 0.7.
max_tokens (int, optional): Maximum number of tokens to generate. Defaults to 4000.
top_p (float, optional): Controls diversity. Defaults to 0.9.
n (int, optional): Number of completions to generate. Defaults to 1.
fp (int, optional): Frequency penalty. Defaults to 16.
system_prompt (str, optional): System prompt for the conversation. Defaults to None.
Returns:
str: The generated text completion.
Raises:
SystemExit: If an API error, connection error, or rate limit error occurs.
"""
# Wait for 5 seconds to comply with rate limits
for _ in range(5):
time.sleep(1)
try:
# Create variables to collect the stream of chunks
collected_chunks = []
collected_messages = []
full_reply_content = None
# Get API key from environment
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
raise ValueError("OpenAI API key not found in environment variables")
client = openai.OpenAI(api_key=api_key)
# Prepare messages
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
response = client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens,
n=n,
top_p=top_p,
stream=True,
frequency_penalty=fp,
temperature=temperature
)
# Iterate through the stream of events
for chunk in response:
collected_chunks.append(chunk) # save the event response
chunk_message = chunk.choices[0].delta.content # extract the message
collected_messages.append(chunk_message) # save the message
print(chunk.choices[0].delta.content, end="", flush=True)
# Clean None in collected_messages
collected_messages = [m for m in collected_messages if m is not None]
full_reply_content = ''.join([m for m in collected_messages])
logger.info(f"[openai_chatgpt] Generated response with {len(full_reply_content)} characters")
return full_reply_content
except openai.APIError as e:
logger.error(f"OpenAI API Error: {e}")
raise SystemExit from e
except openai.RateLimitError as e:
logger.error(f"OpenAI Rate Limit Error: {e}")
raise SystemExit from e
except openai.APIConnectionError as e:
logger.error(f"OpenAI API Connection Error: {e}")
raise SystemExit from e
except Exception as e:
logger.error(f"Unexpected error in OpenAI API call: {e}")
raise SystemExit from e