malek-messaoudii commited on
Commit
83c5f9d
·
1 Parent(s): 8ddb255

update mcp part

Browse files
Files changed (4) hide show
  1. main.py +22 -9
  2. mcp/n8n_routes.py +422 -0
  3. routes/mcp_routes.py +13 -98
  4. services/mcp_service.py +139 -20
main.py CHANGED
@@ -56,16 +56,18 @@ def cleanup_on_exit():
56
  logger.warning("Échec du nettoyage final")
57
 
58
  # --- Import des singletons de services ---
 
 
59
  try:
60
  from services.stance_model_manager import stance_model_manager
61
- from services.label_model_manager import kpa_model_manager # Corrigé: kpa_model_manager, pas label_model_manager
62
  logger.info("✓ Gestionnaires de modèles importés")
63
  except ImportError as e:
64
  logger.warning(f"⚠ Impossible d'importer les gestionnaires de modèles: {e}")
65
- stance_model_manager = None
66
- kpa_model_manager = None
67
 
68
  # --- Vérification MCP ---
 
 
69
  try:
70
  from services.mcp_service import init_mcp_server
71
  from routes.mcp_routes import router as mcp_router
@@ -73,7 +75,6 @@ try:
73
  logger.info("✓ Modules MCP détectés")
74
  except ImportError as e:
75
  logger.warning(f"⚠ MCP non disponible: {e}")
76
- MCP_ENABLED = False
77
 
78
  # --- Lifespan / startup API ---
79
  @asynccontextmanager
@@ -185,20 +186,32 @@ except ImportError as e:
185
  except Exception as e:
186
  logger.warning(f"⚠ Échec chargement route Voice Chat: {e}")
187
 
188
- # Main API Routes (KPA, Stance, etc.)
 
189
  try:
190
- from routes import api_router
191
- app.include_router(api_router)
192
  logger.info("✓ Routes API principales chargées")
193
  except ImportError as e:
194
  logger.warning(f"⚠ Routes API principales non trouvées: {e}")
 
 
 
 
 
 
 
 
 
195
  except Exception as e:
196
  logger.warning(f"⚠ Échec chargement routes API principales: {e}")
197
 
198
  # MCP Routes
199
- if MCP_ENABLED:
200
- app.include_router(mcp_router, prefix="/api/v1", tags=["MCP"])
201
  logger.info("✓ Routes MCP chargées")
 
 
202
 
203
  # --- Basic routes ---
204
  @app.get("/health", tags=["Health"])
 
56
  logger.warning("Échec du nettoyage final")
57
 
58
  # --- Import des singletons de services ---
59
+ stance_model_manager = None
60
+ kpa_model_manager = None
61
  try:
62
  from services.stance_model_manager import stance_model_manager
63
+ from services.label_model_manager import kpa_model_manager # Corrigé : import depuis kpa_model_manager.py
64
  logger.info("✓ Gestionnaires de modèles importés")
65
  except ImportError as e:
66
  logger.warning(f"⚠ Impossible d'importer les gestionnaires de modèles: {e}")
 
 
67
 
68
  # --- Vérification MCP ---
69
+ MCP_ENABLED = False
70
+ mcp_router = None
71
  try:
72
  from services.mcp_service import init_mcp_server
73
  from routes.mcp_routes import router as mcp_router
 
75
  logger.info("✓ Modules MCP détectés")
76
  except ImportError as e:
77
  logger.warning(f"⚠ MCP non disponible: {e}")
 
78
 
79
  # --- Lifespan / startup API ---
80
  @asynccontextmanager
 
186
  except Exception as e:
187
  logger.warning(f"⚠ Échec chargement route Voice Chat: {e}")
188
 
189
+ # Main API Routes (KPA, Stance, etc.) - Assumant un api_router qui inclut kpa et stance
190
+ api_router = None
191
  try:
