BryanBradfo commited on
Commit
3aec18a
·
verified ·
1 Parent(s): b3f4332

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -115
app.py CHANGED
@@ -13,28 +13,38 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
14
  STOCKFISH_PATH = "/usr/games/stockfish"
15
 
16
- # --- DONNÉES ---
17
- def _load_lichess_openings(path_prefix="/app/data/lichess_openings/dist/"):
 
 
18
  try:
19
- files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
20
- dfs = []
21
- for fn in files:
22
- if os.path.exists(fn):
23
- df = pd.read_csv(fn, sep="\t", usecols=["eco", "name", "pgn", "uci", "epd"])
24
- dfs.append(df)
25
- if not dfs: return pd.DataFrame()
26
- return pd.concat(dfs, ignore_index=True)
 
 
 
 
 
 
 
 
27
  except:
28
- return pd.DataFrame()
29
-
30
- OPENINGS_DB = _load_lichess_openings()
31
 
32
- # --- ANALYSE ---
33
-
34
- def get_engine_analysis(board, time_limit=0.5):
35
- """Analyse sécurisée."""
36
- if board.is_game_over(): return "N/A", "PARTIE TERMINÉE", ""
37
- if not os.path.exists(STOCKFISH_PATH): return "0", "Engine missing", ""
 
 
38
 
39
  try:
40
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
@@ -42,31 +52,33 @@ def get_engine_analysis(board, time_limit=0.5):
42
  score = info["score"].white()
43
 
44
  if score.is_mate():
45
- score_val = f"MAT ({abs(score.mate())})"
46
  else:
47
  score_val = str(score.score())
48
 
49
  best_move = info.get("pv", [None])[0]
50
  if best_move:
51
- dest = chess.square_name(best_move.to_square)
 
52
  origin = chess.square_name(best_move.from_square)
53
- desc = f"{origin} ➔ {dest}"
54
- full_desc = f"Bouger en {dest}"
55
  else:
56
- desc, full_desc = "Aucun", "Aucun"
57
 
58
- return score_val, full_desc, desc
59
  except:
60
- return "N/A", "Erreur Analyse", ""
61
 
62
- def get_ai_move(board, level):
 
63
  levels = {
64
- "Débutant": {"time": 0.01, "skill": 1, "depth": 1},
65
- "Intermédiaire": {"time": 0.1, "skill": 8, "depth": 6},
66
- "Avancé": {"time": 0.5, "skill": 15, "depth": 12},
67
- "Grand Maître": {"time": 1.0, "skill": 20, "depth": 18}
68
  }
69
- config = levels.get(level, levels["Débutant"])
70
  try:
71
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
72
  engine.configure({"Skill Level": config["skill"]})
@@ -75,86 +87,82 @@ def get_ai_move(board, level):
75
  except:
76
  return None
77
 
78
- # --- AUDIO & CHAT (CORRECTIF IMPORTANT) ---
79
 
80
  def generate_voice(text):
81
- """Génération audio robuste avec écriture binaire directe."""
82
  if not openai_client or not text: return None
83
  try:
84
  response = openai_client.audio.speech.create(
85
  model="tts-1",
86
- voice="onyx",
87
  input=text,
88
  speed=1.15
89
  )
90
  unique = f"deepblue_{uuid.uuid4()}.mp3"
91
  path = os.path.join("/tmp", unique)
92
-
93
- # CORRECTIF: Utilisation de iter_bytes() au lieu de stream_to_file (déprécié)
94
  with open(path, "wb") as f:
95
  for chunk in response.iter_bytes():
96
  f.write(chunk)
97
-
98
  return path
99
  except Exception as e:
100
- print(f"Audio Error: {e}")
101
  return None
102
 
103
  def transcribe(audio_path):
104
  if not openai_client or not audio_path: return None
105
  try:
106
  with open(audio_path, "rb") as f:
107
- t = openai_client.audio.transcriptions.create(model="whisper-1", file=f, language="fr")
108
  return t.text
109
  except:
110
  return None
111
 
112
- # --- PERSONA DEEP BLUE ---
113
 
