Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit 67e411eea8
246 changed files with 23152 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
"""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")}