192
+ from routes import api_router # Ou from routes.api_router import router as api_router si c'est un fichier dédié
193
+ app.include_router(api_router, prefix="/api/v1")
194
  logger.info("✓ Routes API principales chargées")
195
  except ImportError as e:
196
  logger.warning(f"⚠ Routes API principales non trouvées: {e}")
197
+ # Fallback : Inclure directement les sub-routers si api_router n'existe pas
198
+ try:
199
+ from routes.label import router as kpa_router
200
+ app.include_router(kpa_router, prefix="/api/v1/kpa", tags=["KPA"])
201
+ from routes.stance import router as stance_router # Assumant le nom du fichier
202
+ app.include_router(stance_router, prefix="/api/v1/stance", tags=["Stance Detection"])
203
+ logger.info("✓ Routes KPA et Stance chargées en fallback")
204
+ except ImportError:
205
+ logger.warning("⚠ Fallback pour KPA/Stance échoué - vérifiez vos fichiers routes/")
206
  except Exception as e:
207
  logger.warning(f"⚠ Échec chargement routes API principales: {e}")
208
 
209
  # MCP Routes
210
+ if MCP_ENABLED and mcp_router:
211
+ app.include_router(mcp_router, prefix="/api/v1/mcp", tags=["MCP"])
212
  logger.info("✓ Routes MCP chargées")
213
+ else:
214
+ logger.warning("⚠ Routes MCP non chargées (MCP désactivé ou router manquant)")
215
 
216
  # --- Basic routes ---
217
  @app.get("/health", tags=["Health"])