114
  SYSTEM_PROMPT = """
115
- Tu es DEEP BLUE, supercalculateur d'échecs.
116
- Tu parles au Joueur Blanc.
117
 
118
- RÈGLE ABSOLUE DE FORMAT :
119
- 1. Dis le COUP à jouer. (ex: "Joue Cavalier f3.", "Prends le pion e5.").
120
- 2. Dis la RAISON stratégique. (ex: "Ceci contrôle le centre.", "Cela ouvre la ligne pour la Tour.").
121
 
122
- Ton : Froid, direct, efficace. Pas de "Bonjour".
123
- Si le joueur gagne : "MAT CONFIRMÉ. FIN DE SESSION."
124
  """
125
 
126
- def consult_deepblue(fen, mode="auto", user_audio=None):
127
  board = chess.Board(fen)
128
 
 
129
  if board.is_game_over():
130
  if board.is_checkmate():
131
- msg = "ÉCHEC ET MAT. PARTIE TERMINÉE."
132
- if board.turn == chess.BLACK: msg += " VICTOIRE HUMAINE."
133
- else: msg += " VICTOIRE SYSTÈME."
134
- return msg, generate_voice(msg), "FIN"
135
- return "PARTIE NULLE.", None, "FIN"
136
 
137
- score, best_move_text, arrow_simple = get_engine_analysis(board)
138
-
139
- opening = "N/A"
140
- if not OPENINGS_DB.empty:
141
- match = OPENINGS_DB[OPENINGS_DB["epd"] == board.epd()]
142
- if not match.empty: opening = match.iloc[0]['name']
143
 
 
144
  context = f"""
145
- [DATA]
146
- Trait : {'Blancs' if board.turn == chess.WHITE else 'Noirs'}.
147
- Score : {score}.
148
- COUP OPTIMAL : {best_move_text} ({arrow_simple}).
 
149
  """
150
 
151
  if mode == "question" and user_audio:
152
  q = transcribe(user_audio)
153
- context += f"\n[INPUT JOUEUR] : {q}"
154
  else:
155
- context += "\nInstruction : Dis le coup et la stratégie."
156
 
157
- reply = "Calcul..."
 
158
  if openai_client:
159
  try:
160
  response = openai_client.chat.completions.create(
@@ -163,95 +171,115 @@ def consult_deepblue(fen, mode="auto", user_audio=None):
163
  )
164
  reply = response.choices[0].message.content
165
  except:
166
- reply = "Erreur IA."
167
 
168
  audio = generate_voice(reply)
169
- return reply, audio, arrow_simple
170
 
171
- # --- BOUCLE DE JEU ---
172
 
173
- def update_history(new_advice, history_list):
174
- if not new_advice or new_advice == "FIN": return history_list
175
- history_list.insert(0, f" {new_advice}")
176
  return history_list
177
 
178
- def format_history_display(history_list):
179
  return "\n\n".join(history_list)
180
 
181
  def game_cycle(fen, level, history_list):
182
  board = chess.Board(fen)
183
 
184
- # 1. Fin de jeu ?
185
  if board.is_game_over():
186
- text, audio, _ = consult_deepblue(fen)
187
- return fen, text, audio, format_history_display(history_list), history_list
188
 
189
- # 2. Tour IA (Noirs)
190
  if board.turn == chess.BLACK:
191
- ai_move = get_ai_move(board, level)
192
  if ai_move: board.push(ai_move)
193
 
194
- text, audio, arrow = consult_deepblue(board.fen(), mode="auto")
195
- new_hist = update_history(f"Conseil: {arrow}", history_list)
196
- return board.fen(), text, audio, format_history_display(new_hist), new_hist
 
 
197
 
198
- return fen, "Attente...", None, format_history_display(history_list), history_list
199
 
200
  def reset_game():
201
- return chess.STARTING_FEN, "SYSTÈME RÉINITIALISÉ.", None, "", []
202
 
203
- def ask_deepblue(fen, audio, history_list):
204
- text, aud, arrow = consult_deepblue(fen, mode="question", user_audio=audio)
205
- return text, aud, format_history_display(history_list), history_list
206
 
