lucadipalma commited on
Commit
59048dd
·
1 Parent(s): 08959c5

modify graph to set limits; visualization bugs

Browse files
graph.png CHANGED
pages/play.py CHANGED
@@ -278,6 +278,7 @@ with gr.Blocks(fill_width=True) as demo:
278
 
279
  def update_api_key_inputs(red_team, blue_team):
280
  """Update visibility and state of API key inputs based on team selection."""
 
281
  required_teams = get_required_teams(red_team, blue_team)
282
 
283
  # Return visibility states for each team's API input
@@ -315,8 +316,6 @@ with gr.Blocks(fill_width=True) as demo:
315
  accumulated_messages = list(messages) if messages else []
316
  async for new_msg, guessed, updated_board, updated_chat_history, winners in graph.stream_graph(user_msg, messages, players, board, dropdown, guessed, chat_history, is_human_playing, turn):
317
 
318
- logger.error(f"Winners: {winners}")
319
-
320
  if len(new_msg) > len(accumulated_messages):
321
  # Add only the new messages that weren't there before
322
  accumulated_messages = list(new_msg)
@@ -328,16 +327,20 @@ with gr.Blocks(fill_width=True) as demo:
328
  feed_html = format_messages_as_feed(accumulated_messages, players, winners)
329
  yield feed_html, guessed, messages, updated_board, updated_chat_history, winners
330
 
331
- def update_plot(guessed_words, board, previous_bord_img):
 
 
 
 
332
  if not guessed_words or not board:
333
- return previous_bord_img
 
334
  logger.info(f"Calling update_plot: {guessed_words}")
335
  board_img = plot_game_board_with_guesses(board, guessed_words)
336
  return board_img
337
 
338
  def deactivate_send(game, winner_and_score):
339
  if winner_and_score is None or game is None:
340
- logger.error(f"Winner state: {winner_and_score}")
341
  return gr.Button(visible=True)
342
  else:
343
  return gr.Button(visible=False)
@@ -482,7 +485,7 @@ with gr.Blocks(fill_width=True) as demo:
482
 
483
  guessed_words_state.change(
484
  fn=update_plot,
485
- inputs=[guessed_words_state, board_state, board_plot],
486
  outputs=[board_plot]
487
  )
488
 
 
278
 
279
  def update_api_key_inputs(red_team, blue_team):
280
  """Update visibility and state of API key inputs based on team selection."""
281
+
282
  required_teams = get_required_teams(red_team, blue_team)
283
 
284
  # Return visibility states for each team's API input
 
316
  accumulated_messages = list(messages) if messages else []
317
  async for new_msg, guessed, updated_board, updated_chat_history, winners in graph.stream_graph(user_msg, messages, players, board, dropdown, guessed, chat_history, is_human_playing, turn):
318
 
 
 
319
  if len(new_msg) > len(accumulated_messages):
320
  # Add only the new messages that weren't there before
321
  accumulated_messages = list(new_msg)
 
327
  feed_html = format_messages_as_feed(accumulated_messages, players, winners)
328
  yield feed_html, guessed, messages, updated_board, updated_chat_history, winners
329
 
330
+ def update_plot(guessed_words, board, game_state):
331
+ # Don't update if no game is active
332
+ if game_state is None:
333
+ return gr.update() # No update
334
+
335
  if not guessed_words or not board:
336
+ return gr.update() # No update
337
+
338
  logger.info(f"Calling update_plot: {guessed_words}")
339
  board_img = plot_game_board_with_guesses(board, guessed_words)
340
  return board_img
341
 
342
  def deactivate_send(game, winner_and_score):
343
  if winner_and_score is None or game is None:
 
344
  return gr.Button(visible=True)
345
  else:
346
  return gr.Button(visible=False)
 
485
 
486
  guessed_words_state.change(
487
  fn=update_plot,
488
+ inputs=[guessed_words_state, board_state, game_state], # Remove board_plot from inputs
489
  outputs=[board_plot]
490
  )
