smart tdee
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -11,6 +12,7 @@ from app.api.schemas import (
|
||||
SessionDetailOut,
|
||||
SessionOut,
|
||||
)
|
||||
from app.auth.deps import get_current_user
|
||||
from app.chat.generation import (
|
||||
GenerationBusyError,
|
||||
get_active_handle,
|
||||
@@ -19,12 +21,18 @@ from app.chat.generation import (
|
||||
subscribe_generation,
|
||||
)
|
||||
from app.chat.service import ChatService
|
||||
from app.auth.deps import get_current_user
|
||||
from app.config import get_settings
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.vision import VisionService, format_user_messages, vision_debug_payloads
|
||||
from app.vision.analyze import VisionUnavailableError
|
||||
from app.vision.preprocess import prepare_image
|
||||
from app.vision.storage import format_upload_images_markdown, save_upload
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"}
|
||||
|
||||
|
||||
@router.post("/sessions", response_model=SessionOut)
|
||||
def create_session(payload: SessionCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> SessionOut:
|
||||
@@ -108,11 +116,95 @@ def delete_session(session_id: int, db: Session = Depends(get_db), user: User =
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _collect_form_uploads(form) -> list:
|
||||
uploads: list = []
|
||||
seen_ids: set[int] = set()
|
||||
|
||||
def _append(item) -> None:
|
||||
if item is None or not hasattr(item, "read"):
|
||||
return
|
||||
item_id = id(item)
|
||||
if item_id in seen_ids:
|
||||
return
|
||||
seen_ids.add(item_id)
|
||||
uploads.append(item)
|
||||
|
||||
if hasattr(form, "getlist"):
|
||||
for item in form.getlist("images"):
|
||||
_append(item)
|
||||
single = form.get("image")
|
||||
_append(single)
|
||||
return uploads
|
||||
|
||||
|
||||
async def _analyze_upload(raw: bytes, *, caption: str, user_id: int):
|
||||
prepared = prepare_image(raw)
|
||||
filename = save_upload(prepared, user_id=user_id)
|
||||
result = await VisionService().analyze_prepared(prepared, user_hint=caption)
|
||||
return result, filename
|
||||
|
||||
|
||||
async def _parse_message_request(
|
||||
request: Request,
|
||||
*,
|
||||
user_id: int,
|
||||
) -> tuple[str, dict | None]:
|
||||
content_type = (request.headers.get("content-type") or "").lower()
|
||||
if "multipart/form-data" not in content_type:
|
||||
try:
|
||||
body = await request.json()
|
||||
except json.JSONDecodeError as exc:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON body") from exc
|
||||
payload = MessageCreate.model_validate(body)
|
||||
return payload.content, None
|
||||
|
||||
form = await request.form()
|
||||
caption = str(form.get("content") or "").strip()
|
||||
uploads = _collect_form_uploads(form)
|
||||
if not uploads:
|
||||
raise HTTPException(status_code=400, detail="Field 'images' or 'image' is required for multipart upload")
|
||||
|
||||
max_images = max(1, int(get_settings().vision_max_images))
|
||||
if len(uploads) > max_images:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Too many images (max {max_images})",
|
||||
)
|
||||
|
||||
raw_images: list[bytes] = []
|
||||
for upload in uploads:
|
||||
raw = await upload.read()
|
||||
if not raw:
|
||||
raise HTTPException(status_code=400, detail="Empty image file")
|
||||
mime = getattr(upload, "content_type", None) or "application/octet-stream"
|
||||
if mime not in ALLOWED_IMAGE_TYPES:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported image type: {mime}")
|
||||
raw_images.append(raw)
|
||||
|
||||
try:
|
||||
analyzed = await asyncio.gather(
|
||||
*(_analyze_upload(raw, caption=caption, user_id=user_id) for raw in raw_images)
|
||||
)
|
||||
except VisionUnavailableError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
results = [item[0] for item in analyzed]
|
||||
filenames = [item[1] for item in analyzed]
|
||||
debug = vision_debug_payloads(results)
|
||||
vision_text = format_user_messages(caption, results)
|
||||
images_md = format_upload_images_markdown(user_id, filenames)
|
||||
user_text = f"{images_md}\n\n{vision_text}" if images_md else vision_text
|
||||
if not user_text.strip():
|
||||
raise HTTPException(status_code=400, detail="Could not build message from image")
|
||||
return user_text, debug
|
||||
|
||||
|
||||
@router.post("/sessions/{session_id}/messages")
|
||||
async def send_message(
|
||||
session_id: int,
|
||||
payload: MessageCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> StreamingResponse:
|
||||
service = ChatService(db, user.id)
|
||||
if not service.get_session(session_id):
|
||||
@@ -121,16 +213,19 @@ async def send_message(
|
||||
if is_generation_active(session_id):
|
||||
raise HTTPException(status_code=409, detail="Generation already in progress")
|
||||
|
||||
# Сохраняем user до стрима: иначе при обрыве SSE сообщение не попадает в БД.
|
||||
service.save_user_message(session_id, payload.content)
|
||||
user_text, vision_debug = await _parse_message_request(request, user_id=user.id)
|
||||
|
||||
service.save_user_message(session_id, user_text)
|
||||
|
||||
try:
|
||||
handle = await start_generation(session_id, user.id, payload.content)
|
||||
handle = await start_generation(session_id, user.id, user_text)
|
||||
except GenerationBusyError:
|
||||
raise HTTPException(status_code=409, detail="Generation already in progress") from None
|
||||
|
||||
async def event_stream():
|
||||
try:
|
||||
if vision_debug:
|
||||
yield ChatService._sse("vision", vision_debug)
|
||||
async for chunk in subscribe_generation(handle):
|
||||
yield chunk
|
||||
except asyncio.CancelledError:
|
||||
@@ -155,4 +250,3 @@ def context_preview(
|
||||
) -> dict:
|
||||
service = ChatService(db, user.id)
|
||||
return service.context_preview(session_id, query=query)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user