Spaces:
Sleeping
Sleeping
Download tools.py from psykoss/Final_Assignment_Template: direct link, hf CLI and curl.
- Browser
- Download file 6.94 kB
-
https://huggingface.co/spaces/psykoss/Final_Assignment_Template/resolve/main/tools.py
- Command line
-
hf download hf://spaces/psykoss/Final_Assignment_Template/tools.py
-
curl -L -o tools.py https://huggingface.co/spaces/psykoss/Final_Assignment_Template/resolve/main/tools.py
6.94 kB
| """Tools for the GAIA agent: web search, arithmetic, and image recognition.""" | |
| import ast | |
| import base64 | |
| import math | |
| import operator | |
| import re | |
| from typing import Any | |
| import requests | |
| from ddgs import DDGS | |
| from langchain_core.messages import HumanMessage | |
| from prompts import IMAGE_ANALYSIS_PROMPT | |
| MAX_IMAGES_PER_QUESTION = 3 | |
| _IMAGE_EXTENSIONS = r"(?:jpg|jpeg|png|gif|webp|bmp|svg)" | |
| _URL_TAIL = r"(?:\?[^\s\)\]\"\'<>]*)?" | |
| # Safe operators for GAIA-style numeric questions | |
| _SAFE_OPERATORS = { | |
| ast.Add: operator.add, | |
| ast.Sub: operator.sub, | |
| ast.Mult: operator.mul, | |
| ast.Div: operator.truediv, | |
| ast.FloorDiv: operator.floordiv, | |
| ast.Pow: operator.pow, | |
| ast.Mod: operator.mod, | |
| ast.USub: operator.neg, | |
| ast.UAdd: operator.pos, | |
| } | |
| def _eval_ast(node: ast.AST) -> float: | |
| if isinstance(node, ast.Constant): | |
| if isinstance(node.value, (int, float)): | |
| return float(node.value) | |
| raise ValueError(f"Unsupported constant: {node.value}") | |
| if isinstance(node, ast.Num): # pragma: no cover - py<3.8 | |
| return float(node.n) | |
| if isinstance(node, ast.BinOp): | |
| op = _SAFE_OPERATORS.get(type(node.op)) | |
| if not op: | |
| raise ValueError(f"Unsupported operator: {type(node.op).__name__}") | |
| return op(_eval_ast(node.left), _eval_ast(node.right)) | |
| if isinstance(node, ast.UnaryOp): | |
| op = _SAFE_OPERATORS.get(type(node.op)) | |
| if not op: | |
| raise ValueError(f"Unsupported unary op: {type(node.op).__name__}") | |
| return op(_eval_ast(node.operand)) | |
| if isinstance(node, ast.Call): | |
| if isinstance(node.func, ast.Name): | |
| name = node.func.id | |
| args = [_eval_ast(a) for a in node.args] | |
| if name == "sqrt" and len(args) == 1: | |
| return math.sqrt(args[0]) | |
| if name == "round" and len(args) in (1, 2): | |
| return float(round(args[0], int(args[1]) if len(args) == 2 else 0)) | |
| if name == "abs" and len(args) == 1: | |
| return abs(args[0]) | |
| raise ValueError("Unsupported function call") | |
| raise ValueError(f"Unsupported expression node: {type(node).__name__}") | |
| def _normalize_expression(expr: str) -> str: | |
| expr = expr.strip() | |
| expr = expr.replace("×", "*").replace("÷", "/").replace("^", "**") | |
| expr = re.sub(r"\s+", "", expr) | |
| return expr | |
| def calculate(expression: str) -> str: | |
| """ | |
| Evaluate a numeric expression safely (numbers, + - * / // ** %, sqrt, abs, round). | |
| Returns a string result for the agent to use as evidence. | |
| """ | |
| expr = _normalize_expression(expression) | |
| if not expr: | |
| return "Error: empty expression" | |
| try: | |
| tree = ast.parse(expr, mode="eval") | |
| result = _eval_ast(tree.body) | |
| if result == int(result) and "." not in str(expr).split("/")[-1]: | |
| return str(int(result)) | |
| return str(round(result, 10)).rstrip("0").rstrip(".") | |
| except Exception as e: | |
| return f"Error: {e}" | |
| def run_calculations(expressions: list[str]) -> list[str]: | |
| """Run multiple calculations; skip empty or duplicate expressions.""" | |
| seen: set[str] = set() | |
| outputs: list[str] = [] | |
| for raw in expressions: | |
| expr = str(raw).strip() | |
| if not expr or expr in seen: | |
| continue | |
| seen.add(expr) | |
| result = calculate(expr) | |
| outputs.append(f"{expr} = {result}") | |
| return outputs | |
| def _message_content(response: Any) -> str: | |
| content = response.content | |
| if isinstance(content, str): | |
| return content.strip() | |
| if isinstance(content, list): | |
| parts = [] | |
| for block in content: | |
| if isinstance(block, str): | |
| parts.append(block) | |
| elif isinstance(block, dict) and block.get("text"): | |
| parts.append(str(block["text"])) | |
| return "\n".join(parts).strip() | |
| return str(content).strip() | |
| def extract_image_urls(text: str) -> list[str]: | |
| """Find image URLs in question text (markdown, HTML, plain links).""" | |
| candidates: list[str] = [] | |
| patterns = [ | |
| rf"https?://[^\s\)\]\"\'<>]+\.{_IMAGE_EXTENSIONS}{_URL_TAIL}", | |
| r"!\[[^\]]*\]\((https?://[^\)]+)\)", | |
| r"https?://huggingface\.co/datasets/[^\s\)\]\"\'<>]+", | |
| r"https?://[^\s\)\]\"\'<>]*(?:/image|/images|/file|/files)[^\s\)\]\"\'<>]*", | |
| ] | |
| for pattern in patterns: | |
| for match in re.finditer(pattern, text, re.IGNORECASE): | |
| url = match.group(1) if match.lastindex else match.group(0) | |
| url = url.rstrip(".,;:") | |
| if url.startswith("http"): | |
| candidates.append(url) | |
| seen: set[str] = set() | |
| deduped: list[str] = [] | |
| for url in candidates: | |
| key = url.lower() | |
| if key not in seen: | |
| seen.add(key) | |
| deduped.append(url) | |
| if len(deduped) >= MAX_IMAGES_PER_QUESTION: | |
| break | |
| return deduped | |
| def _fetch_image_base64(url: str) -> tuple[str, str]: | |
| """Download image and return (media_type, base64_data).""" | |
| headers = {"User-Agent": "Mozilla/5.0 (compatible; GAIA-Agent/1.0)"} | |
| resp = requests.get(url, timeout=45, headers=headers) | |
| resp.raise_for_status() | |
| content_type = (resp.headers.get("content-type") or "image/jpeg").split(";")[0].strip() | |
| if not content_type.startswith("image/"): | |
| content_type = "image/jpeg" | |
| return content_type, base64.standard_b64encode(resp.content).decode("ascii") | |
| def describe_image(url: str, question: str, vision_model: Any) -> str: | |
| """ | |
| Analyze an image URL with Claude vision and return a text description. | |
| """ | |
| try: | |
| media_type, data = _fetch_image_base64(url) | |
| prompt = IMAGE_ANALYSIS_PROMPT.format(question=question, url=url) | |
| message = HumanMessage( | |
| content=[ | |
| {"type": "text", "text": prompt}, | |
| { | |
| "type": "image", | |
| "source": { | |
| "type": "base64", | |
| "media_type": media_type, | |
| "data": data, | |
| }, | |
| }, | |
| ] | |
| ) | |
| return _message_content(vision_model.invoke([message])) | |
| except Exception as e: | |
| return f"Vision error: {e}" | |
| def analyze_images(urls: list[str], question: str, vision_model: Any) -> list[str]: | |
| """Run vision on up to MAX_IMAGES_PER_QUESTION URLs.""" | |
| results: list[str] = [] | |
| for url in urls[:MAX_IMAGES_PER_QUESTION]: | |
| description = describe_image(url, question, vision_model) | |
| results.append(f"[Image: {url}]\n{description}") | |
| return results | |
| def search_web(query: str) -> str: | |
| """Search the web for information related to the query.""" | |
| results = DDGS().text(query, max_results=8) | |
| if not results: | |
| return "No results found." | |
| return "\n\n".join( | |
| f"{r.get('title', '')}: {r.get('body', r.get('href', ''))}" | |
| for r in results | |
| ) | |