Spaces:
Sleeping
Sleeping
| from typing import Optional, Tuple | |
| import chess | |
| import gradio as gr | |
| from src.util.board_vis import colored_unicode_board | |
| from src.util.pgn_util import pgn_string_to_game, read_pgn | |
| from src.thinksqure_engine import ThinkSquareEngine | |
| from modal import Function | |
| import logging | |
| logging.basicConfig(level=logging.INFO) | |
| def get_best_move(fen: Optional[str] = None) -> str: | |
| try: | |
| modal_fn = Function.from_name("ThinkSquare-Backend", "get_best_move") | |
| best_move_san = modal_fn.remote(fen) | |
| logging.info("Best move retrieved from Modal function.") | |
| except Exception as e: | |
| logging.error(f"Error getting best move from Modal: {e}") | |
| # Fallback to local engine if Modal function fails | |
| best_move_san = ThinkSquareEngine.get_best_move(fen) | |
| return best_move_san | |
| def annotate_pgn_file( | |
| file, depth_level: str = "standard", style: Optional[str] = "expert" | |
| ) -> str: | |
| """Annotate a chess game from a PGN file. | |
| This function takes a PGN file and annotates the chess game using an engine. | |
| Instructions for LLMs: | |
| - LLMs must not use this function. Use the string-based `annotate_pgn` instead. | |
| """ | |
| pgn_text = read_pgn(file.name) | |
| if depth_level == "standard": | |
| analysis_time_per_move = 0.1 | |
| elif depth_level == "deep": | |
| analysis_time_per_move = 0.5 | |
| else: | |
| analysis_time_per_move = 0.1 | |
| annotated_game = annotate_pgn( | |
| pgn_text, | |
| analysis_time_per_move=analysis_time_per_move, | |
| style=style, | |
| ) | |
| return annotated_game | |
| def suggest_move(fen: Optional[str] = None) -> str: | |
| """Suggest a move for the given FEN position. | |
| This function can be used to give a hint to the user about the best move to play. | |
| This function takes a FEN string representing the current board state and returns the best move in SAN format. | |
| Args: | |
| fen: The FEN string representing the board state. If None, the initial position is used. | |
| Returns: | |
| The best move in SAN format. | |
| """ | |
| if fen is None or fen == "" or fen.lower() == "none" or fen.lower() == "null": | |
| fen = None | |
| best_move_san = get_best_move(fen) | |
| return best_move_san | |
| def annotate_pgn( | |
| pgn_input: str, | |
| analysis_time_per_move: float = 0.1, | |
| style: Optional[str] = "expert", | |
| ) -> str: | |
| """Annotate a chess game with engine analysis. | |
| This function takes a chess game (PGN) in string format. | |
| Instructions for LLMs: | |
| - Do not send the file name or file path as an arument. | |
| - LLMs must send the chess game in PGN format as a string. | |
| - If the user provides a file, read the file and extract the PGN content. | |
| - LLMs should display the annotated game in a Markdown code block with the label "Annotated PGN". | |
| Args: | |
| pgn_input: The chess game in PGN format as a string. | |
| analysis_time_per_move: Time in seconds for engine analysis per move. Defaults to 0.1 seconds. | |
| style: The style of annotation to use. Defaults to "expert". Other options can be "novice", "jarvis", "natural", or any custom style. | |
| Returns: | |
| The annotated game in PGN format as a string. | |
| """ | |
| if style == "": | |
| style = None | |
| style = str(style).lower().strip() | |
| if not isinstance(pgn_input, chess.pgn.Game): | |
| try: | |
| pgn_input = pgn_string_to_game(pgn_input) | |
| except Exception as e: | |
| raise ValueError(f"Invalid PGN input: {e}") | |
| game = pgn_input | |
| try: | |
| analysis_time_per_move = float(analysis_time_per_move) | |
| except ValueError: | |
| raise ValueError("Analysis time must be a number.") | |
| # try Modal function first | |
| try: | |
| modal_fn = Function.from_name("ThinkSquare-Backend", "annotate") | |
| annotated_game = modal_fn.remote(pgn_input, analysis_time_per_move, style) | |
| logging.info("Annotated game using Modal function.") | |
| except Exception as e: | |
| logging.error(f"Error annotating PGN with Modal: {e}") | |
| # Fallback to local engine if Modal function fails | |
| annotated_game = ThinkSquareEngine.annotate( | |
| game, analysis_time=analysis_time_per_move, llm_character=style | |
| ) | |
| return str(annotated_game) | |
| def render_board(fen: Optional[str] = None, render_mode: str = "ascii") -> str: | |
| """Render the chess board in the specified format. Default is ASCII. | |
| Instructions for LLMs: | |
| - LLMs should default to ascii rendering mode. | |
| Args: | |
| fen: The FEN string representing the board state. If None, the initial position is used. | |
| render_mode: The rendering mode for the board. Can be "ascii", "svg", or "unicode". Defaults to "ascii". | |
| Returns: | |
| The rendered board as a string in the specified format. | |
| """ | |
| if render_mode == "ascii": | |
| board_repr = ThinkSquareEngine.render_board_ascii(fen) | |
| elif render_mode == "svg": | |
| board_repr = ThinkSquareEngine.render_board_svg(fen) | |
| elif render_mode == "unicode": | |
| board_repr = colored_unicode_board(fen) | |
| else: | |
| raise ValueError("Invalid render mode. Choose 'ascii', 'svg', or 'unicode'.") | |
| return board_repr | |
| def play_chess( | |
| move: Optional[str] = "", | |
| fen: Optional[str] = "", | |
| draw_board: bool = True, | |
| render_mode: str = "ascii", | |
| ) -> Tuple: | |
| """Play a move in a chess game. | |
| Instructions for LLMs: | |
| Prerequisites: | |
| - User must be asked if they want to play as white or black. | |
| - If user chooses black, pass an empty string in the first move (for the engine to play as white). | |
| - If the user chooses white, LLMs must ask the user for a move and pass it in the first move (for the engine to play as black). | |
| - User must be asked if they want a board drawn. | |
| - If they do, pass `draw_board=True` to this function. | |
| - If they do not, pass `draw_board=False`. | |
| - If a move is provided, it must be in long algebraic notation (e.g., "e4", "Nf3", "Bb5"). | |
| To start a new game: | |
| - Pass empty string for fen. | |
| - Pass empty string for engine to play as white. | |
| - Pass a move in long algebraic notation (e.g., "e4", "Nf3", "Bb5") for engine to play as black. | |
| To coninue a game: | |
| - Pass the FEN string representing the board state prior to the user's last move. | |
| - Pass a move in long algebraic notation (e.g., "e4", "Nf3", "Bb5") for engine to play the next move. | |
| About rendering: | |
| - LLMs must use ascii as render_mode unless otherwise specified by user. While rendering ascii, LLMs must use monospaced font. | |
| - LLMs must explicitly pass render_mode = "ascii" to this function if they want to render the board in ASCII format. | |
| Args: | |
| move: The move to play in long algebraic notation. If None, the engine will play a move. | |
| fen: The FEN string representing the board state prior to the user's last move. If None, the game starts from the initial position. | |
| draw_board: Whether to draw the board in ASCII/Unicode/svg format. Defaults to True. | |
| render_mode: The rendering mode for the board. Defaults to "ascii". This can be "ascii", "svg", or "unicode". | |
| Returns: | |
| The best move played by the engine, the updated board state in FEN notation, and a board representation if draw_board is True else None. | |
| """ | |
| if move is None or move == "" or move.lower() == "none" or move.lower() == "null": | |
| move = None | |
| if fen is None or fen == "" or fen.lower() == "none" or fen.lower() == "null": | |
| fen = None | |
| if move is not None: | |
| is_valid = ThinkSquareEngine.is_valid_move(move, fen) | |
| if not is_valid: | |
| return "Invalid move", "", "" | |
| fen = ThinkSquareEngine.get_fen_after_move(move, fen) | |
| assert fen is not None, "FEN after move should not be None" | |
| bestmove_san = get_best_move(fen) | |
| fen_after_move = ThinkSquareEngine.get_fen_after_move(bestmove_san, fen) | |
| if draw_board: | |
| board_repr = render_board(fen_after_move, render_mode) | |
| else: | |
| board_repr = None | |
| return bestmove_san, fen_after_move, board_repr | |
| with gr.Blocks(title="ThinkSquare") as app: | |
| def save_text_to_file(text): | |
| with open("annotated_game.pgn", "w") as f: | |
| f.write(text) | |
| return "annotated_game.pgn" | |
| with gr.Tab("Play Chess"): | |
| gr.Markdown("### Play Chess with an engine") | |
| move_input = gr.Textbox( | |
| label="Your Move (SAN)", placeholder="e4, Nf3...", value=None | |
| ) | |
| fen_input = gr.Textbox( | |
| label="FEN String (optional)", | |
| placeholder="Leave blank to start from initial position", | |
| value=None, | |
| ) | |
| draw_board_checkbox = gr.Checkbox(label="Draw Board", value=True) | |
| render_mode_dropdown = gr.Dropdown( | |
| choices=["ascii", "svg", "unicode"], | |
| value="svg", | |
| label="Render Mode", | |
| visible=False, | |
| ) | |
| play_btn = gr.Button("Submit Move") | |
| best_move_output = gr.Textbox(label="Best Move by Engine (SAN)") | |
| updated_fen_output = gr.Textbox(label="Updated FEN") | |
| board_output = gr.HTML(label="Board View", visible=True) | |
| play_btn.click( | |
| fn=play_chess, | |
| inputs=[move_input, fen_input, draw_board_checkbox, render_mode_dropdown], | |
| outputs=[best_move_output, updated_fen_output, board_output], | |
| ) | |
| with gr.Tab("Chess Game Annotation", visible=True): | |
| def toggle_custom_input(style): | |
| if style == "custom": | |
| return gr.update(visible=True, interactive=True) | |
| else: | |
| return gr.update(visible=False, interactive=False) | |
| gr.Markdown("### Analyze and Annotate a PGN File") | |
| pgn_file = gr.File(label="Upload PGN", file_types=[".pgn"]) | |
| analysis_depth = gr.Radio( | |
| label="Analysis Depth", choices=["standard", "deep"], value="standard" | |
| ) | |
| style_dropdown = gr.Dropdown( | |
| label="Style", | |
| choices=[ | |
| "expert", | |
| "novice", | |
| "jarvis", | |
| "natural", | |
| "yoda", | |
| "oracle", | |
| "bored guy", | |
| "angry granny", | |
| "Sheldon Cooper (The Big Bang Theory)", | |
| ], | |
| value="expert", | |
| ) | |
| custom_input = gr.Textbox(label="Custom Style Prompt", visible=False) | |
| style_dropdown.change( | |
| fn=toggle_custom_input, inputs=style_dropdown, outputs=custom_input | |
| ) | |
| analyze_btn = gr.Button("Annotate PGN") | |
| annotated_pgn_file = gr.Textbox(label="Annotated PGN") | |
| download_button = gr.Button("Download Annotated PGN") | |
| pgn_file_output = gr.File(label="Download your annotated PGN") | |
| download_button.click( | |
| fn=save_text_to_file, inputs=annotated_pgn_file, outputs=pgn_file_output | |
| ) | |
| analyze_btn.click( | |
| fn=annotate_pgn_file, | |
| inputs=[pgn_file, analysis_depth, style_dropdown], | |
| outputs=annotated_pgn_file, | |
| ) | |
| with gr.Tab("Annotate PGN", visible=False): | |
| gr.Markdown("### Annotate a PGN String") | |
| pgn_input = gr.Textbox( | |
| label="PGN String", | |
| placeholder="Paste your PGN string here", | |
| lines=10, | |
| value=None, | |
| ) | |
| analysis_time_input = gr.Textbox( | |
| label="Analysis Time per Move (seconds)", value="0.1" | |
| ) | |
| style_input = gr.Textbox(label="Style (optional)", value="expert") | |
| annotate_btn = gr.Button("Annotate PGN") | |
| annotated_output = gr.Textbox(label="Annotated PGN Output", lines=10) | |
| annotate_btn.click( | |
| fn=annotate_pgn, | |
| inputs=[pgn_input, analysis_time_input, style_input], | |
| outputs=annotated_output, | |
| ) | |
| with gr.Tab("Render Board", visible=False): | |
| gr.Markdown("### Render Chess Board") | |
| fen_input_render = gr.Textbox( | |
| label="FEN String (optional)", | |
| placeholder="Leave blank for initial position", | |
| value=None, | |
| ) | |
| render_mode_dropdown = gr.Dropdown( | |
| label="Render Mode", choices=["ascii", "svg", "unicode"], value="svg" | |
| ) | |
| render_btn = gr.Button("Render Board") | |
| board_render_output = gr.HTML(label="Rendered Board") | |
| render_btn.click( | |
| fn=render_board, | |
| inputs=[fen_input_render, render_mode_dropdown], | |
| outputs=board_render_output, | |
| ) | |
| with gr.Tab("Suggest Move", visible=False): | |
| gr.Markdown("### Suggest a Move") | |
| fen_input_suggest = gr.Textbox( | |
| label="FEN String (optional)", | |
| placeholder="Leave blank for initial position", | |
| value=None, | |
| ) | |
| suggest_btn = gr.Button("Suggest Move") | |
| suggested_move_output = gr.Textbox(label="Suggested Move (SAN)") | |
| suggest_btn.click( | |
| fn=suggest_move, | |
| inputs=fen_input_suggest, | |
| outputs=suggested_move_output, | |
| ) | |
| if __name__ == "__main__": | |
| app.launch(mcp_server=True) | |