File size: 5,497 Bytes
d28f1ed
 
 
 
247c14c
 
 
48d6c63
 
d28f1ed
48d6c63
d28f1ed
 
 
 
 
 
 
595f77d
48d6c63
d28f1ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f36e50
 
 
 
 
 
 
 
 
d28f1ed
8f36e50
d28f1ed
 
8f36e50
 
d28f1ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48d6c63
 
d28f1ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595f77d
48d6c63
 
 
 
 
 
 
a7400dd
48d6c63
d28f1ed
 
a7400dd
 
ce67322
a7400dd
 
 
ce67322
 
 
 
 
 
d28f1ed
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""
CAPL Routeur IA API
Main FastAPI application with AI agent routing.
"""
from dotenv import load_dotenv
load_dotenv()  # charge .env pour tout le process

from pathlib import Path

from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from contextlib import asynccontextmanager
import logging

from config import settings
from api.routes import auth, completion, transcription, models, realtime
from api.routes import documents
from api.routes import voice

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan event handler for startup and shutdown."""
    # Startup
    logger.info(f"Starting {settings.api_title} v{settings.api_version}")
    logger.info(f"Environment: {settings.environment}")

    from services.voice.ice_servers import get_ice_servers
    from pipecat.transports.smallwebrtc.request_handler import SmallWebRTCRequestHandler

    raw_ice, rtc_ice = get_ice_servers()
    app.state.voice_ice_servers = raw_ice
    app.state.voice_handler = SmallWebRTCRequestHandler(ice_servers=rtc_ice)
    logger.info("Voice WebRTC handler initialized with %d ICE server(s)", len(rtc_ice))

    yield

    # Shutdown
    logger.info("Shutting down API")
    if hasattr(app.state, "voice_handler") and app.state.voice_handler is not None:
        await app.state.voice_handler.close()


# Create FastAPI app
app = FastAPI(
    title=settings.api_title,
    version=settings.api_version,
    description="""
    # CAPL Routeur IA API
    
    API sécurisée pour l'interaction avec des agents IA basés sur LangGraph.
    
    ## Fonctionnalités principales:
    
    - **Authentification JWT** pour sécuriser l'accès
    - **Completion texte** avec support du streaming (SSE)
    - **Multi-modèles**: OpenAI (GPT-4, GPT-3.5) et Mistral AI
    - **Multi-agents**: Architecture extensible pour différents types d'agents
    - **Transcription audio**: Conversion audio vers texte avec Whisper
    - **Conversation vocale**: WebRTC via Pipecat (STT + LangGraph + TTS)
    - **Temps réel**: Support WebSocket
    
    ## Authentification
    
    1. Obtenez un token JWT via `POST /auth/token`
    2. Incluez le token dans le header: `Authorization: Bearer <token>`
    3. Utilisez le token pour toutes les requêtes protégées
    
    ## Architecture
    
    - **Clean Architecture** avec séparation domain/services/api
    - **SOLID principles** pour une extensibilité maximale
    - **LangGraph** pour l'orchestration des agents IA
    """,
    lifespan=lifespan,
    docs_url="/docs",
    redoc_url="/redoc"
)

# CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # À restreindre en production
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# Exception handlers
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    """Handle validation errors with detailed messages."""
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            "error": "Validation Error",
            "detail": exc.errors(),
        }
    )


@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
    """Handle unexpected exceptions."""
    logger.error(f"Unexpected error: {str(exc)}", exc_info=True)
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={
            "error": "Internal Server Error",
            "detail": str(exc) if settings.environment == "development" else "An unexpected error occurred"
        }
    )


# Root endpoint
@app.get("/", tags=["Root"])
async def root():
    """Root endpoint with API information."""
    return {
        "name": settings.api_title,
        "version": settings.api_version,
        "status": "running",
        "environment": settings.environment,
        "docs": "/docs",
        "health": "/health"
    }


# Include routers
app.include_router(auth.router)
app.include_router(models.router)
app.include_router(completion.router)
app.include_router(transcription.router)
app.include_router(realtime.router)
app.include_router(documents.router)
app.include_router(voice.router)

STATIC_DIR = Path(__file__).resolve().parent / "static"


@app.get("/voice-test", tags=["Voice"])
async def serve_voice_page():
    """Serve the standalone WebRTC voice test page (SmallWebRTC)."""
    return FileResponse(str(STATIC_DIR / "voice.html"))


@app.get("/voice-daily", tags=["Voice"])
async def serve_voice_daily_page():
    """Serve the Daily.co voice test page with Prebuilt UI (works on HF Spaces)."""
    return FileResponse(str(STATIC_DIR / "voice_daily.html"))


@app.get("/voice-daily-minimal", tags=["Voice"])
async def serve_voice_daily_minimal_page():
    """Serve the Daily.co minimal page (no iframe, only bot audio in own UI)."""
    return FileResponse(str(STATIC_DIR / "voice_daily_minimal.html"))


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app:app",
        host="0.0.0.0",
        port=7860,
        reload=True if settings.environment == "development" else False
    )