malek-messaoudii commited on
Commit
f171734
·
1 Parent(s): 812086d

Update expose

Browse files
Files changed (3) hide show
  1. main.py +10 -4
  2. models/__init__.py +17 -0
  3. routes/mcp_routes.py +226 -7
main.py CHANGED
@@ -204,11 +204,17 @@ except ImportError as e:
204
  except Exception as e:
205
  logger.warning(f"⚠ Échec chargement routes API principales: {e}")
206
 
207
- # MCP Routes - CORRIGÉ : Supprimé include_router (conflit double prefix) - mount gère tout
208
- # if MCP_ENABLED and mcp_router: # Commenté pour éviter /mcp/mcp/...
209
- # app.include_router(mcp_router, prefix="/api/v1", tags=["MCP"])
210
- # logger.info("✓ Routes MCP chargées")
211
  if MCP_ENABLED:
 
 
 
 
 
 
 
 
 
212
  logger.info("✓ MCP monté via lifespan (endpoints auto-gérés)")
213
  else:
214
  logger.warning("⚠ MCP désactivé")
 
204
  except Exception as e:
205
  logger.warning(f"⚠ Échec chargement routes API principales: {e}")
206
 
207
+ # MCP Routes - Routes FastAPI pour Swagger UI + mount pour compatibilité MCP
 
 
 
208
  if MCP_ENABLED:
209
+ try:
210
+ from routes.mcp_routes import router as mcp_router
211
+ app.include_router(mcp_router) # Pas de prefix car déjà dans le router
212
+ logger.info("✓ Routes MCP FastAPI chargées (visibles dans Swagger)")
213
+ except ImportError as e:
214
+ logger.warning(f"⚠ Routes MCP FastAPI non trouvées: {e}")
215
+ except Exception as e:
216
+ logger.warning(f"⚠ Échec chargement routes MCP FastAPI: {e}")
217
+
218
  logger.info("✓ MCP monté via lifespan (endpoints auto-gérés)")
219
  else:
220
  logger.warning("⚠ MCP désactivé")
models/__init__.py CHANGED
@@ -22,6 +22,16 @@ from .health import (
22
  HealthResponse,
23
  )
24
 
 
 
 
 
 
 
 
 
 
 
25
  __all__ = [
26
  # Stance schemas
27
  "StanceRequest",
@@ -36,4 +46,11 @@ __all__ = [
36
  "KPAHealthResponse",
37
  # Health schemas
38
  "HealthResponse",
 
 
 
 
 
 
 
39
  ]
 
22
  HealthResponse,
23
  )
24
 
25
+ # Import MCP-related schemas
26
+ from .mcp_models import (
27
+ ToolCallRequest,
28
+ ToolCallResponse,
29
+ ToolInfo,
30
+ ToolListResponse,
31
+ ResourceInfo,
32
+ ResourceListResponse,
33
+ )
34
+
35
  __all__ = [
36
  # Stance schemas
37
  "StanceRequest",
 
46
  "KPAHealthResponse",
47
  # Health schemas
48
  "HealthResponse",
49
+ # MCP schemas
50
+ "ToolCallRequest",
51
+ "ToolCallResponse",
52
+ "ToolInfo",
53
+ "ToolListResponse",
54
+ "ResourceInfo",
55
+ "ResourceListResponse",
56
  ]
routes/mcp_routes.py CHANGED
@@ -1,13 +1,232 @@
1
- """Routes pour exposer MCP via FastAPI (fallback minimal)"""
 
 
 
 
 
2
 
3
- from fastapi import APIRouter
4
  from services.mcp_service import mcp_server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- router = APIRouter(prefix="/mcp", tags=["MCP"])
7
 
8
- @router.get("/health")
9
  async def mcp_health():
