87 lines
4.3 KiB
Python
87 lines
4.3 KiB
Python
"""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")}
|