Download tools.py from gilra/wreck: direct link, hf CLI and curl.
- Browser
- Download file 18 kB
-
https://huggingface.co/spaces/gilra/wreck/resolve/main/tools.py
- Command line
-
hf download hf://spaces/gilra/wreck/tools.py
-
curl -L -o tools.py https://huggingface.co/spaces/gilra/wreck/resolve/main/tools.py
18 kB
| # tools.py — tool definitions only | |
| import math | |
| import uuid | |
| import re | |
| import requests | |
| from bs4 import BeautifulSoup | |
| from smolagents import tool, FinalAnswerTool | |
| from sentence_transformers import SentenceTransformer | |
| import chromadb | |
| # ── Memory singletons ─────────────────────────────────────────────────────── | |
| _embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") | |
| _chroma = chromadb.PersistentClient(path="./memory_db") | |
| _col = _chroma.get_or_create_collection("agent_memory") | |
| # ── Tools ─────────────────────────────────────────────────────────────────── | |
| def search_tool(query: str) -> str: | |
| """Search the web for current facts, news, prices, or data. | |
| Args: | |
| query: Short precise factual search query. | |
| """ | |
| try: | |
| from ddgs import DDGS | |
| results = list(DDGS().text(query, max_results=5)) | |
| if not results: | |
| return "No results found." | |
| return "\n\n".join( | |
| f"{r['title']}\n{r['href']}\n{r['body']}" for r in results | |
| ) | |
| except Exception as e: | |
| return f"search_tool error: {e}" | |
| def fetch_webpage(url: str) -> str: | |
| """Fetch the full readable plain text of a webpage. Already stripped of HTML tags. | |
| Do NOT call BeautifulSoup on the result — it is already plain text, not HTML. | |
| Args: | |
| url: Full URL to fetch. | |
| """ | |
| try: | |
| resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15) | |
| resp.raise_for_status() | |
| soup = BeautifulSoup(resp.text, "html.parser") | |
| for tag in soup(["script", "style", "nav", "footer", "header"]): | |
| tag.decompose() | |
| return soup.get_text(separator="\n", strip=True)[:15000] | |
| except Exception as e: | |
| return f"fetch_webpage error: {e}" | |
| def wikipedia_search(query: str, sentences: int = 5) -> str: | |
| """Look up a topic on Wikipedia and return a summary. Use for factual questions | |
| about people, places, history, science — faster than fetch_webpage for known entities. | |
| Args: | |
| query: Topic to look up (e.g. 'Australia', 'Albert Einstein'). | |
| sentences: Number of summary sentences to return (default 5). | |
| """ | |
| try: | |
| import wikipedia | |
| return wikipedia.summary(query, sentences=sentences, auto_suggest=True) | |
| except Exception as e: | |
| return f"wikipedia_search error: {e}" | |
| def wikipedia_section(page_title: str, section: str) -> str: | |
| """Get a specific section from a Wikipedia page — use when you need a section | |
| like 'Discography', 'Career', 'Albums' rather than a short summary. | |
| Args: | |
| page_title: Exact Wikipedia page title (e.g. 'Mercedes Sosa'). | |
| section: Section heading to find (e.g. 'Discography'). | |
| """ | |
| try: | |
| import wikipedia | |
| page = wikipedia.page(page_title, auto_suggest=False) | |
| text = page.content | |
| idx = text.find(section) | |
| return text[idx:idx + 3000] if idx != -1 else text[:3000] | |
| except Exception as e: | |
| return f"wikipedia_section error: {e}" | |
| def read_pdf(file_path: str) -> str: | |
| """Extract all text from a PDF file. | |
| Args: | |
| file_path: Local path to the PDF. | |
| """ | |
| try: | |
| from pypdf import PdfReader | |
| text = "\n".join(p.extract_text() or "" for p in PdfReader(file_path).pages).strip() | |
| return text[:12000] if text else "PDF has no extractable text (may be image-only)." | |
| except Exception as e: | |
| return f"read_pdf error: {e}" | |
| def read_csv_file(file_path: str, max_rows: int = 200) -> str: | |
| """Load a CSV file and return its structure and data as text. | |
| Args: | |
| file_path: Local path to the CSV. | |
| max_rows: Maximum rows to return (default 200). | |
| """ | |
| import pandas as pd | |
| for enc in ("utf-8", "latin-1", "cp1252", "utf-8-sig"): | |
| try: | |
| df = pd.read_csv(file_path, encoding=enc) | |
| return f"Shape: {df.shape[0]}×{df.shape[1]}\nColumns: {list(df.columns)}\n\n{df.head(max_rows).to_string(index=False)}" | |
| except UnicodeDecodeError: | |
| continue | |
| except Exception as e: | |
| return f"read_csv_file error: {e}" | |
| return "read_csv_file error: could not decode file with any known encoding" | |
| def read_excel_file(file_path: str, sheet_name: str = None, max_rows: int = 200) -> str: | |
| """Load an Excel file and return its structure and data as text. | |
| Lists all sheet names first, then reads the requested sheet. | |
| Args: | |
| file_path: Local path to the Excel file (.xlsx or .xls). | |
| sheet_name: Sheet name or index to read (default: first sheet). | |
| max_rows: Maximum rows to return (default 200). | |
| """ | |
| try: | |
| import pandas as pd | |
| xl = pd.ExcelFile(file_path) | |
| sheets = xl.sheet_names | |
| target = sheet_name if sheet_name else sheets[0] | |
| df = pd.read_excel(file_path, sheet_name=target) | |
| header = f"Sheets: {sheets}\nReading: '{target}'\nShape: {df.shape[0]}×{df.shape[1]}\nColumns: {list(df.columns)}\n\n" | |
| return header + df.head(max_rows).to_string(index=False) | |
| except Exception as e: | |
| return f"read_excel_file error: {e}" | |
| def calculator(expression: str) -> str: | |
| """Evaluate a mathematical expression and return the exact result. | |
| Supports: + - * / ** % sqrt sin cos tan log log10 pi e abs round. | |
| Args: | |
| expression: e.g. 'sqrt(144) + 2**10' | |
| """ | |
| try: | |
| safe = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")} | |
| safe.update({"abs": abs, "round": round, "int": int, "float": float}) | |
| return str(eval(expression, {"__builtins__": {}}, safe)) | |
| except Exception as e: | |
| return f"calculator error: {e}" | |
| def count_and_find(text: str, count_type: str = "words", pattern: str = None) -> str: | |
| """Count elements in text or find regex pattern occurrences. | |
| Args: | |
| text: Text to analyse. | |
| count_type: 'words' | 'characters' | 'lines' | 'sentences' | 'pattern'. | |
| pattern: Regex string — required when count_type is 'pattern'. | |
| """ | |
| try: | |
| if count_type == "words": return str(len(text.split())) | |
| if count_type == "characters": return str(len(text)) | |
| if count_type == "lines": return str(len(text.splitlines())) | |
| if count_type == "sentences": return str(len(re.split(r"[.!?]+", text.strip()))) | |
| if count_type == "pattern" and pattern: | |
| m = re.findall(pattern, text) | |
| return f"{len(m)} matches: {m[:20]}" | |
| return "Invalid count_type. Use: words | characters | lines | sentences | pattern." | |
| except Exception as e: | |
| return f"count_and_find error: {e}" | |
| def arxiv_search(query: str, max_results: int = 3) -> str: | |
| """Search arXiv for academic papers. | |
| Args: | |
| query: Search query. | |
| max_results: Number of results (default 3). | |
| """ | |
| try: | |
| import arxiv | |
| search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance) | |
| parts = [] | |
| for p in search.results(): | |
| authors = ", ".join(a.name for a in p.authors[:3]) | |
| parts.append(f"Title: {p.title}\nAuthors: {authors}\nDate: {p.published.strftime('%Y-%m')}\nAbstract: {p.summary[:400]}\nURL: {p.entry_id}") | |
| return "\n\n---\n\n".join(parts) if parts else "No results." | |
| except Exception as e: | |
| return f"arxiv_search error: {e}" | |
| def flight_time_from_delhi(latitude: float, longitude: float, speed_kmph: float = 850.0) -> str: | |
| """Calculate flight distance and time from Delhi (Haversine). | |
| Args: | |
| latitude: Destination latitude. | |
| longitude: Destination longitude. | |
| speed_kmph: Flight speed km/h (default 850). | |
| """ | |
| r = 6371.0 | |
| la1, lo1 = math.radians(28.6139), math.radians(77.2090) | |
| la2, lo2 = math.radians(latitude), math.radians(longitude) | |
| a = math.sin((la2-la1)/2)**2 + math.cos(la1)*math.cos(la2)*math.sin((lo2-lo1)/2)**2 | |
| d = r * 2 * math.asin(math.sqrt(a)) | |
| return f"{d:.0f} km, {d/speed_kmph:.2f} hours" | |
| def transcribe_audio(file_path: str) -> str: | |
| """Transcribe an audio file to text using speech recognition. | |
| Args: | |
| file_path: Local path to audio file (.mp3, .wav, .m4a, etc.) | |
| """ | |
| import os | |
| import requests as _req | |
| try: | |
| ext = os.path.splitext(file_path)[1].lower().lstrip(".") | |
| mime_map = { | |
| "mp3": "audio/mpeg", "wav": "audio/wav", | |
| "m4a": "audio/mp4", "ogg": "audio/ogg", "flac": "audio/flac", | |
| } | |
| content_type = mime_map.get(ext, "audio/mpeg") | |
| token = os.environ.get("HF_TOKEN", "") | |
| with open(file_path, "rb") as f: | |
| audio_bytes = f.read() | |
| resp = _req.post( | |
| "https://api-inference.huggingface.co/models/openai/whisper-large-v3", | |
| headers={"Authorization": f"Bearer {token}", "Content-Type": content_type}, | |
| data=audio_bytes, | |
| timeout=60, | |
| ) | |
| resp.raise_for_status() | |
| result = resp.json() | |
| return result.get("text", str(result)) | |
| except Exception as e: | |
| return f"transcribe_audio error: {e}" | |
| def extract_text_from_image(file_path: str) -> str: | |
| """Extract all text and describe visual content from an image using a vision model. | |
| Use for screenshots, diagrams, charts, tables, or any image containing text. | |
| Args: | |
| file_path: Local path to image file (.png, .jpg, .jpeg, .gif, .webp, etc.) | |
| """ | |
| import os | |
| import base64 | |
| from huggingface_hub import InferenceClient | |
| if not os.path.exists(file_path): | |
| return f"extract_text_from_image error: file not found: {file_path}" | |
| try: | |
| ext = file_path.rsplit(".", 1)[-1].lower() | |
| mime = { | |
| "jpg": "image/jpeg", "jpeg": "image/jpeg", | |
| "png": "image/png", "gif": "image/gif", "webp": "image/webp", | |
| }.get(ext, "image/jpeg") | |
| with open(file_path, "rb") as f: | |
| b64 = base64.b64encode(f.read()).decode("utf-8") | |
| client = InferenceClient(token=os.environ.get("HF_TOKEN", "")) | |
| prompt = ( | |
| "1. Extract EVERY piece of text visible in this image exactly as written.\n" | |
| "2. If there is a table, reproduce it row by row with all values.\n" | |
| "3. If there is a chart or graph, state all axis labels, data points, and exact values.\n" | |
| "4. If there is a chess board, describe the position in FEN notation.\n" | |
| "5. State any numbers, dates, names, or codes precisely." | |
| ) | |
| resp = client.chat_completion( | |
| model="Qwen/Qwen2.5-VL-72B-Instruct", | |
| messages=[{ | |
| "role": "user", | |
| "content": [ | |
| {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}, | |
| {"type": "text", "text": prompt}, | |
| ], | |
| }], | |
| max_tokens=2000, | |
| ) | |
| return resp.choices[0].message.content | |
| except Exception as e: | |
| return f"extract_text_from_image error: {e}" | |
| def get_youtube_transcript(video_url: str) -> str: | |
| """Get the full transcript of a YouTube video. Use for any question referencing a YouTube URL. | |
| Args: | |
| video_url: Full YouTube URL (e.g. https://www.youtube.com/watch?v=...) | |
| """ | |
| try: | |
| import re as _re | |
| from youtube_transcript_api import YouTubeTranscriptApi | |
| vid = _re.search(r"(?:v=|youtu\.be/)([^&\n?#]+)", video_url) | |
| if not vid: | |
| return "get_youtube_transcript error: could not extract video ID from URL" | |
| video_id = vid.group(1) | |
| ytt = YouTubeTranscriptApi() | |
| transcript = ytt.fetch(video_id) | |
| return " ".join(t.text for t in transcript)[:10000] | |
| except Exception as e: | |
| return f"get_youtube_transcript error: {e}" | |
| def extract_table_from_url(url: str) -> str: | |
| """Extract tables from a webpage as readable text. | |
| Use when the answer is likely inside an HTML table (sports stats, rankings, schedules, etc.). | |
| Args: | |
| url: Full URL of the page containing the table. | |
| """ | |
| try: | |
| import pandas as pd | |
| tables = pd.read_html(url) | |
| if not tables: | |
| return "No tables found on this page." | |
| parts = [] | |
| for i, t in enumerate(tables[:5]): | |
| parts.append(f"[Table {i+1}]\n{t.to_string(index=False)}") | |
| return "\n\n".join(parts)[:12000] | |
| except Exception as e: | |
| return f"extract_table_from_url error: {e}" | |
| def download_and_read(url: str) -> str: | |
| """Download a file from a URL and return its content. | |
| Supports PDF, CSV, Excel, plain text. Use when a question links directly to a file. | |
| Args: | |
| url: Direct URL to the file. | |
| """ | |
| import os | |
| import tempfile | |
| try: | |
| resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=30) | |
| resp.raise_for_status() | |
| content_type = resp.headers.get("Content-Type", "") | |
| suffix = ".bin" | |
| if "pdf" in content_type: | |
| suffix = ".pdf" | |
| elif "csv" in content_type or url.endswith(".csv"): | |
| suffix = ".csv" | |
| elif "excel" in content_type or url.endswith((".xlsx", ".xls")): | |
| suffix = ".xlsx" | |
| elif "text" in content_type: | |
| return resp.text[:12000] | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: | |
| tmp.write(resp.content) | |
| tmp_path = tmp.name | |
| try: | |
| if suffix == ".pdf": | |
| from pypdf import PdfReader | |
| text = "\n".join(p.extract_text() or "" for p in PdfReader(tmp_path).pages) | |
| return text[:12000] | |
| elif suffix == ".csv": | |
| import pandas as pd | |
| df = pd.read_csv(tmp_path) | |
| return f"Shape: {df.shape}\n{df.head(100).to_string(index=False)}" | |
| elif suffix == ".xlsx": | |
| import pandas as pd | |
| df = pd.read_excel(tmp_path) | |
| return f"Shape: {df.shape}\n{df.head(100).to_string(index=False)}" | |
| else: | |
| return resp.text[:12000] | |
| finally: | |
| os.unlink(tmp_path) | |
| except Exception as e: | |
| return f"download_and_read error: {e}" | |
| def analyze_chess_position(fen: str, depth: int = 15) -> str: | |
| """Analyze a chess position and return the best move and evaluation. | |
| Use for any question involving a chess board position or asking for the best move. | |
| Args: | |
| fen: FEN string of the chess position (e.g. 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1'). | |
| depth: Search depth (default 15). | |
| """ | |
| try: | |
| import chess | |
| import chess.engine | |
| import shutil | |
| board = chess.Board(fen) | |
| # Try stockfish if available | |
| stockfish_path = shutil.which("stockfish") | |
| if stockfish_path: | |
| with chess.engine.SimpleEngine.popen_uci(stockfish_path) as eng: | |
| result = eng.analyse(board, chess.engine.Limit(depth=depth)) | |
| best_move = result["pv"][0] if result.get("pv") else None | |
| score = result["score"].white() | |
| move_san = board.san(best_move) if best_move else "unknown" | |
| return f"Best move: {move_san}\nFEN: {fen}\nScore: {score}\nTurn: {'White' if board.turn else 'Black'}" | |
| # Fallback: list legal moves and basic board info | |
| legal = [board.san(m) for m in board.legal_moves] | |
| checks = [m for m in legal if "+" in m or "#" in m] | |
| return ( | |
| f"Position: {fen}\nTurn: {'White' if board.turn else 'Black'}\n" | |
| f"Legal moves ({len(legal)}): {', '.join(legal[:30])}\n" | |
| f"Checks/mates: {checks if checks else 'none'}" | |
| ) | |
| except Exception as e: | |
| return f"analyze_chess_position error: {e}" | |
| def save_memory(text: str, key: str = "", tag: str = "") -> str: | |
| """Save a fact to long-term memory. | |
| Call ONLY when user explicitly says: remember / save / store / note that. | |
| Args: | |
| text: Fact to save. | |
| key: Memory key (e.g. 'office', 'son'). | |
| tag: Optional category. | |
| """ | |
| emb = _embedder.encode(text).tolist() | |
| mid = key if key else str(uuid.uuid4()) | |
| _col.upsert(ids=[mid], embeddings=[emb], documents=[text], metadatas=[{"key": key, "tag": tag}]) | |
| return f"Saved: {text}" | |
| def retrieve_memory(query: str) -> str: | |
| """Retrieve saved personal facts from long-term memory. | |
| Call whenever user refers to 'my office / son / broker / trip / project / city'. | |
| Args: | |
| query: What to search for. | |
| """ | |
| q_emb = _embedder.encode(query).tolist() | |
| res = _col.query(query_embeddings=[q_emb], n_results=3) | |
| if not res["documents"] or not res["documents"][0]: | |
| return "No saved memory found." | |
| return res["documents"][0][0] | |
| # ── Registry ──────────────────────────────────────────────────────────────── | |
| TOOL_LIST = [ | |
| search_tool, fetch_webpage, | |
| wikipedia_search, wikipedia_section, FinalAnswerTool(), | |
| read_pdf, read_csv_file, read_excel_file, | |
| calculator, count_and_find, arxiv_search, | |
| flight_time_from_delhi, | |
| get_youtube_transcript, extract_table_from_url, download_and_read, | |
| analyze_chess_position, | |
| transcribe_audio, extract_text_from_image, | |
| save_memory, retrieve_memory, | |
| ] | |
| AUTHORIZED_IMPORTS = [ | |
| "pandas", "numpy", "math", "re", "json", "io", | |
| "openpyxl", "requests", "bs4", "pypdf", | |
| "datetime", "collections", "statistics", "arxiv", | |
| ] | |