mcp/n8n_routes.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Routes FastAPI pour intégration n8n avec MCP
3
+ À ajouter dans votre app.py principal
4
+ """
5
+ from fastapi import APIRouter, HTTPException, BackgroundTasks, UploadFile, File
6
+ from pydantic import BaseModel
7
+ from typing import Dict, Any, Optional, List
8
+ import logging
9
+ from datetime import datetime
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Router pour n8n
14
+ n8n_router = APIRouter(prefix="/n8n", tags=["n8n"])
15
+
16
+ # ==================== MODELS ====================
17
+
18
+ class N8NToolRequest(BaseModel):
19
+ """Request model pour appels n8n"""
20
+ tool_name: str
21
+ arguments: Dict[str, Any]
22
+ context: Optional[Dict[str, Any]] = None
23
+ async_callback: Optional[str] = None # URL pour callback asynchrone
24
+
25
+ class Config:
26
+ json_schema_extra = {
27
+ "example": {
28
+ "tool_name": "predict_stance",
29
+ "arguments": {
30
+ "topic": "climate change",
31
+ "argument": "We need renewable energy"
32
+ },
33
+ "context": {
34
+ "session_id": "session_123",
35
+ "user_id": "user_456"
36
+ }
37
+ }
38
+ }
39
+
40
+ class N8NBatchRequest(BaseModel):
41
+ """Request pour traitement batch"""
42
+ tool_name: str
43
+ items: List[Dict[str, Any]]
44
+ batch_size: int = 10
45
+ parallel: bool = False
46
+
47
+ class Config:
48
+ json_schema_extra = {
49
+ "example": {
50
+ "tool_name": "predict_stance",
51
+ "items": [
52
+ {"topic": "AI", "argument": "AI will help humanity"},
53
+ {"topic": "AI", "argument": "AI is dangerous"}
54
+ ],
55
+ "batch_size": 10
56
+ }
57
+ }
58
+
59
+ class N8NPipelineRequest(BaseModel):
60
+ """Request pour pipeline complexe"""
61
+ pipeline_name: str
62
+ input_data: Dict[str, Any]
63
+ steps: List[Dict[str, Any]]
64
+
65
+ class Config:
66
+ json_schema_extra = {
67
+ "example": {
68
+ "pipeline_name": "debate_analysis",
69
+ "input_data": {
70
+ "topic": "climate change",
71
+ "text": "We must act now"
72
+ },
73
+ "steps": [
74
+ {"tool": "predict_stance", "output_key": "stance"},
75
+ {"tool": "predict_kpa", "use_previous": True}
76
+ ]
77
+ }
78
+ }
79
+
80
+ class N8NResponse(BaseModel):
81
+ """Response standardisée pour n8n"""
82
+ success: bool
83
+ data: Optional[Dict[str, Any]] = None
84
+ error: Optional[str] = None
85
+ execution_time: float
86
+ timestamp: datetime = datetime.now()
87
+
88
+ # ==================== ENDPOINTS ====================
89
+
90
+ @n8n_router.post("/execute", response_model=N8NResponse)
91
+ async def execute_tool(request: N8NToolRequest):
92
+ """
93
+ Endpoint principal pour exécuter un outil MCP depuis n8n
94
+ """
95
+ import time
96
+ start_time = time.time()
97
+
98
+ try:
99
+ from mcp.server import MCPServer
100
+ from mcp import server # Importer votre instance MCP
101
+
102
+ # Exécuter l'outil
103
+ result = await server.call_tool(
104
+ tool_name=request.tool_name,
105
+ arguments=request.arguments
106
+ )
107
+
108
+ # Ajouter le contexte si fourni
109
+ if request.context:
110
+ result["context"] = request.context
111
+
112
+ execution_time = time.time() - start_time
113
+
114
+ return N8NResponse(
115
+ success=True,
116
+ data=result,
117
+ execution_time=execution_time
118
+ )
119
+
120
+ except Exception as e:
121
+ logger.error(f"Tool execution failed: {str(e)}")
122
+ execution_time = time.time() - start_time
123
+
124
+ return N8NResponse(
125
+ success=False,
126
+ error=str(e),
127
+ execution_time=execution_time
128
+ )
129
+
130
+ @n8n_router.post("/batch", response_model=N8NResponse)
131
+ async def batch_execute(request: N8NBatchRequest):
132
+ """
133
+ Endpoint pour traitement batch depuis n8n
134
+ """
135
+ import time
136
+ import asyncio
137
+ start_time = time.time()
138
+
139
+ try:
140
+ from mcp import server
141
+
142
+ results = []
143
+
144
+ # Traitement séquentiel ou parallèle
145
+ if request.parallel:
146
+ # Traitement parallèle
147
+ tasks = []
148
+ for item in request.items:
149
+ task = server.call_tool(
150
+ tool_name=request.tool_name,
151
+ arguments=item
152
+ )
153
+ tasks.append(task)
154
+
155
+ results = await asyncio.gather(*tasks, return_exceptions=True)
156
+ else:
157
+ # Traitement séquentiel par batch
158
+ for i in range(0, len(request.items), request.batch_size):
159
+ batch = request.items[i:i + request.batch_size]
160
+
161
+ for item in batch:
162
+ try:
163
+ result = await server.call_tool(
164
+ tool_name=request.tool_name,
165
+ arguments=item
166
+ )
167
+ results.append(result)
168
+ except Exception as e:
169
+ results.append({"error": str(e), "item": item})
170
+
171
+ execution_time = time.time() - start_time
172
+
173
+ return N8NResponse(
174
+ success=True,
175
+ data={
176
+ "results": results,
177
+ "total": len(results),
178
+ "successful": sum(1 for r in results if not isinstance(r, Exception) and "error" not in r),
179
+ "failed": sum(1 for r in results if isinstance(r, Exception) or "error" in r)
180
+ },
181
+ execution_time=execution_time
182
+ )
183
+
184
+ except Exception as e:
185
+ logger.error(f"Batch execution failed: {str(e)}")
186
+ execution_time = time.time() - start_time
187
+
188
+ return N8NResponse(
189
+ success=False,
190
+ error=str(e),
191
+ execution_time=execution_time
192
+ )
193
+
194
+ @n8n_router.post("/pipeline", response_model=N8NResponse)
195
+ async def execute_pipeline(request: N8NPipelineRequest):
196
+ """
197
+ Endpoint pour exécuter un pipeline multi-étapes
198
+ """
199
+ import time
200
+ start_time = time.time()
201
+
202
+ try:
203
+ from mcp import server
204
+
205
+ pipeline_context = {"input": request.input_data}
206
+ results = {}
207
+
208
+ for step in request.steps:
209
+ tool_name = step["tool"]
210
+ output_key = step.get("output_key", tool_name)
211
+ use_previous = step.get("use_previous", False)
212
+
213
+ # Préparer les arguments
214
+ if use_previous:
215
+ # Utiliser le résultat de l'étape précédente
216
+ arguments = {**request.input_data, **results}
217
+ else:
218
+ arguments = step.get("arguments", request.input_data)
219
+
220
+ # Exécuter l'étape
221
+ result = await server.call_tool(
222
+ tool_name=tool_name,
223
+ arguments=arguments
224
+ )
225
+
226
+ results[output_key] = result
227
+ pipeline_context[output_key] = result
228
+
229
+ execution_time = time.time() - start_time
230
+
231
+ return N8NResponse(
232
+ success=True,
233
+ data={
234
+ "pipeline": request.pipeline_name,
235
+ "results": results,
236
+ "context": pipeline_context
237
+ },
238
+ execution_time=execution_time
239
+ )
240
+
241
+ except Exception as e:
242
+ logger.error(f"Pipeline execution failed: {str(e)}")
243
+ execution_time = time.time() - start_time
244
+
245
+ return N8NResponse(
246
+ success=False,
247
+ error=str(e),
248
+ execution_time=execution_time
249
+ )
250
+
251
+ @n8n_router.post("/voice-pipeline")
252
+ async def voice_debate_pipeline(
253
+ audio: UploadFile = File(...),
254
+ topic: str = None,
255
+ session_id: str = None
256
+ ):
257
+ """
258
+ Pipeline complet : Audio → STT → Stance → KPA → Argument Generation → TTS
259
+ Optimisé pour n8n
260
+ """
261
+ import time
262
+ import tempfile
263
+ import os
264
+ start_time = time.time()
265
+
266
+ try:
267
+ from mcp import server
268
+ from services.stt_service import transcribe_audio
269
+ from services.tts_service import text_to_speech
270
+
271
+ # 1. Sauvegarder l'audio temporairement
272
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
273
+ content = await audio.read()
274
+ tmp.write(content)
275
+ tmp_path = tmp.name
276
+
277
+ try:
278
+ # 2. Speech-to-Text
279
+ transcription = await transcribe_audio(tmp_path)
280
+ user_text = transcription.get("text", "")
281
+
282
+ # 3. Stance Detection
283
+ stance_result = await server.call_tool(
284
+ "predict_stance",
285
+ {"topic": topic, "argument": user_text}
286
+ )
287
+
288
+ # 4. KPA Matching (optionnel)
289
+ # kpa_result = await mcp_server.call_tool(...)
290
+
291
+ # 5. Generate Counter-Argument
292
+ opposite_stance = "CON" if stance_result["predicted_stance"] == "PRO" else "PRO"
293
+ counter_arg_result = await server.call_tool(
294
+ "generate_argument",
295
+ {
296
+ "prompt": f"Generate a {opposite_stance} argument about {topic}",
297
+ "context": f"User said: {user_text}",
298
+ "stance": opposite_stance
299
+ }
300
+ )
301
+
302
+ # 6. Text-to-Speech du contre-argument
303
+ tts_audio_path = await text_to_speech(
304
+ counter_arg_result["generated_argument"]
305
+ )
306
+
307
+ execution_time = time.time() - start_time
308
+
309
+ return N8NResponse(
310
+ success=True,
311
+ data={
312
+ "transcription": user_text,
313
+ "stance_analysis": stance_result,
314
+ "counter_argument": counter_arg_result,
315
+ "audio_response_path": tts_audio_path,
316
+ "session_id": session_id
317
+ },
318
+ execution_time=execution_time
319
+ )
320
+
321
+ finally:
322
+ # Nettoyer le fichier temporaire
323
+ if os.path.exists(tmp_path):
324
+ os.remove(tmp_path)
325
+
326
+ except Exception as e:
327
+ logger.error(f"Voice pipeline failed: {str(e)}")
328
+ return N8NResponse(
329
+ success=False,
330
+ error=str(e),
331
+ execution_time=time.time() - start_time
332
+ )
333
+
334
+ @n8n_router.get("/tools")
335
+ async def list_tools():
336
+ """
337
+ Liste tous les outils disponibles (format n8n-friendly)
338
+ """
339
+ try:
340
+ from mcp import server
341
+ tools = await server.list_tools()
342
+
343
+ return {
344
+ "success": True,
345
+ "tools": tools,
346
+ "total": len(tools)
347
+ }
348
+ except Exception as e:
349
+ raise HTTPException(status_code=500, detail=str(e))
350
+
351
+ @n8n_router.get("/resources")
352
+ async def list_resources():
353
+ """
354
+ Liste toutes les ressources disponibles (format n8n-friendly)
355
+ """
356
+ try:
357
+ from mcp import server
358
+ resources = await server.list_resources()
359
+
360
+ return {
361
+ "success": True,
362
+ "resources": resources,
363
+ "total": len(resources)
364
+ }
365
+ except Exception as e:
366
+ raise HTTPException(status_code=500, detail=str(e))
367
+
368
+ @n8n_router.get("/health")
369
+ async def health_check():
370
+ """
371
+ Health check pour n8n monitoring
372
+ """
373
+ from services.stance_model_manager import stance_model_manager
374
+ from services.label_model_manager import kpa_model_manager
375
+
376
+ return {
377
+ "status": "healthy",
378
+ "timestamp": datetime.now().isoformat(),
379
+ "models": {
380
+ "stance": stance_model_manager.model_loaded if stance_model_manager else False,
381
+ "kpa": kpa_model_manager.model_loaded if kpa_model_manager else False
382
+ },
383
+ "services": {
384
+ "stt": True, # Vérifier si GROQ_API_KEY existe
385
+ "tts": True,
386
+ "chat": True
387
+ }
388
+ }
389
+
390
+ # ==================== WEBHOOKS ====================
391
+
392
+ @n8n_router.post("/webhook/debate-result")
393
+ async def webhook_debate_result(data: Dict[str, Any], background_tasks: BackgroundTasks):
394
+ """
395
+ Webhook pour recevoir les résultats de débat depuis n8n
396
+ Peut être utilisé pour stocker, notifier, etc.
397
+ """
398
+ logger.info(f"Received debate result webhook: {data}")
399
+
400
+ # Traiter en arrière-plan
401
+ background_tasks.add_task(process_debate_result, data)
402
+
403
+ return {"status": "received", "message": "Processing in background"}
404
+
405
+ async def process_debate_result(data: Dict[str, Any]):
406
+ """
407
+ Traiter les résultats de débat en arrière-plan
408
+ """
409
+ # TODO: Implémenter votre logique
410
+ # - Sauvegarder dans DB
411
+ # - Envoyer des notifications
412
+ # - Mettre à jour des métriques
413
+ logger.info(f"Processing debate result: {data}")
414
+
415
+ # ==================== EXPORT ====================
416
+
417
+ def register_n8n_routes(app):
418
+ """
419
+ Enregistrer les routes n8n dans l'application FastAPI
420
+ """
421
+ app.include_router(n8n_router)
422
+ logger.info("n8n routes registered successfully")
routes/mcp_routes.py CHANGED
@@ -1,104 +1,19 @@
1
- from fastapi import APIRouter, HTTPException, Depends
2
- from typing import List, Dict, Any
3
- import logging
4
- from models.mcp_models import (
5
- ToolCallRequest,
6
- ToolCallResponse,
7
- ResourceListResponse,
8
- ToolListResponse
9
- )
10
- from services.mcp_service import get_mcp_server
11
 
12
  router = APIRouter(prefix="/mcp", tags=["MCP"])
13
- logger = logging.getLogger(__name__)
14
 
 
 
 
 
 
15
  @router.get("/health")
16
  async def mcp_health():
17
- """Health check for MCP server"""
18
- return {
19
- "status": "healthy",
20
- "service": "Model Context Protocol",
21
- "version": "1.0.0"
22
- }
23
-
24
- @router.get("/resources", response_model=ResourceListResponse)
25
- async def list_resources():
26
- """List all MCP resources"""
27
- try:
28
- server = get_mcp_server()
29
- resources = await server.list_resources()
30
- return ResourceListResponse(
31
- resources=resources,
32
- count=len(resources)
33
- )
34
- except Exception as e:
35
- logger.error(f"Error listing resources: {str(e)}")
36
- raise HTTPException(status_code=500, detail=str(e))
37
-
38
- @router.get("/tools", response_model=ToolListResponse)
39
- async def list_tools():
40
- """List all MCP tools"""
41
- try:
42
- server = get_mcp_server()
43
- tools = await server.list_tools()
44
- return ToolListResponse(
45
- tools=tools,
46
- count=len(tools)
47
- )
48
- except Exception as e:
49
- logger.error(f"Error listing tools: {str(e)}")
50
- raise HTTPException(status_code=500, detail=str(e))
51
-
52
- @router.post("/tools/call", response_model=ToolCallResponse)
53
- async def call_tool(request: ToolCallRequest):
54
- """Call an MCP tool"""
55
- try:
56
- server = get_mcp_server()
57
- result = await server.call_tool(
58
- tool_name=request.tool_name,
59
- arguments=request.arguments
60
- )
61
-
62
- return ToolCallResponse(
63
- success=True,
64
- result=result,
65
- tool_name=request.tool_name
66
- )
67
- except HTTPException:
68
- raise
69
- except Exception as e:
70
- logger.error(f"Error calling tool {request.tool_name}: {str(e)}")
71
- return ToolCallResponse(
72
- success=False,
73
- error=str(e),
74
- tool_name=request.tool_name
75
- )
76
 
77
- @router.post("/tools/batch")
78
- async def batch_call_tools(requests: List[ToolCallRequest]):
79
- """Call multiple MCP tools"""
80
- results = []
81
- for request in requests:
82
- try:
83
- server = get_mcp_server()
84
- result = await server.call_tool(
85
- tool_name=request.tool_name,
86
- arguments=request.arguments
87
- )
88
- results.append({
89
- "tool_name": request.tool_name,
90
- "success": True,
91
- "result": result
92
- })
93
- except Exception as e:
94
- results.append({
95
- "tool_name": request.tool_name,
96
- "success": False,
97
- "error": str(e)
98
- })
99
-
100
- return {
101
- "results": results,
102
- "total": len(results),
103
- "successful": sum(1 for r in results if r["success"])
104
- }
 
1
+ """Routes pour exposer MCP via FastAPI"""
2
+
3
+ from fastapi import APIRouter
4
+ from starlette.routing import Mount
5
+ from services.mcp_service import mcp_server # Importe l'instance
 
 
 
 
 
6
 
7
  router = APIRouter(prefix="/mcp", tags=["MCP"])
 
8
 
9
+ # Monter l'app FastMCP entière comme sub-app (gère /tools, /call, etc. automatiquement)
10
+ from starlette.applications import Starlette
11
+ mcp_starlette = Starlette(routes=[Mount("/", app=mcp_server.streamable_http_app())])
12
+
13
+ # Inclure le Starlette comme route FastAPI (via sub-app mounting)
14
  @router.get("/health")
15
  async def mcp_health():
16
+ return {"status": "MCP ready", "tools": [t.name for t in mcp_server.tools]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ # Le mounting principal se fait dans main.py via app.include_router
19
+ # Mais pour compat, on peut exposer directement
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
services/mcp_service.py CHANGED
@@ -1,24 +1,143 @@
1
- from mcp.server import MCPServer
2
- from fastapi import FastAPI
 
 
3
  import logging
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  logger = logging.getLogger(__name__)
6
 
7
- _mcp_server = None
8
-
9
- def init_mcp_server(app: FastAPI):
10
- """Initialize MCP server"""
11
- global _mcp_server
12
- try:
13
- _mcp_server = MCPServer(app)
14
- logger.info("✓ MCP Server initialized successfully")
15
- return _mcp_server
16
- except Exception as e:
17
- logger.error(f" Failed to initialize MCP server: {str(e)}")
18
- raise
19
-
20
- def get_mcp_server() -> MCPServer:
21
- """Get MCP server instance"""
22
- if _mcp_server is None:
23
- raise RuntimeError("MCP server not initialized")
24
- return _mcp_server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Service pour initialiser le serveur MCP avec FastMCP"""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+ from typing import Dict, Any
5
  import logging
