Files
aiform_dev/backend/app/api/events.py
AI Assistant 647abf6578 feat: Интеграция n8n + Redis Pub/Sub + SSE для real-time обработки заявок
🎯 Основные изменения:

Backend:
-  Добавлен SSE endpoint для real-time событий (/api/v1/events/{task_id})
-  Redis Pub/Sub для публикации/подписки на события OCR/Vision
-  Удален aioboto3 из requirements.txt (конфликт зависимостей)
-  Добавлен OCR worker (deprecated, логика перенесена в n8n)

Frontend (React):
-  Автогенерация claim_id и session_id
-  Клиентская конвертация файлов в PDF (JPG/PNG/HEIC/WEBP)
-  Сжатие изображений до 2MB перед конвертацией
-  SSE подписка на события OCR/Vision в Step1Policy
-  Валидация документов (полис vs неподходящий контент)
-  Real-time прогресс загрузки и обработки файлов
-  Интеграция с n8n webhooks для проверки полиса и загрузки файлов

n8n Workflows:
-  Проверка полиса в MySQL + запись в PostgreSQL
-  Загрузка файлов в S3 + OCR + Vision AI
-  Публикация событий в Redis через backend API
-  Валидация документов (распознавание полисов ERV)

Документация:
- 📝 N8N_INTEGRATION.md - интеграция с n8n
- 📝 N8N_SQL_QUERIES.md - SQL запросы для workflows
- 📝 N8N_PDF_COMPRESS.md - сжатие PDF
- 📝 N8N_STIRLING_COMPRESS.md - интеграция Stirling-PDF

Утилиты:
- 🔧 monitor_redis.py/sh - мониторинг Redis Pub/Sub
- 🔧 test_redis_events.sh - тестирование событий
- 🔧 pdfConverter.ts - клиентская конвертация в PDF

Архитектура:
React → n8n webhooks (sync) → MySQL/PostgreSQL/S3
      → n8n workflows (async) → OCR/Vision → Redis Pub/Sub → SSE → React
2025-10-27 08:33:16 +03:00

132 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
SSE (Server-Sent Events) для real-time обновлений через Redis Pub/Sub
"""
import asyncio
import json
from fastapi import APIRouter, Body
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Dict, Any
from app.services.redis_service import redis_service
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
class EventPublish(BaseModel):
"""Модель для публикации события"""
event_type: str = "ocr_completed"
status: str
message: str
data: Dict[str, Any] = {}
timestamp: str = None
@router.post("/events/{task_id}")
async def publish_event(task_id: str, event: EventPublish):
"""
Публикация события в Redis канал
Используется n8n для отправки событий (OCR, AI и т.д.)
Args:
task_id: ID задачи
event: Данные события
Returns:
Статус публикации
"""
try:
channel = f"ocr_events:{task_id}"
event_data = {
"event_type": event.event_type,
"status": event.status,
"message": event.message,
"data": event.data,
"timestamp": event.timestamp
}
# Публикуем в Redis
event_json = json.dumps(event_data, ensure_ascii=False)
await redis_service.publish(channel, event_json)
logger.info(f"📢 Event published to {channel}: {event.status}")
return {
"success": True,
"channel": channel,
"event": event_data
}
except Exception as e:
logger.error(f"❌ Failed to publish event: {e}")
return {
"success": False,
"error": str(e)
}
@router.get("/events/{task_id}")
async def stream_events(task_id: str):
"""
SSE стрим событий обработки OCR
Args:
task_id: ID задачи
Returns:
StreamingResponse с событиями
"""
async def event_generator():
"""Генератор событий из Redis Pub/Sub"""
channel = f"ocr_events:{task_id}"
# Подписываемся на канал Redis
pubsub = redis_service.redis.pubsub()
await pubsub.subscribe(channel)
logger.info(f"📡 Client subscribed to {channel}")
# Отправляем начальное событие
yield f"data: {json.dumps({'status': 'connected', 'message': 'Подключено к событиям'})}\n\n"
try:
# Слушаем события
while True:
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=30.0)
if message and message['type'] == 'message':
event_data = message['data'].decode('utf-8')
# Отправляем событие клиенту
yield f"data: {event_data}\n\n"
# Если обработка завершена - закрываем соединение
event = json.loads(event_data)
if event.get('status') in ['completed', 'error']:
logger.info(f"✅ Task {task_id} finished, closing SSE")
break
# Пинг каждые 30 сек чтобы соединение не закрылось
await asyncio.sleep(0.1)
except asyncio.CancelledError:
logger.info(f"❌ Client disconnected from {channel}")
finally:
await pubsub.unsubscribe(channel)
await pubsub.close()
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Отключаем буферизацию nginx
}
)