ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
22
backend/llm_providers/__init__.py
Normal file
22
backend/llm_providers/__init__.py
Normal 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"
|
||||
]
|
||||
109
backend/llm_providers/anthropic_provider.py
Normal file
109
backend/llm_providers/anthropic_provider.py
Normal 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)}"
|
||||
135
backend/llm_providers/deepseek_provider.py
Normal file
135
backend/llm_providers/deepseek_provider.py
Normal 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)}"
|
||||
339
backend/llm_providers/gemini_provider.py
Normal file
339
backend/llm_providers/gemini_provider.py
Normal 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)
|
||||
234
backend/llm_providers/main_text_generation.py
Normal file
234
backend/llm_providers/main_text_generation.py
Normal 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
|
||||
128
backend/llm_providers/openai_provider.py
Normal file
128
backend/llm_providers/openai_provider.py
Normal 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
|
||||
Reference in New Issue
Block a user