gabejavitt commited on
Commit
26b8984
Β·
verified Β·
1 Parent(s): 2614830

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +208 -176
app.py CHANGED
@@ -882,202 +882,233 @@ def scrape_and_retrieve(url: str, query: str) -> str:
882
  except Exception as e:
883
  return f"Error processing page: {str(e)}\n{traceback.format_exc()}"
884
 
885
- def analyze_chess_position(args: str, state: AgentState) -> str:
 
 
 
 
 
886
  """
887
- Analyze chess position using Stockfish engine via lichess API or python-chess
888
- Input format: "image_path|description" or just FEN notation
 
 
 
 
 
 
 
 
 
 
 
889
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  try:
891
- # Try to use python-chess with Stockfish
 
 
 
 
 
 
892
  try:
893
- import chess
894
- import chess.engine
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
895
 
896
- # Check if we have an image to analyze first
897
- if '|' in args and os.path.exists(args.split('|')[0]):
898
- image_path = args.split('|')[0]
899
-
900
- # Use Gemini to extract FEN from image
901
- print("πŸ“Έ Extracting chess position from image...")
902
- img = Image.open(image_path)
903
- model = genai.GenerativeModel('gemini-2.0-flash-exp')
904
-
905
- fen_prompt = """Analyze this chess board image and provide the position in FEN notation.
906
-
907
- Important instructions:
908
- 1. Carefully identify each piece and its position
909
- 2. Determine whose turn it is (look for indicators in the image)
910
- 3. Return ONLY the FEN string, nothing else
911
- 4. Format: piece_placement active_color castling en_passant halfmove fullmove
912
-
913
- Example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
914
-
915
- If it says "Black to move" or "Black's turn", use 'b' for active color.
916
- If it says "White to move" or "White's turn", use 'w' for active color."""
917
-
918
- response = model.generate_content([fen_prompt, img])
919
- fen = response.text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
 
921
- # Clean up the FEN (remove markdown, explanations, etc.)
922
- fen_lines = fen.split('\n')
923
- for line in fen_lines:
924
- line = line.strip()
925
- # FEN should have spaces and slashes
926
- if '/' in line and ' ' in line and not line.startswith('#'):
927
  fen = line
928
  break
929
-
930
- print(f"πŸ“Š Extracted FEN: {fen}")
931
- else:
932
- # Direct FEN input
933
- fen = args.strip()
934
 
935
- # Parse the position
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  try:
937
- board = chess.Board(fen)
938
- except Exception as e:
939
- return f"N/A_REQUIRED: Invalid FEN notation - {str(e)}"
940
-
941
- # Try to use Stockfish engine
942
- stockfish_paths = [
943
- "/usr/games/stockfish",
944
- "/usr/local/bin/stockfish",
945
- "/opt/homebrew/bin/stockfish",
946
- "stockfish",
947
- "./stockfish"
948
- ]
 
949
 
950
- engine_path = None
951
- for path in stockfish_paths:
952
- if os.path.exists(path) or path == "stockfish":
953
- engine_path = path
954
- break
955
-
956
- if not engine_path:
957
- # Fallback to lichess API
958
- print("⚠️ Stockfish not found locally, using Lichess API...")
959
- return analyze_chess_via_lichess(board.fen(), state)
960
-
961
- # Use local Stockfish
962
- with chess.engine.SimpleEngine.popen_uci(engine_path) as engine:
963
- # Analyze position
964
- info = engine.analyse(board, chess.engine.Limit(depth=20))
965
- best_move = info.get("pv")[0] if "pv" in info else None
966
-
967
- if best_move:
968
- # Convert to algebraic notation
969
- san_move = board.san(best_move)
970
-
971
- # Get evaluation score
972
- score = info.get("score")
973
- score_str = ""
974
- if score:
975
- if score.is_mate():
976
- mate_in = score.relative.moves
977
- score_str = f" (Mate in {abs(mate_in)})"
978
- else:
979
- cp = score.relative.score()
980
- score_str = f" (Eval: {cp/100:.2f})"
981
 
982
- # Check if this move leads to checkmate
983
- board_copy = board.copy()
984
- board_copy.push(best_move)
985
 
986
- result = f"{san_move}{score_str}"
 
 
 
 
 
987
 
988
- if board_copy.is_checkmate():
989
- result += " - Checkmate!"
990
- elif board_copy.is_check():
991
- result += " - Check"
 
 
 
 
992
 
993
- print(f"β™ŸοΈ Best move: {result}")
994
- return result
995
- else:
996
- return "N/A_REQUIRED: Could not determine best move"
997
 
998
- except ImportError:
999
- print("⚠️ python-chess not installed, using Lichess API...")
1000
- # Extract FEN from image if needed
1001
- if '|' in args and os.path.exists(args.split('|')[0]):
1002
- image_path = args.split('|')[0]
1003
- img = Image.open(image_path)
1004
- model = genai.GenerativeModel('gemini-2.0-flash-exp')
1005
-
1006
- response = model.generate_content([
1007
- "Extract the chess position in FEN notation. Return ONLY the FEN string.",
1008
- img
1009
- ])
1010
- fen = response.text.strip()
1011
  else:
1012
- fen = args.strip()
1013
-
1014
- return analyze_chess_via_lichess(fen, state)
1015
-
1016
- except Exception as e:
1017
- state.add_failure('chess', str(e))
1018
- return f"N/A_REQUIRED: Chess analysis failed - {str(e)}"
1019
-
1020
-
1021
- def analyze_chess_via_lichess(fen: str, state: AgentState) -> str:
1022
- """
1023
- Analyze chess position using Lichess cloud API
1024
- """
1025
- try:
1026
- # Lichess cloud evaluation API
1027
- url = "https://lichess.org/api/cloud-eval"
1028
-
1029
- # Clean FEN
1030
- fen = fen.strip().replace('```', '').replace('fen', '').strip()
1031
-
1032
- params = {
1033
- "fen": fen,
1034
- "multiPv": 1 # Get best move only
1035
- }
1036
-
1037
- response = requests.get(url, params=params, timeout=10)
1038
-
1039
- if response.status_code == 200:
1040
- data = response.json()
1041
-
1042
- if "pvs" in data and len(data["pvs"]) > 0:
1043
- best_pv = data["pvs"][0]
1044
 
1045
- # Get the moves in UCI notation
1046
- moves = best_pv.get("moves", "").split()
1047
- if moves:
1048
- # Convert UCI to SAN using python-chess if available
1049
- try:
1050
- import chess
1051
- board = chess.Board(fen)
1052
- uci_move = chess.Move.from_uci(moves[0])
1053
- san_move = board.san(uci_move)
1054
-
1055
- # Get evaluation
1056
- cp = best_pv.get("cp")
1057
- mate = best_pv.get("mate")
1058
-
1059
- if mate is not None:
1060
- eval_str = f" (Mate in {abs(mate)})"
1061
- elif cp is not None:
1062
- eval_str = f" (Eval: {cp/100:.2f})"
1063
- else:
1064
- eval_str = ""
1065
-
1066
- return f"{san_move}{eval_str}"
1067
- except:
1068
- # Return UCI move if can't convert
1069
- return moves[0]
1070
- else:
1071
- return "N/A_REQUIRED: No moves found in analysis"
1072
- else:
1073
- return "N/A_REQUIRED: Position not in Lichess cloud database"
1074
- else:
1075
- state.add_failure('lichess', f'HTTP {response.status_code}')
1076
- return f"N/A_REQUIRED: Lichess API error {response.status_code}"
1077
 
1078
  except Exception as e:
1079
- state.add_failure('lichess', str(e))
1080
- return f"N/A_REQUIRED: Lichess analysis failed - {str(e)}"
 
 
1081
 
1082
  class FinalAnswerInput(BaseModel):
1083
  answer: str = Field(description="Final answer - EXACTLY what was asked, nothing more")
@@ -1124,6 +1155,7 @@ defined_tools = [
1124
  analyze_image,
1125
  get_youtube_transcript,
1126
  scrape_and_retrieve,
 
1127
 
1128
  # Final
1129
  final_answer_tool
 
882
  except Exception as e:
883
  return f"Error processing page: {str(e)}\n{traceback.format_exc()}"
884
 
885
+ class ChessAnalysisInput(BaseModel):
886
+ image_path: str = Field(description="Path to chess board image file")
887
+ description: str = Field(description="Any additional context about the position (optional)", default="")
888
+
889
+ @tool(args_schema=ChessAnalysisInput)
890
+ def analyze_chess_position(image_path: str, description: str = "") -> str:
891
  """
