AIService wraps both Anthropic Claude and OpenAI APIs for product content generation. ImageProcessor handles EXIF geo-tagging with company info and GPS coordinates using piexif. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
172 lines
6.7 KiB
Python
172 lines
6.7 KiB
Python
import json
|
|
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AIService:
|
|
"""AI content generation service supporting Claude and OpenAI."""
|
|
|
|
def __init__(self, provider, api_key, model=None):
|
|
"""
|
|
Args:
|
|
provider: 'claude' or 'openai'
|
|
api_key: API key for the chosen provider
|
|
model: Model name (defaults to claude-sonnet-4-5-20250514 or gpt-4o)
|
|
"""
|
|
self.provider = provider
|
|
self.api_key = api_key
|
|
if model:
|
|
self.model = model
|
|
else:
|
|
self.model = 'claude-sonnet-4-5-20250514' if provider == 'claude' else 'gpt-4o'
|
|
self._client = None
|
|
|
|
def _get_client(self):
|
|
if self._client:
|
|
return self._client
|
|
if self.provider == 'claude':
|
|
try:
|
|
import anthropic
|
|
self._client = anthropic.Anthropic(api_key=self.api_key)
|
|
except ImportError:
|
|
raise RuntimeError("anthropic package not installed. Run: pip install anthropic")
|
|
elif self.provider == 'openai':
|
|
try:
|
|
import openai
|
|
self._client = openai.OpenAI(api_key=self.api_key)
|
|
except ImportError:
|
|
raise RuntimeError("openai package not installed. Run: pip install openai")
|
|
return self._client
|
|
|
|
def generate(self, system_prompt, user_message, max_tokens=2000):
|
|
"""Generate text using the configured AI provider."""
|
|
client = self._get_client()
|
|
try:
|
|
if self.provider == 'claude':
|
|
response = client.messages.create(
|
|
model=self.model,
|
|
max_tokens=max_tokens,
|
|
system=system_prompt,
|
|
messages=[{"role": "user", "content": user_message}],
|
|
)
|
|
return response.content[0].text
|
|
elif self.provider == 'openai':
|
|
response = client.chat.completions.create(
|
|
model=self.model,
|
|
max_tokens=max_tokens,
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_message},
|
|
],
|
|
)
|
|
return response.choices[0].message.content
|
|
except Exception as e:
|
|
_logger.error("AI generation failed (%s): %s", self.provider, str(e))
|
|
raise
|
|
|
|
def generate_product_content(self, product_info, prompts):
|
|
"""Generate all product content at once.
|
|
|
|
Args:
|
|
product_info: dict with keys like name, category, features, raw_description
|
|
prompts: dict with keys: title, short_desc, long_desc, meta_title, meta_desc, keywords
|
|
|
|
Returns:
|
|
dict with generated content for each field
|
|
"""
|
|
context = json.dumps(product_info, indent=2)
|
|
system = (
|
|
"You are an expert e-commerce copywriter and SEO specialist. "
|
|
"You create compelling, SEO-optimized product content for online stores. "
|
|
"Always respond with valid JSON containing the requested fields. "
|
|
"HTML descriptions should use proper semantic HTML tags."
|
|
)
|
|
|
|
user_msg = f"""Based on this product information:
|
|
{context}
|
|
|
|
Generate the following content as a JSON object with these exact keys:
|
|
|
|
1. "title": {prompts.get('title', 'SEO-optimized product title in Title Case')}
|
|
2. "short_description": {prompts.get('short_desc', 'Compelling 2-3 sentence HTML summary')}
|
|
3. "long_description": {prompts.get('long_desc', 'Detailed HTML product description with headings and lists')}
|
|
4. "meta_title": {prompts.get('meta_title', 'SEO meta title under 60 characters')}
|
|
5. "meta_description": {prompts.get('meta_desc', 'SEO meta description under 160 characters')}
|
|
6. "keywords": {prompts.get('keywords', 'Comma-separated SEO keywords')}
|
|
|
|
Respond ONLY with the JSON object, no markdown formatting."""
|
|
|
|
try:
|
|
raw = self.generate(system, user_msg, max_tokens=3000)
|
|
# Try to parse JSON from the response
|
|
# Strip any markdown code fences
|
|
cleaned = raw.strip()
|
|
if cleaned.startswith('```'):
|
|
cleaned = cleaned.split('\n', 1)[1]
|
|
if cleaned.endswith('```'):
|
|
cleaned = cleaned[:-3]
|
|
cleaned = cleaned.strip()
|
|
return json.loads(cleaned)
|
|
except json.JSONDecodeError:
|
|
_logger.warning("AI returned non-JSON response, returning raw text")
|
|
return {
|
|
'title': raw[:200] if raw else '',
|
|
'short_description': '',
|
|
'long_description': raw or '',
|
|
'meta_title': '',
|
|
'meta_description': '',
|
|
'keywords': '',
|
|
}
|
|
|
|
def generate_single_field(self, product_info, prompt, field_name):
|
|
"""Generate a single field using the given prompt."""
|
|
context = json.dumps(product_info, indent=2)
|
|
system = (
|
|
"You are an expert e-commerce copywriter and SEO specialist. "
|
|
"Respond with ONLY the requested content, no explanations or formatting."
|
|
)
|
|
user_msg = f"Product info:\n{context}\n\nTask: {prompt}"
|
|
|
|
result = self.generate(system, user_msg, max_tokens=1500)
|
|
return result.strip() if result else ''
|
|
|
|
def generate_image_metadata(self, product_name, product_category, prompt_alt, prompt_caption):
|
|
"""Generate SEO metadata for a product image.
|
|
|
|
Returns:
|
|
dict with: alt_text, caption, title, description
|
|
"""
|
|
system = (
|
|
"You are an SEO specialist for e-commerce product images. "
|
|
"Generate metadata that helps with image SEO and accessibility. "
|
|
"Respond ONLY with a JSON object."
|
|
)
|
|
user_msg = f"""Product: {product_name}
|
|
Category: {product_category}
|
|
|
|
Generate image metadata as JSON:
|
|
- "alt_text": {prompt_alt} (under 125 characters)
|
|
- "caption": {prompt_caption}
|
|
- "title": SEO-optimized image title
|
|
- "description": Descriptive image text for SEO
|
|
|
|
Respond ONLY with the JSON object."""
|
|
|
|
try:
|
|
raw = self.generate(system, user_msg, max_tokens=500)
|
|
cleaned = raw.strip()
|
|
if cleaned.startswith('```'):
|
|
cleaned = cleaned.split('\n', 1)[1]
|
|
if cleaned.endswith('```'):
|
|
cleaned = cleaned[:-3]
|
|
cleaned = cleaned.strip()
|
|
return json.loads(cleaned)
|
|
except (json.JSONDecodeError, Exception):
|
|
return {
|
|
'alt_text': product_name,
|
|
'caption': product_name,
|
|
'title': product_name,
|
|
'description': product_name,
|
|
}
|