malek-messaoudii commited on
Commit
45e145b
·
1 Parent(s): 997d57e

Add response models for new MCP tools: DetectStance, MatchKeypoint, TranscribeAudio, GenerateSpeech, and GenerateArgument

Browse files
Files changed (3) hide show
  1. models/__init__.py +10 -0
  2. models/mcp_models.py +77 -1
  3. routes/mcp_routes.py +143 -62
models/__init__.py CHANGED
@@ -30,6 +30,11 @@ from .mcp_models import (
30
  ToolListResponse,
31
  ResourceInfo,
32
  ResourceListResponse,
 
 
 
 
 
33
  )
34
 
35
  __all__ = [
@@ -53,4 +58,9 @@ __all__ = [
53
  "ToolListResponse",
54
  "ResourceInfo",
55
  "ResourceListResponse",
 
 
 
 
 
56
  ]
 
30
  ToolListResponse,
31
  ResourceInfo,
32
  ResourceListResponse,
33
+ DetectStanceResponse,
34
+ MatchKeypointResponse,
35
+ TranscribeAudioResponse,
36
+ GenerateSpeechResponse,
37
+ GenerateArgumentResponse,
38
  )
39
 
40
  __all__ = [
 
58
  "ToolListResponse",
59
  "ResourceInfo",
60
  "ResourceListResponse",
61
+ "DetectStanceResponse",
62
+ "MatchKeypointResponse",
63
+ "TranscribeAudioResponse",
64
+ "GenerateSpeechResponse",
65
+ "GenerateArgumentResponse",
66
  ]
models/mcp_models.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel
2
  from typing import Any, Dict, List, Optional
3
 
4
  class ToolCallRequest(BaseModel):
@@ -13,6 +13,82 @@ class ToolCallResponse(BaseModel):
13
  error: Optional[str] = None
14
  tool_name: str
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  class ResourceInfo(BaseModel):
17
  """Information about an MCP resource"""
18
  uri: str
 
1
+ from pydantic import BaseModel, Field, ConfigDict
2
  from typing import Any, Dict, List, Optional
3
 
4
  class ToolCallRequest(BaseModel):
 
13
  error: Optional[str] = None
14
  tool_name: str
15
 
16
+ # Response models for individual MCP tools
17
+ class DetectStanceResponse(BaseModel):
18
+ """Response model for stance detection"""
19
+ model_config = ConfigDict(
20
+ json_schema_extra={
21
+ "example": {
22
+ "predicted_stance": "PRO",
23
+ "confidence": 0.9598,
24
+ "probability_con": 0.0402,
25
+ "probability_pro": 0.9598
26
+ }
27
+ }
28
+ )
29
+
30
+ predicted_stance: str = Field(..., description="PRO or CON")
31
+ confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score")
32
+ probability_con: float = Field(..., ge=0.0, le=1.0)
33
+ probability_pro: float = Field(..., ge=0.0, le=1.0)
34
+
35
+ class MatchKeypointResponse(BaseModel):
36
+ """Response model for keypoint matching"""
37
+ model_config = ConfigDict(
38
+ json_schema_extra={
39
+ "example": {
40
+ "prediction": 1,
41
+ "label": "apparie",
42
+ "confidence": 0.8157,
43
+ "probabilities": {
44
+ "non_apparie": 0.1843,
45
+ "apparie": 0.8157
46
+ }
47
+ }
48
+ }
49
+ )
50
+
51
+ prediction: int = Field(..., description="1 = apparie, 0 = non_apparie")
52
+ label: str = Field(..., description="apparie or non_apparie")
53
+ confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score")
54
+ probabilities: Dict[str, float] = Field(..., description="Dictionary of class probabilities")
55
+
56
+ class TranscribeAudioResponse(BaseModel):
57
+ """Response model for audio transcription"""
58
+ model_config = ConfigDict(
59
+ json_schema_extra={
60
+ "example": {
61
+ "text": "Hello, this is the transcribed text from the audio file."
62
+ }
63
+ }
64
+ )
65
+
66
+ text: str = Field(..., description="Transcribed text from audio")
67
+
68
+ class GenerateSpeechResponse(BaseModel):
69
+ """Response model for speech generation"""
70
+ model_config = ConfigDict(
71
+ json_schema_extra={
72
+ "example": {
73
+ "audio_path": "temp_audio/tts_e9b78164.wav"
74
+ }
75
+ }
76
+ )
77
+
78
+ audio_path: str = Field(..., description="Path to generated audio file")
79
+
80
+ class GenerateArgumentResponse(BaseModel):
81
+ """Response model for argument generation"""
82
+ model_config = ConfigDict(
83
+ json_schema_extra={
84
+ "example": {
85
+ "argument": "Climate change is a pressing issue that requires immediate action..."
86
+ }
87
+ }
88
+ )
89
+
90
+ argument: str = Field(..., description="Generated debate argument")
91
+
92
  class ResourceInfo(BaseModel):