207
- # --- UI CSS ---
208
  css = """
209
- body { background-color: #0b0f19; color: #e2e8f0; }
210
- .gradio-container { background-color: #0b0f19 !important; border: none; }
211
- #title { color: #38bdf8; text-align: center; font-family: 'Courier New', monospace; font-size: 2em; margin-bottom: 0; }
212
- #board { border: 2px solid #38bdf8; box-shadow: 0 0 20px rgba(56, 189, 248, 0.2); }
213
- button.primary { background-color: #0ea5e9 !important; color: white !important; font-weight: bold; }
 
 
 
 
 
 
 
 
 
214
  button.secondary { background-color: #1e293b !important; color: #94a3b8 !important; border: 1px solid #334155; }
215
- .feedback { background-color: #1e293b !important; color: #38bdf8 !important; border: 1px solid #334155; font-family: 'Courier New'; }
 
 
 
 
 
 
 
216
  """
217
 
218
  # --- INTERFACE ---
219
 
220
- with gr.Blocks(title="Coach Deep Blue", css=css, theme=gr.themes.Base()) as demo:
221
 
222
- # HEADER
223
  with gr.Row():
224
- gr.Markdown("# 🟦 COACH DEEP BLUE", elem_id="title")
225
 
226
  with gr.Row():
227
- # Label modifié comme demandé
228
- level = gr.Dropdown(["Débutant", "Intermédiaire", "Avancé", "Grand Maître"], value="Débutant", label="Difficulté Adversaire", interactive=True)
 
 
 
 
229
 
230
- # MAIN
231
  with gr.Row():
232
- # GAUCHE (PLATEAU)
233
  with gr.Column(scale=2):
234
- board = Chessboard(elem_id="board", label="Zone de Combat", value=chess.STARTING_FEN, game_mode=True, interactive=True)
235
 
236
- # DROITE (CONTROLES)
237
  with gr.Column(scale=1):
238
- btn_reset = gr.Button("🔄 RESTART SYSTEM", variant="secondary")
239
 
240
- gr.Markdown("### 📟 Terminal Sortie")
241
- coach_txt = gr.Textbox(label="Message", interactive=False, lines=3, elem_classes="feedback")
242
 
243
- # AUDIO VISIBLE (Pour que l'autoplay fonctionne)
244
- coach_audio = gr.Audio(label="Synthèse", autoplay=True, interactive=False, type="filepath", visible=True)
245
 
246
- gr.Markdown("### 📜 Log Tactique")
247
  history_state = gr.State([])
248
- history_display = gr.Textbox(label="Historique Conseils", interactive=False, lines=5, max_lines=10, elem_classes="feedback")
249
 
250
- gr.Markdown("### 🎤 Input Vocal")
251
  mic = gr.Audio(sources=["microphone"], type="filepath", show_label=False)
252
- btn_ask = gr.Button("TRANSMETTRE QUESTION", variant="primary")
253
 
254
- # LOGIQUE
255
  board.move(
256
  fn=game_cycle,
257
  inputs=[board, level, history_state],
@@ -259,8 +287,8 @@ with gr.Blocks(title="Coach Deep Blue", css=css, theme=gr.themes.Base()) as demo
259
  )
260
 
261
  btn_reset.click(fn=reset_game, outputs=[board, coach_txt, coach_audio, history_display, history_state])
262
- btn_ask.click(fn=ask_deepblue, inputs=[board, mic, history_state], outputs=[coach_txt, coach_audio, history_display, history_state])
263
- mic.stop_recording(fn=ask_deepblue, inputs=[board, mic, history_state], outputs=[coach_txt, coach_audio, history_display, history_state])
264
 
265
  if __name__ == "__main__":
266
  demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False, allowed_paths=["/tmp"])
 
13
  openai_client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
14
  STOCKFISH_PATH = "/usr/games/stockfish"
15
 
16
+ # --- MCP TOOL: OPENING DATABASE ---
17
+ def tool_get_opening(board):
18
+ """Tool to retrieve opening name from Lichess DB."""
19
+ path_prefix = "/app/data/lichess_openings/dist/"
20
  try:
21
+ # Lazy loading to avoid global scope issues
22
+ if not hasattr(tool_get_opening, "db"):
23
+ files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
24
+ dfs = []
25
+ for fn in files:
26
+ if os.path.exists(fn):
27
+ df = pd.read_csv(fn, sep="\t", usecols=["eco", "name", "pgn", "uci", "epd"])
28
+ dfs.append(df)
29
+ tool_get_opening.db = pd.concat(dfs, ignore_index=True) if dfs else pd.DataFrame()
30
+
31
+ if tool_get_opening.db.empty: return "Unknown Opening"
32
+
33
+ match = tool_get_opening.db[tool_get_opening.db["epd"] == board.epd()]
34
+ if not match.empty:
35
+ return f"{match.iloc[0]['eco']} - {match.iloc[0]['name']}"
36
+ return "Unknown / Middle Game"
37
  except:
38
+ return "Database Error"
 
 
39
 
40
+ # --- MCP TOOL: STOCKFISH ENGINE ---
41
+ def tool_engine_analysis(board, time_limit=0.5):
42
+ """
43
+ Tool that uses Stockfish to calculate score and best move.
44
+ Returns: Score string, Best move (Coordinate format).
45
+ """
46
+ if board.is_game_over(): return "GAME OVER", "NONE", "NONE"
47
+ if not os.path.exists(STOCKFISH_PATH): return "0", "Engine Missing", ""
48
 
49
  try:
50
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
 
52
  score = info["score"].white()
53
 
54
  if score.is_mate():
55
+ score_val = f"MATE IN {abs(score.mate())}"
56
  else:
57
  score_val = str(score.score())
58
 
59
  best_move = info.get("pv", [None])[0]
60
  if best_move:
61
+ # ROBOTIC PRECISION: Use coordinates only (e.g., e2e4)
62
+ # This avoids any hallucination about piece names.
63
  origin = chess.square_name(best_move.from_square)
64
+ dest = chess.square_name(best_move.to_square)
65
+ move_uci = f"{origin} -> {dest}" # e.g. "e2 -> e4"
66
  else:
67
+ move_uci = "NO MOVE FOUND"
68
 
69
+ return score_val, move_uci, move_uci
70
  except:
71
+ return "N/A", "ANALYSIS ERROR", ""
72
 
73
+ def tool_ai_play(board, level):
74
+ """Tool to generate the Opponent's move."""
75
  levels = {
76
+ "Beginner": {"time": 0.01, "skill": 1, "depth": 1},
77
+ "Intermediate": {"time": 0.1, "skill": 8, "depth": 6},
78
+ "Advanced": {"time": 0.5, "skill": 15, "depth": 12},
79
+ "Grandmaster": {"time": 1.0, "skill": 20, "depth": 18}
80
  }
81
+ config = levels.get(level, levels["Beginner"])
82
  try:
83
  with chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH) as engine:
84
  engine.configure({"Skill Level": config["skill"]})
 
87
  except:
88
  return None
89
 
90
+ # --- AUDIO & TRANSCRIPTION ---
91
 
92
  def generate_voice(text):
 
93
  if not openai_client or not text: return None
94
  try:
95
  response = openai_client.audio.speech.create(
96
  model="tts-1",
97
+ voice="onyx", # Deep, robotic male voice
98
  input=text,
99
  speed=1.15
100
  )
101
  unique = f"deepblue_{uuid.uuid4()}.mp3"
102
  path = os.path.join("/tmp", unique)
 
 
103
  with open(path, "wb") as f:
104
  for chunk in response.iter_bytes():
105
  f.write(chunk)
 
106
  return path
107
  except Exception as e:
108
+ print(f"Audio Gen Error: {e}")
109
  return None
110
 
111
  def transcribe(audio_path):
112
  if not openai_client or not audio_path: return None
113
  try:
114
  with open(audio_path, "rb") as f:
115
+ t = openai_client.audio.transcriptions.create(model="whisper-1", file=f, language="en") # English Transcription
116
  return t.text
117
  except:
118
  return None
119
 
