Files
kwork/app/ai.py
2026-07-17 12:21:53 +07:00

81 lines
2.8 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,
) -> 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}"
)