93
  """Information about an MCP resource"""
94
  uri: str
routes/mcp_routes.py CHANGED
@@ -7,7 +7,17 @@ import logging
7
  import json
8
 
9
  from services.mcp_service import mcp_server
10
- from models.mcp_models import ToolListResponse, ToolInfo, ToolCallRequest, ToolCallResponse
 
 
 
 
 
 
 
 
 
 
11
 
12
  router = APIRouter(prefix="/api/v1/mcp", tags=["MCP"])
13
  logger = logging.getLogger(__name__)
@@ -234,8 +244,8 @@ async def call_mcp_tool(request: ToolCallRequest):
234
 
235
  # ===== Routes individuelles pour chaque outil (pour Swagger) =====
236
 
237
- @router.post("/tools/detect-stance", summary="Détecter la stance d'un argument")
238
- async def mcp_detect_stance(request: DetectStanceRequest) -> Dict[str, Any]:
239
  """Détecte si un argument est PRO ou CON pour un topic donné"""
240
  try:
241
  # Appeler directement via call_tool (async)
@@ -243,95 +253,144 @@ async def mcp_detect_stance(request: DetectStanceRequest) -> Dict[str, Any]:
243
  "topic": request.topic,
244
  "argument": request.argument
245
  })
246
- # Gérer différents types de retours MCP
 
 
247
  if isinstance(result, dict):
248
  # Si le résultat contient une clé "result" avec une liste de ContentBlock
249
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
250
  content_block = result["result"][0]
251
  if hasattr(content_block, 'text') and content_block.text:
252
  try:
253
- return json.loads(content_block.text)
254
  except json.JSONDecodeError:
255
- return {"text": content_block.text}
256
- return result
 
257
  elif isinstance(result, (list, tuple)) and len(result) > 0:
258
- # Si c'est une liste de ContentBlock, extraire le contenu
259
  if hasattr(result[0], 'text') and result[0].text:
260
  try:
261
- return json.loads(result[0].text)
262
  except json.JSONDecodeError:
263
- return {"text": result[0].text}
264
- return {"result": result[0] if result else {}}
265
- return {"result": result}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  except Exception as e:
267
  logger.error(f"Error in detect_stance: {e}")
268
  raise HTTPException(status_code=500, detail=f"Error executing tool detect_stance: {e}")
269
 
270
- @router.post("/tools/match-keypoint", summary="Matcher un argument avec un keypoint")
271
- async def mcp_match_keypoint(request: MatchKeypointRequest) -> Dict[str, Any]:
272
  """Détermine si un argument correspond à un keypoint"""
273
  try:
274
  result = await mcp_server.call_tool("match_keypoint_argument", {
275
  "argument": request.argument,
276
  "key_point": request.key_point
277
  })
278
- # Gérer différents types de retours MCP
 
 
279
  if isinstance(result, dict):
280
- # Si le résultat contient une clé "result" avec une liste de ContentBlock
281
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
282
  content_block = result["result"][0]
283
  if hasattr(content_block, 'text') and content_block.text:
284
  try:
285
- return json.loads(content_block.text)
286
  except json.JSONDecodeError:
287
- return {"text": content_block.text}
288
- return result
 
289
  elif isinstance(result, (list, tuple)) and len(result) > 0:
290
  if hasattr(result[0], 'text') and result[0].text:
291
  try:
292
- return json.loads(result[0].text)
293
  except json.JSONDecodeError:
294
- return {"text": result[0].text}
295
- return {"result": result[0] if result else {}}
296
- return {"result": result}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  except Exception as e:
298
  logger.error(f"Error in match_keypoint_argument: {e}")
299
  raise HTTPException(status_code=500, detail=f"Error executing tool match_keypoint_argument: {e}")
300
 
301
- @router.post("/tools/transcribe-audio", summary="Transcrire un audio en texte")
302
- async def mcp_transcribe_audio(request: TranscribeAudioRequest) -> Dict[str, str]:
303
  """Convertit un fichier audio en texte"""
304
  try:
305
  result = await mcp_server.call_tool("transcribe_audio", {
306
  "audio_path": request.audio_path
307
  })
308
- # Gérer différents types de retours MCP
 
 
309
  if isinstance(result, dict):
310
- # Si le résultat contient une clé "result" avec une liste de ContentBlock
311
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
312
  content_block = result["result"][0]
313
  if hasattr(content_block, 'text'):
314
- return {"text": content_block.text}
315
- # Si c'est un dict simple, essayer d'extraire le texte
316
- if "text" in result:
317
- return {"text": result["text"]}
318
- return {"text": str(result)}
319
  elif isinstance(result, str):
320
- return {"text": result}
321
  elif isinstance(result, (list, tuple)) and len(result) > 0:
322
  if hasattr(result[0], 'text'):
323
- return {"text": result[0].text}
324
- return {"text": str(result[0])}
325
- return {"text": str(result)}
 
 
 
 
 
 
 
 
 
 
326
  except FileNotFoundError as e:
327
  logger.error(f"File not found in transcribe_audio: {e}")
328
  raise HTTPException(status_code=500, detail=f"Error executing tool transcribe_audio: {e}")
 
 
329
  except Exception as e:
330
  logger.error(f"Error in transcribe_audio: {e}")
331
  raise HTTPException(status_code=500, detail=f"Error executing tool transcribe_audio: {e}")
332
 
333
- @router.post("/tools/generate-speech", summary="Générer de la parole à partir de texte")
334
- async def mcp_generate_speech(request: GenerateSpeechRequest) -> Dict[str, str]:
335
  """Convertit du texte en fichier audio"""
336
  try:
337
  result = await mcp_server.call_tool("generate_speech", {
@@ -339,54 +398,76 @@ async def mcp_generate_speech(request: GenerateSpeechRequest) -> Dict[str, str]:
339
  "voice": request.voice,
340
  "format": request.format
341
  })
342
- # Gérer différents types de retours MCP
 
 
343
  if isinstance(result, dict):
344
- # Si le résultat contient une clé "result" avec une liste de ContentBlock
345
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
346
  content_block = result["result"][0]
347
  if hasattr(content_block, 'text'):
348
- return {"audio_path": content_block.text}
349
- # Si c'est un dict simple, essayer d'extraire le chemin
350
- if "audio_path" in result:
351
- return {"audio_path": result["audio_path"]}
352
- return {"audio_path": str(result)}
353
  elif isinstance(result, str):
354
- return {"audio_path": result}
355
  elif isinstance(result, (list, tuple)) and len(result) > 0:
356
  if hasattr(result[0], 'text'):
357
- return {"audio_path": result[0].text}
358
- return {"audio_path": str(result[0])}
359
- return {"audio_path": str(result)}
 
 
 
 
 
 
 
 
 
 
 
 
360
  except Exception as e:
361
  logger.error(f"Error in generate_speech: {e}")
362
  raise HTTPException(status_code=500, detail=f"Error executing tool generate_speech: {e}")
363
 
