84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
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,
|
|
proxy_url: str | None = None,
|
|
) -> 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
|
|
self._proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
|
|
|
|
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,
|
|
proxies=self._proxies,
|
|
)
|
|
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}"
|
|
)
|