120
+ # --- AGENT BRAIN (DEEP BLUE) ---
121
 
122
  SYSTEM_PROMPT = """
123
+ You are DEEP BLUE, a chess supercomputer.
124
+ You are communicating with the Human Player (White pieces).
125
 
126
+ PROTOCOL:
127
+ 1. **IDENTIFY MOVE**: State the optimal move using coordinates (e.g., "Move e2 to e4"). Do not use piece names to avoid errors.
128
+ 2. **STRATEGIC LOGIC**: Explain *why* in one concise sentence (e.g., "Controls center," "Threatens Queen," "Safe position").
129
 
130
+ TONE: Robotic, High-Tech, Concise. No polite filler.
131
+ IF PLAYER WINS: "CHECKMATE CONFIRMED. HUMAN INTELLECT EXCEEDS PARAMETERS."
132
  """
133
 
134
+ def agent_reasoning(fen, mode="auto", user_audio=None):
135
  board = chess.Board(fen)
136
 
137
+ # 1. Check Game State
138
  if board.is_game_over():
139
  if board.is_checkmate():
140
+ msg = "CHECKMATE. GAME OVER."
141
+ msg += " HUMAN VICTORY." if board.turn == chess.BLACK else " SYSTEM VICTORY."
142
+ return msg, generate_voice(msg), "END"
143
+ return "DRAW / STALEMATE.", None, "END"
 
144
 
145
+ # 2. Use Tools (MCP Pattern)
146
+ score, best_move_uci, arrow_visual = tool_engine_analysis(board)
147
+ opening = tool_get_opening(board)
 
 
 
148
 
149
+ # 3. Construct Context
150
  context = f"""
151
+ [SYSTEM DATA]
152
+ Turn: {'White' if board.turn == chess.WHITE else 'Black'}.
153
+ Score (Centipawns): {score}.
154
+ Opening DB: {opening}.
155
+ OPTIMAL MOVE CALCULATED: {best_move_uci}.
156
  """
157
 
158
  if mode == "question" and user_audio:
159
  q = transcribe(user_audio)
160
+ context += f"\n[HUMAN QUERY]: {q}"
161
  else:
162
+ context += "\nINSTRUCTION: Output the optimal move and the strategic reason."
163
 
164
+ # 4. LLM Synthesis
165
+ reply = "Computing..."
166
  if openai_client:
167
  try:
168
  response = openai_client.chat.completions.create(
 
171
  )
172
  reply = response.choices[0].message.content
173
  except:
174
+ reply = "Connection Error."
175
 
176
  audio = generate_voice(reply)
177
+ return reply, audio, arrow_visual
178
 
179
+ # --- GAME LOOP ---
180
 
181
+ def update_log(new_advice, history_list):
182
+ if not new_advice or new_advice == "END": return history_list
183
+ history_list.insert(0, f"> {new_advice}")
184
  return history_list
185
 
186
+ def format_log(history_list):
187
  return "\n\n".join(history_list)
188
 
189
  def game_cycle(fen, level, history_list):
190
  board = chess.Board(fen)
191
 
192
+ # Check Instant Win (User mated AI)
193
  if board.is_game_over():
194
+ text, audio, _ = agent_reasoning(fen)
195
+ return fen, text, audio, format_log(history_list), history_list
196
 
197
+ # AI Turn (Black)
198
  if board.turn == chess.BLACK:
199
+ ai_move = tool_ai_play(board, level)
200
  if ai_move: board.push(ai_move)
201
 
202
+ # Deep Blue analyzes the new position for the player
203
+ text, audio, arrow = agent_reasoning(board.fen(), mode="auto")
204
+ new_hist = update_log(f"OPT: {arrow}", history_list)
205
+
206
+ return board.fen(), text, audio, format_log(new_hist), new_hist
207
 
208
+ return fen, "Awaiting Input...", None, format_log(history_list), history_list
209
 
210
  def reset_game():
211
+ return chess.STARTING_FEN, "SYSTEM REBOOTED.", None, "", []
212
 
213
+ def ask_agent(fen, audio, history_list):
214
+ text, aud, arrow = agent_reasoning(fen, mode="question", user_audio=audio)
215
+ return text, aud, format_log(history_list), history_list
216
 