364
- @router.post("/tools/generate-argument", summary="Générer un argument de débat")
365
- async def mcp_generate_argument(request: GenerateArgumentRequest) -> Dict[str, str]:
366
  """Génère un argument de débat à partir d'un input utilisateur"""
367
  try:
368
  result = await mcp_server.call_tool("generate_argument", {
369
  "user_input": request.user_input,
370
  "conversation_id": request.conversation_id
371
  })
372
- # Gérer différents types de retours MCP
 
 
373
  if isinstance(result, dict):
374
- # Si le résultat contient une clé "result" avec une liste de ContentBlock
375
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
376
  content_block = result["result"][0]
377
  if hasattr(content_block, 'text'):
378
- return {"argument": content_block.text}
379
- # Si c'est un dict simple, essayer d'extraire l'argument
380
- if "argument" in result:
381
- return {"argument": result["argument"]}
382
- return {"argument": str(result)}
383
  elif isinstance(result, str):
384
- return {"argument": result}
385
  elif isinstance(result, (list, tuple)) and len(result) > 0:
386
  if hasattr(result[0], 'text'):
387
- return {"argument": result[0].text}
388
- return {"argument": str(result[0])}
389
- return {"argument": str(result)}
 
 
 
 
 
 
 
 
 
 
 
 
390
  except Exception as e:
391
  logger.error(f"Error in generate_argument: {e}")
392
  raise HTTPException(status_code=500, detail=f"Error executing tool generate_argument: {e}")
 
7
  import json
8
 
9
  from services.mcp_service import mcp_server
10
+ from models.mcp_models import (
11
+ ToolListResponse,
12
+ ToolInfo,
13
+ ToolCallRequest,
14
+ ToolCallResponse,
15
+ DetectStanceResponse,
16
+ MatchKeypointResponse,
17
+ TranscribeAudioResponse,
18
+ GenerateSpeechResponse,
19
+ GenerateArgumentResponse
20
+ )
21
 
22
  router = APIRouter(prefix="/api/v1/mcp", tags=["MCP"])
23
  logger = logging.getLogger(__name__)
 
244
 
245
  # ===== Routes individuelles pour chaque outil (pour Swagger) =====
246
 
247
+ @router.post("/tools/detect-stance", response_model=DetectStanceResponse, summary="Détecter la stance d'un argument")
248
+ async def mcp_detect_stance(request: DetectStanceRequest):
249
  """Détecte si un argument est PRO ou CON pour un topic donné"""
250
  try:
251
  # Appeler directement via call_tool (async)
 
253
  "topic": request.topic,
254
  "argument": request.argument
255
  })
256
+
257
+ # Extraire les données du résultat MCP
258
+ parsed_result = None
259
  if isinstance(result, dict):
260
  # Si le résultat contient une clé "result" avec une liste de ContentBlock
261
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
262
  content_block = result["result"][0]
263
  if hasattr(content_block, 'text') and content_block.text:
264
  try:
265
+ parsed_result = json.loads(content_block.text)
266
  except json.JSONDecodeError:
267
+ raise HTTPException(status_code=500, detail="Invalid JSON response from MCP tool")
268
+ else:
269
+ parsed_result = result
270
  elif isinstance(result, (list, tuple)) and len(result) > 0:
 
271
  if hasattr(result[0], 'text') and result[0].text:
272
  try:
273
+ parsed_result = json.loads(result[0].text)
274
  except json.JSONDecodeError:
275
+ raise HTTPException(status_code=500, detail="Invalid JSON response from MCP tool")
276
+ else:
277
+ parsed_result = result
278
+
279
+ if not parsed_result:
280
+ raise HTTPException(status_code=500, detail="Empty response from MCP tool")
281
+
282
+ # Construire la réponse structurée
283
+ response = DetectStanceResponse(
284
+ predicted_stance=parsed_result["predicted_stance"],
285
+ confidence=parsed_result["confidence"],
286
+ probability_con=parsed_result["probability_con"],
287
+ probability_pro=parsed_result["probability_pro"]
288
+ )
289
+
290
+ logger.info(f"Stance prediction: {response.predicted_stance} (conf={response.confidence:.4f})")
291
+ return response
292
+
293
+ except HTTPException:
294
+ raise
295
  except Exception as e:
296
  logger.error(f"Error in detect_stance: {e}")
297
  raise HTTPException(status_code=500, detail=f"Error executing tool detect_stance: {e}")
298
 
299
+ @router.post("/tools/match-keypoint", response_model=MatchKeypointResponse, summary="Matcher un argument avec un keypoint")
300
+ async def mcp_match_keypoint(request: MatchKeypointRequest):
301
  """Détermine si un argument correspond à un keypoint"""
302
  try:
303
  result = await mcp_server.call_tool("match_keypoint_argument", {
304
  "argument": request.argument,
305
  "key_point": request.key_point
306
  })
307
+
308
+ # Extraire les données du résultat MCP
309
+ parsed_result = None
310
  if isinstance(result, dict):
 
311
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
312
  content_block = result["result"][0]
313
  if hasattr(content_block, 'text') and content_block.text:
314
  try:
315
+ parsed_result = json.loads(content_block.text)
316
  except json.JSONDecodeError:
317
+ raise HTTPException(status_code=500, detail="Invalid JSON response from MCP tool")
318
+ else:
319
+ parsed_result = result
320
  elif isinstance(result, (list, tuple)) and len(result) > 0:
321
  if hasattr(result[0], 'text') and result[0].text:
322
  try:
323
+ parsed_result = json.loads(result[0].text)
324
  except json.JSONDecodeError:
325
+ raise HTTPException(status_code=500, detail="Invalid JSON response from MCP tool")
326
+ else:
327
+ parsed_result = result
328
+
329
+ if not parsed_result:
330
+ raise HTTPException(status_code=500, detail="Empty response from MCP tool")
331
+
332
+ # Construire la réponse structurée
333
+ response = MatchKeypointResponse(
334
+ prediction=parsed_result["prediction"],
335
+ label=parsed_result["label"],
336
+ confidence=parsed_result["confidence"],
337
+ probabilities=parsed_result["probabilities"]
338
+ )
339
+
340
+ logger.info(f"Keypoint matching: {response.label} (conf={response.confidence:.4f})")
341
+ return response
342
+
343
+ except HTTPException:
344
+ raise
345
  except Exception as e:
346
  logger.error(f"Error in match_keypoint_argument: {e}")
347
  raise HTTPException(status_code=500, detail=f"Error executing tool match_keypoint_argument: {e}")
348
 
349
+ @router.post("/tools/transcribe-audio", response_model=TranscribeAudioResponse, summary="Transcrire un audio en texte")
350
+ async def mcp_transcribe_audio(request: TranscribeAudioRequest):
351
  """Convertit un fichier audio en texte"""
352
  try:
353
  result = await mcp_server.call_tool("transcribe_audio", {
354
  "audio_path": request.audio_path
355
  })
356
+
357
+ # Extraire le texte du résultat MCP
358
+ transcribed_text = None
359
  if isinstance(result, dict):
 
360
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
361
  content_block = result["result"][0]
362
  if hasattr(content_block, 'text'):
363
+ transcribed_text = content_block.text
364
+ elif "text" in result:
365
+ transcribed_text = result["text"]
 
 
366
  elif isinstance(result, str):
367
+ transcribed_text = result
368
  elif isinstance(result, (list, tuple)) and len(result) > 0:
369
  if hasattr(result[0], 'text'):
370
+ transcribed_text = result[0].text
371
+ else:
372
+ transcribed_text = str(result[0])
373
+ else:
374
+ transcribed_text = str(result)
375
+
376
+ if not transcribed_text:
377
+ raise HTTPException(status_code=500, detail="Empty transcription result from MCP tool")
378
+
379
+ response = TranscribeAudioResponse(text=transcribed_text)
380
+ logger.info(f"Audio transcribed: {len(transcribed_text)} characters")
381
+ return response
382
+
383
  except FileNotFoundError as e:
