- StepDocumentsNew.tsx: поэкранная загрузка документов - StepWaitingClaim.tsx: ожидание формирования заявления с SSE - StepDraftSelection.tsx: поддержка новых статусов черновиков - documents.py: API для загрузки документов - NEW_FLOW_ARCHITECTURE.md: документация новой архитектуры Флоу: Description → Documents → Waiting → Claim Review → SMS Статусы: draft_new, draft_docs_progress, draft_docs_complete, draft_claim_ready
271 lines
9.2 KiB
Python
271 lines
9.2 KiB
Python
"""
|
||
Documents API Routes - Загрузка и обработка документов
|
||
|
||
Новый флоу: поэкранная загрузка документов
|
||
"""
|
||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Request
|
||
from typing import Optional
|
||
import httpx
|
||
import json
|
||
import uuid
|
||
from datetime import datetime
|
||
import logging
|
||
from ..services.redis_service import redis_service
|
||
from ..config import settings
|
||
|
||
router = APIRouter(prefix="/api/v1/documents", tags=["Documents"])
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# n8n webhook для загрузки документов
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK = "https://n8n.clientright.pro/webhook/document-upload"
|
||
|
||
|
||
@router.post("/upload")
|
||
async def upload_document(
|
||
request: Request,
|
||
file: UploadFile = File(...),
|
||
claim_id: str = Form(...),
|
||
session_id: str = Form(...),
|
||
document_type: str = Form(...),
|
||
unified_id: Optional[str] = Form(None),
|
||
contact_id: Optional[str] = Form(None),
|
||
):
|
||
"""
|
||
Загрузка одного документа.
|
||
|
||
Принимает файл и метаданные, отправляет в n8n для:
|
||
1. Сохранения в S3
|
||
2. OCR обработки
|
||
3. Обновления черновика в PostgreSQL
|
||
|
||
После успешной обработки n8n публикует событие document_ocr_completed в Redis.
|
||
"""
|
||
try:
|
||
# Генерируем уникальный ID файла
|
||
file_id = f"doc_{uuid.uuid4().hex[:12]}"
|
||
|
||
logger.info(
|
||
"📤 Document upload received",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_name": file.filename,
|
||
"file_size": file.size if hasattr(file, 'size') else 'unknown',
|
||
"content_type": file.content_type,
|
||
},
|
||
)
|
||
|
||
# Читаем содержимое файла
|
||
file_content = await file.read()
|
||
file_size = len(file_content)
|
||
|
||
# Формируем данные для отправки в n8n
|
||
form_data = {
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"original_filename": file.filename,
|
||
"content_type": file.content_type or "application/octet-stream",
|
||
"file_size": str(file_size),
|
||
"upload_timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
if unified_id:
|
||
form_data["unified_id"] = unified_id
|
||
if contact_id:
|
||
form_data["contact_id"] = contact_id
|
||
|
||
# Файл для multipart
|
||
files = {
|
||
"file": (file.filename, file_content, file.content_type or "application/octet-stream")
|
||
}
|
||
|
||
# Отправляем в n8n
|
||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||
response = await client.post(
|
||
N8N_DOCUMENT_UPLOAD_WEBHOOK,
|
||
data=form_data,
|
||
files=files,
|
||
)
|
||
|
||
response_text = response.text or ""
|
||
|
||
if response.status_code == 200:
|
||
logger.info(
|
||
"✅ Document uploaded to n8n",
|
||
extra={
|
||
"claim_id": claim_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"response_preview": response_text[:200],
|
||
},
|
||
)
|
||
|
||
# Парсим ответ от n8n
|
||
try:
|
||
n8n_response = json.loads(response_text)
|
||
except json.JSONDecodeError:
|
||
n8n_response = {"raw": response_text}
|
||
|
||
# Публикуем событие в Redis для фронтенда
|
||
event_data = {
|
||
"event_type": "document_uploaded",
|
||
"status": "processing",
|
||
"claim_id": claim_id,
|
||
"session_id": session_id,
|
||
"document_type": document_type,
|
||
"file_id": file_id,
|
||
"original_filename": file.filename,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
await redis_service.publish(
|
||
f"ocr_events:{session_id}",
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"file_id": file_id,
|
||
"document_type": document_type,
|
||
"ocr_status": "processing",
|
||
"message": "Документ загружен и отправлен на обработку",
|
||
"n8n_response": n8n_response,
|
||
}
|
||
|
||
else:
|
||
logger.error(
|
||
"❌ n8n document upload error",
|
||
extra={
|
||
"status_code": response.status_code,
|
||
"body": response_text[:500],
|
||
},
|
||
)
|
||
raise HTTPException(
|
||
status_code=response.status_code,
|
||
detail=f"Ошибка n8n: {response_text}",
|
||
)
|
||
|
||
except httpx.TimeoutException:
|
||
logger.error("⏱️ n8n document upload timeout")
|
||
raise HTTPException(status_code=504, detail="Таймаут загрузки документа")
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Document upload error")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка загрузки документа: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.get("/status/{claim_id}")
|
||
async def get_documents_status(claim_id: str):
|
||
"""
|
||
Получить статус обработки документов для заявки.
|
||
|
||
Возвращает:
|
||
- Список загруженных документов и их OCR статус
|
||
- Общий прогресс обработки
|
||
"""
|
||
try:
|
||
# TODO: Запрос в PostgreSQL для получения статуса документов
|
||
# Пока возвращаем mock данные
|
||
|
||
return {
|
||
"success": True,
|
||
"claim_id": claim_id,
|
||
"documents": [],
|
||
"ocr_progress": {
|
||
"total": 0,
|
||
"completed": 0,
|
||
"processing": 0,
|
||
"failed": 0,
|
||
},
|
||
"wizard_ready": False,
|
||
"claim_ready": False,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Error getting documents status")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка получения статуса: {str(e)}",
|
||
)
|
||
|
||
|
||
@router.post("/generate-list")
|
||
async def generate_documents_list(request: Request):
|
||
"""
|
||
Запрос на генерацию списка документов для проблемы.
|
||
|
||
Принимает описание проблемы, отправляет в n8n для быстрого AI-анализа.
|
||
n8n публикует результат в Redis канал ocr_events:{session_id} с event_type=documents_list_ready.
|
||
"""
|
||
try:
|
||
body = await request.json()
|
||
|
||
session_id = body.get("session_id")
|
||
problem_description = body.get("problem_description")
|
||
|
||
if not session_id or not problem_description:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="session_id и problem_description обязательны",
|
||
)
|
||
|
||
logger.info(
|
||
"📝 Generate documents list request",
|
||
extra={
|
||
"session_id": session_id,
|
||
"description_length": len(problem_description),
|
||
},
|
||
)
|
||
|
||
# Публикуем событие в Redis для n8n
|
||
event_data = {
|
||
"type": "generate_documents_list",
|
||
"session_id": session_id,
|
||
"claim_id": body.get("claim_id"),
|
||
"unified_id": body.get("unified_id"),
|
||
"phone": body.get("phone"),
|
||
"problem_description": problem_description,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
|
||
channel = f"{settings.redis_prefix}documents_list"
|
||
|
||
subscribers = await redis_service.publish(
|
||
channel,
|
||
json.dumps(event_data, ensure_ascii=False)
|
||
)
|
||
|
||
logger.info(
|
||
"✅ Documents list request published",
|
||
extra={
|
||
"channel": channel,
|
||
"subscribers": subscribers,
|
||
},
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Запрос на генерацию списка документов отправлен",
|
||
"channel": channel,
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
|
||
except Exception as e:
|
||
logger.exception("❌ Error generating documents list")
|
||
raise HTTPException(
|
||
status_code=500,
|
||
detail=f"Ошибка генерации списка: {str(e)}",
|
||
)
|
||
|