491
 
support/build_graph.py CHANGED
@@ -11,6 +11,7 @@ from langgraph.prebuilt import ToolNode
11
  from support.log_manager import logger
12
  from support.my_tools import captain_agent_tools, boss_agent_tools
13
  from support.tools.boss_agent_tools import create_fake_tool_call
 
14
  from support.prompts import captain_agent_system_prompt, player_agent_system_prompt, boss_agent_system_prompt
15
  from typing import Annotated, List, Literal, Optional
16
  from typing_extensions import TypedDict
@@ -38,6 +39,11 @@ class State(TypedDict):
38
  end_round: bool
39
  winner_and_score: tuple
40
 
 
 
 
 
 
41
 
42
  class MyGraph:
43
  def __init__(self):
@@ -64,6 +70,8 @@ class MyGraph:
64
  builder.add_node("red_captain", self.red_captain)
65
  builder.add_node("red_agent_1", self.red_agent_1)
66
  builder.add_node("red_agent_2", self.red_agent_2)
 
 
67
 
68
  # Tool nodes
69
  choose_word_tool = ToolNode(tools=[boss_agent_tools[0]])
@@ -84,10 +92,11 @@ class MyGraph:
84
  self.boss_choice,
85
  {
86
  "choose_word_tool": "choose_word_tool",
87
- "red_boss": "red_boss",
88
  },
89
  )
90
- # builder.add_edge("red_boss", "choose_word_tool")
 
91
  builder.add_edge("choose_word_tool", "red_captain")
92
 
93
  builder.add_conditional_edges(
@@ -97,10 +106,11 @@ class MyGraph:
97
  "final_choice": "final_choice",
98
  "transfer_to_agent_1": "transfer_to_agent_1",
99
  "transfer_to_agent_2": "transfer_to_agent_2",
100
- "red_captain": "red_captain",
101
  },
102
  )
103
 
 
104
  builder.add_edge("transfer_to_agent_1", "red_agent_1")
105
  builder.add_edge("transfer_to_agent_2", "red_agent_2")
106
  builder.add_edge("red_agent_1", "red_captain")
@@ -119,6 +129,8 @@ class MyGraph:
119
  builder.add_node("blue_captain", self.blue_captain)
120
  builder.add_node("blue_agent_1", self.blue_agent_1)
121
  builder.add_node("blue_agent_2", self.blue_agent_2)
 
 
122
 
123
  # Tool nodes
124
  choose_word_tool = ToolNode(tools=[boss_agent_tools[0]])
@@ -139,10 +151,11 @@ class MyGraph:
139
  self.boss_choice,
140
  {
141
  "choose_word_tool": "choose_word_tool",
142
- "blue_boss": "blue_boss",
143
  },
144
  )
145
- # builder.add_edge("blue_boss", "choose_word_tool")
 
146
  builder.add_edge("choose_word_tool", "blue_captain")
147
 
148
  builder.add_conditional_edges(
@@ -152,10 +165,11 @@ class MyGraph:
152
  "final_choice": "final_choice",
153
  "transfer_to_agent_1": "transfer_to_agent_1",
154
  "transfer_to_agent_2": "transfer_to_agent_2",
155
- "blue_captain": "blue_captain",
156
  },
157
  )
158
 
 
159
  builder.add_edge("transfer_to_agent_1", "blue_agent_1")
160
  builder.add_edge("transfer_to_agent_2", "blue_agent_2")
161
  builder.add_edge("blue_agent_1", "blue_captain")
@@ -320,7 +334,13 @@ class MyGraph:
320
  llm_with_tools = captain.model.bind_tools(
321
  captain_agent_tools,
322
  )
323
- answer = await llm_with_tools.ainvoke(new_message)
 
 
 
 
 
 
324
 
