ADOLF CONTENT FACTORY — Раздел 3: AI Pipeline
Проект: Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ SEO-контента Ð´Ð»Ñ ÐºÐ°Ñ€Ñ‚Ð¾Ñ‡ÐµÐº товаровМодуль: Content Factory
ВерÑиÑ: 1.0
Дата: Январь 2026
3.1 Ðазначение
AI Pipeline — конвейер обработки данных и генерации контента Ñ Ð¸Ñпользованием AI-моделей.Ðтапы пайплайна
РаÑпределение AI-моделей
| Ðтап | Модель | Ðазначение |
|---|---|---|
| Ðнализ контекÑта | GPT-5 mini | Извлечение ключевых Ñлов, ÑÑ‚Ñ€ÑƒÐºÑ‚ÑƒÑ€Ð¸Ð·Ð°Ñ†Ð¸Ñ |
| Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Title | Claude Opus 4.5 | Креативное название Ñ SEO |
| Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Description | Claude Opus 4.5 | Продающее опиÑание |
| Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Attributes | Claude Opus 4.5 | Заполнение характериÑтик |
| Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ SEO-тегов | Claude Opus 4.5 | Ключевые Ñлова Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñка |
| Visual Prompting | GPT-5 mini | ТЗ Ð´Ð»Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½ÐµÑ€Ð° |
3.2 Context Builder
3.2.1 Ðазначение
Сбор и Ð°Ð³Ñ€ÐµÐ³Ð°Ñ†Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… из различных иÑточников Ð´Ð»Ñ Ñ„Ð¾Ñ€Ð¼Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚ÐµÐºÑта генерации.3.2.2 ИÑточники данных
3.2.3 Структура контекÑта
Copy
from dataclasses import dataclass, field
from typing import Optional, List, Dict
@dataclass
class BrandSettings:
"""ÐаÑтройки ÑÑ‚Ð¸Ð»Ñ Ð±Ñ€ÐµÐ½Ð´Ð°."""
brand_id: str
brand_name: str
tone: str # "Ñтильный", "заботливый"
accent_words: List[str] = field(default_factory=list)
forbidden_words: List[str] = field(default_factory=list)
title_template: Optional[str] = None # Шаблон названиÑ
description_style: Optional[str] = None
@dataclass
class ProductKnowledge:
"""Данные о товаре из Knowledge Base."""
composition: Optional[str] = None # СоÑтав ткани
size_chart: Optional[str] = None # Ð Ð°Ð·Ð¼ÐµÑ€Ð½Ð°Ñ Ñетка
care_instructions: Optional[str] = None # Уход за изделием
features: List[str] = field(default_factory=list)
source_documents: List[str] = field(default_factory=list)
@dataclass
class ManualInput:
"""Ручной ввод пользователÑ."""
key_features: List[str] = field(default_factory=list)
target_audience: Optional[str] = None
unique_selling_points: List[str] = field(default_factory=list)
additional_notes: Optional[str] = None
# Ð”Ð»Ñ Visual Prompting
known_issues: List[str] = field(default_factory=list)
photo_requirements: List[str] = field(default_factory=list)
@dataclass
class GenerationContext:
"""Полный контекÑÑ‚ Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ð¸."""
# ИдентификациÑ
sku: str
marketplace: str
category: str
category_id: int
# Текущие данные карточки
current_title: Optional[str] = None
current_description: Optional[str] = None
current_attributes: Dict = field(default_factory=dict)
# Обогащение
brand_settings: Optional[BrandSettings] = None
product_knowledge: Optional[ProductKnowledge] = None
manual_input: Optional[ManualInput] = None
# Метаданные
user_id: str = ""
request_id: str = ""
3.2.4 Ð ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Context Builder
Copy
class ContextBuilder:
"""ПоÑтроитель контекÑта Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ð¸."""
def __init__(
self,
knowledge_api: KnowledgeAPI,
settings_repo: SettingsRepository
):
self.knowledge_api = knowledge_api
self.settings_repo = settings_repo
async def build(
self,
card_data: CardData,
manual_input: Optional[ManualInput] = None,
user_id: str = ""
) -> GenerationContext:
"""Сборка контекÑта Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ð¸."""
# Получение наÑтроек бренда
brand_settings = await self._get_brand_settings(card_data.brand)
# ПоиÑк данных в Knowledge Base
product_knowledge = await self._search_knowledge(card_data.sku)
return GenerationContext(
sku=card_data.sku,
marketplace=card_data.marketplace.value,
category=card_data.category or "",
category_id=card_data.category_id or 0,
current_title=card_data.title,
current_description=card_data.description,
current_attributes=card_data.attributes or {},
brand_settings=brand_settings,
product_knowledge=product_knowledge,
manual_input=manual_input,
user_id=user_id,
request_id=generate_request_id()
)
async def _get_brand_settings(self, brand_name: str) -> Optional[BrandSettings]:
"""Получение наÑтроек бренда."""
# Определение brand_id по названию
brand_id = self._resolve_brand_id(brand_name)
settings = await self.settings_repo.get_content_settings(brand_id)
if not settings:
return None
return BrandSettings(
brand_id=brand_id,
brand_name=brand_name,
tone=settings.get("tone", "нейтральный"),
accent_words=settings.get("accent_words", []),
forbidden_words=settings.get("forbidden_words", []),
title_template=settings.get("title_template"),
description_style=settings.get("description_style")
)
def _resolve_brand_id(self, brand_name: str) -> str:
"""Определение brand_id по названию бренда."""
brand_lower = brand_name.lower() if brand_name else ""
if "кидÑ" in brand_lower or "kids" in brand_lower:
return "ohana_kids"
elif "охана" in brand_lower or "ohana" in brand_lower:
return "ohana_market"
else:
return "ohana_market" # Default
async def _search_knowledge(self, sku: str) -> Optional[ProductKnowledge]:
"""ПоиÑк данных о товаре в Knowledge Base."""
try:
results = await self.knowledge_api.search(
query=f"артикул {sku} ÑоÑтав размер характериÑтики уход",
filters={"category": "product"},
limit=5
)
if not results:
return None
# ÐÐ³Ñ€ÐµÐ³Ð°Ñ†Ð¸Ñ Ñ€ÐµÐ·ÑƒÐ»ÑŒÑ‚Ð°Ñ‚Ð¾Ð²
knowledge = ProductKnowledge()
for result in results:
text = result.get("content", "").lower()
source = result.get("source", "")
knowledge.source_documents.append(source)
# Извлечение ÑоÑтава
if "ÑоÑтав" in text and not knowledge.composition:
knowledge.composition = self._extract_composition(result["content"])
# Извлечение инÑтрукций по уходу
if "уход" in text or "Ñтирк" in text:
knowledge.care_instructions = self._extract_care(result["content"])
return knowledge
except Exception as e:
logger.warning(f"Knowledge search failed for {sku}: {e}")
return None
def _extract_composition(self, text: str) -> Optional[str]:
"""Извлечение ÑоÑтава из текÑта."""
# ПроÑÑ‚Ð°Ñ Ñ€ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ, в продакшене — через AI
lines = text.split("\n")
for line in lines:
if "ÑоÑтав" in line.lower():
return line.strip()
return None
def _extract_care(self, text: str) -> Optional[str]:
"""Извлечение инÑтрукций по уходу."""
lines = text.split("\n")
for line in lines:
if "уход" in line.lower() or "Ñтирк" in line.lower():
return line.strip()
return None
3.3 Analyzer (GPT-5 mini)
3.3.1 Ðазначение
Ðнализ контекÑта и извлечение Ñтруктурированных данных Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ð¸.3.3.2 Задачи анализатора
| Задача | ОпиÑание | Выход |
|---|---|---|
| Извлечение ключевых Ñлов | Определение SEO-релевантных терминов | СпиÑок keywords |
| Определение USP | Уникальные преимущеÑтва товара | СпиÑок USP |
| Ðнализ категории | Специфика категории Ð´Ð»Ñ Ð¾Ð¿Ð¸ÑÐ°Ð½Ð¸Ñ | Category insights |
| Структура опиÑÐ°Ð½Ð¸Ñ | Ð ÐµÐºÐ¾Ð¼ÐµÐ½Ð´ÑƒÐµÐ¼Ð°Ñ Ñтруктура | Outline |
3.3.3 Промпт анализатора
Copy
ANALYZER_SYSTEM_PROMPT = """
Ты — SEO-аналитик Ð´Ð»Ñ Ð¼Ð°Ñ€ÐºÐµÑ‚Ð¿Ð»ÐµÐ¹Ñов. Ð¢Ð²Ð¾Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° — проанализировать данные о товаре и подготовить Ñтруктурированную информацию Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ð¸ контента карточки.
ПРÐВИЛРÐÐÐЛИЗÐ:
1. Извлекай только релевантные ключевые Ñлова Ð´Ð»Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ð¸
2. ОпределÑй уникальные преимущеÑтва товара (USP)
3. Учитывай Ñпецифику маркетплейÑа
4. Формируй Ñтруктуру опиÑÐ°Ð½Ð¸Ñ Ð½Ð° оÑнове категории
ФОРМÐТ ОТВЕТР— Ñтрого JSON:
{
"keywords": ["ключевое Ñлово 1", "ключевое Ñлово 2", ...],
"usp": ["преимущеÑтво 1", "преимущеÑтво 2", ...],
"category_insights": {
"important_attributes": ["атрибут 1", "атрибут 2"],
"buyer_concerns": ["что важно покупателю 1", "что важно покупателю 2"],
"recommended_structure": ["раздел 1", "раздел 2"]
},
"title_keywords": ["ключевое Ñлово Ð´Ð»Ñ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ 1", ...],
"description_outline": ["пункт 1", "пункт 2", ...]
}
"""
ANALYZER_USER_PROMPT = """
Проанализируй данные о товаре:
ÐРТИКУЛ: {sku}
МÐРКЕТПЛЕЙС: {marketplace}
КÐТЕГОРИЯ: {category}
ТЕКУЩЕЕ ÐÐЗВÐÐИЕ: {current_title}
ТЕКУЩЕЕ ОПИСÐÐИЕ: {current_description}
ДÐÐÐЫЕ ИЗ БÐЗЫ ЗÐÐÐИЙ:
- СоÑтав: {composition}
- Уход: {care_instructions}
- ОÑобенноÑти: {features}
ДОПОЛÐИТЕЛЬÐÐЯ ИÐФОРМÐЦИЯ ОТ ПОЛЬЗОВÐТЕЛЯ:
- Ключевые оÑобенноÑти: {key_features}
- Ð¦ÐµÐ»ÐµÐ²Ð°Ñ Ð°ÑƒÐ´Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ: {target_audience}
- Уникальные преимущеÑтва: {unique_selling_points}
Верни JSON Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð¾Ð¼.
"""
3.3.4 Ð ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð°Ñ‚Ð¾Ñ€Ð°
Copy
import json
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class AnalysisResult:
"""Результат анализа контекÑта."""
keywords: List[str]
usp: List[str]
category_insights: Dict
title_keywords: List[str]
description_outline: List[str]
raw_response: Dict
class ContentAnalyzer:
"""Ðнализатор контекÑта Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ð¸."""
def __init__(self, gpt_client: GPTClient):
self.gpt_client = gpt_client
async def analyze(self, context: GenerationContext) -> AnalysisResult:
"""Ðнализ контекÑта и извлечение Ñтруктурированных данных."""
# Подготовка данных Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð¼Ð¿Ñ‚Ð°
features = []
composition = ""
care = ""
if context.product_knowledge:
features = context.product_knowledge.features
composition = context.product_knowledge.composition or ""
care = context.product_knowledge.care_instructions or ""
key_features = []
target_audience = ""
usp_input = []
if context.manual_input:
key_features = context.manual_input.key_features
target_audience = context.manual_input.target_audience or ""
usp_input = context.manual_input.unique_selling_points
# Формирование промпта
user_prompt = ANALYZER_USER_PROMPT.format(
sku=context.sku,
marketplace=context.marketplace,
category=context.category,
current_title=context.current_title or "Ðе указано",
current_description=context.current_description or "Ðе указано",
composition=composition or "Ðе указано",
care_instructions=care or "Ðе указано",
features=", ".join(features) if features else "Ðе указано",
key_features=", ".join(key_features) if key_features else "Ðе указано",
target_audience=target_audience or "Ðе указано",
unique_selling_points=", ".join(usp_input) if usp_input else "Ðе указано"
)
# Вызов GPT-5 mini
response = await self.gpt_client.chat_completion(
model="gpt-5-mini",
messages=[
{"role": "system", "content": ANALYZER_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
],
temperature=0.3,
max_tokens=1000,
response_format={"type": "json_object"}
)
# ПарÑинг результата
try:
data = json.loads(response.content)
except json.JSONDecodeError:
data = self._parse_fallback(response.content)
return AnalysisResult(
keywords=data.get("keywords", []),
usp=data.get("usp", []),
category_insights=data.get("category_insights", {}),
title_keywords=data.get("title_keywords", []),
description_outline=data.get("description_outline", []),
raw_response=data
)
def _parse_fallback(self, content: str) -> Dict:
"""Fallback-парÑинг при ошибке JSON."""
return {
"keywords": [],
"usp": [],
"category_insights": {},
"title_keywords": [],
"description_outline": []
}
3.4 Generator (Claude Opus 4.5)
3.4.1 Ðазначение
Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ ÐºÑ€ÐµÐ°Ñ‚Ð¸Ð²Ð½Ð¾Ð³Ð¾ SEO-оптимизированного контента Ð´Ð»Ñ ÐºÐ°Ñ€Ñ‚Ð¾Ñ‡ÐºÐ¸ товара.3.4.2 Генерируемый контент
| Поле | ОпиÑание | Лимиты |
|---|---|---|
| Title | SEO-название товара | 100-255 Ñимволов |
| Description | Продающее опиÑание | 3000-6000 Ñимволов |
| Attributes | ХарактериÑтики товара | ЗавиÑит от категории |
| SEO Tags | Ключевые Ñлова | 5-20 тегов |
3.4.3 Промпты генератора
System Prompt (базовый)
Copy
GENERATOR_SYSTEM_PROMPT = """
Ты — профеÑÑиональный копирайтер Ð´Ð»Ñ Ð¼Ð°Ñ€ÐºÐµÑ‚Ð¿Ð»ÐµÐ¹Ñов Ñ Ð¾Ð¿Ñ‹Ñ‚Ð¾Ð¼ в SEO и продающих текÑтах.
ТВОИ ЗÐДÐЧИ:
1. Создавать SEO-оптимизированные Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ñ‚Ð¾Ð²Ð°Ñ€Ð¾Ð²
2. ПиÑать продающие опиÑÐ°Ð½Ð¸Ñ Ñ ÐºÐ»ÑŽÑ‡ÐµÐ²Ñ‹Ð¼Ð¸ Ñловами
3. Формулировать характериÑтики товара
4. Подбирать релевантные SEO-теги
ОБЩИЕ ПРÐВИЛÐ:
- ИÑпользуй еÑтеÑтвенный Ñзык, избегай переÑпама ключевыми Ñловами
- Пиши на руÑÑком Ñзыке
- Учитывай Ñпецифику категории товара
- Делай акцент на преимущеÑтвах Ð´Ð»Ñ Ð¿Ð¾ÐºÑƒÐ¿Ð°Ñ‚ÐµÐ»Ñ
- Ðе иÑпользуй запрещённые Ñлова бренда
- Следуй тону коммуникации бренда
{brand_instructions}
Ð’ÐЖÐО: Соблюдай лимиты Ñимволов Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ маркетплейÑа.
"""
BRAND_INSTRUCTIONS_TEMPLATE = """
ÐÐСТРОЙКИ БРЕÐДР"{brand_name}":
- Тон коммуникации: {tone}
- Слова-акценты (иÑпользовать): {accent_words}
- Запрещённые Ñлова (ÐЕ иÑпользовать): {forbidden_words}
- Стиль опиÑаниÑ: {description_style}
"""
Промпт Ð´Ð»Ñ Title
Copy
TITLE_PROMPT = """
Создай SEO-оптимизированное название товара.
ИСХОДÐЫЕ ДÐÐÐЫЕ:
- Текущее название: {current_title}
- КатегориÑ: {category}
- Бренд: {brand_name}
- Ключевые Ñлова Ð´Ð»Ñ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ: {title_keywords}
ТРЕБОВÐÐИЯ:
- Длина: макÑимум {max_length} Ñимволов
- Включи бренд в начале или конце
- ИÑпользуй 2-3 ключевых Ñлова
- Ðазвание должно быть информативным и привлекательным
ФОРМÐТ ОТВЕТÐ:
Верни только название товара, без поÑÑнений.
"""
Промпт Ð´Ð»Ñ Description
Copy
DESCRIPTION_PROMPT = """
Ðапиши продающее опиÑание товара.
ИСХОДÐЫЕ ДÐÐÐЫЕ:
- Ðазвание: {title}
- КатегориÑ: {category}
- Бренд: {brand_name}
КЛЮЧЕВЫЕ СЛОВР(включи еÑтеÑтвенно в текÑÑ‚):
{keywords}
УÐИКÐЛЬÐЫЕ ПРЕИМУЩЕСТВР(USP):
{usp}
ДОПОЛÐИТЕЛЬÐÐЯ ИÐФОРМÐЦИЯ:
- СоÑтав: {composition}
- Уход за изделием: {care_instructions}
СТРУКТУРРОПИСÐÐИЯ:
{description_outline}
ТРЕБОВÐÐИЯ:
- Длина: {min_length}-{max_length} Ñимволов
- Первый абзац — захватывающий, Ñ Ð³Ð»Ð°Ð²Ð½Ñ‹Ð¼ преимущеÑтвом
- ИÑпользуй буллеты Ð´Ð»Ñ Ñ…Ð°Ñ€Ð°ÐºÑ‚ÐµÑ€Ð¸Ñтик
- Завершай призывом к дейÑтвию
- Включи ключевые Ñлова еÑтеÑтвенно (не более 3-4% плотноÑть)
ФОРМÐТ ОТВЕТÐ:
Верни только текÑÑ‚ опиÑаниÑ, без поÑÑнений.
"""
Промпт Ð´Ð»Ñ Attributes
Copy
ATTRIBUTES_PROMPT = """
Заполни характериÑтики товара Ð´Ð»Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ð¸ "{category}".
ИСХОДÐЫЕ ДÐÐÐЫЕ:
- Текущие характериÑтики: {current_attributes}
- СоÑтав: {composition}
- ОÑобенноÑти: {features}
Ð’ÐЖÐЫЕ ÐТРИБУТЫ ДЛЯ КÐТЕГОРИИ:
{important_attributes}
ТРЕБОВÐÐИЯ:
- Заполни вÑе обÑзательные атрибуты
- ИÑпользуй точные значениÑ
- Формат значений должен ÑоответÑтвовать требованиÑм маркетплейÑа
ФОРМÐТ ОТВЕТР— JSON:
{{
"attribute_name_1": "значение",
"attribute_name_2": "значение"
}}
"""
Промпт Ð´Ð»Ñ SEO Tags
Copy
SEO_TAGS_PROMPT = """
Подбери SEO-теги Ð´Ð»Ñ Ñ‚Ð¾Ð²Ð°Ñ€Ð°.
ИСХОДÐЫЕ ДÐÐÐЫЕ:
- Ðазвание: {title}
- КатегориÑ: {category}
- Ключевые Ñлова из анализа: {keywords}
ТРЕБОВÐÐИЯ:
- КоличеÑтво тегов: 10-15
- Включи выÑокочаÑтотные и низкочаÑтотные запроÑÑ‹
- Теги должны быть релевантны товару
- Без дублированиÑ
ФОРМÐТ ОТВЕТР— JSON:
{{
"tags": ["тег1", "тег2", "тег3", ...]
}}
"""
3.4.4 Ð ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ‚Ð¾Ñ€Ð°
Copy
@dataclass
class GeneratedContent:
"""Сгенерированный контент."""
title: str
description: str
attributes: Dict[str, str]
seo_tags: List[str]
generation_metadata: Dict
class ContentGenerator:
"""Генератор контента Ñ Ð¸Ñпользованием Claude Opus."""
# Лимиты по маркетплейÑам
LIMITS = {
"wb": {"title": 100, "description_min": 500, "description_max": 5000},
"ozon": {"title": 255, "description_min": 500, "description_max": 6000},
"ym": {"title": 150, "description_min": 300, "description_max": 3000}
}
def __init__(self, claude_client: ClaudeClient):
self.claude_client = claude_client
async def generate(
self,
context: GenerationContext,
analysis: AnalysisResult
) -> GeneratedContent:
"""Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ð¿Ð¾Ð»Ð½Ð¾Ð³Ð¾ контента карточки."""
# Получение лимитов Ð´Ð»Ñ Ð¼Ð°Ñ€ÐºÐµÑ‚Ð¿Ð»ÐµÐ¹Ñа
limits = self.LIMITS.get(context.marketplace, self.LIMITS["wb"])
# Формирование ÑиÑтемного промпта Ñ Ð½Ð°Ñтройками бренда
system_prompt = self._build_system_prompt(context.brand_settings)
# Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Title
title = await self._generate_title(
context, analysis, system_prompt, limits["title"]
)
# Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Description
description = await self._generate_description(
context, analysis, system_prompt, title,
limits["description_min"], limits["description_max"]
)
# Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Attributes
attributes = await self._generate_attributes(
context, analysis, system_prompt
)
# Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ SEO Tags
seo_tags = await self._generate_seo_tags(
context, analysis, system_prompt, title
)
return GeneratedContent(
title=title,
description=description,
attributes=attributes,
seo_tags=seo_tags,
generation_metadata={
"model": "claude-opus-4.5",
"context_sku": context.sku,
"marketplace": context.marketplace,
"keywords_used": analysis.keywords[:10]
}
)
def _build_system_prompt(self, brand_settings: Optional[BrandSettings]) -> str:
"""Формирование ÑиÑтемного промпта Ñ Ð½Ð°Ñтройками бренда."""
brand_instructions = ""
if brand_settings:
brand_instructions = BRAND_INSTRUCTIONS_TEMPLATE.format(
brand_name=brand_settings.brand_name,
tone=brand_settings.tone,
accent_words=", ".join(brand_settings.accent_words) or "не указаны",
forbidden_words=", ".join(brand_settings.forbidden_words) or "нет",
description_style=brand_settings.description_style or "Ñтандартный"
)
return GENERATOR_SYSTEM_PROMPT.format(brand_instructions=brand_instructions)
async def _generate_title(
self,
context: GenerationContext,
analysis: AnalysisResult,
system_prompt: str,
max_length: int
) -> str:
"""Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ñ‚Ð¾Ð²Ð°Ñ€Ð°."""
user_prompt = TITLE_PROMPT.format(
current_title=context.current_title or "Ðе указано",
category=context.category,
brand_name=context.brand_settings.brand_name if context.brand_settings else "",
title_keywords=", ".join(analysis.title_keywords[:5]),
max_length=max_length
)
response = await self.claude_client.chat_completion(
model="claude-opus-4.5",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=200
)
title = response.content.strip()
# Обрезка по лимиту
if len(title) > max_length:
title = title[:max_length-3] + "..."
return title
async def _generate_description(
self,
context: GenerationContext,
analysis: AnalysisResult,
system_prompt: str,
title: str,
min_length: int,
max_length: int
) -> str:
"""Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ð¾Ð¿Ð¸ÑÐ°Ð½Ð¸Ñ Ñ‚Ð¾Ð²Ð°Ñ€Ð°."""
composition = ""
care = ""
if context.product_knowledge:
composition = context.product_knowledge.composition or "Ðе указан"
care = context.product_knowledge.care_instructions or "Ðе указан"
user_prompt = DESCRIPTION_PROMPT.format(
title=title,
category=context.category,
brand_name=context.brand_settings.brand_name if context.brand_settings else "",
keywords="\n".join(f"- {kw}" for kw in analysis.keywords[:10]),
usp="\n".join(f"- {u}" for u in analysis.usp[:5]),
composition=composition,
care_instructions=care,
description_outline="\n".join(f"- {item}" for item in analysis.description_outline),
min_length=min_length,
max_length=max_length
)
response = await self.claude_client.chat_completion(
model="claude-opus-4.5",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=2000
)
description = response.content.strip()
# Обрезка по лимиту
if len(description) > max_length:
description = description[:max_length-3] + "..."
return description
async def _generate_attributes(
self,
context: GenerationContext,
analysis: AnalysisResult,
system_prompt: str
) -> Dict[str, str]:
"""Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ð°Ñ‚Ñ€Ð¸Ð±ÑƒÑ‚Ð¾Ð² товара."""
features = []
composition = ""
if context.product_knowledge:
features = context.product_knowledge.features
composition = context.product_knowledge.composition or ""
important_attrs = analysis.category_insights.get("important_attributes", [])
user_prompt = ATTRIBUTES_PROMPT.format(
category=context.category,
current_attributes=json.dumps(context.current_attributes, ensure_ascii=False),
composition=composition or "Ðе указан",
features=", ".join(features) if features else "Ðе указаны",
important_attributes=", ".join(important_attrs) if important_attrs else "Ñтандартные"
)
response = await self.claude_client.chat_completion(
model="claude-opus-4.5",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.3,
max_tokens=1000,
response_format={"type": "json_object"}
)
try:
attributes = json.loads(response.content)
except json.JSONDecodeError:
attributes = context.current_attributes or {}
return attributes
async def _generate_seo_tags(
self,
context: GenerationContext,
analysis: AnalysisResult,
system_prompt: str,
title: str
) -> List[str]:
"""Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ SEO-тегов."""
user_prompt = SEO_TAGS_PROMPT.format(
title=title,
category=context.category,
keywords=", ".join(analysis.keywords[:15])
)
response = await self.claude_client.chat_completion(
model="claude-opus-4.5",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.5,
max_tokens=500,
response_format={"type": "json_object"}
)
try:
data = json.loads(response.content)
tags = data.get("tags", [])
except json.JSONDecodeError:
tags = analysis.keywords[:10]
return tags[:20] # МакÑимум 20 тегов
3.5 Visual Prompting Generator
3.5.1 Ðазначение
Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñ‚ÐµÑ…Ð½Ð¸Ñ‡ÐµÑкого Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½ÐµÑ€Ð° на оÑнове ручного ввода пользователÑ.3.5.2 Промпт Visual Prompting
Copy
VISUAL_PROMPTING_SYSTEM = """
Ты — ÑкÑперт по визуальному контенту Ð´Ð»Ñ Ð¼Ð°Ñ€ÐºÐµÑ‚Ð¿Ð»ÐµÐ¹Ñов.
Ð¢Ð²Ð¾Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° — ÑоÑтавить ТЗ Ð´Ð»Ñ Ñ„Ð¾Ñ‚Ð¾Ð³Ñ€Ð°Ñ„Ð°/дизайнера на оÑнове информации о проблемах товара.
ПРÐВИЛÐ:
1. Формулируй конкретные рекомендации Ð´Ð»Ñ Ñ„Ð¾Ñ‚Ð¾
2. Учитывай типичные проблемы категории
3. Предлагай ÑпоÑобы визуально подчеркнуть преимущеÑтва
4. Рекомендуй ракурÑÑ‹ и детали Ð´Ð»Ñ Ñъёмки
"""
VISUAL_PROMPTING_USER = """
СоÑтавь ТЗ Ð´Ð»Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½ÐµÑ€Ð°/фотографа.
ТОВÐÐ : {title}
КÐТЕГОРИЯ: {category}
БРЕÐД: {brand_name}
ИЗВЕСТÐЫЕ ПРОБЛЕМЫ (на что жалуютÑÑ Ð¿Ð¾ÐºÑƒÐ¿Ð°Ñ‚ÐµÐ»Ð¸):
{known_issues}
ТРЕБОВÐÐИЯ К ФОТО ОТ ПОЛЬЗОВÐТЕЛЯ:
{photo_requirements}
ОСОБЕÐÐОСТИ ТОВÐÐ Ð:
{features}
Сформируй ТЗ в виде ÑпиÑка конкретных рекомендаций Ð´Ð»Ñ Ñ„Ð¾Ñ‚Ð¾ÑеÑÑии.
"""
3.5.3 Ð ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Visual Prompting
Copy
@dataclass
class VisualPromptingResult:
"""Результат Visual Prompting."""
recommendations: List[str]
photo_angles: List[str]
detail_shots: List[str]
styling_tips: List[str]
raw_text: str
class VisualPromptingGenerator:
"""Генератор ТЗ Ð´Ð»Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½ÐµÑ€Ð°."""
def __init__(self, gpt_client: GPTClient):
self.gpt_client = gpt_client
async def generate(
self,
context: GenerationContext,
title: str
) -> VisualPromptingResult:
"""Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ð¢Ð— Ð´Ð»Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½ÐµÑ€Ð°."""
# Проверка Ð½Ð°Ð»Ð¸Ñ‡Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… Ð´Ð»Ñ Visual Prompting
if not context.manual_input:
return VisualPromptingResult(
recommendations=["Ðет данных Ð´Ð»Ñ Ñ„Ð¾Ñ€Ð¼Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¢Ð—"],
photo_angles=[],
detail_shots=[],
styling_tips=[],
raw_text="Ð”Ð»Ñ Ñ„Ð¾Ñ€Ð¼Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¢Ð— необходимо указать проблемы или Ñ‚Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ Ðº фото."
)
known_issues = context.manual_input.known_issues
photo_requirements = context.manual_input.photo_requirements
if not known_issues and not photo_requirements:
return VisualPromptingResult(
recommendations=["Ðет данных Ð´Ð»Ñ Ñ„Ð¾Ñ€Ð¼Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¢Ð—"],
photo_angles=[],
detail_shots=[],
styling_tips=[],
raw_text="Укажите извеÑтные проблемы товара или Ñ‚Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ Ðº фото."
)
features = []
if context.product_knowledge:
features = context.product_knowledge.features
user_prompt = VISUAL_PROMPTING_USER.format(
title=title,
category=context.category,
brand_name=context.brand_settings.brand_name if context.brand_settings else "",
known_issues="\n".join(f"- {issue}" for issue in known_issues) or "Ðе указаны",
photo_requirements="\n".join(f"- {req}" for req in photo_requirements) or "Ðе указаны",
features="\n".join(f"- {f}" for f in features) or "Ðе указаны"
)
response = await self.gpt_client.chat_completion(
model="gpt-5-mini",
messages=[
{"role": "system", "content": VISUAL_PROMPTING_SYSTEM},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
max_tokens=1000
)
raw_text = response.content.strip()
# ПарÑинг Ñтруктурированного ответа
return self._parse_response(raw_text)
def _parse_response(self, text: str) -> VisualPromptingResult:
"""ПарÑинг ответа в Ñтруктурированный формат."""
lines = [line.strip() for line in text.split("\n") if line.strip()]
recommendations = []
photo_angles = []
detail_shots = []
styling_tips = []
current_section = "recommendations"
for line in lines:
line_lower = line.lower()
# Определение Ñекции
if "ракурÑ" in line_lower or "угол" in line_lower:
current_section = "angles"
elif "детал" in line_lower or "крупн" in line_lower:
current_section = "details"
elif "Ñтил" in line_lower or "оформ" in line_lower:
current_section = "styling"
# Добавление в ÑоответÑтвующую Ñекцию
if line.startswith("-") or line.startswith("•") or line[0].isdigit():
clean_line = line.lstrip("-•0123456789. ")
if current_section == "angles":
photo_angles.append(clean_line)
elif current_section == "details":
detail_shots.append(clean_line)
elif current_section == "styling":
styling_tips.append(clean_line)
else:
recommendations.append(clean_line)
# ЕÑли парÑинг не удалÑÑ, вÑе в recommendations
if not recommendations and not photo_angles and not detail_shots:
recommendations = lines
return VisualPromptingResult(
recommendations=recommendations or ["См. полный текÑÑ‚ ТЗ"],
photo_angles=photo_angles,
detail_shots=detail_shots,
styling_tips=styling_tips,
raw_text=text
)
3.6 Content Validator
3.6.1 Ðазначение
Проверка Ñгенерированного контента перед публикацией.3.6.2 Правила валидации
| Правило | ОпиÑание | Severity |
|---|---|---|
| Длина Title | СоответÑтвие лимитам маркетплейÑа | Error |
| Длина Description | СоответÑтвие лимитам | Error |
| Запрещённые Ñлова | Проверка по ÑпиÑку бренда | Error |
| Бренд в Title | Корректное название бренда | Warning |
| СпецÑимволы | ÐедопуÑтимые Ñимволы | Warning |
| Ключевые Ñлова | Ðаличие оÑновных keywords | Info |
| Спам-проверка | ПереÑпам ключевыми Ñловами | Warning |
3.6.3 Ð ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð²Ð°Ð»Ð¸Ð´Ð°Ñ‚Ð¾Ñ€Ð°
Copy
from enum import Enum
from dataclasses import dataclass
from typing import List
class ValidationSeverity(Enum):
ERROR = "error"
WARNING = "warning"
INFO = "info"
@dataclass
class ValidationIssue:
"""Проблема валидации."""
field: str
message: str
severity: ValidationSeverity
suggestion: Optional[str] = None
@dataclass
class ValidationResult:
"""Результат валидации."""
is_valid: bool
issues: List[ValidationIssue]
@property
def errors(self) -> List[ValidationIssue]:
return [i for i in self.issues if i.severity == ValidationSeverity.ERROR]
@property
def warnings(self) -> List[ValidationIssue]:
return [i for i in self.issues if i.severity == ValidationSeverity.WARNING]
class ContentValidator:
"""Валидатор Ñгенерированного контента."""
# Лимиты по маркетплейÑам
LIMITS = {
"wb": {"title_max": 100, "desc_min": 100, "desc_max": 5000},
"ozon": {"title_max": 255, "desc_min": 100, "desc_max": 6000},
"ym": {"title_max": 150, "desc_min": 100, "desc_max": 3000}
}
# Общие запрещённые Ñлова
GLOBAL_FORBIDDEN = [
"лучший", "номер 1", "Ñамый", "единÑтвенный",
"гарантированно", "100%", "навÑегда"
]
# СпецÑимволы Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ
FORBIDDEN_CHARS = ["<", ">", "{", "}", "|", "\\", "^", "`"]
def validate(
self,
content: GeneratedContent,
context: GenerationContext
) -> ValidationResult:
"""Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ Ñгенерированного контента."""
issues: List[ValidationIssue] = []
marketplace = context.marketplace
limits = self.LIMITS.get(marketplace, self.LIMITS["wb"])
# Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ Title
issues.extend(self._validate_title(content.title, limits, context))
# Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ Description
issues.extend(self._validate_description(content.description, limits, context))
# Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ Ð·Ð°Ð¿Ñ€ÐµÑ‰Ñ‘Ð½Ð½Ñ‹Ñ… Ñлов
issues.extend(self._validate_forbidden_words(content, context))
# Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ ÑпецÑимволов
issues.extend(self._validate_special_chars(content))
# Проверка на Ñпам
issues.extend(self._validate_keyword_spam(content))
# Определение валидноÑти (нет ошибок)
is_valid = not any(i.severity == ValidationSeverity.ERROR for i in issues)
return ValidationResult(is_valid=is_valid, issues=issues)
def _validate_title(
self,
title: str,
limits: dict,
context: GenerationContext
) -> List[ValidationIssue]:
"""Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ."""
issues = []
max_len = limits["title_max"]
if len(title) > max_len:
issues.append(ValidationIssue(
field="title",
message=f"Длина Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ ({len(title)}) превышает лимит ({max_len})",
severity=ValidationSeverity.ERROR,
suggestion=f"Сократите название до {max_len} Ñимволов"
))
if len(title) < 10:
issues.append(ValidationIssue(
field="title",
message="Ðазвание Ñлишком короткое",
severity=ValidationSeverity.WARNING,
suggestion="Добавьте ключевые характериÑтики товара"
))
# Проверка Ð½Ð°Ð»Ð¸Ñ‡Ð¸Ñ Ð±Ñ€ÐµÐ½Ð´Ð°
if context.brand_settings:
brand_name = context.brand_settings.brand_name.lower()
if brand_name not in title.lower():
issues.append(ValidationIssue(
field="title",
message="Ðазвание не Ñодержит бренд",
severity=ValidationSeverity.WARNING,
suggestion=f"Добавьте '{context.brand_settings.brand_name}' в название"
))
return issues
def _validate_description(
self,
description: str,
limits: dict,
context: GenerationContext
) -> List[ValidationIssue]:
"""Ð’Ð°Ð»Ð¸Ð´Ð°Ñ†Ð¸Ñ Ð¾Ð¿Ð¸ÑаниÑ."""
issues = []
min_len = limits["desc_min"]
max_len = limits["desc_max"]
if len(description) > max_len:
issues.append(ValidationIssue(
field="description",
message=f"Длина опиÑÐ°Ð½Ð¸Ñ ({len(description)}) превышает лимит ({max_len})",
severity=ValidationSeverity.ERROR,
suggestion=f"Сократите опиÑание до {max_len} Ñимволов"
))
if len(description) < min_len:
issues.append(ValidationIssue(
field="description",
message=f"ОпиÑание Ñлишком короткое ({len(description)} < {min_len})",
severity=ValidationSeverity.WARNING,
suggestion="Добавьте больше информации о товаре"
))
return issues
def _validate_forbidden_words(
self,
content: GeneratedContent,
context: GenerationContext
) -> List[ValidationIssue]:
"""Проверка запрещённых Ñлов."""
issues = []
# Собираем вÑе запрещённые Ñлова
forbidden = list(self.GLOBAL_FORBIDDEN)
if context.brand_settings and context.brand_settings.forbidden_words:
forbidden.extend(context.brand_settings.forbidden_words)
# Проверка Title
title_lower = content.title.lower()
for word in forbidden:
if word.lower() in title_lower:
issues.append(ValidationIssue(
field="title",
message=f"Ðазвание Ñодержит запрещённое Ñлово: '{word}'",
severity=ValidationSeverity.ERROR,
suggestion=f"Удалите или замените Ñлово '{word}'"
))
# Проверка Description
desc_lower = content.description.lower()
for word in forbidden:
if word.lower() in desc_lower:
issues.append(ValidationIssue(
field="description",
message=f"ОпиÑание Ñодержит запрещённое Ñлово: '{word}'",
severity=ValidationSeverity.ERROR,
suggestion=f"Удалите или замените Ñлово '{word}'"
))
return issues
def _validate_special_chars(self, content: GeneratedContent) -> List[ValidationIssue]:
"""Проверка ÑпецÑимволов."""
issues = []
for char in self.FORBIDDEN_CHARS:
if char in content.title:
issues.append(ValidationIssue(
field="title",
message=f"Ðазвание Ñодержит недопуÑтимый Ñимвол: '{char}'",
severity=ValidationSeverity.WARNING,
suggestion="Удалите ÑпецÑимволы из названиÑ"
))
if char in content.description:
issues.append(ValidationIssue(
field="description",
message=f"ОпиÑание Ñодержит недопуÑтимый Ñимвол: '{char}'",
severity=ValidationSeverity.WARNING,
suggestion="Удалите ÑпецÑимволы из опиÑаниÑ"
))
return issues
def _validate_keyword_spam(self, content: GeneratedContent) -> List[ValidationIssue]:
"""Проверка на переÑпам ключевыми Ñловами."""
issues = []
# ПроÑÑ‚Ð°Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ°: еÑли Ñлово повторÑетÑÑ Ð±Ð¾Ð»ÐµÐµ 5 раз
words = content.description.lower().split()
word_count = {}
for word in words:
if len(word) > 3: # Игнорируем короткие Ñлова
word_count[word] = word_count.get(word, 0) + 1
total_words = len(words)
for word, count in word_count.items():
density = count / total_words * 100
if density > 4: # Более 4% — переÑпам
issues.append(ValidationIssue(
field="description",
message=f"Возможный переÑпам: Ñлово '{word}' вÑтречаетÑÑ {count} раз ({density:.1f}%)",
severity=ValidationSeverity.WARNING,
suggestion="ИÑпользуйте Ñинонимы Ð´Ð»Ñ Ñ€Ð°Ð·Ð½Ð¾Ð¾Ð±Ñ€Ð°Ð·Ð¸Ñ Ñ‚ÐµÐºÑта"
))
return issues
3.7 ОркеÑтратор Pipeline
3.7.1 ÐžÐ±Ñ‰Ð°Ñ Ñхема
3.7.2 Ð ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¾Ñ€ÐºÐµÑтратора
Copy
@dataclass
class GenerationResult:
"""Полный результат генерации."""
success: bool
draft_id: Optional[str] = None
content: Optional[GeneratedContent] = None
visual_prompting: Optional[VisualPromptingResult] = None
validation: Optional[ValidationResult] = None
error: Optional[str] = None
class ContentPipelineOrchestrator:
"""ОркеÑтратор AI Pipeline."""
def __init__(
self,
context_builder: ContextBuilder,
analyzer: ContentAnalyzer,
generator: ContentGenerator,
visual_generator: VisualPromptingGenerator,
validator: ContentValidator,
draft_repo: DraftRepository
):
self.context_builder = context_builder
self.analyzer = analyzer
self.generator = generator
self.visual_generator = visual_generator
self.validator = validator
self.draft_repo = draft_repo
async def generate(
self,
card_data: CardData,
manual_input: Optional[ManualInput] = None,
user_id: str = "",
include_visual_prompting: bool = False
) -> GenerationResult:
"""Полный цикл генерации контента."""
try:
# 1. Сборка контекÑта
context = await self.context_builder.build(
card_data=card_data,
manual_input=manual_input,
user_id=user_id
)
# 2. Ðнализ
analysis = await self.analyzer.analyze(context)
# 3. Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ ÐºÐ¾Ð½Ñ‚ÐµÐ½Ñ‚Ð°
content = await self.generator.generate(context, analysis)
# 4. Visual Prompting (опционально)
visual_prompting = None
if include_visual_prompting:
visual_prompting = await self.visual_generator.generate(
context, content.title
)
# 5. ВалидациÑ
validation = self.validator.validate(content, context)
# 6. Сохранение черновика
draft_id = await self.draft_repo.save_draft(
sku=card_data.sku,
marketplace=card_data.marketplace.value,
content=content,
validation=validation,
user_id=user_id
)
return GenerationResult(
success=True,
draft_id=draft_id,
content=content,
visual_prompting=visual_prompting,
validation=validation
)
except Exception as e:
logger.error(f"Generation failed for {card_data.sku}: {e}")
return GenerationResult(
success=False,
error=str(e)
)
3.8 ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ AI-клиентов
3.8.1 GPT Client (Timeweb AI)
Copy
class GPTClient:
"""Клиент Ð´Ð»Ñ GPT-5 mini через Timeweb AI."""
def __init__(self):
self.base_url = os.getenv("TIMEWEB_AI_URL", "https://api.timeweb.cloud/ai")
self.api_key = os.getenv("TIMEWEB_AI_KEY")
async def chat_completion(
self,
model: str,
messages: List[dict],
temperature: float = 0.7,
max_tokens: int = 1000,
response_format: Optional[dict] = None
) -> ChatResponse:
"""Вызов Chat Completion API."""
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
if response_format:
payload["response_format"] = response_format
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/v1/chat/completions",
json=payload,
headers={"Authorization": f"Bearer {self.api_key}"}
) as resp:
data = await resp.json()
return ChatResponse(
content=data["choices"][0]["message"]["content"],
usage=data.get("usage", {})
)
3.8.2 Claude Client (OpenAI API)
Copy
class ClaudeClient:
"""Клиент Ð´Ð»Ñ Claude Opus 4.5."""
def __init__(self):
self.base_url = "https://api.anthropic.com"
self.api_key = os.getenv("CLAUDE_API_KEY")
async def chat_completion(
self,
model: str,
messages: List[dict],
temperature: float = 0.7,
max_tokens: int = 1000,
response_format: Optional[dict] = None
) -> ChatResponse:
"""Вызов Claude API."""
# ÐšÐ¾Ð½Ð²ÐµÑ€Ñ‚Ð°Ñ†Ð¸Ñ Ñ„Ð¾Ñ€Ð¼Ð°Ñ‚Ð° Ñообщений
system_content = ""
user_messages = []
for msg in messages:
if msg["role"] == "system":
system_content = msg["content"]
else:
user_messages.append(msg)
payload = {
"model": "claude-opus-4-5-20251101",
"max_tokens": max_tokens,
"messages": user_messages
}
if system_content:
payload["system"] = system_content
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/v1/messages",
json=payload,
headers={
"x-api-key": self.api_key,
"anthropic-version": "2024-01-01",
"content-type": "application/json"
}
) as resp:
data = await resp.json()
return ChatResponse(
content=data["content"][0]["text"],
usage=data.get("usage", {})
)
Документ подготовлен: Январь 2026
ВерÑиÑ: 1.0
СтатуÑ: Черновик