clementpep commited on
Commit
015cfbb
·
1 Parent(s): f2aa790

fix: openai inferences

Browse files
Files changed (2) hide show
  1. backend/ai_service.py +167 -47
  2. backend/main.py +33 -14
backend/ai_service.py CHANGED
@@ -7,20 +7,63 @@ from typing import Optional
7
  from openai import OpenAI
8
  from backend.config import settings
9
  import asyncio
 
 
 
 
 
 
 
 
10
 
11
 
12
  class AIService:
13
- """Service for AI-generated game content."""
 
 
 
 
 
 
 
 
 
14
 
15
  def __init__(self):
 
 
 
 
 
 
 
 
 
 
 
16
  self.enabled = settings.USE_OPENAI and bool(settings.OPENAI_API_KEY)
17
  self.client = None
18
 
19
  if self.enabled:
20
  try:
21
- self.client = OpenAI(api_key=settings.OPENAI_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  except Exception as e:
23
- print(f"Failed to initialize OpenAI client: {e}")
24
  self.enabled = False
25
 
26
  async def generate_scenario(
@@ -31,9 +74,17 @@ class AIService:
31
  ) -> Optional[str]:
32
  """
33
  Generate a mystery scenario based on the game setup.
34
- Returns None if AI is disabled or if generation fails.
 
 
 
 
 
 
 
35
  """
36
  if not self.enabled or not self.client:
 
37
  return None
38
 
39
  try:
@@ -54,18 +105,23 @@ VOCABULAIRE À UTILISER (subtilement):
54
 
55
  COMMENCE obligatoirement par Desland se trompant sur son nom, puis introduis le meurtre avec son ton sarcastique et suspect caractéristique. Moque subtilement la situation et l'intelligence des enquêteurs. Utilise subtilement 1-2 expressions du vocabulaire."""
56
 
57
- # Run with timeout
58
  response = await asyncio.wait_for(
59
- asyncio.to_thread(self._generate_text, prompt), timeout=10.0
60
  )
61
 
 
 
 
 
 
62
  return response
63
 
64
  except asyncio.TimeoutError:
65
- print("AI scenario generation timed out")
66
  return None
67
  except Exception as e:
68
- print(f"Error generating scenario: {e}")
69
  return None
70
 
71
  async def generate_suggestion_comment(
@@ -79,14 +135,25 @@ COMMENCE obligatoirement par Desland se trompant sur son nom, puis introduis le
79
  ) -> Optional[str]:
80
  """
81
  Generate a sarcastic comment from Desland about a suggestion.
82
- Returns None if AI is disabled or if generation fails.
 
 
 
 
 
 
 
 
 
 
83
  """
84
- print(
85
- f"[AI Service] generate_suggestion_comment called: enabled={self.enabled}, client={self.client is not None}"
 
86
  )
87
 
88
  if not self.enabled or not self.client:
89
- print(f"[AI Service] AI disabled or client not initialized")
90
  return None
91
 
92
  try:
@@ -112,22 +179,23 @@ VOCABULAIRE À UTILISER (subtilement):
112
  Ton narratif: {narrative_tone}
113
  Sois sarcastique, condescendant et incisif. Moque la logique (ou l'absence de logique) de la suggestion. Utilise subtilement 1 expression du vocabulaire si approprié."""
114
 
115
- print(f"[AI Service] Calling OpenAI API...")
116
  response = await asyncio.wait_for(
117
- asyncio.to_thread(self._generate_text, prompt), timeout=10.0
118
  )
119
- print(f"[AI Service] OpenAI response received: {response}")
 
 
 
 
120
 
121
  return response
122
 
123
  except asyncio.TimeoutError:
124
- print("[AI Service] AI comment generation timed out")
125
  return None
126
  except Exception as e:
127
- import traceback
128
-
129
- print(f"[AI Service] Error generating comment: {e}")
130
- print(traceback.format_exc())
131
  return None
132
 
133
  async def generate_accusation_comment(
@@ -141,9 +209,20 @@ Sois sarcastique, condescendant et incisif. Moque la logique (ou l'absence de lo
141
  ) -> Optional[str]:
142
  """
143
  Generate a comment from Desland about an accusation.
144
- Returns None if AI is disabled or if generation fails.
 
 
 
 
 
 
 
 
 
 
145
  """
146
  if not self.enabled or not self.client:
 
147
  return None
148
 
149
  try:
@@ -168,17 +247,25 @@ VOCABULAIRE À UTILISER (subtilement):
168
 
169
  Rends-le incisif et mémorable. Utilise subtilement 1 expression du vocabulaire si approprié."""
170
 
 
 
 
171
  response = await asyncio.wait_for(
172
- asyncio.to_thread(self._generate_text, prompt), timeout=10.0
173
  )
174
 
 
 
 
 
 
175
  return response
176
 
177
  except asyncio.TimeoutError:
178
- print("AI comment generation timed out")
179
  return None
180
  except Exception as e:
181
- print(f"Error generating comment: {e}")
182
  return None
183
 
184
  async def generate_victory_comment(
@@ -191,12 +278,21 @@ Rends-le incisif et mémorable. Utilise subtilement 1 expression du vocabulaire
191
  ) -> Optional[str]:
192
  """
193
  Generate a skeptical victory comment from Desland.
194
- Returns None if AI is disabled or if generation fails.
 
 
 
 
 
 
 
 
 
195
  """
196
- print(f"[AI Service] generate_victory_comment called for {player_name}")
197
 
198
  if not self.enabled or not self.client:
199
- print(f"[AI Service] AI disabled or client not initialized")
200
  return None
201
 
202
  try:
@@ -213,34 +309,51 @@ IMPORTANT: Desland est SCEPTIQUE et JALOUX. Il minimise la victoire en suggéran
213
  Ton narratif: {narrative_tone}
214
  Sois sarcastique, minimise la victoire, suggère que c'était de la chance."""
215
 
216
- print(f"[AI Service] Calling OpenAI API...")
217
  response = await asyncio.wait_for(
218
- asyncio.to_thread(self._generate_text, prompt), timeout=10.0
219
  )
220
- print(f"[AI Service] OpenAI response received: {response}")
 
 
 
 
221
 
222
  return response
223
 
224
  except asyncio.TimeoutError:
225
- print("[AI Service] AI victory comment generation timed out")
226
  return None
227
  except Exception as e:
228
- import traceback
229
-
230
- print(f"[AI Service] Error generating victory comment: {e}")
231
- print(traceback.format_exc())
232
  return None
233
 
234
  def _generate_text(self, prompt: str) -> str:
235
  """
236
  Internal method to generate text using OpenAI API.
 
 
 
 
 
 
 
 
 
 
237
  """
238
  if not self.client:
239
- print("[AI Service] _generate_text: No client")
240
  return ""
241
 
242
  try:
243
- print("[AI Service] _generate_text: Calling OpenAI API...")
 
 
 
 
 
 
244
  response = self.client.chat.completions.create(
245
  model="gpt-5-nano",
246
  messages=[
@@ -274,22 +387,29 @@ Garde tes réponses brèves (1 phrase pour les commentaires, 2-3 pour les scéna
274
  ],
275
  )
276
 
277
- print(
278
- f"[AI Service] _generate_text: Response received, choices={len(response.choices)}"
 
279
  )
280
- if response.choices:
 
281
  content = response.choices[0].message.content
282
- print(f"[AI Service] _generate_text: Content={content}")
283
- return content.strip() if content else ""
 
 
 
 
284
  else:
285
- print("[AI Service] _generate_text: No choices in response")
286
  return ""
287
 
288
  except Exception as e:
289
- import traceback
290
-
291
- print(f"[AI Service] _generate_text error: {e}")
292
- print(traceback.format_exc())
 
293
  return ""
294
 
295
 
 
7
  from openai import OpenAI
8
  from backend.config import settings
9
  import asyncio
10
+ import logging
11
+ import httpx
12
+
13
+ # Configure logger for AI service
14
+ logger = logging.getLogger(__name__)
15
+ logging.basicConfig(
16
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
17
+ )
18
 
19
 
20
  class AIService:
21
+ """
22
+ Service for AI-generated game content using OpenAI API.
23
+
24
+ This service provides AI-powered narrative generation for the game,
25
+ including scenario creation and character commentary.
26
+
27
+ Attributes:
28
+ enabled (bool): Whether the AI service is active and ready to use
29
+ client (OpenAI): OpenAI API client instance
30
+ """
31
 
32
  def __init__(self):
33
+ """
34
+ Initialize the AI service.
35
+
36
+ Checks configuration and creates OpenAI client if enabled.
37
+ Falls back to disabled state if initialization fails.
38
+
39
+ The client is configured with:
40
+ - Extended timeout: 30 seconds total (connect: 5s, read: 25s)
41
+ - Automatic retries: 3 attempts with exponential backoff
42
+ - This handles network instability and API rate limits gracefully
43
+ """
44
  self.enabled = settings.USE_OPENAI and bool(settings.OPENAI_API_KEY)
45
  self.client = None
46
 
47
  if self.enabled:
48
  try:
49
+ # Configure timeout with granular control
50
+ # Total 30s: 5s to connect, 25s to read response
51
+ timeout = httpx.Timeout(
52
+ 30.0, # Total timeout
53
+ connect=5.0, # Connection timeout
54
+ read=25.0, # Read timeout (API processing time)
55
+ write=5.0 # Write timeout
56
+ )
57
+
58
+ # Initialize client with timeout and retry strategy
59
+ self.client = OpenAI(
60
+ api_key=settings.OPENAI_API_KEY,
61
+ timeout=timeout,
62
+ max_retries=3 # Retry up to 3 times on network errors
63
+ )
64
+ logger.info("OpenAI client initialized successfully (timeout=30s, retries=3)")
65
  except Exception as e:
66
+ logger.error(f"Failed to initialize OpenAI client: {e}", exc_info=True)
67
  self.enabled = False
68
 
69
  async def generate_scenario(
 
74
  ) -> Optional[str]:
75
  """
76
  Generate a mystery scenario based on the game setup.
77
+
78
+ Args:
79
+ rooms: List of room names available in the game
80
+ characters: List of character names available in the game
81
+ narrative_tone: The narrative tone for the scenario (default: "🕵️ Sérieuse")
82
+
83
+ Returns:
84
+ Generated scenario text or None if AI is disabled or generation fails
85
  """
86
  if not self.enabled or not self.client:
87
+ logger.debug("AI service not enabled or client not initialized")
88
  return None
89
 
90
  try:
 
105
 
106
  COMMENCE obligatoirement par Desland se trompant sur son nom, puis introduis le meurtre avec son ton sarcastique et suspect caractéristique. Moque subtilement la situation et l'intelligence des enquêteurs. Utilise subtilement 1-2 expressions du vocabulaire."""
107
 
108
+ logger.info("Generating scenario with AI")
109
  response = await asyncio.wait_for(
110
+ asyncio.to_thread(self._generate_text, prompt), timeout=35.0
111
  )
112
 
113
+ if response:
114
+ logger.info("Scenario generated successfully")
115
+ else:
116
+ logger.warning("Scenario generation returned empty response")
117
+
118
  return response
119
 
120
  except asyncio.TimeoutError:
121
+ logger.error("AI scenario generation timed out after 35 seconds")
122
  return None
123
  except Exception as e:
124
+ logger.error(f"Error generating scenario: {e}", exc_info=True)
125
  return None
126
 
127
  async def generate_suggestion_comment(
 
135
  ) -> Optional[str]:
136
  """
137
  Generate a sarcastic comment from Desland about a suggestion.
138
+
139
+ Args:
140
+ player_name: Name of the player making the suggestion
141
+ character: Character suggested as the culprit
142
+ weapon: Weapon suggested as the murder weapon
143
+ room: Room suggested as the crime scene
144
+ was_disproven: Whether the suggestion was disproven by another player
145
+ narrative_tone: The narrative tone for the comment (default: "🕵️ Sérieuse")
146
+
147
+ Returns:
148
+ Generated comment text or None if AI is disabled or generation fails
149
  """
150
+ logger.debug(
151
+ f"generate_suggestion_comment called: enabled={self.enabled}, "
152
+ f"client_exists={self.client is not None}, player={player_name}"
153
  )
154
 
155
  if not self.enabled or not self.client:
156
+ logger.debug("AI service not enabled or client not initialized")
157
  return None
158
 
159
  try:
 
179
  Ton narratif: {narrative_tone}
180
  Sois sarcastique, condescendant et incisif. Moque la logique (ou l'absence de logique) de la suggestion. Utilise subtilement 1 expression du vocabulaire si approprié."""
181
 
182
+ logger.info(f"Generating suggestion comment for {player_name}")
183
  response = await asyncio.wait_for(
184
+ asyncio.to_thread(self._generate_text, prompt), timeout=35.0
185
  )
186
+
187
+ if response:
188
+ logger.info(f"Suggestion comment generated: {response[:50]}...")
189
+ else:
190
+ logger.warning("Suggestion comment generation returned empty response")
191
 
192
  return response
193
 
194
  except asyncio.TimeoutError:
195
+ logger.error("AI comment generation timed out after 35 seconds")
196
  return None
197
  except Exception as e:
198
+ logger.error(f"Error generating suggestion comment: {e}", exc_info=True)
 
 
 
199
  return None
200
 
201
  async def generate_accusation_comment(
 
209
  ) -> Optional[str]:
210
  """
211
  Generate a comment from Desland about an accusation.
212
+
213
+ Args:
214
+ player_name: Name of the player making the accusation
215
+ character: Character accused as the culprit
216
+ weapon: Weapon accused as the murder weapon
217
+ room: Room accused as the crime scene
218
+ was_correct: Whether the accusation was correct
219
+ narrative_tone: The narrative tone for the comment (default: "🕵️ Sérieuse")
220
+
221
+ Returns:
222
+ Generated comment text or None if AI is disabled or generation fails
223
  """
224
  if not self.enabled or not self.client:
225
+ logger.debug("AI service not enabled or client not initialized")
226
  return None
227
 
228
  try:
 
247
 
248
  Rends-le incisif et mémorable. Utilise subtilement 1 expression du vocabulaire si approprié."""
249
 
250
+ logger.info(
251
+ f"Generating accusation comment for {player_name} (correct={was_correct})"
252
+ )
253
  response = await asyncio.wait_for(
254
+ asyncio.to_thread(self._generate_text, prompt), timeout=35.0
255
  )
256
 
257
+ if response:
258
+ logger.info(f"Accusation comment generated: {response[:50]}...")
259
+ else:
260
+ logger.warning("Accusation comment generation returned empty response")
261
+
262
  return response
263
 
264
  except asyncio.TimeoutError:
265
+ logger.error("AI comment generation timed out after 35 seconds")
266
  return None
267
  except Exception as e:
268
+ logger.error(f"Error generating accusation comment: {e}", exc_info=True)
269
  return None
270
 
271
  async def generate_victory_comment(
 
278
  ) -> Optional[str]:
279
  """
280
  Generate a skeptical victory comment from Desland.
281
+
282
+ Args:
283
+ player_name: Name of the winning player
284
+ character: The actual culprit character
285
+ weapon: The actual murder weapon
286
+ room: The actual crime scene room
287
+ narrative_tone: The narrative tone for the comment (default: "🕵️ Sérieuse")
288
+
289
+ Returns:
290
+ Generated victory comment or None if AI is disabled or generation fails
291
  """
292
+ logger.info(f"generate_victory_comment called for {player_name}")
293
 
294
  if not self.enabled or not self.client:
295
+ logger.debug("AI service not enabled or client not initialized")
296
  return None
297
 
298
  try:
 
309
  Ton narratif: {narrative_tone}
310
  Sois sarcastique, minimise la victoire, suggère que c'était de la chance."""
311
 
312
+ logger.info(f"Generating victory comment for {player_name}")
313
  response = await asyncio.wait_for(
314
+ asyncio.to_thread(self._generate_text, prompt), timeout=35.0
315
  )
316
+
317
+ if response:
318
+ logger.info(f"Victory comment generated: {response[:50]}...")
319
+ else:
320
+ logger.warning("Victory comment generation returned empty response")
321
 
322
  return response
323
 
324
  except asyncio.TimeoutError:
325
+ logger.error("AI victory comment generation timed out after 35 seconds")
326
  return None
327
  except Exception as e:
328
+ logger.error(f"Error generating victory comment: {e}", exc_info=True)
 
 
 
329
  return None
330
 
331
  def _generate_text(self, prompt: str) -> str:
332
  """
333
  Internal method to generate text using OpenAI API.
334
+
335
+ Args:
336
+ prompt: The user prompt to send to the AI model
337
+
338
+ Returns:
339
+ Generated text response or empty string if generation fails
340
+
341
+ Note:
342
+ This method is synchronous and should be called via asyncio.to_thread()
343
+ from async methods to avoid blocking the event loop.
344
  """
345
  if not self.client:
346
+ logger.error("_generate_text called but client is not initialized")
347
  return ""
348
 
349
  try:
350
+ import time
351
+ start_time = time.time()
352
+ logger.debug("Calling OpenAI API with chat completion (model: gpt-5-nano)")
353
+
354
+ # Call OpenAI API without max_tokens or temperature parameters
355
+ # The API will use default values which are appropriate for most use cases
356
+ # The client has built-in retry logic (3 attempts) and 30s timeout
357
  response = self.client.chat.completions.create(
358
  model="gpt-5-nano",
359
  messages=[
 
387
  ],
388
  )
389
 
390
+ elapsed_time = time.time() - start_time
391
+ logger.debug(
392
+ f"OpenAI API response received in {elapsed_time:.2f}s with {len(response.choices)} choices"
393
  )
394
+
395
+ if response.choices and len(response.choices) > 0:
396
  content = response.choices[0].message.content
397
+ if content:
398
+ logger.debug(f"Generated content ({len(content)} chars): {content[:100]}...")
399
+ return content.strip()
400
+ else:
401
+ logger.warning("Response content is None or empty")
402
+ return ""
403
  else:
404
+ logger.warning("No choices in OpenAI API response")
405
  return ""
406
 
407
  except Exception as e:
408
+ elapsed_time = time.time() - start_time
409
+ logger.error(
410
+ f"Error in _generate_text after {elapsed_time:.2f}s: {e}",
411
+ exc_info=True
412
+ )
413
  return ""
414
 
415
 
backend/main.py CHANGED
@@ -10,12 +10,20 @@ from fastapi.responses import FileResponse
10
  from pydantic import BaseModel
11
  from typing import Optional, List
12
  import os
 
13
 
14
  from backend.models import CreateGameRequest, GameStatus
15
  from backend.game_manager import game_manager
16
  from backend.game_engine import GameEngine
17
  from backend.defaults import get_default_game_config, DEFAULT_THEMES
18
 
 
 
 
 
 
 
 
19
  app = FastAPI(title="Cluedo Custom API")
20
 
21
  # CORS for development
@@ -133,16 +141,19 @@ async def start_game(game_id: str):
133
  if game and game.use_ai and not game.scenario:
134
  try:
135
  from backend.ai_service import ai_service
136
- print(f"[AI] Generating scenario...")
137
  game.scenario = await ai_service.generate_scenario(
138
  game.rooms,
139
  [c.name for c in game.characters],
140
  game.narrative_tone
141
  )
142
- print(f"[AI] Generated scenario: {game.scenario[:100]}...")
143
- game_manager.save_games()
 
 
 
144
  except Exception as e:
145
- print(f"[AI] AI scenario generation failed: {e}")
146
 
147
  return {
148
  "status": "started",
@@ -255,7 +266,7 @@ async def roll_dice(game_id: str, req: DiceRollRequest):
255
  import random
256
  ai_comment = random.choice(prompts)
257
  except Exception as e:
258
- print(f"AI comment generation failed: {e}")
259
 
260
  # Mark player as having rolled
261
  if player:
@@ -310,8 +321,7 @@ async def make_suggestion(game_id: str, req: SuggestionRequest):
310
  if game.use_ai:
311
  try:
312
  from backend.ai_service import ai_service
313
- import asyncio
314
- print(f"[AI] Generating suggestion comment for {player_name}...")
315
  ai_comment = await ai_service.generate_suggestion_comment(
316
  player_name,
317
  req.suspect,
@@ -320,9 +330,12 @@ async def make_suggestion(game_id: str, req: SuggestionRequest):
320
  can_disprove,
321
  game.narrative_tone
322
  )
323
- print(f"[AI] Generated comment: {ai_comment}")
 
 
 
324
  except Exception as e:
325
- print(f"[AI] AI comment generation failed: {e}")
326
 
327
  result = {
328
  "suggestion": f"{req.suspect} + {req.weapon} + {req.room}",
@@ -382,7 +395,7 @@ async def make_accusation(game_id: str, req: AccusationRequest):
382
  if game.use_ai:
383
  try:
384
  from backend.ai_service import ai_service
385
- print(f"[AI] Generating accusation comment for {player_name}...")
386
  ai_comment = await ai_service.generate_accusation_comment(
387
  player_name,
388
  req.suspect,
@@ -391,11 +404,14 @@ async def make_accusation(game_id: str, req: AccusationRequest):
391
  is_correct,
392
  game.narrative_tone
393
  )
394
- print(f"[AI] Generated comment: {ai_comment}")
 
 
 
395
 
396
  # Generate victory comment if correct
397
  if is_correct:
398
- print(f"[AI] Generating victory comment for {player_name}...")
399
  victory_comment = await ai_service.generate_victory_comment(
400
  player_name,
401
  req.suspect,
@@ -403,9 +419,12 @@ async def make_accusation(game_id: str, req: AccusationRequest):
403
  req.room,
404
  game.narrative_tone
405
  )
406
- print(f"[AI] Generated victory comment: {victory_comment}")
 
 
 
407
  except Exception as e:
408
- print(f"[AI] AI comment generation failed: {e}")
409
 
410
  # Record turn with AI comment
411
  GameEngine.add_turn_record(
 
10
  from pydantic import BaseModel
11
  from typing import Optional, List
12
  import os
13
+ import logging
14
 
15
  from backend.models import CreateGameRequest, GameStatus
16
  from backend.game_manager import game_manager
17
  from backend.game_engine import GameEngine
18
  from backend.defaults import get_default_game_config, DEFAULT_THEMES
19
 
20
+ # Configure logger
21
+ logger = logging.getLogger(__name__)
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
+
27
  app = FastAPI(title="Cluedo Custom API")
28
 
29
  # CORS for development
 
141
  if game and game.use_ai and not game.scenario:
142
  try:
143
  from backend.ai_service import ai_service
144
+ logger.info("Generating AI scenario for game start")
145
  game.scenario = await ai_service.generate_scenario(
146
  game.rooms,
147
  [c.name for c in game.characters],
148
  game.narrative_tone
149
  )
150
+ if game.scenario:
151
+ logger.info(f"Generated scenario: {game.scenario[:100]}...")
152
+ game_manager.save_games()
153
+ else:
154
+ logger.warning("AI scenario generation returned None")
155
  except Exception as e:
156
+ logger.error(f"AI scenario generation failed: {e}", exc_info=True)
157
 
158
  return {
159
  "status": "started",
 
266
  import random
267
  ai_comment = random.choice(prompts)
268
  except Exception as e:
269
+ logger.error(f"AI comment generation failed: {e}", exc_info=True)
270
 
271
  # Mark player as having rolled
272
  if player:
 
321
  if game.use_ai:
322
  try:
323
  from backend.ai_service import ai_service
324
+ logger.info(f"Generating AI suggestion comment for {player_name}")
 
325
  ai_comment = await ai_service.generate_suggestion_comment(
326
  player_name,
327
  req.suspect,
 
330
  can_disprove,
331
  game.narrative_tone
332
  )
333
+ if ai_comment:
334
+ logger.info(f"Generated comment: {ai_comment[:50]}...")
335
+ else:
336
+ logger.warning("AI comment generation returned None")
337
  except Exception as e:
338
+ logger.error(f"AI comment generation failed: {e}", exc_info=True)
339
 
340
  result = {
341
  "suggestion": f"{req.suspect} + {req.weapon} + {req.room}",
 
395
  if game.use_ai:
396
  try:
397
  from backend.ai_service import ai_service
398
+ logger.info(f"Generating AI accusation comment for {player_name}")
399
  ai_comment = await ai_service.generate_accusation_comment(
400
  player_name,
401
  req.suspect,
 
404
  is_correct,
405
  game.narrative_tone
406
  )
407
+ if ai_comment:
408
+ logger.info(f"Generated accusation comment: {ai_comment[:50]}...")
409
+ else:
410
+ logger.warning("AI accusation comment generation returned None")
411
 
412
  # Generate victory comment if correct
413
  if is_correct:
414
+ logger.info(f"Generating AI victory comment for {player_name}")
415
  victory_comment = await ai_service.generate_victory_comment(
416
  player_name,
417
  req.suspect,
 
419
  req.room,
420
  game.narrative_tone
421
  )
422
+ if victory_comment:
423
+ logger.info(f"Generated victory comment: {victory_comment[:50]}...")
424
+ else:
425
+ logger.warning("AI victory comment generation returned None")
426
  except Exception as e:
427
+ logger.error(f"AI comment generation failed: {e}", exc_info=True)
428
 
429
  # Record turn with AI comment
430
  GameEngine.add_turn_record(