6
 
7
+ from fastapi import FastAPI # Ajouté pour résoudre l'erreur Pylance "FastAPI is not defined"
8
+
9
+ from services.stance_model_manager import stance_model_manager
10
+ from services.label_model_manager import kpa_model_manager # Corrigé : import depuis kpa_model_manager (cohérent avec main.py)
11
+ from services.stt_service import speech_to_text
12
+ from services.tts_service import text_to_speech
13
+ from services.chat_service import generate_chat_response
14
+ # Note : Adapte les imports models si tes schémas sont dans un fichier unique (ex. models/schemas.py)
15
+ # from models.stance import StanceRequest, StanceResponse # Si séparé
16
+ # from models.kpa import PredictionRequest, PredictionResponse # Si séparé
17
+ # Ou si un seul fichier models.py :
18
+ # from models import StanceRequest, StanceResponse, PredictionRequest, PredictionResponse
19
+
20
  logger = logging.getLogger(__name__)
21
 
22
+ # Créer l'instance FastMCP (nom du serveur, JSON pour responses structurées)
23
+ mcp_server = FastMCP("NLP-Debater-MCP", json_response=True, stateless_http=False) # Stateful pour sessions voice chat
24
+
25
+ # Tool pour Stance Detection
26
+ @mcp_server.tool()
27
+ def detect_stance(topic: str, argument: str) -> Dict[str, Any]:
28
+ """
29
+ Détecte la stance (PRO/CON) d'un argument par rapport à un topic.
30
+
31
+ Args:
32
+ topic: Le sujet de débat (ex. "Assisted suicide should be a criminal offence")
33
+ argument: L'argument à classifier (ex. "People have the right to choose...")
34
+
35
+ Returns:
36
+ Dict avec predicted_stance, confidence, probabilities.
37
+ """
38
+ if not stance_model_manager.model_loaded:
39
+ raise ValueError("Modèle stance non chargé")
40
+
41
+ result = stance_model_manager.predict(topic, argument)
42
+ return {
43
+ "predicted_stance": result["predicted_stance"],
44
+ "confidence": result["confidence"],
45
+ "probability_con": result["probability_con"],
46
+ "probability_pro": result["probability_pro"]
47
+ }
48
+
49
+ # Tool pour Key-Point Argument Matching (KPA)
50
+ @mcp_server.tool()
51
+ def match_keypoint_argument(argument: str, key_point: str) -> Dict[str, Any]:
52
+ """
53
+ Prédit si un argument matche un key-point (apparie/non_apparie).
54
+
55
+ Args:
56
+ argument: Texte de l'argument
57
+ key_point: Le key-point de référence
58
+
59
+ Returns:
60
+ Dict avec prediction (0/1), label, confidence, probabilities.
61
+ """
62
+ if not kpa_model_manager.model_loaded:
63
+ raise ValueError("Modèle KPA non chargé")
64
+
65
+ result = kpa_model_manager.predict(argument, key_point)
66
+ return {
67
+ "prediction": result["prediction"],
68
+ "label": result["label"],
69
+ "confidence": result["confidence"],
70
+ "probabilities": result["probabilities"]
71
+ }
72
+
73
+ # Tool pour STT (Speech-to-Text) - Note : Pour audio, utilise un upload via resource ou adapte
74
+ @mcp_server.tool()
75
+ def transcribe_audio(audio_path: str) -> str:
76
+ """
77
+ Transcrit un fichier audio en texte (via Groq Whisper).
78
+
79
+ Args:
80
+ audio_path: Chemin vers le fichier audio (ex. temp file)
81
+
82
+ Returns:
83
+ Texte transcrit.
84
+ """
85
+ return speech_to_text(audio_path)
86
+
87
+ # Tool pour TTS (Text-to-Speech)
88
+ @mcp_server.tool()
89
+ def generate_speech(text: str, voice: str = "Aaliyah-PlayAI", format: str = "wav") -> str:
90
+ """
91
+ Génère un fichier audio à partir de texte (via Groq TTS).
92
+
93
+ Args:
94
+ text: Texte à synthétiser
95
+ voice: Voix (défaut: Aaliyah-PlayAI)
96
+ format: wav ou mp3
97
+
98
+ Returns:
99
+ Chemin vers le fichier audio généré.
100
+ """
101
+ return text_to_speech(text, voice, format)
102
+
103
+ # Tool pour Argument Generation (Chatbot)
104
+ @mcp_server.tool()
105
+ def generate_argument(user_input: str, conversation_id: str = None) -> str:
106
+ """
107
+ Génère une réponse argumentative via chatbot (via Groq Llama).
108
+
109
+ Args:
110
+ user_input: Input utilisateur
111
+ conversation_id: ID de session (optionnel)
112
+
113
+ Returns:
114
+ Réponse générée.
115
+ """
116
+ return generate_chat_response(user_input, conversation_id)
117
+
118
+ # Resource exemple : Prompt template pour débats
119
+ @mcp_server.resource("debate://prompt")
120
+ def get_debate_prompt(topic: str) -> str:
121
+ """Récupère un template de prompt pour générer des arguments sur un topic."""
122
+ return f"Tu es un expert en débat. Génère 3 arguments PRO pour le topic: {topic}. Sois concis et persuasif."
123
+
124
+ def init_mcp_server(app: FastAPI) -> None: # Retiré les quotes : FastAPI est maintenant importé
125
+ """
126
+ Initialise et monte le serveur MCP sur l'app FastAPI.
127
+ Ajoute les routes MCP à /api/v1/mcp (ex. : /tools, /call, /resources).
128
+ """
129
+ # Monter l'app MCP sur FastAPI (via Starlette Mount, compatible FastAPI)
130
+ from starlette.routing import Mount
131
+
132
+ # Créer l'app MCP streamable
133
+ mcp_app = mcp_server.streamable_http_app(streamable_http_path="/mcp") # Chemin racine /mcp
134
+
135
+ # Monter directement sur l'app FastAPI (app.mount pour sub-app)
136
+ app.mount("/api/v1/mcp", mcp_app) # Monte à /api/v1/mcp - gère /tools, /call, etc.
137
+
138
+ logger.info("✓ Serveur MCP initialisé et monté sur /api/v1/mcp avec tools NLP/STT/TTS")
139
+
140
+ # Pour batch ou health, ajoute si besoin (ce tool est déjà décoré)
141
+ @mcp_server.tool()
142
+ def health_check() -> Dict[str, Any]:
143
+ return {"status": "healthy", "tools": list(mcp_server.tools.keys())}