325
  logger.info(f"[RED CAPTAIN ANSWER]: {answer}")
326
  logger.info("°°°"*50)
@@ -576,7 +596,13 @@ class MyGraph:
576
  llm_with_tools = captain.model.bind_tools(
577
  captain_agent_tools,
578
  )
579
- answer = await llm_with_tools.ainvoke(new_message)
 
 
 
 
 
 
580
 
581
  logger.info(f"[BLUE CAPTAIN ANSWER] : {answer}")
582
  logger.info("°°°"*50)
@@ -718,6 +744,38 @@ class MyGraph:
718
  "turn": turn+1
719
  }
720
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  def judge(self, state: State):
722
  """Evaluate guesses, update board, check win/lose conditions."""
723
 
@@ -1074,7 +1132,7 @@ class MyGraph:
1074
  logger.warning("[WARNING] NESSUN TOOL E' STATO RICHIAMATO, TORNIAMO DAL BOSS")
1075
  current_team = state['current_team']
1076
  logger.info(f"[CURRENT TEAM: {current_team}]")
1077
- return f"{current_team}_boss"
1078
 
1079
  def should_continue(self, state: State) -> str:
1080
  """Determine whether to continue to tools or return a final answer."""
@@ -1099,7 +1157,7 @@ class MyGraph:
1099
  logger.warning("[WARNING] NESSUN TOOL E' STATO RICHIAMATO, TORNIAMO DAL CAPITANO")
1100
  current_team = state['current_team']
1101
  logger.info(f"[CURRENT TEAM: {current_team}]")
1102
- return f"{current_team}_captain"
1103
 
1104
  def _convert_to_string(self, value):
1105
  """Helper to convert any value to string"""
@@ -1247,7 +1305,11 @@ class MyGraph:
1247
  "teams_reviewed": [],
1248
  "history_guessed_words": guessed_words,
1249
  "end_round": False,
1250
- "winner_and_score": None
 
 
 
 
1251
  }
1252
 
1253
  final_msg = ""
 
11
  from support.log_manager import logger
12
  from support.my_tools import captain_agent_tools, boss_agent_tools
13
  from support.tools.boss_agent_tools import create_fake_tool_call
14
+ from support.tools.captain_agent_tools import create_agent_fake_tool_call
15
  from support.prompts import captain_agent_system_prompt, player_agent_system_prompt, boss_agent_system_prompt
16
  from typing import Annotated, List, Literal, Optional
17
  from typing_extensions import TypedDict
 
39
  end_round: bool
40
  winner_and_score: tuple
41
 
42
+ red_boss_is_called_counter: int
43
+ blue_boss_is_called_counter: int
44
+ red_captain_is_called_counter: int
45
+ blue_captain_is_called_counter: int
46
+
47
 
48
  class MyGraph:
49
  def __init__(self):
 
70
  builder.add_node("red_captain", self.red_captain)
71
  builder.add_node("red_agent_1", self.red_agent_1)
72
  builder.add_node("red_agent_2", self.red_agent_2)
73
+ builder.add_node("red_boss_is_called", self.red_boss_is_called)
74
+ builder.add_node("red_captain_is_called", self.red_captain_is_called)
75
 
76
  # Tool nodes
77
  choose_word_tool = ToolNode(tools=[boss_agent_tools[0]])
 
92
  self.boss_choice,
93
  {
94
  "choose_word_tool": "choose_word_tool",
95
+ "red_boss_is_called": "red_boss_is_called",
96
  },
97
  )
98
+
99
+ builder.add_edge("red_boss_is_called", "red_boss")
100
  builder.add_edge("choose_word_tool", "red_captain")
101
 
102
  builder.add_conditional_edges(
 
106
  "final_choice": "final_choice",
107
  "transfer_to_agent_1": "transfer_to_agent_1",
108
  "transfer_to_agent_2": "transfer_to_agent_2",
109
+ "red_captain_is_called": "red_captain_is_called",
110
  },
