"""AI-помощник: статус, чат (любой вошедший пользователь, если включено), настройки и проверка (администратор).""" from typing import Literal, Optional from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, Field from sqlalchemy.orm import Session from .. import assistant as A, settings_store as ss from ..db import get_db from ..models import User from ..security import current_user, require_portal_admin as require_admin from .catalog import commit router = APIRouter(prefix="/assistant", tags=["assistant"]) class Msg(BaseModel): role: Literal["user", "assistant"] content: str = Field(max_length=4000) class ChatIn(BaseModel): messages: list[Msg] = Field(min_length=1, max_length=40) class ConfigIn(BaseModel): enabled: Optional[bool] = None provider: Optional[Literal["openai", "anthropic", "gigachat"]] = None base_url: Optional[str] = Field(default=None, max_length=300) model: Optional[str] = Field(default=None, max_length=200) api_key: Optional[str] = Field(default=None, max_length=2000) # пустая строка — удалить ключ; null — не менять scope: Optional[str] = Field(default=None, max_length=2000) # доп. указания помощнику от администратора @router.get("/status") def status(db: Session = Depends(get_db), _: User = Depends(current_user)): return {"enabled": A.ready(db)} @router.post("/chat") async def chat(data: ChatIn, request: Request, db: Session = Depends(get_db), u: User = Depends(current_user)): token = request.headers.get("authorization", "")[7:] try: return await A.chat(request.app, db, u, token, [m.model_dump() for m in data.messages]) except A.AssistantError as e: raise HTTPException(429 if "Слишком много" in str(e) else 503 if "выключен" in str(e) or "провайдер" in str(e).lower() else 422, str(e)) @router.get("/config") def get_config(db: Session = Depends(get_db), _: User = Depends(require_admin)): return A.public_cfg(db) @router.put("/config") def put_config(data: ConfigIn, db: Session = Depends(get_db), _: User = Depends(require_admin)): d = data.model_dump(exclude_unset=True) if d.get("base_url") and not d["base_url"].strip().lower().startswith(("https://", "http://")): raise HTTPException(422, "Адрес провайдера должен начинаться с https:// (или http:// для локальной модели)") for k, v in d.items(): if v is None: continue ss.set_(db, "assistant_" + k, ("1" if v else "0") if isinstance(v, bool) else str(v).strip()) commit(db) c = A.cfg(db) if c["enabled"] and (not c["model"] or not (c["api_key"] or "localhost" in c["base_url"] or "127.0.0.1" in c["base_url"])): ss.set_(db, "assistant_enabled", "0") commit(db) raise HTTPException(422, "Для включения укажите модель и ключ API (без ключа — только локальная модель)") return A.public_cfg(db) @router.post("/test") async def test(request: Request, db: Session = Depends(get_db), u: User = Depends(require_admin)): """Проверка подключения: короткий вопрос по сервису и вопрос не по теме (должен быть отклонён).""" c = A.cfg(db) if not c["model"]: raise HTTPException(422, "Укажите модель") try: token = request.headers.get("authorization", "")[7:] if not A.ready(db): # проверяем и до включения: временно считаем помощника включённым c["enabled"] = True ok = await A.chat(request.app, db, u, token, [{"role": "user", "content": "Какие у меня задания на сегодня и где их посмотреть?"}], cfg_override=c) off = await A.complete(c, A.system_prompt(u, c["scope"]), [{"role": "user", "content": "Напиши стихотворение про весну"}]) except A.AssistantError as e: raise HTTPException(502, str(e)) return {"ok": True, "answer": ok["reply"][:600], "tools": ok["tools"], "offtopic_filtered": off.strip().upper().startswith("OFFTOPIC")}