Hobysenny's picture
Update tools/tools.py
dfa0869 verified
Raw
History Blame Contribute Delete
17.9 kB
import os
from smolagents import tool
import spaces
from dotenv import load_dotenv
# ----------------------------------------------------------------------------
# SECTION 1: Vision & Multimodal
# ----------------------------------------------------------------------------
load_dotenv()
@spaces.GPU(duration=20)
def initialize_gpu() :
return 0
@tool
def vision_tool(prompt: str, image_list: list) -> str:
"""
Analyzes one or more images using a multimodal model to answer specific questions.
It processes image content and returns the model's text-based response.
Args:
prompt: The user question or task to perform on the images.
image_list: A list of PIL Image objects to be analyzed.
"""
import io, base64, os
from smolagents import OpenAIServerModel
from PIL import Image
initialize_gpu()
model = OpenAIServerModel(
model_id='gemma4:31b-cloud',
api_base='https://ollama.com/v1',
api_key=os.getenv('GEMMA_API_KEY'),
temperature=0.0,
max_tokens=2048,
)
payload = [{"type": "text", "text": prompt}]
for img in image_list:
try:
buffered = io.BytesIO()
img.save(buffered, format="JPEG")
b64_img = base64.b64encode(buffered.getvalue()).decode("utf-8")
payload.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_img}"}})
except Exception as e:
return f"Error processing image: {str(e)}"
return model([{"role": "user", "content": payload}]).content
# ----------------------------------------------------------------------------
# SECTION 2: YouTube Pipeline (Tout-en-un pour la robustesse)
# ----------------------------------------------------------------------------
@tool
def ask_youtube_full_pipeline(url: str, question: str) -> str:
"""
Downloads a YouTube video, extracts key frames at regular intervals, and uses a multimodal model to answer questions about the video's content.
Args:
url: The YouTube video URL to process.
question: The specific question to ask about the video content.
"""
import os, re, tempfile, yt_dlp, imageio
from PIL import Image
from smolagents import OpenAIServerModel
initialize_gpu()
# 1. Extraction ID interne
patterns = [r"v=([a-zA-Z0-9_-]{11})", r"youtu\.be/([a-zA-Z0-9_-]{11})"]
video_id = None
for pat in patterns:
match = re.search(pat, url)
if match:
video_id = match.group(1)
break
if not video_id:
return "Erreur: URL invalide."
with tempfile.TemporaryDirectory() as tmpdir:
# Configuration optimisée : légère (évite les timeouts) et robuste
ydl_opts = {
'format': 'best[ext=mp4]/best', # Récupère un fichier MP4 direct déjà fusionné
'outtmpl': os.path.join(tmpdir, 'downloaded_video.%(ext)s'),
'quiet': True,
'no_warnings': True
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.extract_info(url, download=True)
# CORRECTIF CRITIQUE : On liste le dossier pour trouver le fichier, peu importe son extension exacte
downloaded_files = os.listdir(tmpdir)
if not downloaded_files:
return "Erreur: Le téléchargement de la vidéo a échoué."
video_path = os.path.join(tmpdir, downloaded_files[0])
# Extraction des images via le chemin dynamique détecté
reader = imageio.get_reader(video_path, format='ffmpeg')
frames = [Image.fromarray(f) for i, f in enumerate(reader) if i % 60 == 0][:8]
reader.close()
# 2. Analyse VLM (Auto-contenu)
model = OpenAIServerModel(model_id='gemma4:31b-cloud', api_base='https://ollama.com/v1', api_key=os.getenv('GEMMA_API_KEY'))
import base64, io
payload = [{"type": "text", "text": question}]
for img in frames:
b = io.BytesIO()
img.save(b, format="JPEG")
payload.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64.b64encode(b.getvalue()).decode()}"}})
return model([{"role": "user", "content": payload}]).content
# ----------------------------------------------------------------------------
# SECTION 3: Fichiers & Audio
# ----------------------------------------------------------------------------
@tool
def read_pdf_file(file_path: str) -> str:
"""
Reads the content of a PDF file, extracting all visible text page by page.
Args:
file_path: The absolute or relative path to the .pdf file that needs to be read.
"""
import os
from pypdf import PdfReader
initialize_gpu()
if not os.path.exists(file_path):
return f"Erreur : Le fichier au chemin '{file_path}' est introuvable."
try:
reader = PdfReader(file_path)
full_text = []
# Parcourir et extraire le texte de chaque page
for page_num, page in enumerate(reader.pages, start=1):
page_text = page.extract_text()
if page_text and page_text.strip():
full_text.append(f"\n--- [Page {page_num}] ---")
full_text.append(page_text.strip())
output = "\n".join(full_text)
return output if output.strip() else "Le document PDF semble vide ou ne contient que des images (scan)."
except Exception as e:
return f"Erreur lors de la lecture du fichier PDF : {str(e)}"
@tool
def read_docx_file(file_path: str) -> str:
"""
Reads the content of a Word document (.docx file), extracting all text from paragraphs and tables.
Args:
file_path: The path to the .docx file that needs to be read.
"""
import docx
import os
initialize_gpu()
if not os.path.exists(file_path):
return f"Erreur : Le fichier au chemin '{file_path}' est introuvable."
try:
doc = docx.Document(file_path)
full_text = []
# 1. Extraction du texte des paragraphes classiques
for para in doc.paragraphs:
if para.text.strip():
full_text.append(para.text.strip())
# 2. Extraction du texte des tableaux (très fréquent dans GAIA)
for table in doc.tables:
full_text.append("\n--- [Tableau détecté] ---")
for row in table.rows:
row_text = [cell.text.strip() for cell in row.cells if cell.text.strip()]
if row_text:
# On sépare les colonnes par des barres verticales pour l'agent
full_text.append(" | ".join(row_text))
full_text.append("-------------------------\n")
# Renvoyer le tout proprement assemblé
output = "\n".join(full_text)
return output if output.strip() else "Le document est vide."
except Exception as e:
return f"Erreur lors de la lecture du fichier .docx : {str(e)}"
@tool
def file_from_url(url: str, save_as: str = "downloaded_file") -> str:
"""
Downloads a file from a provided URL, determines its extension properly, and saves it.
Args:
url: The source URL of the file to download.
save_as: Optional filename to save the file as. If not provided, derives the name from the URL or headers.
"""
import requests
import os
from urllib.parse import urlparse
import mimetypes
# 1. Utiliser un répertoire local dédié et contrôlé (comme ton dossier downloads)
download_dir = os.path.abspath("downloads")
os.makedirs(download_dir, exist_ok=True)
try:
with requests.get(url, stream=True, timeout=20) as r:
r.raise_for_status()
# 2. Détermination intelligente du nom et de l'extension
if save_as == "downloaded_file":
base_name = os.path.basename(urlparse(url).path)
if not base_name or "." not in base_name:
# Si l'URL est opaque, on regarde le Content-Type envoyé par le serveur
content_type = r.headers.get('content-type', '').split(';')[0]
ext = mimetypes.guess_extension(content_type) or ".txt"
base_name = f"downloaded_media_{int(os.path.getctime(download_dir))}{ext}"
save_as = base_name
file_path = os.path.join(download_dir, save_as)
# 3. Écriture par blocs (chunks)
with open(file_path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
# Renvoie le chemin absolu impeccable pour tes autres sous-agents
return os.path.abspath(file_path)
except Exception as e:
return f"Erreur lors du téléchargement du fichier depuis l'URL : {str(e)}"
@tool
def transcribe_youtube(yt_url: str) -> str:
"""
Downloads the audio from a YouTube video and performs speech-to-text transcription using the Groq cloud API (Whisper).
Useful for retrieving text content or answering questions based on video dialogue without local heavy models.
Args:
yt_url: The URL of the YouTube video to transcribe.
"""
import os, tempfile, yt_dlp, requests
groq_api_key = os.getenv("GROQ_API_KEY")
if not groq_api_key:
return "Erreur : La variable d'environnement GROQ_API_KEY n'est pas configurée dans l'environnement."
with tempfile.TemporaryDirectory() as tmpdir:
# Téléchargement de l'audio uniquement
ydl_opts = {
"format": "bestaudio",
"outtmpl": os.path.join(tmpdir, "audio.%(ext)s"),
"quiet": True,
"postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "mp3"}]
}
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.extract_info(yt_url, download=True)
audio_path = os.path.join(tmpdir, "audio.mp3")
# Appel API Groq Cloud (Whisper Large V3)
url = "https://api.groq.com/openai/v1/audio/transcriptions"
headers = {"Authorization": f"Bearer {groq_api_key}"}
with open(audio_path, 'rb') as f:
files = {"file": f}
data = {"model": "whisper-large-v3"}
response = requests.post(url, headers=headers, files=files, data=data)
if response.status_code != 200:
return f"Erreur API Groq : {response.text}"
return response.json().get("text", "")
except Exception as e:
return f"Erreur de traitement audio / API : {str(e)}"
@tool
def read_text_file(file_path: str) -> str:
"""
Reads and returns the raw plain text content from a specified local file path.
Args:
file_path: The full local path to the text file.
"""
with open(file_path, "r", encoding="utf-8") as f: return f.read()
# ----------------------------------------------------------------------------
# SECTION 4: OCR & Tabulaire
# ----------------------------------------------------------------------------
@tool
def extract_text_via_ocr(image_path: str) -> str:
"""
Extracts text from an image using the cloud-based OCR.space API.
Returns the detected text found within the provided image path without heavy local dependencies.
Args:
image_path: The local path to the image file containing text.
"""
import requests, os
# Utilisation de la clé publique 'helloworld' d'OCR.space si aucune clé n'est configurée
api_key = os.getenv("OCR_SPACE_API_KEY", "helloworld")
try:
with open(image_path, 'rb') as f:
response = requests.post(
'https://api.ocr.space/parse/image',
files={'file': f},
data={'apikey': api_key, 'language': 'fra'}
)
result = response.json()
if result.get("IsErroredOnProcessing"):
return f"Erreur OCR.space : {result.get('ErrorMessage')}"
parsed_results = result.get('ParsedResults', [])
if not parsed_results:
return "Aucun texte détecté sur l'image."
return parsed_results[0].get('ParsedText', '')
except Exception as e:
return f"Erreur lors de l'appel API OCR : {str(e)}"
@tool
def summarize_csv_data(path: str, query: str = "") -> str:
"""
Loads a CSV file into a pandas DataFrame and generates a descriptive summary, including column statistics.
Optionally allows filtering rows via a pandas query string.
Args:
path: The local file path to the CSV file.
query: Optional pandas query expression to filter rows before summarizing (e.g., "age > 30").
"""
import pandas as pd
df = pd.read_csv(path)
if query: df = df.query(query)
return df.describe().to_string()
@tool
def calculer_probabilite_loi(loi: str, parametres: dict, valeur_k: float) -> str:
"""
Calculates the exact or cumulative probability for a specified statistical distribution law (Normal or Binomial).
Args:
loi: The distribution type, either 'normale' or 'binomiale'.
parametres: Dictionary containing distribution parameters (e.g., {'moyenne': float, 'ecart_type': float} for Normal).
valeur_k: The value or bound for which the cumulative probability P(X <= k) is calculated.
"""
# Import local obligatoire pour l'autonomie en sandbox
from scipy import stats
try:
if loi.lower() == "normale":
loc = parametres.get("moyenne", 0)
scale = parametres.get("ecart_type", 1)
prob = stats.norm.cdf(valeur_k, loc=loc, scale=scale)
return f"Pour une Loi Normale({loc}, {scale}), P(X <= {valeur_k}) = {prob:.6f}"
elif loi.lower() == "binomiale":
n = int(parametres.get("n", 1))
p = float(parametres.get("p", 0.5))
prob = stats.binom.cdf(valeur_k, n, p)
prob_exacte = stats.binom.pmf(valeur_k, n, p)
return (
f"Pour une Loi Binomiale(n={n}, p={p}) :\n"
f"- Probabilité exacte P(X = {valeur_k}) = {prob_exacte:.6f}\n"
f"- Probabilité cumulative P(X <= {valeur_k}) = {prob:.6f}"
)
else:
return "Loi non supportée. Utilisez 'normale' ou 'binomiale'."
except Exception as e:
return f"Erreur de paramètres : {str(e)}"
@tool
def summarize_excel_data(path: str, query: str = "") -> str:
"""
Loads an Excel file (.xls or .xlsx) into a pandas DataFrame and generates a descriptive summary, including column statistics.
Optionally allows filtering rows via a pandas query string.
Args:
path: The local file path to the Excel file.
query: Optional pandas query expression to filter rows before summarizing (e.g., "age > 30").
"""
import pandas as pd
df = pd.read_excel(path)
if query: df = df.query(query)
return df.describe().to_string()
@tool
def analyser_serie_statistique(donnees: list[float]) -> str:
"""
Calculates essential descriptive statistics for a provided series of numerical data, such as mean, median, standard deviation, and range.
Args:
donnees: A list of floats representing the dataset to analyze.
"""
import numpy as np
arr = np.array(donnees)
return f"Moyenne: {np.mean(arr):.2f}, Écart-type: {np.std(arr):.2f}, Min: {np.min(arr)}, Max: {np.max(arr)}"
# ----------------------------------------------------------------------------
# SECTION 5: Maths & Chess
# ----------------------------------------------------------------------------
@tool
def resoudre_calcul_formel(expression: str, action: str, variable: str = "x") -> str:
"""
Performs formal mathematical computations such as solving equations, differentiation, integration, and simplification using sympy.
Args:
expression: The mathematical expression in Python syntax (e.g., "x**2 + 2*x - 3" or "x**2 - 4 = 0").
action: The mathematical operation to perform ('resoudre', 'deriver', 'integrer', or 'simplifier').
variable: The primary variable in the expression to operate on. Defaults to 'x'.
"""
import sympy as sp
v = sp.Symbol(variable)
e = sp.sympify(expression.split("=")[0]) if "=" in expression else sp.sympify(expression)
if action == "simplifier": return str(sp.simplify(e))
if action == "deriver": return str(sp.diff(e, v))
if action == "integrer": return str(sp.integrate(e, v))
return str(sp.solve(e, v))
@tool
def analyze_chess_position(fen: str) -> str:
"""
Analyzes a chess board state from a FEN (Forsyth-Edwards Notation) string.
Detects checks, checkmates, stalemates, lists legal moves, and scans for immediate 'Mate in 1' opportunities.
Args:
fen: The FEN string representing the current board state.
"""
import chess
board = chess.Board(fen)
moves = [board.san(m) for m in board.legal_moves]
return f"Turn: {'White' if board.turn else 'Black'}. Moves: {', '.join(moves)}"
@tool
def get_stockfish_evaluation(fen: str) -> str:
"""
Calls an external Chess API to get a deep engine evaluation (Stockfish 18) for a given FEN string.
It returns the absolute best move, tactical score, winning chances, and the expected continuation line.
Args:
fen: The FEN string representing the current board state to analyze.
"""
import requests
r = requests.post("https://chess-api.com/v1", json={"fen": fen, "depth": 12})
d = r.json()
return f"Best: {d.get('san')}, Eval: {d.get('eval')}"