892
+ Analyzes a chess position from an image and returns the best move.
893
+
894
+ Use this tool when:
895
+ - Question mentions chess, checkmate, or chess notation
896
+ - An image file shows a chess board
897
+ - Need to find the best move in a position
898
+
899
+ The tool will:
900
+ 1. Extract the position from the image using Gemini Vision
901
+ 2. Analyze it using Lichess cloud API
902
+ 3. Return the best move in algebraic notation (e.g., "Qh5" or "Nf6")
903
+
904
+ Example: analyze_chess_position(image_path="/tmp/chess_board.png")
905
  """
906
+ if not image_path:
907
+ return "Error: image_path is required."
908
+
909
+ print(f"β™ŸοΈ Analyzing chess position from: {image_path}")
910
+
911
+ # Find the file
912
+ chess_image = find_file(image_path)
913
+
914
+ # If not found via find_file, try direct path (for /tmp files on HF)
915
+ if not chess_image and os.path.exists(image_path):
916
+ chess_image = Path(image_path)
917
+
918
+ if not chess_image or not chess_image.exists():
919
+ return f"Error: Chess board image not found at '{image_path}'. Check the [FILE ATTACHED: ...] path in the question."
920
+
921
+ print(f"βœ“ Found chess image at: {chess_image}")
922
+
923
  try:
924
+ # Step 1: Extract FEN notation from image using Gemini
925
+ GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY")
926
+ if not GOOGLE_API_KEY:
927
+ return "Error: GEMINI_API_KEY not set in Space secrets."
928
+
929
+ print("πŸ“Έ Extracting chess position from image using Gemini...")
930
+
931
  try:
932
+ # Load image
933
+ img = Image.open(chess_image)
934
+ print(f" Image loaded: {img.size}, mode: {img.mode}")
935
+
936
+ # Convert to RGB if needed
937
+ if img.mode not in ['RGB', 'RGBA']:
938
+ img = img.convert('RGB')
939
+
940
+ # Encode to base64
941
+ buffered = io.BytesIO()
942
+ img.save(buffered, format="JPEG")
943
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
944
+
945
+ # Use Gemini Vision to extract FEN
946
+ vision_llm = ChatGoogleGenerativeAI(
947
+ model="gemini-2.0-flash-exp",
948
+ google_api_key=GOOGLE_API_KEY,
949
+ temperature=0
950
+ )
951
 
952
+ fen_prompt = """Analyze this chess board image and provide the position in FEN notation.
953
+
954
+ CRITICAL INSTRUCTIONS:
955
+ 1. Carefully identify each piece (uppercase for White: K, Q, R, B, N, P; lowercase for Black: k, q, r, b, n, p)
956
+ 2. Read the board from rank 8 (top) to rank 1 (bottom), left to right
957
+ 3. Determine whose turn it is:
958
+ - If text says "Black to move" or "Black's turn" β†’ use 'b'
959
+ - If text says "White to move" or "White's turn" β†’ use 'w'
960
+ - If no indication, analyze the position and make your best guess
961
+ 4. Return ONLY the FEN string in this exact format:
962
+ piece_placement active_color castling en_passant halfmove fullmove
963
+
964
+ Example FEN: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
965
+
966
+ For piece placement:
967
+ - Use numbers for empty squares (1-8)
968
+ - Use '/' to separate ranks
969
+ - White pieces: UPPERCASE (K, Q, R, B, N, P)
970
+ - Black pieces: lowercase (k, q, r, b, n, p)
971
+
972
+ Return ONLY the FEN string, nothing else."""
973
+
974
+ message = HumanMessage(
975
+ content=[
976
+ {"type": "text", "text": fen_prompt},
977
+ {
978
+ "type": "image_url",
979
+ "image_url": f"data:image/jpeg;base64,{img_base64}"
980
+ }
981
+ ]
982
+ )
983
+
984
+ response = vision_llm.invoke([message])
985
+ fen_raw = response.content.strip()
986
+
987
+ print(f"πŸ“ Raw FEN response: {fen_raw}")
988
+
989
+ # Clean up FEN (remove markdown, explanations, etc.)
990
+ fen = None
991
+ fen_lines = fen_raw.split('\n')
992
+
993
+ for line in fen_lines:
994
+ line = line.strip()
995
+ # Remove markdown code fences
996
+ line = line.replace('```', '').replace('fen', '').strip()
997
 
998
+ # FEN should have '/' for ranks and spaces for components
999
+ if '/' in line and ' ' in line and not line.startswith('#'):
1000
+ # Basic validation: should have pieces or numbers, slashes, and spaces
1001
+ if any(c in line for c in 'kqrbnpKQRBNP12345678'):
 
 
1002
  fen = line
1003
  break
 
 
 
 
 
1004
 
1005
+ if not fen:
1006
+ # Try to extract from any line with slash and space
1007
+ for line in fen_lines:
1008
+ if '/' in line and ' ' in line:
1009
+ fen = line.strip()
1010
+ break
1011
+
1012
+ if not fen:
1013
+ return f"Error: Could not extract valid FEN notation from image. Gemini response: {fen_raw[:200]}"
1014
+
1015
+ print(f"βœ“ Extracted FEN: {fen}")
1016
+
1017
+ except Exception as e:
1018
+ return f"Error extracting position from image: {str(e)}\n{traceback.format_exc()}"
1019
+
1020
+ # Step 2: Analyze position using Lichess cloud API
1021
+ print("πŸ” Analyzing position with Lichess cloud engine...")
1022
+
1023
+ try:
1024
+ # Try python-chess for better move notation (optional)
1025
  try:
1026
+ import chess
1027
+ use_python_chess = True
1028
+ print("βœ“ Using python-chess for move conversion")
1029
+ except ImportError:
1030
+ use_python_chess = False
1031
+ print("⚠️ python-chess not available, using UCI notation")
1032
+
1033
+ # Query Lichess cloud evaluation
1034
+ lichess_url = "https://lichess.org/api/cloud-eval"
1035
+ params = {
1036
+ "fen": fen.strip(),
1037
+ "multiPv": 1 # Get only the best move
1038
+ }
1039
 
1040
+ response = requests.get(lichess_url, params=params, timeout=15)
1041
+
1042
+ if response.status_code != 200:
1043
+ return f"Error: Lichess API returned status {response.status_code}. Position may not be in cloud database."
1044
+
1045
+ data = response.json()
1046
+ print(f"πŸ“Š Lichess response: {data}")
1047
+
1048
+ # Extract best move
1049
+ if "pvs" not in data or len(data["pvs"]) == 0:
1050
+ return "Error: Position not found in Lichess cloud database. Try a different position or check if the FEN is valid."
1051
+
1052
+ best_pv = data["pvs"][0]
1053
+ moves = best_pv.get("moves", "").split()
1054
+
1055
+ if not moves:
1056
+ return "Error: No moves found in Lichess analysis."
1057
+
1058
+ best_move_uci = moves[0]
1059
+ print(f"🎯 Best move (UCI): {best_move_uci}")
1060
+
1061
+ # Convert UCI to Standard Algebraic Notation (SAN)
1062
+ if use_python_chess:
1063
+ try:
1064
+ board = chess.Board(fen)
1065
+ uci_move = chess.Move.from_uci(best_move_uci)
1066
+ san_move = board.san(uci_move)
 
 
 
 
1067
 
1068
+ # Get evaluation
1069
+ cp = best_pv.get("cp") # centipawns
1070
+ mate = best_pv.get("mate") # mate in X moves
1071
 
1072
+ if mate is not None:
1073
+ eval_str = f" (Mate in {abs(mate)})"
1074
+ elif cp is not None:
1075
+ eval_str = f" (Eval: {cp/100:+.2f})"
1076
+ else:
1077
+ eval_str = ""
1078
 
1079
+ # Check if move leads to check/checkmate
1080
+ board.push(uci_move)
1081
+ if board.is_checkmate():
1082
+ check_str = " - Checkmate!"
1083
+ elif board.is_check():
1084
+ check_str = " - Check"
1085
+ else:
1086
+ check_str = ""
1087
 
1088
+ final_result = f"{san_move}{eval_str}{check_str}"
1089
+ print(f"βœ… Best move: {final_result}")
1090
+ return final_result
 
1091
 
1092
+ except Exception as e:
1093
+ print(f"⚠️ Could not convert to SAN: {e}")
1094
+ # Fall back to UCI notation
1095
+ return best_move_uci
 
 
 
 
 
 
 
 
 
1096
  else:
1097
+ # Return UCI notation if python-chess not available
1098
+ return best_move_uci
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1099
 
1100
+ except requests.Timeout:
1101
+ return "Error: Lichess API request timed out. Try again."
1102
+ except requests.RequestException as e:
1103
+ return f"Error querying Lichess API: {str(e)}"
1104
+ except Exception as e:
1105
+ return f"Error analyzing position: {str(e)}\n{traceback.format_exc()}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1106
 
1107
  except Exception as e:
1108
+ error_msg = f"Chess analysis failed: {str(e)}"
1109
+ print(f"❌ {error_msg}")
1110
+ print(traceback.format_exc())
1111
+ return error_msg
1112
 
1113
  class FinalAnswerInput(BaseModel):
1114
  answer: str = Field(description="Final answer - EXACTLY what was asked, nothing more")
 
1155
  analyze_image,
1156
  get_youtube_transcript,
1157
  scrape_and_retrieve,
1158
+ analyze_chess_position,
1159
 
1160
  # Final
1161
  final_answer_tool