WilliamRabuel commited on
Commit
4ff7b57
·
verified ·
1 Parent(s): 145e1cf

Create tools.py

Browse files
Files changed (1) hide show
  1. tools.py +482 -0
tools.py ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import time
5
+ import requests
6
+ from typing import List, Dict, Any, Optional
7
+ from pathlib import Path
8
+ import tempfile
9
+ from io import BytesIO
10
+
11
+ # Imports pour les outils
12
+ from duckduckgo_search import DDGS
13
+ from bs4 import BeautifulSoup
14
+ from selenium import webdriver
15
+ from selenium.webdriver.chrome.options import Options
16
+ from selenium.webdriver.common.by import By
17
+ from selenium.webdriver.support.ui import WebDriverWait
18
+ from selenium.webdriver.support import expected_conditions as EC
19
+ from webdriver_manager.chrome import ChromeDriverManager
20
+ from fake_useragent import UserAgent
21
+
22
+ # Imports pour la lecture de fichiers
23
+ import PyPDF2
24
+ from docx import Document
25
+ import openpyxl
26
+ import pandas as pd
27
+ import markdown
28
+ import magic
29
+ from PIL import Image
30
+ import numpy as np
31
+
32
+ from smolagents import tool
33
+
34
+ class AdvancedWebSearchTool:
35
+ """Outil de recherche web avancé avec DuckDuckGo"""
36
+
37
+ def __init__(self):
38
+ self.ddgs = DDGS()
39
+ self.ua = UserAgent()
40
+
41
+ @tool
42
+ def search_web(self, query: str, max_results: int = 10, region: str = "fr-fr") -> List[Dict[str, str]]:
43
+ """
44
+ Recherche avancée sur le web avec DuckDuckGo
45
+
46
+ Args:
47
+ query: Requête de recherche
48
+ max_results: Nombre maximum de résultats (défaut: 10)
49
+ region: Région de recherche (défaut: fr-fr)
50
+
51
+ Returns:
52
+ Liste de dictionnaires avec title, url, snippet
53
+ """
54
+ try:
55
+ print(f"🔍 Recherche: '{query}' (max: {max_results})")
56
+
57
+ results = []
58
+ search_results = self.ddgs.text(
59
+ keywords=query,
60
+ region=region,
61
+ max_results=max_results,
62
+ timelimit="m" # Résultats récents
63
+ )
64
+
65
+ for result in search_results:
66
+ results.append({
67
+ "title": result.get("title", ""),
68
+ "url": result.get("href", ""),
69
+ "snippet": result.get("body", "")
70
+ })
71
+
72
+ print(f"✅ Trouvé {len(results)} résultats")
73
+ return results
74
+
75
+ except Exception as e:
76
+ print(f"❌ Erreur lors de la recherche: {e}")
77
+ return [{"error": str(e)}]
78
+
79
+ class AdvancedWebScrapingTool:
80
+ """Outil de scraping web avancé avec Selenium et BeautifulSoup"""
81
+
82
+ def __init__(self):
83
+ self.ua = UserAgent()
84
+ self._driver = None
85
+
86
+ def _get_driver(self):
87
+ """Initialise le driver Selenium si nécessaire"""
88
+ if self._driver is None:
89
+ options = Options()
90
+ options.add_argument("--headless")
91
+ options.add_argument("--no-sandbox")
92
+ options.add_argument("--disable-dev-shm-usage")
93
+ options.add_argument(f"--user-agent={self.ua.random}")
94
+ options.add_argument("--disable-gpu")
95
+ options.add_argument("--window-size=1920,1080")
96
+
97
+ try:
98
+ self._driver = webdriver.Chrome(
99
+ options=options
100
+ )
101
+ except Exception as e:
102
+ print(f"❌ Erreur driver Selenium: {e}")
103
+ return None
104
+ return self._driver
105
+
106
+ @tool
107
+ def scrape_website(self, url: str, extract_text: bool = True, extract_links: bool = False) -> Dict[str, Any]:
108
+ """
109
+ Scrape un site web et extrait le contenu
110
+
111
+ Args:
112
+ url: URL à scraper
113
+ extract_text: Extraire le texte principal
114
+ extract_links: Extraire les liens
115
+
116
+ Returns:
117
+ Dictionnaire avec le contenu extrait
118
+ """
119
+ try:
120
+ print(f"🌐 Scraping: {url}")
121
+
122
+ # Tentative avec requests d'abord (plus rapide)
123
+ headers = {
124
+ 'User-Agent': self.ua.random,
125
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
126
+ 'Accept-Language': 'fr-FR,fr;q=0.5',
127
+ 'Accept-Encoding': 'gzip, deflate',
128
+ 'Connection': 'keep-alive',
129
+ }
130
+
131
+ response = requests.get(url, headers=headers, timeout=30)
132
+ response.raise_for_status()
133
+
134
+ soup = BeautifulSoup(response.content, 'html.parser')
135
+
136
+ result = {
137
+ "url": url,
138
+ "title": "",
139
+ "text": "",
140
+ "links": [],
141
+ "status": "success"
142
+ }
143
+
144
+ # Titre
145
+ title_tag = soup.find('title')
146
+ if title_tag:
147
+ result["title"] = title_tag.get_text().strip()
148
+
149
+ # Texte principal
150
+ if extract_text:
151
+ # Supprime les scripts et styles
152
+ for script in soup(["script", "style", "nav", "footer", "header"]):
153
+ script.decompose()
154
+
155
+ # Extrait le texte principal
156
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile(r'content|main|article'))
157
+
158
+ if main_content:
159
+ text = main_content.get_text(separator=' ', strip=True)
160
+ else:
161
+ text = soup.get_text(separator=' ', strip=True)
162
+
163
+ # Nettoie le texte
164
+ text = re.sub(r'\s+', ' ', text)
165
+ result["text"] = text[:10000] # Limite à 10k caractères
166
+
167
+ # Liens
168
+ if extract_links:
169
+ links = []
170
+ for link in soup.find_all('a', href=True):
171
+ href = link['href']
172
+ if href.startswith('http'):
173
+ links.append({
174
+ "url": href,
175
+ "text": link.get_text().strip()
176
+ })
177
+ result["links"] = links[:50] # Limite à 50 liens
178
+
179
+ print(f"✅ Scraping réussi: {len(result['text'])} caractères")
180
+ return result
181
+
182
+ except requests.exceptions.RequestException as e:
183
+ # Tentative avec Selenium si requests échoue
184
+ return self._scrape_with_selenium(url, extract_text, extract_links)
185
+ except Exception as e:
186
+ print(f"❌ Erreur scraping: {e}")
187
+ return {"error": str(e), "url": url}
188
+
189
+ def _scrape_with_selenium(self, url: str, extract_text: bool, extract_links: bool) -> Dict[str, Any]:
190
+ """Scraping avec Selenium en fallback"""
191
+ try:
192
+ print("🤖 Tentative avec Selenium...")
193
+ driver = self._get_driver()
194
+ if not driver:
195
+ return {"error": "Impossible d'initialiser le driver", "url": url}
196
+
197
+ driver.get(url)
198
+ WebDriverWait(driver, 10).until(
199
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
200
+ )
201
+
202
+ result = {
203
+ "url": url,
204
+ "title": driver.title,
205
+ "text": "",
206
+ "links": [],
207
+ "status": "success_selenium"
208
+ }
209
+
210
+ if extract_text:
211
+ # Supprime les éléments indésirables
212
+ driver.execute_script("""
213
+ var elements = document.querySelectorAll('script, style, nav, footer, header');
214
+ for (var i = 0; i < elements.length; i++) {
215
+ elements[i].remove();
216
+ }
217
+ """)
218
+
219
+ text = driver.find_element(By.TAG_NAME, "body").text
220
+ result["text"] = text[:10000]
221
+
222
+ if extract_links:
223
+ links = []
224
+ for link in driver.find_elements(By.TAG_NAME, "a"):
225
+ href = link.get_attribute("href")
226
+ if href and href.startswith("http"):
227
+ links.append({
228
+ "url": href,
229
+ "text": link.text.strip()
230
+ })
231
+ result["links"] = links[:50]
232
+
233
+ return result
234
+
235
+ except Exception as e:
236
+ print(f"❌ Erreur Selenium: {e}")
237
+ return {"error": str(e), "url": url}
238
+
239
+ def __del__(self):
240
+ """Nettoie le driver à la destruction"""
241
+ if self._driver:
242
+ try:
243
+ self._driver.quit()
244
+ except:
245
+ pass
246
+
247
+ class FileReaderTool:
248
+ """Outil de lecture de fichiers multi-format"""
249
+
250
+ @tool
251
+ def read_file(self, file_path: str) -> Dict[str, Any]:
252
+ """
253
+ Lit un fichier et extrait son contenu selon le format
254
+
255
+ Args:
256
+ file_path: Chemin vers le fichier
257
+
258
+ Returns:
259
+ Dictionnaire avec le contenu du fichier
260
+ """
261
+ try:
262
+ print(f"📄 Lecture du fichier: {file_path}")
263
+
264
+ if not os.path.exists(file_path):
265
+ return {"error": f"Fichier non trouvé: {file_path}"}
266
+
267
+ # Détection du type de fichier
268
+ file_ext = Path(file_path).suffix.lower()
269
+
270
+ result = {
271
+ "file_path": file_path,
272
+ "file_type": file_ext,
273
+ "content": "",
274
+ "metadata": {},
275
+ "status": "success"
276
+ }
277
+
278
+ # PDF
279
+ if file_ext == '.pdf':
280
+ result.update(self._read_pdf(file_path))
281
+
282
+ # Word Documents
283
+ elif file_ext in ['.docx', '.doc']:
284
+ result.update(self._read_docx(file_path))
285
+
286
+ # Excel
287
+ elif file_ext in ['.xlsx', '.xls']:
288
+ result.update(self._read_excel(file_path))
289
+
290
+ # CSV
291
+ elif file_ext == '.csv':
292
+ result.update(self._read_csv(file_path))
293
+
294
+ # JSON
295
+ elif file_ext == '.json':
296
+ result.update(self._read_json(file_path))
297
+
298
+ # Markdown
299
+ elif file_ext in ['.md', '.markdown']:
300
+ result.update(self._read_markdown(file_path))
301
+
302
+ # Texte simple
303
+ elif file_ext in ['.txt', '.log', '.py', '.js', '.html', '.css']:
304
+ result.update(self._read_text(file_path))
305
+
306
+ # Images
307
+ elif file_ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
308
+ result.update(self._read_image(file_path))
309
+
310
+ else:
311
+ # Tentative de lecture comme texte
312
+ result.update(self._read_text(file_path))
313
+
314
+ print(f"✅ Fichier lu avec succès: {len(str(result['content']))} caractères")
315
+ return result
316
+
317
+ except Exception as e:
318
+ print(f"❌ Erreur lecture fichier: {e}")
319
+ return {"error": str(e), "file_path": file_path}
320
+
321
+ def _read_pdf(self, file_path: str) -> Dict[str, Any]:
322
+ """Lit un fichier PDF"""
323
+ try:
324
+ with open(file_path, 'rb') as file:
325
+ pdf_reader = PyPDF2.PdfReader(file)
326
+ text = ""
327
+ for page in pdf_reader.pages:
328
+ text += page.extract_text() + "\n"
329
+
330
+ return {
331
+ "content": text.strip(),
332
+ "metadata": {
333
+ "pages": len(pdf_reader.pages),
334
+ "title": pdf_reader.metadata.get('/Title', '') if pdf_reader.metadata else ''
335
+ }
336
+ }
337
+ except Exception as e:
338
+ return {"error": f"Erreur PDF: {e}"}
339
+
340
+ def _read_docx(self, file_path: str) -> Dict[str, Any]:
341
+ """Lit un fichier Word"""
342
+ try:
343
+ doc = Document(file_path)
344
+ text = "\n".join([paragraph.text for paragraph in doc.paragraphs])
345
+
346
+ return {
347
+ "content": text,
348
+ "metadata": {
349
+ "paragraphs": len(doc.paragraphs),
350
+ "core_properties": str(doc.core_properties.title) if doc.core_properties.title else ""
351
+ }
352
+ }
353
+ except Exception as e:
354
+ return {"error": f"Erreur DOCX: {e}"}
355
+
356
+ def _read_excel(self, file_path: str) -> Dict[str, Any]:
357
+ """Lit un fichier Excel"""
358
+ try:
359
+ df = pd.read_excel(file_path, sheet_name=None)
360
+
361
+ content = {}
362
+ for sheet_name, sheet_df in df.items():
363
+ content[sheet_name] = {
364
+ "data": sheet_df.to_dict('records')[:100], # Limite à 100 lignes
365
+ "shape": sheet_df.shape,
366
+ "columns": list(sheet_df.columns)
367
+ }
368
+
369
+ return {
370
+ "content": content,
371
+ "metadata": {
372
+ "sheets": list(df.keys()),
373
+ "total_sheets": len(df)
374
+ }
375
+ }
376
+ except Exception as e:
377
+ return {"error": f"Erreur Excel: {e}"}
378
+
379
+ def _read_csv(self, file_path: str) -> Dict[str, Any]:
380
+ """Lit un fichier CSV"""
381
+ try:
382
+ df = pd.read_csv(file_path)
383
+
384
+ return {
385
+ "content": {
386
+ "data": df.head(100).to_dict('records'), # Premières 100 lignes
387
+ "shape": df.shape,
388
+ "columns": list(df.columns),
389
+ "dtypes": df.dtypes.to_dict()
390
+ },
391
+ "metadata": {
392
+ "rows": len(df),
393
+ "columns": len(df.columns)
394
+ }
395
+ }
396
+ except Exception as e:
397
+ return {"error": f"Erreur CSV: {e}"}
398
+
399
+ def _read_json(self, file_path: str) -> Dict[str, Any]:
400
+ """Lit un fichier JSON"""
401
+ try:
402
+ with open(file_path, 'r', encoding='utf-8') as file:
403
+ data = json.load(file)
404
+
405
+ return {
406
+ "content": data,
407
+ "metadata": {
408
+ "type": type(data).__name__,
409
+ "size": len(str(data))
410
+ }
411
+ }
412
+ except Exception as e:
413
+ return {"error": f"Erreur JSON: {e}"}
414
+
415
+ def _read_markdown(self, file_path: str) -> Dict[str, Any]:
416
+ """Lit un fichier Markdown"""
417
+ try:
418
+ with open(file_path, 'r', encoding='utf-8') as file:
419
+ content = file.read()
420
+ html = markdown.markdown(content)
421
+
422
+ return {
423
+ "content": content,
424
+ "metadata": {
425
+ "html_version": html,
426
+ "lines": len(content.split('\n'))
427
+ }
428
+ }
429
+ except Exception as e:
430
+ return {"error": f"Erreur Markdown: {e}"}
431
+
432
+ def _read_text(self, file_path: str) -> Dict[str, Any]:
433
+ """Lit un fichier texte"""
434
+ try:
435
+ # Détection de l'encodage
436
+ encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
437
+
438
+ for encoding in encodings:
439
+ try:
440
+ with open(file_path, 'r', encoding=encoding) as file:
441
+ content = file.read()
442
+ return {
443
+ "content": content,
444
+ "metadata": {
445
+ "encoding": encoding,
446
+ "lines": len(content.split('\n')),
447
+ "characters": len(content)
448
+ }
449
+ }
450
+ except UnicodeDecodeError:
451
+ continue
452
+
453
+ return {"error": "Impossible de décoder le fichier"}
454
+
455
+ except Exception as e:
456
+ return {"error": f"Erreur texte: {e}"}
457
+
458
+ def _read_image(self, file_path: str) -> Dict[str, Any]:
459
+ """Lit une image et extrait les métadonnées"""
460
+ try:
461
+ with Image.open(file_path) as img:
462
+ return {
463
+ "content": f"Image {img.format} - Taille: {img.size[0]}x{img.size[1]}",
464
+ "metadata": {
465
+ "format": img.format,
466
+ "size": img.size,
467
+ "mode": img.mode,
468
+ "path": file_path
469
+ }
470
+ }
471
+ except Exception as e:
472
+ return {"error": f"Erreur image: {e}"}
473
+
474
+ # Initialisation globale des outils
475
+ web_search_tool = AdvancedWebSearchTool()
476
+ web_scraping_tool = AdvancedWebScrapingTool()
477
+ file_reader_tool = FileReaderTool()
478
+
479
+ # Export des outils pour smolagents
480
+ search_web = web_search_tool.search_web
481
+ scrape_website = web_scraping_tool.scrape_website
482
+ read_file = file_reader_tool.read_file