gabejavitt commited on
Commit
8c67a4c
Β·
verified Β·
1 Parent(s): fac3e4d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -169
app.py CHANGED
@@ -650,7 +650,7 @@ def analyze_image(file_path: str, query: str) -> str:
650
 
651
  # Use Gemini Vision
652
  vision_llm = ChatGoogleGenerativeAI(
653
- model="gemini-2.0-flash-exp",
654
  google_api_key=GOOGLE_API_KEY,
655
  temperature=0
656
  )
@@ -816,22 +816,22 @@ class ChessAnalysisInput(BaseModel):
816
  image_path: str = Field(description="Path to chess board image file")
817
  description: str = Field(description="Any additional context about the position (optional)", default="")
818
 
819
- @tool(args_schema=ChessAnalysisInput)
820
  def analyze_chess_position(image_path: str, description: str = "") -> str:
821
  """
822
- Analyzes a chess position from an image and returns the best move.
 
 
 
 
 
 
823
 
824
  Use this tool when:
825
  - Question mentions chess, checkmate, or chess notation
826
  - An image file shows a chess board
827
  - Need to find the best move in a position
828
 
829
- The tool will:
830
- 1. Extract the position from the image using Gemini Vision
831
- 2. Analyze it using Lichess cloud API
832
- 3. Return the best move in algebraic notation (e.g., "Qh5" or "Nf6")
833
-
834
- Example: analyze_chess_position(image_path="/tmp/chess_board.png")
835
  """
836
  if not image_path:
837
  return "Error: image_path is required."
@@ -841,7 +841,7 @@ def analyze_chess_position(image_path: str, description: str = "") -> str:
841
  # Find the file
842
  chess_image = find_file(image_path)
843
 
844
- # If not found via find_file, try direct path (for /tmp files on HF)
845
  if not chess_image and os.path.exists(image_path):
846
  chess_image = Path(image_path)
847
 
@@ -851,188 +851,212 @@ def analyze_chess_position(image_path: str, description: str = "") -> str:
851
  print(f"βœ“ Found chess image at: {chess_image}")
852
 
853
  try:
854
- # Step 1: Extract FEN notation from image using Gemini
 
 
855
  GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY")
856
  if not GOOGLE_API_KEY:
857
  return "Error: GEMINI_API_KEY not set in Space secrets."
858
 
859
  print("πŸ“Έ Extracting chess position from image using Gemini...")
860
 
861
- try:
862
- # Load image
863
- img = Image.open(chess_image)
864
- print(f" Image loaded: {img.size}, mode: {img.mode}")
865
-
866
- # Convert to RGB if needed
867
- if img.mode not in ['RGB', 'RGBA']:
868
- img = img.convert('RGB')
869
-
870
- # Encode to base64
871
- buffered = io.BytesIO()
872
- img.save(buffered, format="JPEG")
873
- img_base64 = base64.b64encode(buffered.getvalue()).decode()
874
-
875
- # Use Gemini Vision to extract FEN
876
- vision_llm = ChatGoogleGenerativeAI(
877
- model="gemini-2.0-flash",
878
- google_api_key=GOOGLE_API_KEY,
879
- temperature=0
880
- )
881
-
882
- fen_prompt = """Analyze this chess board image and provide the position in FEN notation.
883
 
884
  CRITICAL INSTRUCTIONS:
885
- 1. Carefully identify each piece (uppercase for White: K, Q, R, B, N, P; lowercase for Black: k, q, r, b, n, p)
 
 
 
886
  2. Read the board from rank 8 (top) to rank 1 (bottom), left to right
 
 
 
887
  3. Determine whose turn it is:
888
- - If text says "Black to move" or "Black's turn" β†’ use 'b'
889
- - If text says "White to move" or "White's turn" β†’ use 'w'
890
- - If no indication, analyze the position and make your best guess
891
- 4. Return ONLY the FEN string in this exact format:
892
- piece_placement active_color castling en_passant halfmove fullmove
893
 
894
- Example FEN: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
 
895
 
896
- For piece placement:
897
- - Use numbers for empty squares (1-8)
898
- - Use '/' to separate ranks
899
- - White pieces: UPPERCASE (K, Q, R, B, N, P)
900
- - Black pieces: lowercase (k, q, r, b, n, p)
901
 
902
  Return ONLY the FEN string, nothing else."""