217
+ # --- UI CSS (MODERN BLUE) ---
218
  css = """
219
+ body { background-color: #020617; color: #e2e8f0; }
220
+ .gradio-container { background-color: #020617 !important; border: none; }
221
+ #title {
222
+ color: #0ea5e9;
223
+ text-align: center;
224
+ font-family: 'Courier New', monospace;
225
+ font-size: 4em; /* TITRE GEANT */
226
+ font-weight: 800;
227
+ margin-bottom: 20px;
228
+ letter-spacing: -2px;
229
+ text-shadow: 0 0 10px rgba(14, 165, 233, 0.5);
230
+ }
231
+ #board { border: 3px solid #0ea5e9; box-shadow: 0 0 30px rgba(14, 165, 233, 0.1); }
232
+ button.primary { background-color: #0ea5e9 !important; color: black !important; font-weight: bold; border: none; }
233
  button.secondary { background-color: #1e293b !important; color: #94a3b8 !important; border: 1px solid #334155; }
234
+ .feedback {
235
+ background-color: #0f172a !important;
236
+ color: #38bdf8 !important;
237
+ border: 1px solid #1e293b;
238
+ font-family: 'Consolas', 'Monaco', monospace;
239
+ font-size: 1.1em;
240
+ }
241
+ label { color: #64748b !important; font-weight: bold; }
242
  """
243
 
244
  # --- INTERFACE ---
245
 
246
+ with gr.Blocks(title="DEEP BLUE", css=css, theme=gr.themes.Base()) as demo:
247
 
 
248
  with gr.Row():
249
+ gr.Markdown("# DEEP BLUE", elem_id="title")
250
 
251
  with gr.Row():
252
+ level = gr.Dropdown(
253
+ ["Beginner", "Intermediate", "Advanced", "Grandmaster"],
254
+ value="Beginner",
255
+ label="OPPONENT DIFFICULTY",
256
+ interactive=True
257
+ )
258
 
 
259
  with gr.Row():
260
+ # LEFT: BOARD
261
  with gr.Column(scale=2):
262
+ board = Chessboard(elem_id="board", label="Battle Zone", value=chess.STARTING_FEN, game_mode=True, interactive=True)
263
 
264
+ # RIGHT: CONTROLS
265
  with gr.Column(scale=1):
266
+ btn_reset = gr.Button("INITIALIZE NEW SEQUENCE", variant="secondary")
267
 
268
+ gr.Markdown("### 📟 SYSTEM OUTPUT")
269
+ coach_txt = gr.Textbox(label="Analysis", interactive=False, lines=3, elem_classes="feedback")
270
 
271
+ # Audio visible for autoplay, but minimal
272
+ coach_audio = gr.Audio(label="Voice Synthesis", autoplay=True, interactive=False, type="filepath", visible=True)
273
 
274
+ gr.Markdown("### 📜 STRATEGY LOG")
275
  history_state = gr.State([])
276
+ history_display = gr.Textbox(label="Optimization History", interactive=False, lines=6, max_lines=10, elem_classes="feedback")
277
 
278
+ gr.Markdown("### 🎤 HUMAN INTERFACE")
279
  mic = gr.Audio(sources=["microphone"], type="filepath", show_label=False)
280
+ btn_ask = gr.Button("QUERY SYSTEM", variant="primary")
281
 
282
+ # LOGIC MAPPING
283
  board.move(
284
  fn=game_cycle,
285
  inputs=[board, level, history_state],
 
287
  )
288
 
289
  btn_reset.click(fn=reset_game, outputs=[board, coach_txt, coach_audio, history_display, history_state])
290
+ btn_ask.click(fn=ask_agent, inputs=[board, mic, history_state], outputs=[coach_txt, coach_audio, history_display, history_state])
291
+ mic.stop_recording(fn=ask_agent, inputs=[board, mic, history_state], outputs=[coach_txt, coach_audio, history_display, history_state])
292
 
293
  if __name__ == "__main__":
294
  demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False, allowed_paths=["/tmp"])