Agent-evaluations / tools.py
WilliamRabuel's picture
Update tools.py
d5bc100 verified
raw
history blame
20.6 kB
import os
import re
import json
import time
import requests
from typing import List, Dict, Any, Optional
from pathlib import Path
import tempfile
from io import BytesIO
# Imports pour les outils
from ddgs import DDGS
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
from fake_useragent import UserAgent
from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound
# Imports pour la lecture de fichiers
import PyPDF2
from docx import Document
import openpyxl
import pandas as pd
import markdown
import magic
from PIL import Image
import numpy as np
from smolagents import tool
class AdvancedWebSearchTool:
"""Outil de recherche web avancé avec DuckDuckGo"""
def __init__(self):
self.ddgs = DDGS()
self.ua = UserAgent()
def search_web(self, query: str, max_results: int = 10, region: str = "en-en") -> List[Dict[str, str]]:
"""
Recherche avancée sur le web avec DuckDuckGo
Args:
query: Requête de recherche
max_results: Nombre maximum de résultats (défaut: 10)
region: Région de recherche (défaut: en-en)
Returns:
Liste de dictionnaires avec title, url, snippet
"""
try:
print(f"🔍 Recherche: '{query}' (max: {max_results})")
results = []
search_results = self.ddgs.text(
query=query,
region=region,
max_results=max_results,
timelimit="m" # Résultats récents
)
for result in search_results:
results.append({
"title": result.get("title", ""),
"url": result.get("href", ""),
"snippet": result.get("body", "")
})
print(f"✅ Trouvé {len(results)} résultats")
return results
except Exception as e:
print(f"❌ Erreur lors de la recherche: {e}")
return [{"error": str(e)}]
class AdvancedWebScrapingTool:
"""Outil de scraping web avancé avec Selenium et BeautifulSoup"""
def __init__(self):
self.ua = UserAgent()
self._driver = None
def _get_driver(self):
"""Initialise le driver Selenium si nécessaire"""
if self._driver is None:
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument(f"--user-agent={self.ua.random}")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1920,1080")
try:
self._driver = webdriver.Chrome(
options=options
)
except Exception as e:
print(f"❌ Erreur driver Selenium: {e}")
return None
return self._driver
def scrape_website(self, url: str, extract_text: bool = True, extract_links: bool = False) -> Dict[str, Any]:
"""
Scrape un site web et extrait le contenu
Args:
url: URL à scraper
extract_text: Extraire le texte principal
extract_links: Extraire les liens
Returns:
Dictionnaire avec le contenu extrait
"""
try:
print(f"🌐 Scraping: {url}")
# Tentative avec requests d'abord (plus rapide)
headers = {
'User-Agent': self.ua.random,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr,en-EN,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
result = {
"url": url,
"title": "",
"text": "",
"links": [],
"status": "success"
}
# Titre
title_tag = soup.find('title')
if title_tag:
result["title"] = title_tag.get_text().strip()
# Texte principal
if extract_text:
# Supprime les scripts et styles
for script in soup(["script", "style", "nav", "footer", "header"]):
script.decompose()
# Extrait le texte principal
main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile(r'content|main|article'))
if main_content:
text = main_content.get_text(separator=' ', strip=True)
else:
text = soup.get_text(separator=' ', strip=True)
# Nettoie le texte
text = re.sub(r'\s+', ' ', text)
result["text"] = text[:10000] # Limite à 10k caractères
# Liens
if extract_links:
links = []
for link in soup.find_all('a', href=True):
href = link['href']
if href.startswith('http'):
links.append({
"url": href,
"text": link.get_text().strip()
})
result["links"] = links[:50] # Limite à 50 liens
print(f"✅ Scraping réussi: {len(result['text'])} caractères")
return result
except requests.exceptions.RequestException as e:
# Tentative avec Selenium si requests échoue
return self._scrape_with_selenium(url, extract_text, extract_links)
except Exception as e:
print(f"❌ Erreur scraping: {e}")
return {"error": str(e), "url": url}
def _scrape_with_selenium(self, url: str, extract_text: bool, extract_links: bool) -> Dict[str, Any]:
"""Scraping avec Selenium en fallback"""
try:
print("🤖 Tentative avec Selenium...")
driver = self._get_driver()
if not driver:
return {"error": "Impossible d'initialiser le driver", "url": url}
driver.get(url)
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
result = {
"url": url,
"title": driver.title,
"text": "",
"links": [],
"status": "success_selenium"
}
if extract_text:
# Supprime les éléments indésirables
driver.execute_script("""
var elements = document.querySelectorAll('script, style, nav, footer, header');
for (var i = 0; i < elements.length; i++) {
elements[i].remove();
}
""")
text = driver.find_element(By.TAG_NAME, "body").text
result["text"] = text[:10000]
if extract_links:
links = []
for link in driver.find_elements(By.TAG_NAME, "a"):
href = link.get_attribute("href")
if href and href.startswith("http"):
links.append({
"url": href,
"text": link.text.strip()
})
result["links"] = links[:50]
return result
except Exception as e:
print(f"❌ Erreur Selenium: {e}")
return {"error": str(e), "url": url}
def __del__(self):
"""Nettoie le driver à la destruction"""
if self._driver:
try:
self._driver.quit()
except:
pass
class FileReaderTool:
"""Outil de lecture de fichiers multi-format"""
def read_file(self, file_path: str) -> Dict[str, Any]:
"""
Lit un fichier et extrait son contenu selon le format
Args:
file_path: Chemin vers le fichier
Returns:
Dictionnaire avec le contenu du fichier
"""
try:
print(f"📄 Lecture du fichier: {file_path}")
if not os.path.exists(file_path):
return {"error": f"Fichier non trouvé: {file_path}"}
# Détection du type de fichier
file_ext = Path(file_path).suffix.lower()
result = {
"file_path": file_path,
"file_type": file_ext,
"content": "",
"metadata": {},
"status": "success"
}
# PDF
if file_ext == '.pdf':
result.update(self._read_pdf(file_path))
# Word Documents
elif file_ext in ['.docx', '.doc']:
result.update(self._read_docx(file_path))
# Excel
elif file_ext in ['.xlsx', '.xls']:
result.update(self._read_excel(file_path))
# CSV
elif file_ext == '.csv':
result.update(self._read_csv(file_path))
# JSON
elif file_ext == '.json':
result.update(self._read_json(file_path))
# Markdown
elif file_ext in ['.md', '.markdown']:
result.update(self._read_markdown(file_path))
# Texte simple
elif file_ext in ['.txt', '.log', '.py', '.js', '.html', '.css']:
result.update(self._read_text(file_path))
# Images
elif file_ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
result.update(self._read_image(file_path))
else:
# Tentative de lecture comme texte
result.update(self._read_text(file_path))
print(f"✅ Fichier lu avec succès: {len(str(result['content']))} caractères")
return result
except Exception as e:
print(f"❌ Erreur lecture fichier: {e}")
return {"error": str(e), "file_path": file_path}
def _read_pdf(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier PDF"""
try:
with open(file_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
text = ""
for page in pdf_reader.pages:
text += page.extract_text() + "\n"
return {
"content": text.strip(),
"metadata": {
"pages": len(pdf_reader.pages),
"title": pdf_reader.metadata.get('/Title', '') if pdf_reader.metadata else ''
}
}
except Exception as e:
return {"error": f"Erreur PDF: {e}"}
def _read_docx(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier Word"""
try:
doc = Document(file_path)
text = "\n".join([paragraph.text for paragraph in doc.paragraphs])
return {
"content": text,
"metadata": {
"paragraphs": len(doc.paragraphs),
"core_properties": str(doc.core_properties.title) if doc.core_properties.title else ""
}
}
except Exception as e:
return {"error": f"Erreur DOCX: {e}"}
def _read_excel(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier Excel"""
try:
df = pd.read_excel(file_path, sheet_name=None)
content = {}
for sheet_name, sheet_df in df.items():
content[sheet_name] = {
"data": sheet_df.to_dict('records')[:100], # Limite à 100 lignes
"shape": sheet_df.shape,
"columns": list(sheet_df.columns)
}
return {
"content": content,
"metadata": {
"sheets": list(df.keys()),
"total_sheets": len(df)
}
}
except Exception as e:
return {"error": f"Erreur Excel: {e}"}
def _read_csv(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier CSV"""
try:
df = pd.read_csv(file_path)
return {
"content": {
"data": df.head(100).to_dict('records'), # Premières 100 lignes
"shape": df.shape,
"columns": list(df.columns),
"dtypes": df.dtypes.to_dict()
},
"metadata": {
"rows": len(df),
"columns": len(df.columns)
}
}
except Exception as e:
return {"error": f"Erreur CSV: {e}"}
def _read_json(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier JSON"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
return {
"content": data,
"metadata": {
"type": type(data).__name__,
"size": len(str(data))
}
}
except Exception as e:
return {"error": f"Erreur JSON: {e}"}
def _read_markdown(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier Markdown"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
html = markdown.markdown(content)
return {
"content": content,
"metadata": {
"html_version": html,
"lines": len(content.split('\n'))
}
}
except Exception as e:
return {"error": f"Erreur Markdown: {e}"}
def _read_text(self, file_path: str) -> Dict[str, Any]:
"""Lit un fichier texte"""
try:
# Détection de l'encodage
encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as file:
content = file.read()
return {
"content": content,
"metadata": {
"encoding": encoding,
"lines": len(content.split('\n')),
"characters": len(content)
}
}
except UnicodeDecodeError:
continue
return {"error": "Impossible de décoder le fichier"}
except Exception as e:
return {"error": f"Erreur texte: {e}"}
def _read_image(self, file_path: str) -> Dict[str, Any]:
"""Lit une image et extrait les métadonnées"""
try:
with Image.open(file_path) as img:
return {
"content": f"Image {img.format} - Taille: {img.size[0]}x{img.size[1]}",
"metadata": {
"format": img.format,
"size": img.size,
"mode": img.mode,
"path": file_path
}
}
except Exception as e:
return {"error": f"Erreur image: {e}"}
# Initialisation et Export des Outils
# On instancie les classes qui contiennent la logique des outils
_web_search_tool_instance = AdvancedWebSearchTool()
_web_scraping_tool_instance = AdvancedWebScrapingTool()
_file_reader_tool_instance = FileReaderTool()
# On définit des fonctions "wrapper" autonomes et on les décore avec @tool
@tool
def search_web(query: str, max_results: int = 10, region: str = "fr-fr") -> List[Dict[str, str]]:
"""
Recherche avancée sur le web avec DuckDuckGo pour trouver des informations à jour.
Args:
query: Requête de recherche.
max_results: Nombre maximum de résultats à retourner.
region: Région de recherche (ex: 'fr-fr', 'wt-wt').
Returns:
Liste de résultats de recherche, chacun contenant un titre, une URL et un snippet.
"""
return _web_search_tool_instance.search_web(query, max_results=max_results, region=region)
@tool
def scrape_website(url: str, extract_text: bool = True, extract_links: bool = False) -> Dict[str, Any]:
"""
Scrape une page web pour en extraire le contenu textuel principal, le titre et les liens.
Args:
url: URL de la page web à scraper.
extract_text: Si True, extrait le texte principal de la page.
extract_links: Si True, extrait les liens hypertextes de la page.
Returns:
Un dictionnaire contenant le contenu extrait du site.
"""
return _web_scraping_tool_instance.scrape_website(url, extract_text=extract_text, extract_links=extract_links)
@tool
def read_file(file_path: str) -> Dict[str, Any]:
"""
Lit un fichier local et en extrait le contenu. Gère divers formats (PDF, DOCX, XLSX, CSV, JSON, TXT, etc.).
Args:
file_path: Chemin d'accès local au fichier à lire.
Returns:
Un dictionnaire contenant le contenu du fichier et ses métadonnées.
"""
return _file_reader_tool_instance.read_file(file_path)
@tool
def get_youtube_transcript(video_url: str) -> Dict[str, Any]:
"""
Récupère la transcription d'une vidéo YouTube à partir de son URL standard.
Args:
video_url (str): L'URL complète et standard de la vidéo YouTube dont la transcription est nécessaire.
Returns:
Un dictionnaire contenant la transcription complète sous la clé 'transcript' en cas de succès,
ou un message d'erreur sous la clé 'error' en cas d'échec.
"""
try:
video_id = None
# Cherche l'ID après "v="
if "v=" in video_url:
video_id = video_url.split("v=")[1].split('&')[0]
# Cherche l'ID dans les URLs du benchmark
elif video_url.startswith("http://googleusercontent.com/youtube.com/"):
video_id = video_url.split('/')[-1]
if not video_id:
return {"error": f"Impossible d'extraire l'ID de la vidéo depuis l'URL : {video_url}"}
print(f"📖 Récupération de la transcription pour la vidéo YouTube : {video_id}")
# On demande la transcription en anglais (le plus courant) et en français.
transcript_list = YouTubeTranscriptApi.get_transcript(video_id, languages=['en', 'fr'])
transcript_text = " ".join([item['text'] for item in transcript_list])
return {"status": "success", "transcript": transcript_text[:15000]}
except (TranscriptsDisabled, NoTranscriptFound) as e:
error_msg = f"Impossible de récupérer la transcription pour {video_id}: {type(e).__name__}"
print(f"✅ {error_msg}")
return {"status": "error", "error": error_msg}
except Exception as e:
error_msg = f"Erreur inattendue pour {video_id}: {e}"
print(f"❌ {error_msg}")
return {"error": error_msg}