10
- """Health check pour MCP (fallback)"""
11
- return {"status": "MCP ready", "tools": [t.name for t in mcp_server.tools]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # Note : Pas de mount ici – tout est géré par init_mcp_server(app.mount)
 
 
 
 
 
 
 
 
 
 
 
1
+ """Routes pour exposer MCP via FastAPI pour Swagger UI"""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from typing import Dict, Any, Optional
5
+ from pydantic import BaseModel, Field
6
+ import logging
7
 
 
8
  from services.mcp_service import mcp_server
9
+ from models.mcp_models import ToolListResponse, ToolInfo, ToolCallRequest, ToolCallResponse
10
+
11
+ router = APIRouter(prefix="/api/v1/mcp", tags=["MCP"])
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ # ===== Models pour chaque outil MCP =====
16
+
17
+ class DetectStanceRequest(BaseModel):
18
+ """Request pour détecter la stance d'un argument"""
19
+ topic: str = Field(..., description="Le sujet du débat")
20
+ argument: str = Field(..., description="L'argument à analyser")
21
+
22
+ class Config:
23
+ json_schema_extra = {
24
+ "example": {
25
+ "topic": "Climate change is real",
26
+ "argument": "Rising global temperatures prove it"
27
+ }
28
+ }
29
+
30
+ class MatchKeypointRequest(BaseModel):
31
+ """Request pour matcher un argument avec un keypoint"""
32
+ argument: str = Field(..., description="L'argument à évaluer")
33
+ key_point: str = Field(..., description="Le keypoint de référence")
34
+
35
+ class Config:
36
+ json_schema_extra = {
37
+ "example": {
38
+ "argument": "Renewable energy reduces emissions",
39
+ "key_point": "Environmental benefits"
40
+ }
41
+ }
42
+
43
+ class TranscribeAudioRequest(BaseModel):
44
+ """Request pour transcrire un audio"""
45
+ audio_path: str = Field(..., description="Chemin vers le fichier audio")
46
+
47
+ class Config:
48
+ json_schema_extra = {
49
+ "example": {
50
+ "audio_path": "/path/to/audio.wav"
51
+ }
52
+ }
53
+
54
+ class GenerateSpeechRequest(BaseModel):
55
+ """Request pour générer de la parole"""
56
+ text: str = Field(..., description="Texte à convertir en parole")
57
+ voice: str = Field(default="Aaliyah-PlayAI", description="Voix à utiliser")
58
+ format: str = Field(default="wav", description="Format audio (wav, mp3, etc.)")
59
+
60
+ class Config:
61
+ json_schema_extra = {
62
+ "example": {
63
+ "text": "Hello, this is a test",
64
+ "voice": "Aaliyah-PlayAI",
65
+ "format": "wav"
66
+ }
67
+ }
68
+
69
+ class GenerateArgumentRequest(BaseModel):
70
+ """Request pour générer un argument"""
71
+ user_input: str = Field(..., description="Input utilisateur pour générer l'argument")
72
+ conversation_id: Optional[str] = Field(default=None, description="ID de conversation (optionnel)")
73
+
74
+ class Config:
75
+ json_schema_extra = {
76
+ "example": {
77
+ "user_input": "Generate an argument about climate change",
78
+ "conversation_id": "conv_123"
79
+ }
80
+ }
81
+
82
 
83
+ # ===== Routes MCP =====
84
 
85
+ @router.get("/health", summary="Health Check MCP")
86
  async def mcp_health():
87
+ """Health check pour le serveur MCP"""
88
+ try:
89
+ tools = mcp_server.list_tools()
90
+ tool_names = [tool.name for tool in tools] if tools else []
91
+ return {
92
+ "status": "healthy",
93
+ "tools": tool_names,
94
+ "tool_count": len(tool_names)
95
+ }
96
+ except Exception as e:
97
+ logger.error(f"MCP health check error: {e}")
98
+ raise HTTPException(status_code=500, detail=str(e))
99
+
100
+ @router.get("/tools", response_model=ToolListResponse, summary="Liste des outils MCP")
101
+ async def list_mcp_tools():
102
+ """Liste tous les outils MCP disponibles"""
103
+ try:
104
+ tools = mcp_server.list_tools()
105
+ tool_list = []
106
+ for tool in tools:
107
+ tool_info = ToolInfo(
108
+ name=tool.name,
109
+ description=tool.description or "",
110
+ input_schema=tool.inputSchema if hasattr(tool, 'inputSchema') else {}
111
+ )
112
+ tool_list.append(tool_info)
113
+
114
+ return ToolListResponse(tools=tool_list, count=len(tool_list))
115
+ except Exception as e:
116
+ logger.error(f"Error listing MCP tools: {e}")
117
+ raise HTTPException(status_code=500, detail=str(e))
118
+
119
+ @router.post("/tools/call", response_model=ToolCallResponse, summary="Appeler un outil MCP")
120
+ async def call_mcp_tool(request: ToolCallRequest):
121
+ """Appelle un outil MCP par son nom avec des arguments"""
122
+ try:
123
+ result = mcp_server.call_tool(request.tool_name, request.arguments)
124
+ # Convertir en dict si nécessaire
125
+ if not isinstance(result, dict):
126
+ result = {"result": result}
127
+ return ToolCallResponse(
128
+ success=True,
129
+ result=result,
130
+ tool_name=request.tool_name
131
+ )
132
+ except Exception as e:
133
+ logger.error(f"Error calling MCP tool {request.tool_name}: {e}")
134
+ return ToolCallResponse(
135
+ success=False,
136
+ error=str(e),
137
+ tool_name=request.tool_name
138
+ )
139
+
140
+
141
+ # ===== Routes individuelles pour chaque outil (pour Swagger) =====
142
+
143
+ @router.post("/tools/detect-stance", summary="Détecter la stance d'un argument")
144
+ async def mcp_detect_stance(request: DetectStanceRequest) -> Dict[str, Any]:
145
+ """Détecte si un argument est PRO ou CON pour un topic donné"""
146
+ try:
147
+ result = mcp_server.call_tool("detect_stance", {
148
+ "topic": request.topic,
149
+ "argument": request.argument
150
+ })
151
+ # S'assurer que le résultat est un dict
152
+ if isinstance(result, dict):
153
+ return result
154
+ return {"result": result}
155
+ except Exception as e:
156
+ logger.error(f"Error in detect_stance: {e}")
157
+ raise HTTPException(status_code=500, detail=str(e))
158
+
159
+ @router.post("/tools/match-keypoint", summary="Matcher un argument avec un keypoint")
160
+ async def mcp_match_keypoint(request: MatchKeypointRequest) -> Dict[str, Any]:
161
+ """Détermine si un argument correspond à un keypoint"""
162
+ try:
163
+ result = mcp_server.call_tool("match_keypoint_argument", {
164
+ "argument": request.argument,
165
+ "key_point": request.key_point
166
+ })
167
+ if isinstance(result, dict):
168
+ return result
169
+ return {"result": result}
170
+ except Exception as e:
171
+ logger.error(f"Error in match_keypoint_argument: {e}")
172
+ raise HTTPException(status_code=500, detail=str(e))
173
+
174
+ @router.post("/tools/transcribe-audio", summary="Transcrire un audio en texte")
175
+ async def mcp_transcribe_audio(request: TranscribeAudioRequest) -> Dict[str, str]:
176
+ """Convertit un fichier audio en texte"""
177
+ try:
178
+ result = mcp_server.call_tool("transcribe_audio", {
179
+ "audio_path": request.audio_path
180
+ })
181
+ # Le résultat est déjà une string
182
+ if isinstance(result, str):
183
+ return {"text": result}
184
+ return {"text": str(result)}
185
+ except Exception as e:
186
+ logger.error(f"Error in transcribe_audio: {e}")
187
+ raise HTTPException(status_code=500, detail=str(e))
188
+
189
+ @router.post("/tools/generate-speech", summary="Générer de la parole à partir de texte")
190
+ async def mcp_generate_speech(request: GenerateSpeechRequest) -> Dict[str, str]:
191
+ """Convertit du texte en fichier audio"""
192
+ try:
193
+ result = mcp_server.call_tool("generate_speech", {
194
+ "text": request.text,
195
+ "voice": request.voice,
196
+ "format": request.format
197
+ })
198
+ # Le résultat est déjà une string (chemin du fichier)
199
+ if isinstance(result, str):
200
+ return {"audio_path": result}
201
+ return {"audio_path": str(result)}
202
+ except Exception as e:
203
+ logger.error(f"Error in generate_speech: {e}")
204
+ raise HTTPException(status_code=500, detail=str(e))
205
+
206
+ @router.post("/tools/generate-argument", summary="Générer un argument de débat")
207
+ async def mcp_generate_argument(request: GenerateArgumentRequest) -> Dict[str, str]:
208
+ """Génère un argument de débat à partir d'un input utilisateur"""
209
+ try:
210
+ result = mcp_server.call_tool("generate_argument", {
211
+ "user_input": request.user_input,
212
+ "conversation_id": request.conversation_id
213
+ })
214
+ # Le résultat est déjà une string
215
+ if isinstance(result, str):
216
+ return {"argument": result}
217
+ return {"argument": str(result)}
218
+ except Exception as e:
219
+ logger.error(f"Error in generate_argument: {e}")
220
+ raise HTTPException(status_code=500, detail=str(e))
221
 
222
+ @router.get("/tools/health-check", summary="Health check MCP (outil)")
223
+ async def mcp_tool_health_check() -> Dict[str, Any]:
224
+ """Health check via l'outil MCP"""
225
+ try:
226
+ result = mcp_server.call_tool("health_check", {})
227
+ if isinstance(result, dict):
228
+ return result
229
+ return {"result": result}
230
+ except Exception as e:
231
+ logger.error(f"Error in health_check tool: {e}")
232
+ raise HTTPException(status_code=500, detail=str(e))