903
-
904
- message = HumanMessage(
905
- content=[
906
- {"type": "text", "text": fen_prompt},
907
- {
908
- "type": "image_url",
909
- "image_url": f"data:image/jpeg;base64,{img_base64}"
910
- }
911
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
912
  )
913
-
914
- response = vision_llm.invoke([message])
915
- fen_raw = response.content.strip()
916
-
917
- print(f"πŸ“ Raw FEN response: {fen_raw}")
918
-
919
- # Clean up FEN (remove markdown, explanations, etc.)
920
- fen = None
921
- fen_lines = fen_raw.split('\n')
922
-
923
- for line in fen_lines:
924
- line = line.strip()
925
- # Remove markdown code fences
926
- line = line.replace('```', '').replace('fen', '').strip()
927
-
928
- # FEN should have '/' for ranks and spaces for components
929
- if '/' in line and ' ' in line and not line.startswith('#'):
930
- # Basic validation: should have pieces or numbers, slashes, and spaces
931
- if any(c in line for c in 'kqrbnpKQRBNP12345678'):
932
- fen = line
933
- break
934
-
935
- if not fen:
936
- # Try to extract from any line with slash and space
937
- for line in fen_lines:
938
- if '/' in line and ' ' in line:
939
- fen = line.strip()
940
- break
941
-
942
- if not fen:
943
- return f"Error: Could not extract valid FEN notation from image. Gemini response: {fen_raw[:200]}"
944
-
945
- print(f"βœ“ Extracted FEN: {fen}")
946
-
947
  except Exception as e:
948
- return f"Error extracting position from image: {str(e)}\n{traceback.format_exc()}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949
 
950
- # Step 2: Analyze position using Lichess cloud API
951
- print("πŸ” Analyzing position with Lichess cloud engine...")
 
 
 
952
 
 
 
 
953
  try:
954
- # Try python-chess for better move notation (optional)
955
- try:
956
- import chess
957
- use_python_chess = True
958
- print("βœ“ Using python-chess for move conversion")
959
- except ImportError:
960
- use_python_chess = False
961
- print("⚠️ python-chess not available, using UCI notation")
962
-
963
- # Query Lichess cloud evaluation
964
- lichess_url = "https://lichess.org/api/cloud-eval"
965
- params = {
966
- "fen": fen.strip(),
967
- "multiPv": 1 # Get only the best move
968
- }
969
-
970
- response = requests.get(lichess_url, params=params, timeout=15)
971
-
972
- if response.status_code != 200:
973
- return f"Error: Lichess API returned status {response.status_code}. Position may not be in cloud database."
974
-
975
- data = response.json()
976
- print(f"πŸ“Š Lichess response: {data}")
977
-
978
- # Extract best move
979
- if "pvs" not in data or len(data["pvs"]) == 0:
980
- return "Error: Position not found in Lichess cloud database. Try a different position or check if the FEN is valid."
981
-
982
- best_pv = data["pvs"][0]
983
- moves = best_pv.get("moves", "").split()
984
 
985
- if not moves:
986
- return "Error: No moves found in Lichess analysis."
987
 
988
- best_move_uci = moves[0]
989
- print(f"🎯 Best move (UCI): {best_move_uci}")
990
 
991
- # Convert UCI to Standard Algebraic Notation (SAN)
992
- if use_python_chess:
993
- try:
994
- board = chess.Board(fen)
995
- uci_move = chess.Move.from_uci(best_move_uci)
996
- san_move = board.san(uci_move)
997
-
998
- # Get evaluation
999
- cp = best_pv.get("cp") # centipawns
1000
- mate = best_pv.get("mate") # mate in X moves
1001
-
1002
- if mate is not None:
1003
- eval_str = f" (Mate in {abs(mate)})"
1004
- elif cp is not None:
1005
- eval_str = f" (Eval: {cp/100:+.2f})"
1006
- else:
1007
- eval_str = ""
1008
-
1009
- # Check if move leads to check/checkmate
1010
- board.push(uci_move)
1011
- if board.is_checkmate():
1012
- check_str = " - Checkmate!"
1013
- elif board.is_check():
1014
- check_str = " - Check"
1015
- else:
1016
- check_str = ""
1017
-
1018
- final_result = f"{san_move}{eval_str}{check_str}"
1019
- print(f"βœ… Best move: {final_result}")
1020
- return final_result
1021
-
1022
- except Exception as e:
1023
- print(f"⚠️ Could not convert to SAN: {e}")
1024
- # Fall back to UCI notation
1025
- return best_move_uci
1026
- else:
1027
- # Return UCI notation if python-chess not available
1028
- return best_move_uci
1029
-
1030
- except requests.Timeout:
1031
- return "Error: Lichess API request timed out. Try again."
1032
- except requests.RequestException as e:
1033
- return f"Error querying Lichess API: {str(e)}"
1034
  except Exception as e:
1035
- return f"Error analyzing position: {str(e)}\n{traceback.format_exc()}"
 
 
1036
 
1037
  except Exception as e:
1038
  error_msg = f"Chess analysis failed: {str(e)}"
 
650
 
651
  # Use Gemini Vision
652
  vision_llm = ChatGoogleGenerativeAI(
653
+ model="gemini-2.0-flash",
654
  google_api_key=GOOGLE_API_KEY,
655
  temperature=0
656
  )
 
816
  image_path: str = Field(description="Path to chess board image file")
817
  description: str = Field(description="Any additional context about the position (optional)", default="")
818
 
 
819
  def analyze_chess_position(image_path: str, description: str = "") -> str:
820
  """
821
+ Analyzes a chess position from an image using Stockfish engine.
822
+
823
+ MUCH MORE RELIABLE than Lichess API because:
824
+ - Works offline
825
+ - Analyzes ANY position (not just cloud database)
826
+ - Stronger engine (Stockfish 16+)
827
+ - No rate limits or 404 errors
828
 
829
  Use this tool when:
830
  - Question mentions chess, checkmate, or chess notation
831
  - An image file shows a chess board
832
  - Need to find the best move in a position
833
 
834
+ Returns: Best move in algebraic notation (e.g., "Qh5", "Nf6+", "Rd5")
 
 
 
 
 
835
  """
836
  if not image_path:
837
  return "Error: image_path is required."
 
841
  # Find the file
842
  chess_image = find_file(image_path)
843
 
844
+ # If not found via find_file, try direct path
845
  if not chess_image and os.path.exists(image_path):
846
  chess_image = Path(image_path)
847
 
 
851
  print(f"βœ“ Found chess image at: {chess_image}")
852
 
853
  try:
854
+ # ====================================================================
855
+ # STEP 1: Extract FEN notation from image using Gemini Vision
856
+ # ====================================================================
857
  GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY")
858
  if not GOOGLE_API_KEY:
859
  return "Error: GEMINI_API_KEY not set in Space secrets."
860
 
861
  print("πŸ“Έ Extracting chess position from image using Gemini...")
862
 
863
+ # Load and encode image
864
+ img = Image.open(chess_image)
865
+ print(f" Image loaded: {img.size}, mode: {img.mode}")
866
+
867
+ if img.mode not in ['RGB', 'RGBA']:
868
+ img = img.convert('RGB')
869
+
870
+ buffered = io.BytesIO()
871
+ img.save(buffered, format="JPEG")
872
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
873
+
874
+ # Use Gemini Vision to extract FEN
875
+ vision_llm = ChatGoogleGenerativeAI(
876
+ model="gemini-2.0-flash-exp",
877
+ google_api_key=GOOGLE_API_KEY,
878
+ temperature=0
879
+ )
880
+
881
+ fen_prompt = """Analyze this chess board image and provide the position in FEN notation.
 
 
 
882
 
883
  CRITICAL INSTRUCTIONS:
884
+ 1. Carefully identify each piece:
885
+ - White pieces (UPPERCASE): K=King, Q=Queen, R=Rook, B=Bishop, N=Knight, P=Pawn
886
+ - Black pieces (lowercase): k, q, r, b, n, p
887
+
888
  2. Read the board from rank 8 (top) to rank 1 (bottom), left to right
889
+ - Use numbers (1-8) for consecutive empty squares
890
+ - Use '/' to separate ranks
891
+
892
  3. Determine whose turn it is:
893
+ - Look for text like "Black to move" or "White to move"
894
+ - If unclear, analyze the position context
895
+ - Return 'w' for white, 'b' for black
 
 
896
 
897
+ 4. Return ONLY the FEN string in this format:
898
+ piece_placement active_color castling en_passant halfmove fullmove
899
 
900
+ Example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
 
 
 
 
901
 
902
  Return ONLY the FEN string, nothing else."""
903
+
904
+ message = HumanMessage(
905
+ content=[
906
+ {"type": "text", "text": fen_prompt},
907
+ {
908
+ "type": "image_url",
909
+ "image_url": f"data:image/jpeg;base64,{img_base64}"
910
+ }
911
+ ]
912
+ )
913
+
914
+ response = vision_llm.invoke([message])
915
+ fen_raw = response.content.strip()
916
+ print(f"πŸ“ Raw FEN response: {fen_raw}")
917
+
918
+ # Clean up FEN (remove markdown, explanations, etc.)
919
+ fen = None
920
+ for line in fen_raw.split('\n'):
921
+ line = line.strip().replace('```', '').replace('fen', '')
922
+ # FEN should have '/' for ranks and spaces for components
923
+ if '/' in line and ' ' in line and not line.startswith('#'):
924
+ if any(c in line for c in 'kqrbnpKQRBNP12345678'):
925
+ fen = line
926
+ break
927
+
928
+ if not fen:
929
+ return f"Error: Could not extract valid FEN notation from image. Response: {fen_raw[:200]}"
930
+
931
+ print(f"βœ“ Extracted FEN: {fen}")
932
+
933
+ # ====================================================================
934
+ # STEP 2: Validate FEN with python-chess
935
+ # ====================================================================
936
+ try:
937
+ import chess
938
+ except ImportError:
939
+ return "Error: python-chess not installed. Add 'python-chess' to requirements.txt"
940
+
941
+ try:
942
+ board = chess.Board(fen)
943
+ print(f"βœ“ FEN validated successfully")
944
+ print(f" Turn: {'White' if board.turn else 'Black'}")
945
+ print(f" Legal moves: {board.legal_moves.count()}")
946
+ except ValueError as e:
947
+ return f"Error: Invalid FEN notation: {e}\nExtracted FEN: {fen}"
948
+
949
+ # ====================================================================
950
+ # STEP 3: Analyze with Stockfish
951
+ # ====================================================================
952
+ print("πŸ” Analyzing position with Stockfish...")
953
+
954
+ try:
955
+ from stockfish import Stockfish
956
+ except ImportError:
957
+ return "Error: stockfish not installed. Add 'stockfish' to requirements.txt and install Stockfish binary"
958
+
959
+ # Try to find Stockfish binary
960
+ stockfish_paths = [
961
+ "/usr/games/stockfish", # Linux (apt-get install)
962
+ "/usr/local/bin/stockfish", # Mac (brew install)
963
+ "/usr/bin/stockfish", # Alternative Linux
964
+ "stockfish", # In PATH
965
+ "./stockfish", # Local directory
966
+ "C:\\Program Files\\stockfish\\stockfish.exe" # Windows
967
+ ]
968
+
969
+ stockfish_path = None
970
+ for path in stockfish_paths:
971
+ if os.path.exists(path) or os.path.isfile(path):
972
+ stockfish_path = path
973
+ break
974
+
975
+ if not stockfish_path:
976
+ # Try running 'which stockfish' on Unix systems
977
+ try:
978
+ import subprocess
979
+ result = subprocess.run(['which', 'stockfish'],
980
+ capture_output=True,
981
+ text=True,
982
+ timeout=5)
983
+ if result.returncode == 0:
984
+ stockfish_path = result.stdout.strip()
985
+ except:
986
+ pass
987
+
988
+ if not stockfish_path:
989
+ return """Error: Stockfish binary not found. Install it:
990
+ - Linux: sudo apt-get install stockfish
991
+ - Mac: brew install stockfish
992
+ - Windows: Download from stockfishchess.org
993
+ Or set the path manually in the code."""
994
+
995
+ print(f"βœ“ Found Stockfish at: {stockfish_path}")
996
+
997
+ # Initialize Stockfish
998
+ try:
999
+ stockfish = Stockfish(
1000
+ path=stockfish_path,
1001
+ depth=20, # Analysis depth (higher = stronger but slower)
1002
+ parameters={
1003
+ "Threads": 2,
1004
+ "Minimum Thinking Time": 500, # milliseconds
1005
+ "Hash": 512, # MB of RAM
1006
+ }
1007
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  except Exception as e:
1009
+ return f"Error initializing Stockfish: {e}"
1010
+
1011
+ # Set position
1012
+ stockfish.set_fen_position(fen)
1013
+
1014
+ # Get best move
1015
+ print(" Computing best move...")
1016
+ best_move_uci = stockfish.get_best_move()
1017
+
1018
+ if not best_move_uci:
1019
+ return "Error: Stockfish could not find a legal move. Check if position is valid."
1020
+
1021
+ print(f"🎯 Best move (UCI): {best_move_uci}")
1022
+
1023
+ # Get evaluation
1024
+ evaluation = stockfish.get_evaluation()
1025
+ eval_type = evaluation.get("type", "cp")
1026
+ eval_value = evaluation.get("value", 0)
1027
 
1028
+ if eval_type == "mate":
1029
+ eval_str = f" (Mate in {abs(eval_value)})"
1030
+ else:
1031
+ # Centipawns to pawns
1032
+ eval_str = f" (Eval: {eval_value/100:+.2f})"
1033
 
1034
+ # ====================================================================
1035
+ # STEP 4: Convert UCI to Standard Algebraic Notation (SAN)
1036
+ # ====================================================================
1037
  try:
1038
+ uci_move = chess.Move.from_uci(best_move_uci)
1039
+ san_move = board.san(uci_move)
1040
+
1041
+ # Check if move leads to check/checkmate
1042
+ board.push(uci_move)
1043
+ if board.is_checkmate():
1044
+ check_str = " - Checkmate!"
1045
+ elif board.is_check():
1046
+ check_str = " - Check"
1047
+ else:
1048
+ check_str = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
 
1050
+ final_result = f"{san_move}{eval_str}{check_str}"
1051
+ print(f"βœ… Best move: {final_result}")
1052
 
1053
+ # Return JUST the move notation for clean submission
1054
+ return san_move
1055
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  except Exception as e:
1057
+ print(f"⚠️ Could not convert to SAN: {e}")
1058
+ # Fall back to UCI notation
1059
+ return best_move_uci
1060
 
1061
  except Exception as e:
1062
  error_msg = f"Chess analysis failed: {str(e)}"