111
  )
112
 
113
+ builder.add_edge("red_captain_is_called", "red_captain")
114
  builder.add_edge("transfer_to_agent_1", "red_agent_1")
115
  builder.add_edge("transfer_to_agent_2", "red_agent_2")
116
  builder.add_edge("red_agent_1", "red_captain")
 
129
  builder.add_node("blue_captain", self.blue_captain)
130
  builder.add_node("blue_agent_1", self.blue_agent_1)
131
  builder.add_node("blue_agent_2", self.blue_agent_2)
132
+ builder.add_node("blue_boss_is_called", self.blue_boss_is_called)
133
+ builder.add_node("blue_captain_is_called", self.blue_captain_is_called)
134
 
135
  # Tool nodes
136
  choose_word_tool = ToolNode(tools=[boss_agent_tools[0]])
 
151
  self.boss_choice,
152
  {
153
  "choose_word_tool": "choose_word_tool",
154
+ "blue_boss_is_called": "blue_boss_is_called",
155
  },
156
  )
157
+
158
+ builder.add_edge("blue_boss_is_called", "blue_boss")
159
  builder.add_edge("choose_word_tool", "blue_captain")
160
 
161
  builder.add_conditional_edges(
 
165
  "final_choice": "final_choice",
166
  "transfer_to_agent_1": "transfer_to_agent_1",
167
  "transfer_to_agent_2": "transfer_to_agent_2",
168
+ "blue_captain_is_called": "blue_captain_is_called",
169
  },
170
  )
171
 
172
+ builder.add_edge("blue_captain_is_called", "blue_captain")
173
  builder.add_edge("transfer_to_agent_1", "blue_agent_1")
174
  builder.add_edge("transfer_to_agent_2", "blue_agent_2")
175
  builder.add_edge("blue_agent_1", "blue_captain")
 
334
  llm_with_tools = captain.model.bind_tools(
335
  captain_agent_tools,
336
  )
337
+
338
+ if state['red_captain_is_called_counter'] > 4:
339
+ logger.info("Creating fake answer to stop loop")
340
+ answer = create_agent_fake_tool_call()
341
+ else:
342
+ logger.info(f"red_captain_is_called_counter: {state['red_captain_is_called_counter']}")
343
+ answer = await llm_with_tools.ainvoke(new_message)
344
 
345
  logger.info(f"[RED CAPTAIN ANSWER]: {answer}")
346
  logger.info("°°°"*50)
 
596
  llm_with_tools = captain.model.bind_tools(
597
  captain_agent_tools,
598
  )
599
+
600
+ if state['blue_captain_is_called_counter'] > 4:
601
+ logger.info("Creating fake answer to stop loop")
602
+ answer = create_agent_fake_tool_call()
603
+ else:
604
+ logger.info(f"blue_captain_is_called_counter: {state['blue_captain_is_called_counter']}")
605
+ answer = await llm_with_tools.ainvoke(new_message)
606
 
607
  logger.info(f"[BLUE CAPTAIN ANSWER] : {answer}")
608
  logger.info("°°°"*50)
 
744
  "turn": turn+1
745
  }
746
 
747
+ def red_boss_is_called(self, state: State):
748
+ """Update turn counter"""
749
+ _counter = state.get("red_boss_is_called_counter", 1)
750
+
751
+ return {
752
+ "red_boss_is_called_counter": _counter+1
753
+ }
754
+
755
+ def blue_boss_is_called(self, state: State):
756
+ """Update turn counter"""
757
+ _counter = state.get("blue_boss_is_called_counter", 1)
758
+
759
+ return {
760
+ "blue_boss_is_called_counter": _counter+1
761
+ }
762
+
763
+ def red_captain_is_called(self, state: State):
764
+ """Update turn counter"""
765
+ _counter = state.get("red_captain_is_called_counter", 1)
766
+
767
+ return {
768
+ "red_captain_is_called_counter": _counter+1
769
+ }
770
+
771
+ def blue_captain_is_called(self, state: State):
772
+ """Update turn counter"""
773
+ _counter = state.get("blue_captain_is_called_counter", 1)
774
+
775
+ return {
776
+ "blue_captain_is_called_counter": _counter+1
777
+ }
778
+
779
  def judge(self, state: State):
