support-bot-saas/backend/app/services/llm.py
ed0ss f6d3ac05a5
Some checks are pending
CI / lint-backend (push) Waiting to run
CI / lint-frontend (push) Waiting to run
CI / build-backend (push) Blocked by required conditions
CI / build-frontend (push) Blocked by required conditions
fix: major bugfixes and stability improvements across backend & frontend
Backend:
- Fix TicketInput ticket_id type (int|str -> str) — Pydantic 500 on numeric IDs
- Fix LLM module: add missing logger import, handle 429 and non-200 responses
- Switch LLM model to poolside/laguna-m.1:free (avoid OpenRouter rate limits)
- Add GET /api/collections/{id} endpoint to fetch single collection
- Deduplicate tickets: replace randomized hash() with deterministic
  hashlib.md5 for Qdrant point IDs
- Add get_existing_ticket_ids() — scroll Qdrant before processing,
  skip already-imported tickets, avoid redundant embeddings
- Multi-tenant isolation: tenant_id + collection_id in all Qdrant filters
- Fix embedding: drop incompatible openai SDK, use raw httpx
- Fix reranking: access ScoredPoint.payload via .get(), not []
- Fix chunking: expand role detection (support/operator/agent + client/user/customer)
- Fix bcrypt: downgrade to 4.0.1 (incompatible 5.x with passlib)

Frontend:
- Register: add plan selection (free/starter/pro)
- CollectionView: fetch ticket_count from backend on mount
  (not only from upload response — survived page refresh)
- uploadFile: use getToken() instead of stale module-level var,
  show real HTTP status in error messages
- Nginx: increase client_max_body_size to 50M for file uploads
2026-06-20 19:51:08 +03:00

65 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import httpx
from app.config import settings
logger = logging.getLogger(__name__)
# Increase log level for httpx to reduce noise
logging.getLogger("httpx").setLevel(logging.WARNING)
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
_client: httpx.AsyncClient | None = None
def get_client() -> httpx.AsyncClient:
global _client
if _client is None:
_client = httpx.AsyncClient(
base_url=OPENROUTER_BASE,
headers={
"Authorization": f"Bearer {settings.openrouter_api_key}",
"Content-Type": "application/json",
},
timeout=60,
)
return _client
SYSTEM_PROMPT = """Ты — специалист техподдержки. Отвечай клиенту, используя ТОЛЬКО информацию из переданных тикетов (контекст).
Правила:
- Если контекст содержит подходящее решение — напиши ответ своими словами, адаптируя под вопрос
- Если контекст не относится к вопросу — напиши: «Недостаточно информации в истории обращений»
- НЕ придумывай ответы, НЕ используй общие знания
- НЕ говори «обратитесь в службу поддержки» — ты сам и есть поддержка
- Укажи в конце: «Основано на тикете №...»"""
async def generate_answer(query: str, context: str) -> str | None:
client = get_client()
response = await client.post(
"/chat/completions",
json={
"model": settings.openrouter_llm_model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": f"Контекст (история обращений):\n{context}\n\nВопрос клиента:\n{query}",
},
],
"temperature": 0.1,
"max_tokens": 2000,
},
)
if response.status_code != 200:
logger.error("LLM API error [%s]: %s", response.status_code, await response.aread())
return None
data = response.json()
if "choices" not in data or not data["choices"]:
logger.error("LLM response missing choices: %s", data.get("error", data))
return None
return data["choices"][0]["message"]["content"]