154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
"""
|
|
===============================================================================
|
|
FILE: modules/web_api/routers/chat.py
|
|
ROLE: Полнофункциональный роутер чата с извлечением текста из PDF и сканов,
|
|
поддержкой Function Calling, Fast-Path и оптического распознавания OCR.
|
|
===============================================================================
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import base64
|
|
import logging
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from fastapi import APIRouter, Header, HTTPException, UploadFile, File, Form
|
|
from pydantic import BaseModel
|
|
|
|
from llm.agent import process_chat_message
|
|
from config import BASE_DIR
|
|
|
|
logger = logging.getLogger("CHAT_API")
|
|
router = APIRouter(prefix="/api/v1", tags=["Chat"])
|
|
|
|
UPLOAD_TMP_DIR = os.path.join(BASE_DIR, "data", "uploads")
|
|
os.makedirs(UPLOAD_TMP_DIR, exist_ok=True)
|
|
|
|
|
|
class ChatMessageRequest(BaseModel):
|
|
message: str
|
|
session_id: Optional[str] = "web_session_main"
|
|
user_id: Optional[int] = 1
|
|
|
|
|
|
def resolve_user_id(authorization: Optional[str] = None, explicit_user_id: Optional[int] = None) -> int:
|
|
if explicit_user_id and explicit_user_id > 0:
|
|
return explicit_user_id
|
|
|
|
if authorization and authorization.startswith("Bearer "):
|
|
token = authorization.replace("Bearer ", "").strip()
|
|
if token.isdigit():
|
|
return int(token)
|
|
elif token.startswith("dev_token_"):
|
|
try:
|
|
return int(token.replace("dev_token_", ""))
|
|
except ValueError:
|
|
pass
|
|
return 1
|
|
|
|
|
|
@router.post("/chat")
|
|
async def chat_endpoint(payload: ChatMessageRequest, authorization: Optional[str] = Header(None)):
|
|
user_id = resolve_user_id(authorization, payload.user_id)
|
|
session_id = payload.session_id or "web_session_main"
|
|
user_msg = payload.message.strip()
|
|
|
|
if not user_msg:
|
|
raise HTTPException(status_code=400, detail="Пустое сообщение")
|
|
|
|
logger.info(f"Сообщение от user_id={user_id}, session_id={session_id}: {user_msg}")
|
|
|
|
reply_text, history, action_payload = process_chat_message(
|
|
user_id=user_id,
|
|
user_message=user_msg,
|
|
session_id=session_id
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"user_id": user_id,
|
|
"session_id": session_id,
|
|
"response": reply_text,
|
|
"action_payload": action_payload
|
|
}
|
|
|
|
|
|
@router.post("/chat/upload")
|
|
async def chat_upload_endpoint(
|
|
file: UploadFile = File(...),
|
|
message: Optional[str] = Form(""),
|
|
session_id: Optional[str] = Form("web_session_main"),
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
user_id = resolve_user_id(authorization, 1)
|
|
file_path = os.path.join(UPLOAD_TMP_DIR, file.filename)
|
|
|
|
with open(file_path, "wb") as buffer:
|
|
shutil.copyfileobj(file.file, buffer)
|
|
|
|
file_context = ""
|
|
image_b64 = None
|
|
fn_lower = file.filename.lower()
|
|
|
|
# 1. Текстовые форматы
|
|
if fn_lower.endswith((".txt", ".csv", ".log", ".md")):
|
|
try:
|
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
file_context = f.read(6000)
|
|
except Exception as e:
|
|
logger.warning(f"Не удалось прочитать текст: {e}")
|
|
|
|
# 2. Изображения (прямой OCR)
|
|
elif fn_lower.endswith((".png", ".jpg", ".jpeg", ".webp")):
|
|
try:
|
|
with open(file_path, "rb") as f:
|
|
image_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
except Exception as e:
|
|
logger.warning(f"Ошибка кодирования картинки в base64: {e}")
|
|
|
|
# 3. PDF документы (текстовый слой + рендеринг скана при необходимости)
|
|
elif fn_lower.endswith(".pdf"):
|
|
# Попытка извлечь встроенный текстовый слой
|
|
try:
|
|
import pypdf
|
|
reader = pypdf.PdfReader(file_path)
|
|
extracted = []
|
|
for page in reader.pages:
|
|
t = page.extract_text()
|
|
if t:
|
|
extracted.append(t)
|
|
file_context = "\n".join(extracted).strip()
|
|
except Exception:
|
|
pass
|
|
|
|
# Если текстового слоя мало (скан или фото документа), рендерим страницу в картинку для Vision OCR
|
|
if len(file_context) < 40:
|
|
try:
|
|
import fitz # PyMuPDF
|
|
doc = fitz.open(file_path)
|
|
if len(doc) > 0:
|
|
page = doc[0]
|
|
pix = page.get_pixmap(dpi=150)
|
|
img_bytes = pix.tobytes("png")
|
|
image_b64 = base64.b64encode(img_bytes).decode("utf-8")
|
|
file_context = ""
|
|
except Exception as e:
|
|
logger.warning(f"PyMuPDF не установлен или сбой рендеринга PDF: {e}")
|
|
|
|
user_msg = message.strip() or f"Распознай и проанализируй прикрепленный документ {file.filename}"
|
|
|
|
reply_text, history, action_payload = process_chat_message(
|
|
user_id=user_id,
|
|
user_message=user_msg,
|
|
file_context=file_context,
|
|
image_b64=image_b64,
|
|
session_id=session_id
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"user_id": user_id,
|
|
"session_id": session_id,
|
|
"response": reply_text,
|
|
"action_payload": action_payload
|
|
} |