780
  """Evaluate guesses, update board, check win/lose conditions."""
781
 
 
1132
  logger.warning("[WARNING] NESSUN TOOL E' STATO RICHIAMATO, TORNIAMO DAL BOSS")
1133
  current_team = state['current_team']
1134
  logger.info(f"[CURRENT TEAM: {current_team}]")
1135
+ return f"{current_team}_boss_is_called"
1136
 
1137
  def should_continue(self, state: State) -> str:
1138
  """Determine whether to continue to tools or return a final answer."""
 
1157
  logger.warning("[WARNING] NESSUN TOOL E' STATO RICHIAMATO, TORNIAMO DAL CAPITANO")
1158
  current_team = state['current_team']
1159
  logger.info(f"[CURRENT TEAM: {current_team}]")
1160
+ return f"{current_team}_captain_is_called"
1161
 
1162
  def _convert_to_string(self, value):
1163
  """Helper to convert any value to string"""
 
1305
  "teams_reviewed": [],
1306
  "history_guessed_words": guessed_words,
1307
  "end_round": False,
1308
+ "winner_and_score": None,
1309
+ "red_boss_is_called_counter": 0,
1310
+ "blue_boss_is_called_counter": 0,
1311
+ "red_captain_is_called_counter": 0,
1312
+ "blue_captain_is_called_counter": 0,
1313
  }
1314
 
1315
  final_msg = ""
support/my_tools.py CHANGED
@@ -39,24 +39,6 @@ async def load_tools():
39
  return boss_agent_tools, captain_agent_tools
40
 
41
 
