from __future__ import annotations import asyncio from pathlib import Path import requests from app.models import Offer class OfferReplyGenerator: def __init__( self, api_base_url: str, api_key: str, model: str, system_prompt_file: Path, timeout_seconds: int, ) -> None: self._api_base_url = api_base_url.rstrip("/") self._api_key = api_key self._model = model self._system_prompt = system_prompt_file.read_text(encoding="utf-8").strip() self._timeout_seconds = timeout_seconds async def generate_reply(self, offer: Offer) -> str: return await asyncio.to_thread(self._generate_reply_sync, offer) def _generate_reply_sync(self, offer: Offer) -> str: response = requests.post( f"{self._api_base_url}/chat/completions", headers={ "Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json", }, json={ "model": self._model, "messages": [ {"role": "system", "content": self._system_prompt}, {"role": "user", "content": _build_offer_prompt(offer)}, ], }, timeout=self._timeout_seconds, ) response.raise_for_status() payload = response.json() choices = payload.get("choices") or [] if not choices: raise ValueError("OpenAI-compatible API returned no choices") message = choices[0].get("message") or {} content = message.get("content") if isinstance(content, str) and content.strip(): return content.strip() if isinstance(content, list): text_parts = [item.get("text", "") for item in content if isinstance(item, dict)] text = "\n".join(part.strip() for part in text_parts if part and part.strip()).strip() if text: return text raise ValueError("OpenAI-compatible API returned empty content") def _build_offer_prompt(offer: Offer) -> str: seller_line = f"Исполнитель: @{offer.seller_username}\n" if offer.seller_username else "" price_amount_line = f"Числовой бюджет: {offer.price_amount}\n" if offer.price_amount is not None else "" return ( f"Подготовь сообщение для отклика на этот оффер.\n" f"Источник: {offer.source}\n" f"ID: {offer.external_id}\n" f"Заголовок: {offer.title}\n" f"Категория: {offer.category_id}\n" f"Бюджет: {offer.price}\n" f"{price_amount_line}" f"Опубликовано: {offer.published_at}\n" f"{seller_line}" f"Описание:\n{offer.description}\n\n" f"Ссылка: {offer.url}" )