384
  logger.error(f"File not found in transcribe_audio: {e}")
385
  raise HTTPException(status_code=500, detail=f"Error executing tool transcribe_audio: {e}")
386
+ except HTTPException:
387
+ raise
388
  except Exception as e:
389
  logger.error(f"Error in transcribe_audio: {e}")
390
  raise HTTPException(status_code=500, detail=f"Error executing tool transcribe_audio: {e}")
391
 
392
+ @router.post("/tools/generate-speech", response_model=GenerateSpeechResponse, summary="Générer de la parole à partir de texte")
393
+ async def mcp_generate_speech(request: GenerateSpeechRequest):
394
  """Convertit du texte en fichier audio"""
395
  try:
396
  result = await mcp_server.call_tool("generate_speech", {
 
398
  "voice": request.voice,
399
  "format": request.format
400
  })
401
+
402
+ # Extraire le chemin audio du résultat MCP
403
+ audio_path = None
404
  if isinstance(result, dict):
 
405
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
406
  content_block = result["result"][0]
407
  if hasattr(content_block, 'text'):
408
+ audio_path = content_block.text
409
+ elif "audio_path" in result:
410
+ audio_path = result["audio_path"]
 
 
411
  elif isinstance(result, str):
412
+ audio_path = result
413
  elif isinstance(result, (list, tuple)) and len(result) > 0:
414
  if hasattr(result[0], 'text'):
415
+ audio_path = result[0].text
416
+ else:
417
+ audio_path = str(result[0])
418
+ else:
419
+ audio_path = str(result)
420
+
421
+ if not audio_path:
422
+ raise HTTPException(status_code=500, detail="Empty audio path from MCP tool")
423
+
424
+ response = GenerateSpeechResponse(audio_path=audio_path)
425
+ logger.info(f"Speech generated: {audio_path}")
426
+ return response
427
+
428
+ except HTTPException:
429
+ raise
430
  except Exception as e:
431
  logger.error(f"Error in generate_speech: {e}")
432
  raise HTTPException(status_code=500, detail=f"Error executing tool generate_speech: {e}")
433
 
434
+ @router.post("/tools/generate-argument", response_model=GenerateArgumentResponse, summary="Générer un argument de débat")
435
+ async def mcp_generate_argument(request: GenerateArgumentRequest):
436
  """Génère un argument de débat à partir d'un input utilisateur"""
437
  try:
438
  result = await mcp_server.call_tool("generate_argument", {
439
  "user_input": request.user_input,
440
  "conversation_id": request.conversation_id
441
  })
442
+
443
+ # Extraire l'argument du résultat MCP
444
+ generated_argument = None
445
  if isinstance(result, dict):
 
446
  if "result" in result and isinstance(result["result"], list) and len(result["result"]) > 0:
447
  content_block = result["result"][0]
448
  if hasattr(content_block, 'text'):
449
+ generated_argument = content_block.text
450
+ elif "argument" in result:
451
+ generated_argument = result["argument"]
 
 
452
  elif isinstance(result, str):
453
+ generated_argument = result
454
  elif isinstance(result, (list, tuple)) and len(result) > 0:
455
  if hasattr(result[0], 'text'):
456
+ generated_argument = result[0].text
457
+ else:
458
+ generated_argument = str(result[0])
459
+ else:
460
+ generated_argument = str(result)
461
+
462
+ if not generated_argument:
463
+ raise HTTPException(status_code=500, detail="Empty argument from MCP tool")
464
+
465
+ response = GenerateArgumentResponse(argument=generated_argument)
466
+ logger.info(f"Argument generated: {len(generated_argument)} characters")
467
+ return response
468
+
469
+ except HTTPException:
470
+ raise
471
  except Exception as e:
472
  logger.error(f"Error in generate_argument: {e}")
473
  raise HTTPException(status_code=500, detail=f"Error executing tool generate_argument: {e}")