Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 886 |
"""
|
| 887 |
-
|
| 888 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
try:
|
| 891 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
try:
|
| 893 |
-
|
| 894 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
|
| 921 |
-
#
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
line
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 936 |
try:
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
"
|
| 948 |
-
|
|
|
|
| 949 |
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 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 |
-
#
|
| 983 |
-
|
| 984 |
-
|
| 985 |
|
| 986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
|
| 988 |
-
if
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 992 |
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
return "N/A_REQUIRED: Could not determine best move"
|
| 997 |
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 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 |
-
|
| 1080 |
-
|
|
|
|
|
|
|
| 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
|