42
- # boss_client = MultiServerMCPClient(
43
- # {
44
- # "BossServer": {
45
- # "url": "http://localhost:8000/mcp",
46
- # "transport": "streamable_http",
47
- # }
48
- # }
49
- # )
50
-
51
- # captain_client = MultiServerMCPClient(
52
- # {
53
- # "CaptainServer": {
54
- # "url": "http://localhost:8001/mcp",
55
- # "transport": "streamable_http",
56
- # }
57
- # }
58
- # )
59
-
60
  boss_client = MultiServerMCPClient(
61
  {
62
  "BossServer": {
 
39
  return boss_agent_tools, captain_agent_tools
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  boss_client = MultiServerMCPClient(
43
  {
44
  "BossServer": {
support/style/css.py CHANGED
@@ -1854,7 +1854,7 @@ strong {
1854
  margin-bottom: 16px;
1855
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
1856
  transition: transform 0.2s, box-shadow 0.2s;
1857
- border-left: 4px solid #e0e0e0;
1858
  }
1859
 
1860
  .message_card:hover {
@@ -1908,11 +1908,19 @@ strong {
1908
  padding-left: 52px;
1909
  }
1910
 
1911
- .message_content li,
1912
- .message_content code {
1913
  color: black;
1914
  }
1915
 
 
 
 
 
 
 
 
 
1916
  .message_thinking {
1917
  border-left-color: #9b59b6;
1918
  background: linear-gradient(to right, #f8f4fc, white);
@@ -2247,7 +2255,7 @@ final_css = navbar_css + stats_css + css + """
2247
  margin-bottom: 16px;
2248
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
2249
  transition: transform 0.2s, box-shadow 0.2s;
2250
- border-left: 4px solid #e0e0e0;
2251
  }
2252
 
2253
  .message_card:hover {
 
1854
  margin-bottom: 16px;
1855
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
1856
  transition: transform 0.2s, box-shadow 0.2s;
1857
+ border-left: 8px solid #e0e0e0;
1858
  }
1859
 
1860
  .message_card:hover {
 
1908
  padding-left: 52px;
1909
  }
1910
 
1911
+ .message_content,
1912
+ .message_content * {
1913
  color: black;
1914
  }
1915
 
1916
+ .message_content think{
1917
+ font-style: italic;
1918
+ }
1919
+
1920
+ .message_content .prose * {
1921
+ color: #000000 !important;
1922
+ }
1923
+
1924
  .message_thinking {
1925
  border-left-color: #9b59b6;
1926
  background: linear-gradient(to right, #f8f4fc, white);
 
2255
  margin-bottom: 16px;
2256
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
2257
  transition: transform 0.2s, box-shadow 0.2s;
2258
+ border-left: 8px solid #e0e0e0;
2259
  }
2260
 
2261
  .message_card:hover {
support/tools/captain_agent_tools.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fake tool call generator when human user is playing
2
+
3
+ def create_agent_fake_tool_call():
4
+ from langchain_core.messages import AIMessage
5
+ from uuid import uuid4
6
+ """Creates a fake AIMessage that mimics an LLM tool call"""
7
+ tool_call_id = str(uuid4())
8
+
9
+ fake_message = AIMessage(
10
+ content="",
11
+ tool_calls=[
12
+ {
13
+ "name": "TeamFinalChoice",
14
+ "args": {
15
+ "guesses": ['STOP_TURN'],
16
+ },
17
+ "id": tool_call_id,
18
+ "type": "tool_call"
19
+ }
20
+ ]
21
+ )
22
+
23
+ return fake_message
support/utils.py CHANGED
@@ -1,10 +1,17 @@
1
  import io
2
  import markdown
 
 
 
3
  import matplotlib.pyplot as plt
4
  import matplotlib.patches as mpatches
 
5
  import regex
 
 
6
 
7
  from PIL import Image
 
8
 
9
 
10
  def display_model_name(model_name):
@@ -369,124 +376,14 @@ def format_game_history_html(game_history):
369
  return "".join(games_html)
370
 
371
 
372
- def plot_game_board(board):
373
- """
374
- Create a single combined visualization showing words with their color-coded backgrounds.
375
- Each card displays the word in white text on a colored background (red, blue, beige, black).
376
- """
377
- # Color mapping
378
- color_map = {
379
- 'red': '#dc3545',
380
- 'blue': '#0d6efd',
381
- 'neutral': '#d4c5b9', # beige
382
- 'killer': '#212529' # black
383
- }
384
-
385
- # Get word-color pairs and arrange in 5x5 grid
386
- word_color_pairs = board.get('word_color_pairs', [])
387
- if len(word_color_pairs) < 25:
388
- word_color_pairs = word_color_pairs + [("???", 'neutral')] * (25 - len(word_color_pairs))
389
-
390
- # Create figure with specific size
391
- fig, ax = plt.subplots(figsize=(12, 12))
392
- ax.set_xlim(-0.5, 4.5)
393
- ax.set_ylim(-0.5, 4.5)
394
- ax.set_xticks([])
395
- ax.set_yticks([])
396
- ax.axis('off')
397
- ax.set_aspect('equal')
398
-
399
- # Draw grid of cards
400
- idx = 0
401
- for i in range(5):
402
- for j in range(5):
403
- if idx >= len(word_color_pairs):
404
- break
405
-
406
- word, color = word_color_pairs[idx]
407
- bg_color = color_map.get(color, '#e9ecef')
408
-
409
- # Draw card background with rounded corners effect
410
- rect = mpatches.FancyBboxPatch(
411
- (j - 0.48, i - 0.48), 0.96, 0.96,
412
- boxstyle="round,pad=0.02",
413
- facecolor=bg_color,
414
- edgecolor='#495057',
415
- linewidth=2.5,
416
- zorder=1
417
- )
418
- ax.add_patch(rect)
419
-
420
- # Add word text in white
421
- # Adjust font size based on word length
422
- word_length = len(word)
423
- if word_length > 12:
424
- fontsize = 13
425
- elif word_length > 8:
426
- fontsize = 15
427
- else:
428
- fontsize = 17
429
-
430
- ax.text(
431
- j, i, word,
432
- ha="center", va="center",
433
- fontsize=fontsize,
434
- fontweight='bold',
435
- color='white',
436
- zorder=2,
437
- wrap=True
438
- )
439
-
440
- idx += 1
441
-
442
- plt.gca().invert_yaxis()
443
-
444
- # Add title with starting team info
445
- starting_team = board.get('starting_team', 'red')
446
- title_color = color_map[starting_team]
447
-
448
- fig.suptitle(
449
- f'{starting_team.upper()} TEAM STARTS',
450
- fontsize=18,
451
- fontweight='bold',
452
- color=title_color,
453
- y=0.98
454
- )
455
-
456
- # Add legend
457
- legend_elements = [
458
- mpatches.Patch(facecolor=color_map['red'], edgecolor='#495057', label='Red Team'),
459
- mpatches.Patch(facecolor=color_map['blue'], edgecolor='#495057', label='Blue Team'),
460
- mpatches.Patch(facecolor=color_map['neutral'], edgecolor='#495057', label='Neutral'),
461
- mpatches.Patch(facecolor=color_map['killer'], edgecolor='#495057', label='Killer ☠️')
462
- ]
463
- ax.legend(
464
- handles=legend_elements,
465
- loc='upper center',
466
- bbox_to_anchor=(0.5, -0.02),
467
- ncol=4,
468
- frameon=True,
469
- fancybox=True,
470
- shadow=True,
471
- fontsize=11
472
- )
473
-
474
- plt.tight_layout()
475
-
476
- # Save to buffer
477
- buf = io.BytesIO()
478
- plt.savefig(buf, format="png", bbox_inches="tight", dpi=120, facecolor='white')
479
- buf.seek(0)
480
- plt.close(fig)
481
-
482
- return Image.open(buf)
483
-
484
-
485
  def plot_game_board_with_guesses(board, guessed_words=None):
486
  """
487
  Plot the game board with crosses or marks over guessed words.
488
  """
489
 
 
 
 
490
  color_map = {
491
  'red': '#dc3545',
492
  'blue': '#0d6efd',
@@ -499,7 +396,9 @@ def plot_game_board_with_guesses(board, guessed_words=None):
499
  if len(word_color_pairs) < 25:
500
  word_color_pairs += [("???", 'neutral')] * (25 - len(word_color_pairs))
501
 
502
- fig, ax = plt.subplots(figsize=(12, 12))
 
 
503
  ax.set_xlim(-0.5, 4.5)
504
  ax.set_ylim(-0.5, 4.5)
505
  ax.axis('off')
@@ -514,7 +413,6 @@ def plot_game_board_with_guesses(board, guessed_words=None):
514
  word, color = word_color_pairs[idx]
515
  bg_color = color_map.get(color, '#e9ecef')
516
 
517
- # Draw card background
518
  rect = mpatches.FancyBboxPatch(
519
  (j - 0.48, i - 0.48), 0.96, 0.96,
520
  boxstyle="round,pad=0.02",
@@ -525,7 +423,6 @@ def plot_game_board_with_guesses(board, guessed_words=None):
525
  )
526
  ax.add_patch(rect)
527
 
528
- # Add word
529
  ax.text(
530
  j, i, word,
531
  ha="center", va="center",
@@ -536,7 +433,6 @@ def plot_game_board_with_guesses(board, guessed_words=None):
536
  wrap=True
537
  )
538
 
539
- # If word was guessed, overlay a cross
540
  if word in guessed_words:
541
  ax.plot(
542
  [j - 0.4, j + 0.4], [i - 0.4, i + 0.4],
@@ -549,7 +445,7 @@ def plot_game_board_with_guesses(board, guessed_words=None):
549
 
550
  idx += 1
551
 
552
- plt.gca().invert_yaxis()
553
  starting_team = board.get('starting_team', 'red')
554
  title_color = color_map[starting_team]
555
 
@@ -561,9 +457,26 @@ def plot_game_board_with_guesses(board, guessed_words=None):
561
  y=0.98
562
  )
563
 
564
- plt.tight_layout()
565
- buf = io.BytesIO()
566
- plt.savefig(buf, format="png", bbox_inches="tight", dpi=120, facecolor='white')
567
- buf.seek(0)
568
- plt.close(fig)
569
- return Image.open(buf)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import io
2
  import markdown
3
+
4
+ import matplotlib
5
+ matplotlib.use('Agg')
6
  import matplotlib.pyplot as plt
7
  import matplotlib.patches as mpatches
8
+ import os
9
  import regex
10
+ import tempfile
11
+ import uuid
12
 
13
  from PIL import Image
14
+ from support.log_manager import logger
15
 
16
 
17
  def display_model_name(model_name):
 
376
  return "".join(games_html)
377
 
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  def plot_game_board_with_guesses(board, guessed_words=None):
380
  """
381
  Plot the game board with crosses or marks over guessed words.
382
  """
383
 
384
+ logger.info(f"Board: {board}")
385
+ logger.info(f"Guessed words: {guessed_words}")
386
+
387
  color_map = {
388
  'red': '#dc3545',
389
  'blue': '#0d6efd',
 
396
  if len(word_color_pairs) < 25:
397
  word_color_pairs += [("???", 'neutral')] * (25 - len(word_color_pairs))
398
 
399
+ # Create a new figure explicitly (don't rely on global state)
400
+ fig = plt.figure(figsize=(12, 12))
401
+ ax = fig.add_subplot(111)
402
  ax.set_xlim(-0.5, 4.5)
403
  ax.set_ylim(-0.5, 4.5)
404
  ax.axis('off')
 
413
  word, color = word_color_pairs[idx]
414
  bg_color = color_map.get(color, '#e9ecef')
415
 
 
416
  rect = mpatches.FancyBboxPatch(
417
  (j - 0.48, i - 0.48), 0.96, 0.96,
418
  boxstyle="round,pad=0.02",
 
423
  )
424
  ax.add_patch(rect)
425
 
 
426
  ax.text(
427
  j, i, word,
428
  ha="center", va="center",
 
433
  wrap=True
434
  )
435
 
 
436
  if word in guessed_words:
437
  ax.plot(
438
  [j - 0.4, j + 0.4], [i - 0.4, i + 0.4],
 
445
 
446
  idx += 1
447
 
448
+ ax.invert_yaxis()
449
  starting_team = board.get('starting_team', 'red')
450
  title_color = color_map[starting_team]
451
 
 
457
  y=0.98
458
  )
459
 
460
+ fig.tight_layout()
461
+
462
+ # Create a unique temporary file for this specific request
463
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png', prefix=f'board_{uuid.uuid4().hex}_')
464
+ temp_filename = temp_file.name
465
+ temp_file.close()
466
+
467
+ try:
468
+ # Save to the unique temporary file
469
+ fig.savefig(temp_filename, format="png", bbox_inches="tight", dpi=120, facecolor='white')
470
+ plt.close(fig) # Close the figure immediately
471
+
472
+ # Load the image from the file
473
+ img = Image.open(temp_filename)
474
+ img.load() # Load into memory
475
+
476
+ return img
477
+ finally:
478
+ # Clean up the temporary file
479
+ try:
480
+ os.unlink(temp_filename)
481
+ except:
482
+ pass