Nancy1906 commited on
Commit
07de393
·
verified ·
1 Parent(s): 9480657
Files changed (1) hide show
  1. my_tools.py +144 -695
my_tools.py CHANGED
@@ -1,5 +1,3 @@
1
- # my_tools.py
2
-
3
  import os
4
  import math
5
  import time
@@ -7,14 +5,7 @@ import asyncio
7
  import subprocess
8
  import requests
9
  import pandas as pd
10
- from io import StringIO, BytesIO #acc = ""
11
- for chunk in stream:
12
- delta = getattr(chunk, "text", "")
13
- if not delta and hasattr(chunk, 'parts') and chunk.parts:
14
- delta = chunk.parts[0].text
15
- if delta:
16
- acc += delta
17
- yield CompletionResponse(text=acc, delta BytesIO para leer Excel de contenido web si fuera necesario
18
  from bs4 import BeautifulSoup
19
  from duckduckgo_search import DDGS
20
  import wikipedia
@@ -22,156 +13,64 @@ from pydantic import Field
22
  import google.generativeai as genai
23
 
24
  # LlamaIndex imports
25
- from llama_index.core.llms import ChatMessage, LLMMetadata, LLM, CompletionResponse=delta)
26
- return gen()
27
-
28
- async def astream_complete(self, prompt, formatted=False, **kwargs):
29
- return await asyncio.to_thread(self.stream_complete, prompt, formatted=formatted, **kwargs)
30
-
31
- def astream_chat(self, messages: list[ChatMessage], **kwargs):
32
- ChatMessage.message = property(lambda self: self)
33
  from llama_index.core.tools import FunctionTool
34
  from llama_index.core.agent import ReActAgent
35
- from llama_index.core.callbacks.llama_debug import
36
- return self.stream_chat(messages, **kwargs)
37
 
38
  # -------------------------------------------------------------------
39
- # 2) Herramientas (con nuevas herramientas y mejoras)
40
- # -------------------------------------------------------------------
41
- HEADERS = {'User- LlamaDebugHandler
42
-
43
- # -------------------------------------------------------------------
44
- # 1) GeminiLLM personalizado (SIN CAMBIOS - Asumimos que está bien)
45
  # -------------------------------------------------------------------
 
46
  class GeminiLLM(LLM):
47
- Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/model_name: str = Field(default="models/gemini-1.5-flash-latest")
48
- 537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
49
-
50
- def buscar_web(query: str, max_attempts: int temperature: float = Field(default=0.0) # Mantenemos 0.0 para consistencia
51
 
52
  class Config:
53
  extra = "allow"
54
 
55
  def __init__(self, **kwargs):
56
- super().__ = 2, num_results: int = 5) -> str: # MEJORA: num_results aumentado
57
- for i in range(max_attempts):
58
- try:
59
- with DDGS(headers=HEADERSinit__(**kwargs)
60
  key = os.getenv("GEMINI_API_KEY")
61
  if not key:
62
  raise ValueError("GEMINI_API_KEY no configurada")
63
  genai.configure(api_key=key)
64
- self._gen_cfg = genai.types.GenerationConfig(temperature=, timeout=25) as ddgs:
65
- # MEJORA: 'y' (año) puede ser demasiado restrictivo, quitamos timelimit por defecto.
66
- # El LLM puede especificarlo en la query si es necesario ("self.temperature)
67
  self._model = genai.GenerativeModel(
68
  model_name=self.model_name,
69
  generation_config=self._gen_cfg
70
  )
71
- # Asegurar que LlamaDebugHandler senoticias recientes de X")
72
- results = list(ddgs.text(query, region='es-es', safesearch='moderate', max_results=num_results))
73
- if results:
74
- return "\n". añade si no hay otros handlers
75
- # Lo quitamos de aquí porque es mejor añadirlo explícitamente aljoin(f"Título: {r['title']}\nCuerpo: {r['body']}\nFuente crear el agente o el LLM si se desea
76
- # if not self.callback_manager.handlers:
77
- #: {r['href']}" for r in results)
78
- return "No se encontraron resultados para la búsqueda web."
79
- except Exception as e:
80
- if i < max_attempts - 1:
81
- time. self.callback_manager.add_handler(LlamaDebugHandler())
82
 
83
  @property
84
  def metadata(self):
85
  return LLMMetadata(
86
- context_window=1048576sleep(3 * (i + 1)) # Reducido ligeramente el sleep
87
- else:
88
- return f, # Gemini 1.5 Flash tiene 1M de tokens
89
  num_output=8192,
90
- "Error buscar_web tras {max_attempts} intentos: {e}"
91
-
92
- def reverse_text(text: str) is_chat_model=True,
93
- is_function_calling_model=True, # Gemini es -> str:
94
- return text[::-1]
95
-
96
- def analyze_table(table_md: str, question: str) -> bueno en esto
97
  model_name=self.model_name,
98
  )
99
 
100
- def chat(self, messages, str:
101
- try:
102
- lines = [l for l in table_md.splitlines() if l **kwargs):
103
  hist = []
104
  for m in messages[:-1]:
105
- role = "user".strip()]
106
- lines = [l for l in lines if not l.strip().startswith('|') or '---' not in l]
107
- rows = []
108
- for l in lines:
109
- parts = [c if m.role == "user" else "model"
110
- # Asegurar que content es string
111
- content_str = str(m.content) if m.content is not None else ""
112
- hist.append({"role.strip() for c in l.strip().strip('|').split('|')]
113
- rows.append(parts)
114
- if not rows or len(rows) < 1: # Comprobación de tabla vacía o solo cabecera
115
- ": role, "parts":[{"text": content_str}]})
116
-
117
- last_content_str = str(messages[-1].content) if messages[-1].content is not None else ""
118
  session = self._model.start_chat(history=hist)
119
  try:
120
- resp = session.send_message(last_content_str)
121
  return ChatMessage(role="assistant", content=resp.text)
122
- except Exceptionreturn "Error analyze_table: La tabla Markdown parece estar vacía o mal formateada después de la limpieza."
123
- df = pd.DataFrame(rows[1:], columns=rows[0])
124
-
125
- if 'conmut' in question.lower():
126
- S = rows[0][1:]
127
- counter = set()
128
- for x_label in S:
129
- for y_label in S:
130
- # Asegurarse de que los as e:
131
- # print(f"DEBUG Gemini chat error: {e}") # Para depuración local
132
- # print(f"DEBUG History: {hist}")
133
- # print(f"DEBUG Last message: {last_content_str}")
134
  return ChatMessage(role="assistant", content=f"Error Gemini: {e}")
135
 
136
  async def achat(self, messages, **kwargs):
137
  return await asyncio.to_thread(self.chat, messages, **kwargs)
138
 
139
- # stream_chat, complete, acomplete, stream_complete, astream_ índices de las columnas existan
140
- if x_label not in df.columns or y_label not in df.columns:
141
- continue # Saltar si alguna columna no existe (podría pasar con tablas mal formadas)
142
-
143
- # Localizar valores, asegurándose de que las filas (indexadas por la primera columna) existan
144
- x_row = df[df[rows[0][0]] == x_label]
145
- y_row = df[df[rows[0][0]] == y_label]
146
-
147
- if x_row.empty or y_rowcomplete, astream_chat (SIN CAMBIOS)
148
- # ... (mantener el código de estos métodos igual que en el original) ...
149
- def stream_chat(self, messages, **kwargs):
150
- hist = []
151
- for m in messages[:-1]:
152
- role = "user" if m.role=="user" else "model"
153
- content_str = str(m.content) if m.content is not None else ""
154
- hist.append({"role": role, "parts":[{"text":content_str}]})
155
- last_content_str = str(messages[-1].content) if messages[-1].content is not None else ""
156
- session = self._model.start_chat(history=hist)
157
- stream = session.send_message(last.empty:
158
- continue # Saltar si no se encuentra la fila para x_label o y_label
159
-
160
- val_a_series = x_row[y_label]
161
- val_b_series = y_row[x_label]
162
-
163
- if val_a_series.empty or val_b_series.empty:
164
- continue # Saltar si la celda específica está vacía o no se encuentra
165
-
166
- a = val_a_series.iat[0]
167
- b = val_b_series.iat[0]
168
-
169
- if a != b:
170
- counter.update([x_label, y_label])
171
- return ', '.join(sorted(list(counter))) or 'La operación es conmutativa (no hay contraejemplos).'
172
- return df.to_csv(index=False)
173
- except IndexError as e:
174
- return f"Error analyze_table_content_str, stream=True)
175
  def gen():
176
  acc = ""
177
  for chunk in stream:
@@ -180,32 +79,20 @@ def analyze_table(table_md: str, question: str) -> bueno en esto
180
  delta = chunk.parts[0].text
181
  if delta:
182
  acc += delta
183
- yield ChatMessage(role="assistant", content=acc, additional_kwargs={"delta":delta})
184
  return gen()
185
 
186
- def complete(self, prompt, formatted=False, **kwargs):
187
- try:
188
- resp = self._model.generate_content(str(prompt)) # Asegurar que prompt es string
189
- return CompletionResponse(text=resp.text)
190
- except Exception as e:
191
- return CompletionResponse(text=f"Error complete: {e}")
192
-
193
- async def acomplete(self, prompt, formatted=False, **kwargs):
194
- return await asyncio.to_thread(self.complete, str(prompt), formatted=formatted, ** (IndexError): {e}. Verifique que la tabla tiene cabeceras y datos y la pregunta es adecuada."
195
- except KeyError as e:
196
- return f"Error analyze_table (KeyError): {e}. Alguna columna o etiqueta no se encontró en la tabla."
197
- except Exception as e:
198
- return f"Error analyze_table: {e}"
199
-
200
- def execute_code(code: str) -> str:
201
- try:
202
- # Try eval for simple expressions
203
- try:
204
- # MEJORA: Un entorno un poco más seguro y con más utilidades matemáticas comunes
205
- allowed_globals = {'__builtins__': None, 'math': math, 'pdkwargs) # Asegurar que prompt es string
206
 
207
- def stream_complete(self, prompt, formatted=False, **kwargs):
208
- stream = self._model.generate_content(str(prompt), stream=True) # Asegurar que prompt es string
 
 
 
 
 
 
209
  def gen():
210
  acc = ""
211
  for chunk in stream:
@@ -214,463 +101,142 @@ def execute_code(code: str) -> str:
214
  delta = chunk.parts[0].text
215
  if delta:
216
  acc += delta
217
- yield CompletionResponse(text=acc, delta=delta)
218
  return gen()
219
 
220
- async def astream_complete(self, prompt, formatted=False, **kwargs):
221
- # This needs to be a': pd, 'np': __import__('numpy')} # Ejemplo, añadir según necesidad
222
- allowed_locals = {name: getattr(math, name) for name in dir(math) if not name.startswith("__")}
223
- val = eval(code, allowed_globals, allowed_locals)
224
- return str(val)
225
- except (SyntaxError, Name coroutine that returns an async generator
226
- # For simplicity, we'll call the synchronous version in a thread
227
- # and then adapt its generator. However, a true async implementation
228
- # would use an async http client for genai.
229
- sync_gen = await asyncio.to_thread(self.stream_complete, str(prompt), formatted=formatted, **kwargs)
230
- async def async_gen_wrapper():
231
- for item in sync_gen:
232
- yieldError): # Errores comunes de eval que pueden ser código más complejo
233
- pass # Intentar con subprocess
234
- except Exception as e_eval: # Otros errores de eval
235
- return f"Error eval en código: {e_eval}"
236
-
237
- # Fallback to subprocess
238
- res = subprocess.run(
239
- ["python", "-c", code], capture_output=True, text=True, timeout=10 # MEJORA: timeout aumentado
240
- )
241
- if res.stderr:
242
- return f"Error código (subprocess): {res.stderr.strip()}"
243
- return res.stdout.strip() or "(sin salida de Python)"
244
- except subprocess.TimeoutExpired:
245
- return "Error ejecutar item
246
- return async_gen_wrapper()
247
-
248
-
249
- async def astream_chat(self, messages: list[ChatMessage], **kwargs):
250
- # Similar to astream_complete, wrap the sync generator
251
- sync_gen = await asyncio.to_thread(self.stream_chat, messages, **kwargs)
252
- async def async_gen_wrapper():
253
- for item in sync_gen:
254
- yield item
255
- return async_gen_wrapper código: El código tardó demasiado en ejecutarse (timeout)."
256
- except Exception as e:
257
- return f"Error crítico al ejecutar código: {e}"
258
 
259
- def no_tool_solution(query: str) -> str:
260
- # MEJORA: La respuesta debería ser manejada por el LLM. Esta herramienta solo indica que se usará conocimiento interno.
261
- return "Respuesta basada en conocimiento interno. El LLM procederá a responder directamente."
262
 
263
- def scrape_wikipedia_table()
264
  # -------------------------------------------------------------------
265
- # 2) Herramientas (MODIFICADAS Y NUEVAS)
266
  # -------------------------------------------------------------------
267
- HEADERS = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'} #(query: str, section_title: str, table_index: int = 0) -> str:
268
- """
269
- Busca una página de Wikipedia y extrae la tabla HTML (wikitable) bajo la sección dada.
270
- Devuelve CSV de la tabla. Elige la N-ésima tabla si hay varias.
271
- """
272
- try Agente de usuario más común
273
 
274
- def buscar_web(query: str, max_attempts: int = 2) -> str:
275
- """Busca en la web usando DuckDuckGo. Intenta hasta max_attempts veces."""
276
  for i in range(max_attempts):
277
  try:
278
- # Aumentamos max_results para dar:
279
- wikipedia.set_lang("es") # Asegurar idioma español
280
- page = wikipedia.page(query, auto_suggest=True, redirect=True) # MEJORA: auto_suggest y redirect
281
- html más contexto al LLM
282
  with DDGS(headers=HEADERS, timeout=25) as ddgs:
283
- results = list(ddgs.text(query, region='es-es', safesearch='moderate', timelimit_content = page.html()
284
- soup = BeautifulSoup(html_content, 'html.parser')
285
-
286
- target_section = None
287
- # Buscar encabezados h2 o h3 que contengan el título de la sección
288
- ='y', max_results=5)) # Aumentado a 5
289
  if results:
290
- return "\n".join(f"Fuente {idx+1}:\nTítulo: {r['title']}\nEnlace: {r for header_tag in soup.find_all(['h2', 'h3']):
291
- if header_tag.['href']}\nCuerpo: {r['body']}" for idx, r in enumerate(results))
292
- return "No se encontraron resultados relevantes en la web para esta consulta."
293
  except Exception as e:
294
- ifspan and section_title.lower() in header_tag.span.get_text(strip=True).lower():
295
- target_section = header_tag
296
- break
297
-
298
- if not target_section:
299
- # Intento más permisivo si no se encontró con span exacto
300
- for header_tag in soup.find_all(['h2', 'h3']):
301
- if section_title.lower() in header_tag.get_ i < max_attempts - 1:
302
- time.sleep(2 * (i + 1)) # Reducido el tiempo de espera base
303
  else:
304
- return f"Error en buscar_web tras {max_attempts} intentos: {e}. Prueba una consulta diferente o simplificada."
305
 
 
306
  def reverse_text(text: str) -> str:
307
- """Invierte una cadena de texto."""
308
  return text[::-1]
309
 
 
310
  def analyze_table(table_md: str, question: str) -> str:
311
- """
312
- Procesa una tabla entext(strip=True).lower():
313
- target_section = header_tag
314
- break
315
- if not target_section:
316
- return f"Sección '{section_title}' no encontrada en la página de Wikipedia '{query}'."
317
-
318
- # Encontrar todas las tablas wikitable DESPUÉS del encabezado de la sección
319
- # y ANTES del siguiente encabezado del mismo nivel o superior
320
- tables_after_section = []
321
- for sibling in target_section.find_next_siblings():
322
- if sibling.name in ['h2', 'h3'] and sibling.name <= target_section.name: # <= para h2 vs h2, o h2 vs h3
323
- break # Hemos llegado a la siguiente sección del mismo nivel o superior
324
- if sibling.name == 'table' and 'wikitable' in formato Markdown. Si la pregunta incluye 'conmutatividad',
325
- verifica esta propiedad. De lo contrario, devuelve la tabla en formato CSV.
326
- """
327
- try:
328
- lines = [l for l in table_md.splitlines() if l.strip() and not l.strip().startswith('|--')]
329
- if not lines:
330
- return "Error analyze_table: La tabla Markdown parece estar vacía o mal formateada."
331
-
332
- rows = []
333
- header_skipped = False
334
- for l in lines:
335
- # Skip initial divider lines if any
336
- if '---' in l and not header_skipped :
337
- if not rows: # if there's no header yet, this is the divider after header
338
- header_skipped = True
339
- continue
340
-
341
- parts = [c.strip() for c in l.strip().strip('|').split('|')]
342
- if not any(parts): # Skip empty sibling.get('class', []):
343
- tables_after_section.append(sibling)
344
-
345
- if not tables_after_section:
346
- return f"No se encontró ninguna tabla 'wikitable' después de la sección '{section_title}'."
347
-
348
- if table_index >= len(tables_after_section):
349
- return f"Índice de tabla {table_index} fuera de rango. Se encontraron {len(tables_after_section)} tablas después de la sección."
350
-
351
- selected_table = tables_after_section[table_index]
352
-
353
- # Usar pandas para leer la tabla HTML directamente
354
- dfs = pd.read_html(StringIO(str(selected_table)), flavor='bs4') # StringIO necesario para read_html con string
355
- if not dfs:
356
- return "Pandas no pudo parsear la tabla HTML encontrada."
357
-
358
- df = dfs[0] # Usualmente la primera tabla parseada es la correcta
359
- # Limpieza básica: eliminar filas/columnas completamente vacías
360
- df.dropna(axis=0, how='all', inplace=True)
361
- df.dropna(axis=1, how='all', inplace=True)
362
- return df.to_csv(index=False lines that might result from split
363
- continue
364
- rows.append(parts)
365
-
366
- if not rows or len(rows) < 1: # Need at least a header
367
- return "Error analyze_table: No se pudo extraer ninguna fila válida de la tabla Markdown."
368
-
369
- df = pd.DataFrame(rows[1:], columns=rows[0]) # Asume que la primera fila es el encabezado
370
-
371
- if 'conmutatividad' in question.lower() or 'conmut' in question.lower():
372
- if len(df.columns) < 2 or len(df) < 1:
373
- return "Error analyze_table: La tabla es demasiado pequeña para verificar la conmutatividad."
374
-
375
- first_col_name = df.columns[0]
376
- S = df[first_col_name].tolist() # Elementos para verificar, usualmente de la primera columna/fila
377
- # Asumimos que las cabeceras de las columnas (después de la primera) son los mismos elementos que en la primera columna
378
- # Esto es típico para tablas de Cayley. Si no, esta lógica necesita ajuste.
379
- elements_for_op = df.columns[1:].tolist()
380
-
381
- # Check if elements_for_op are a subset of S, or if S matches elements_for_op
382
- # For simplicity, let's assume the)
383
- except wikipedia.exceptions.PageError:
384
- return f"Error scrape_wikipedia_table: Página de Wikipedia '{query}' no encontrada."
385
- except wikipedia.exceptions.DisambiguationError as e:
386
- return f"Error scrape_wikipedia_table: '{query}' es ambiguo. Posibles opciones: {e.options[:5]}"
387
- except Exception as e:
388
- return f"Error scrape_wikipedia_table: {e}"
389
-
390
- # --- NUEVAS HERRAMIENTAS ---
391
- def read_excel_data(file_url_or_path: str, sheet_name: Any = 0) -> str:
392
- """
393
- Lee datos de un archivo Excel (desde URL o ruta local) y los devuelve como CSV.
394
- El argumento sheet_name puede ser el nombre de la hoja o el índice (0 por defecto).
395
- """
396
  try:
397
- # Intentar detectar si es una URL square part of the table for operation uses elements from the first column
398
- # as both row and column headers for the operation.
399
-
400
- counter_examples = set()
401
- valid_elements_for_lookup = df.columns.tolist()
402
-
403
- for x_val in S:
404
- for y_val in S:
405
- if x_val not in valid_elements_for_lookup or y_val not in valid_elements_for_lookup:
406
- # This check is if S contains elements not in column headers,
407
- # or y_val is not a column header. This logic might need refinement
408
- # based on actual table structures encountered.
409
- continue
410
-
411
  try:
412
- # df.loc[df[first_col_name]==x_val, y_val] da una Serie
413
- # .iat[0] accede al primer elemento de esa Serie
414
- a = df.loc[df[first_col_name] == x_val, y_val].iat[0]
415
- b = df.loc[df[first_col_name] == y_val, x_val].iat[0]
416
- if a != b:
417
- counter_examples.update([x_val, y_val])
418
- except (IndexError, KeyError) as e_lookup:
419
- # Esto puede pasar si y_val no es una columna, o x_val no es una fila
420
- # O si la tabla no es "cuadrada" en el sentido esperado para la operación
421
-
422
- if file_url_or_path.startswith(('http://', 'https://')):
423
- response = requests.get(file_url_or_path, headers=HEADERS, timeout=30)
424
- response.raise_for_status() # Lanza error si la descarga falla
425
- excel_content = BytesIO(response.content)
426
- df = pd.read_excel(excel_content, sheet_name=sheet_name)
427
- else: # Asumir que es una ruta local (aunque en GAIA es improbable que se use)
428
- if not os.path.exists(file_url_or_path):
429
- return f"Error read_excel_data: Archivo local no encontrado en '{file_url_or_path}'."
430
- df = pd.read_excel(file_url_or_path, sheet_name=sheet_name)
431
-
432
- # Limpieza básica: eliminar filas/columnas completamente vacías
433
- df.dropna(axis=0, how='all', inplace=True)
434
- df.dropna(axis=1, how='all', inplace=True)
435
  return df.to_csv(index=False)
436
- except requests.exceptions.RequestException as e:
437
- return f"Error read_excel_data: No se pudo descargar el archivo desde la URL. {e}"
438
- except FileNotFoundError: # Específico para rutas locales
439
- return f"Error read_excel_data: Archivo local no encontrado en '{file_url_or_path}'."
440
- except pd.errors.EmptyDataError:
441
- return "Error read_excel_data: El archivo Excel o la hoja especificada está vacía."
442
- except ValueError as e: # Puede ser por sheet_name inválido
443
- return f"Error read_excel_data: Error al procesar el Excel (posiblemente hoja no encontrada o formato incorrecto). {e}"
444
  except Exception as e:
445
- return f"Error read_excel_data: {e}"
446
-
447
- def classify_botanical(items_list_str: str) -> str:
448
- """
449
- Clasifica una lista de alimentos (separados por comas) en frutas y verduras botánicas.
450
- Utiliza un conocimiento base y puede indicar ítems no reconocidos.
451
- """
452
- items = [item.strip().lower() for item in items_list_str.split(',')]
453
- if not items or (len(items) == 1 and not items[0]):
454
- return "Error classify_botanical # print(f"Debug: Error al buscar {x_val}, {y_val}: {e_lookup}")
455
- continue # Saltar este par si no se puede encontrar
456
-
457
- if counter_examples:
458
- return f"La operación no es conmutativa. Contraejemplos (elementos involucrados): {', '.join(sorted(list(counter_examples)))}"
459
- else:
460
- return "La operación parece ser conmutativa para los elementos y valores dados."
461
-
462
- return f"Tabla procesada en formato CSV:\n{df.to_csv(index=False)}"
463
- except Exception as e:
464
- return f"Error crítico en analyze_table: {e}. Asegúrate de que la tabla Markdown esté bien formateada."
465
-
466
 
 
467
  def execute_code(code: str) -> str:
468
- """Ejecuta código Python de forma segura para cálculos o manipulación de datos simple.
469
- Evita operaciones de sistema o red.
470
- """
471
  try:
472
- # Intenta eval para expresiones matemáticas/lógicas simples y seguras
473
- # Proporciona un entorno limitado para eval
474
- allowed_globals = {"math": math, "__builtins__": {"abs": abs, "min": min, "max": max, "sum": sum, "len": len, "round": round, "str": str, "int": int, "float": float, "list": list, "dict": dict, "tuple": tuple, "True": True, "False": False, "None": None}}
475
- # math.__dict__ es demasiado permisivo. Seamos más explícitos si es necesario.
476
- # Por ahora, el usuario debe usar math.sqrt, etc.
477
-
478
- # Validar el código contra operaciones peligrosas antes de eval o exec
479
- # Esto es una lista negra simple, no exhaustiva. Para un sandbox real se necesita más.
480
- disallowed_keywords = ["import ", "os.", "sys.", "subprocess.", "open(", "eval(", "exec(", "compile(", "socket", "requests", "urllib", "shutil", "glob", "eval\\(", "exec\\("]
481
- if any(keyword in code for keyword in disallowed_keywords):
482
- # También chequear si `code` intenta llamar a `__import__` o acceder a `: La lista de ítems está vacía."
483
-
484
- # Conocimiento botánico base (simplificado, ampliar según sea necesario)
485
- # Fuentes: Conocimiento general botánico. Fruta = desarrolla del ovario de una flor y contiene semillas.
486
- botanical_fruits = {
487
- "tomate", "pepino", "calabacín", "berenjena", "pimiento", "aguacate",
488
- "calabaza", "guisante", "judía verde", "maíz", "aceituna", "manzana", "pera",
489
- "plátano", "naranja", "limón", "uva", "fresa", "frambuesa", "mora", "sandía",
490
- "melón", "kiwi", "mango", "piña", "cereza", "ciruela", "melocotón", "albaricoque",
491
- "higo", "granada", "papaya", "chile", "zapallo", "zapallito", "ají", "pimentón",
492
- "sweet potatoes", "plums", "green beans", "corn", "bell pepper", "acorns", "peanuts", "oreos" # Oreos es claramente NO fruta ni verdura
493
- }
494
- # Verduras culinarias que son botánicamente frutas ya están arriba.
495
- # Estas son partes de plantas como raíces, tallos, hojas.
496
- botanical_vegetables = {
497
- "zanahoria", "patata", "batata", "cebolla", "ajo", "puerro", "apio",
498
- "lechuga", "espinaca", "acelga", "col", "brócoli", "coliflor", "espárrago",
499
- "rábano", "nabo", "remolacha", "alcachofa", "hinojo",
500
- "fresh basil", "broccoli", "celery", "lettuce", "sweet potatoes" # Sweet potatoes (batata) es raíz, verdura
501
- }
502
- # Algunos ítems de la lista original:
503
- # milk, eggs, flour, whole bean coffee, Oreos, rice, whole allspice
504
- non_botanical_produce = { # Ni fruta ni verdura en el sentido de "produce" fresco
505
- "milk", "eggs", "flour", "whole bean coffee", "oreos", "rice", "whole allspice",
506
- "leche", "huevos", "harina", "café en grano", "arroz", "pimienta de jamaica"
507
- }
508
-
509
- fruits_found = []
510
- vegetables_found = []
511
- unrecognized_items = []
512
- other_items = []
513
-
514
- for item_singular in items:
515
- # Intentar una forma singular simple (esto es rudimentario)
516
- item_processed = item_singular
517
- if item_singular.endswith('s') and not item_singular.endswith('ss'): # ej. "apples" -> "apple"
518
- if item_singular[:-1] in botanical_fruits or__builtins__` de forma peligrosa
519
- if "__" in code: # Simplificación: si hay doble guion bajo, sospechar.
520
- return "Error código: El código parece intentar acceder a funcionalidades restringidas o peligrosas (ej. dunder methods/attrs)."
521
- return "Error código: El código contiene palabras clave o patrones no permitidos (ej. import, os, subprocess, file access)."
522
-
523
  try:
524
- # Si es una sola expresión, eval puede ser suficiente
525
- val = eval(code, allowed_globals, {}) # No locals
526
- return f"Resultado de la expresión: {str(val)}"
527
- except (SyntaxError, NameError): # Si no es una expresión simple, o necesita definiciones
528
- # Fallback a subprocess si eval falla o para scripts más complejos (pero aún simples)
529
- # El timeout es crucial.
530
- # Usar python -S para deshabilitar la importación de site-specific paths (más seguro)
531
- # Para mayor seguridad, se podría ejecutar en un contenedor Docker o con `nsjail`.
532
- # Por ahora, confiamos en el timeout y la validación de palabras clave.
533
- # Si se necesita pandas o numpy en el código, esta herramienta tendría que ser más sofisticada
534
- # o el LLM tendría que generar código que no dependa de imports no disponibles en el sandbox.
535
-
536
- # Re-validar para subprocess, ya que el contexto es diferente
537
- if any(keyword in code for keyword in ["import os", "import sys", "subprocess", " open("]): # lista más restrictiva para subprocess
538
- return "Error código: El código para subprocess contiene palabras clave no permitidas."
539
-
540
- # Si el código es multilinea o define funciones, eval no sirve, se necesitaría exec.
541
- # Pero exec es más peligroso. Subprocess es un sandbox mejor si se configura bien.
542
- # Por ahora, si eval falla, intentamos subprocess con el código tal cual.
543
- # Esto asume que el código es un script simple que imprime a stdout.
544
-
545
- # Prepend common safe imports if the LLM is instructed to use them without explicit import
546
- # For example, if we want 'math' to be available implicitly.
547
- # safe_prelude = "import math\n"
548
- # full_code = safe_prelude + code
549
-
550
- res = subprocess.run(
551
- ["python", "-S", "-c", code], capture_output=True, text=True, timeout=10 # Aumentado timeout un poco
552
- )
553
- if res.returncode != 0: # Chequear returncode además de stderr
554
- error_message = res.stderr.strip() if res.stderr else "El código falló sin un mensaje de error específico." item_singular[:-1] in botanical_vegetables:
555
- item_processed = item_singular[:-1]
556
-
557
- # Para el ejemplo específico del prompt, algunos están en inglés y otros no.
558
- # La lista de la compra del prompt original:
559
- # milk, eggs, flour, whole bean coffee, Oreos, sweet potatoes, fresh basil, plums, green beans,
560
- # rice, corn, bell pepper, whole allspice, acorns, broccoli, celery, zucchini, lettuce, peanuts
561
 
562
- if item_processed in botanical_fruits:
563
- fruits_found.append(item_singular) # Devolver el original
564
- elif item_processed in botanical_vegetables:
565
- vegetables_found.append(item_singular)
566
- elif item_processed in non_botanical_produce:
567
- other_items.append(item_singular)
 
568
  else:
569
- # Consulta web simple si no está en las listas (podría ser costoso para muchos ítems)
570
- # print(f"DEBUG: Item '{item_singular}' no reconocido, podría buscar en web.")
571
- # search_result = buscar_web(f"¿{item_singular} es fruta o verdura botánica?", num_results=1)
572
- # Aquí se necesitaría un LLM para interpretar el resultado de la búsqueda.
573
- # Por ahora, lo marcamos como no reconocido por la herramienta.
574
- unrecognized_items.append(item_singular)
575
-
576
- result = []
577
- if fruits_found:
578
- result.append(f"Frutas botánicas: {', '.join(sorted(list(set(fruits_found))))}")
579
- if vegetables_found:
580
- result.append(f"Verduras botánicas: {', '.join(sorted(list(set(vegetables_found))))}")
581
- if other_items:
582
- result.append(f"Otros ítems (ni fruta ni verdura botánica): {', '.join(sorted(list(set(other_items))))}")
583
- if unrecognized_items:
584
- result.append(f"Ítems no reconocidos por esta herramienta (requieren investigación adicional o son ambiguos): {', '.join(sorted(list(set(unrecognized_items))))}")
585
-
586
- return "\n".join(result) if result else "No se pudieron clasificar los ítems o la lista estaba vacía."
587
-
588
-
589
- # Encapsular herramientas
590
- search_tool = FunctionTool.from_defaults(fn=buscar_web, name="web_search", description="Búsqueda web general (DuckDuckGo, 5 resultados). Útil para información actual, definiciones, hechos, o cuando otras herramientas no aplican. Especifica bien tu consulta.")
591
- reverse_tool = FunctionTool.from_defaults(fn=reverse_text, name="reverse_text", description="Invierte una cadena de texto.")
592
- table_tool = FunctionTool.from_defaults(fn=analyze_table, name="analyze_table", description="Procesa tablas en formato Markdown. Puede verificar conmutatividad si se le pide o devolver la tabla como CSV. Asegúrate de que la tabla esté bien formada.")
593
- code_tool = FunctionTool.from_defaults(fn=execute_code, name="execute_code", description="Ejecuta código Python para cálculos matemáticos, manipulación de datos simple (si se proveen como strings o listas), u otras tareas algorítmicas. No puede acceder a internet ni a archivos externos.")
594
- fallback_tool = FunctionTool.from_defaults(fn=no_tool_solution, name="no_tool_solution", description="Úsalo como ÚLTIMO RECURSO si estás seguro de que puedes responder la pregunta con tu conocimiento interno y ninguna otra herramienta es adecuada o ha funcionado. Expl
595
- return f"Error código (código de salida {res.returncode}): {error_message}"
596
- return res.stdout.strip() or "(El código se ejecutó sin salida a stdout)"
597
- except Exception as e_eval: # Captura errores de eval si no fueron SyntaxError o NameError
598
- return f"Error código (eval): {e_eval}"
599
-
600
- except Exception as e_general:
601
- return f"Error crítico al intentar ejecutar código: {e_general}"
602
-
603
- def no_tool_solution(query_context: str) -> str: # Cambiado el parámetro para reflejar su uso
604
- """
605
- Se utiliza cuando ninguna otra herramienta es adecuada o cuando la pregunta requiere
606
- conocimiento general, razonamiento o procesamiento de información ya disponible en la conversación.
607
- Explica brevemente por qué se usa esta opción.
608
- """
609
- return f"Procedo con conocimiento interno y razonamiento. Contexto/Razón: {query_context}"
610
 
611
- def scrape_wikipedia_table(page_title: str, section_heading_substring: str, table_index: int = 0) -> str:
612
- """
613
- Busca una página de Wikipedia por su título exacto y extrae una tabla HTML
614
- bajo una sección que contenga el 'section_heading_substring'.
615
- Devuelve la tabla como CSV. Se puede especificar table_index (0 por defecto) si hay varias tablas en la sección.
616
- """
 
 
 
 
 
 
617
  try:
618
- wikipedia.set_lang("es") # Asegurar que busca en Wikipedia en español si es relevante
619
- page = wikipedia.page(page_title, auto_suggest=False) # Evitar auto_suggest si el título debe ser exacto
620
- html = page.html()
621
- soup = BeautifulSoup(html, 'html.parser')
622
-
623
- target_section_element = None
624
- # Buscar encabezados (h1, h2, h3, h4) que contengan el texto de la sección
625
- for header_level in ['h1', 'h2', 'h3', 'h4']:
626
- headers = soup.find_all(header_level)
627
- for header in headers:
628
- if section_heading_substring.lower() in header.get_text(strip=True).lower():
629
- span_headline = header.find('span', class_='mw-headline')
630
- if span_headline and section_heading_substring.lower() in span_headline.get_text(strip=True).lower():
631
- target_section_element = header
632
- break
633
- if target_section_element:
634
- break
635
-
636
- if not target_section_element:
637
- return f"Sección que contenga '{section_heading_substring}' no encontrada en la página de Wikipedia '{page_title}'. Intenta con un título de sección diferente o verifica el título de la página."
638
-
639
- # Buscar todas las tablas wikitable *después* del encabezado de la sección encontrada
640
- # y dentro del mismo "nivel" (antes del siguiente encabezado del mismo o mayor nivel)
641
- tables_in_section = []
642
- for sibling in target_section_element.find_next_siblings():
643
- # Detener la búsqueda si encontramos otro encabezado del mismo nivel o superior
644
- if sibling.name in ['h1', 'h2', 'h3', 'h4'] and sibling.name <= target_section_element.name:
645
- break
646
- if sibling.name == 'table' and ('wikitable' in sibling.get('class', [])):
647
- tables_in_section.append(sibling)
648
-
649
- if not tables_in_section:
650
- return f"No se encontró ninguna tabla 'wikitable' después de la sección '{section_heading_substring}'."
651
-
652
- if table_index >= len(tables_in_section):
653
- return f"Índice de tabla {table_index} fuera de rango. Se encontraron {len(tables_in_section)} tablas en la sección."
654
-
655
- # Convertir la tabla seleccionada a DataFrame y luego a CSV
656
- # Usar pd.read_htmlica tu razonamiento.")
657
- scrape_tool = FunctionTool.from_defaults(fn=scrape_wikipedia_table,name="scrape_wiki_table", description="Extrae una tabla específica (wikitable) de una página de Wikipedia dada una sección y el índice de la tabla (0 por defecto). Útil para datos estructurados en Wikipedia.")
658
- # NUEVAS
659
- excel_reader_tool = FunctionTool.from_defaults(fn=read_excel_data, name="read_excel_data", description="Lee datos de un archivo Excel accesible por URL (o ruta local, menos común aquí). Devuelve los datos como CSV. Necesita la URL del archivo y opcionalmente el nombre/índice de la hoja.")
660
- botanical_tool = FunctionTool.from_defaults(fn=classify_botanical, name="classify_botanical_foods", description="Clasifica una lista de alimentos (texto separado por comas) en frutas botánicas y verduras botánicas según un conocimiento base. Indica ítems no reconocidos. Útil para listas de la compra o categorización de alimentos.")
661
-
662
-
663
- all_tools = [
664
- search_tool,
665
- scrape_tool, # scrape_tool antes de table_tool si se espera obtener la tabla de la web primero
666
- table_tool,
667
- code_tool,
668
- excel_reader_tool,
669
- botanical_tool,
670
- # reverse_tool, # Menos probable que sea útil en GAIA, pero lo dejamos
671
- fallback_tool # Fallback siempre al final
672
- ]
673
-
674
  # Prompt de sistema REFINADO
675
  system_prompt = (
676
  "Eres Alfred, un agente ReAct eficiente y preciso. Tu objetivo es responder preguntas de forma correcta.\n"
@@ -689,131 +255,14 @@ system_prompt = (
689
  "Herramientas disponibles (usa SOLO estas y con los nombres exactos):\n{tool_descriptions}"
690
  )
691
 
692
- # Inicializar agente
693
- llm = GeminiLLM(model_name="models/gemini-1.5-flash-latest", temperature=0.0) # Temperatura ya estaba en 0.0
694
-
695
- alfred_agent = ReActAgent.from_tools(
696
- tools=all_tools,
697
- llm=llm,
698
- system_prompt=system_prompt,
699
- verbose=True,
700
- max_iterations=15, # MEJORA: Reducido de 20 a 15. Ajustar según sea necesario.
701
- # MEJORA: Añadir el callback manager al agente para que use los handlers definidos en el LLM
702
- callback_manager=llm.callback_manager
703
- )
704
 
705
- # Función pública (sin cambios)
706
  def basic_agent_response(question: str) -> str:
707
- # Limpiar el estado del handler de métricas para cada nueva pregunta
708
- # metrics_handler.tool_usage_counts.clear()
709
- # metrics_handler.tool_errors.clear()
710
- # metrics_handler.tool_timings.clear()
711
- # metrics_handler._tool_starts.clear() # No que puede manejar HTML complejo, pero darle el string de la tabla
712
- # io.StringIO es necesario porque read_html espera un buffer de archivo o una URL
713
- dfs = pd.read_html(StringIO(str(tables_in_section[table_index])), flavor='bs4')
714
- if not dfs:
715
- return "Error scrape_wikipedia_table: pandas no pudo parsear la tabla encontrada."
716
-
717
- df = dfs[0] # Asumimos que la primera tabla parseada es la correcta
718
- return f"Tabla de Wikipedia (sección '{section_heading_substring}', índice {table_index}) en formato CSV:\n{df.to_csv(index=False)}"
719
- except wikipedia.exceptions.PageError:
720
- return f"Error scrape_wikipedia_table: Página de Wikipedia '{page_title}' no encontrada. Verifica el título."
721
- except wikipedia.exceptions.DisambiguationError as e:
722
- return f"Error scrape_wikipedia_table: Título ambiguo '{page_title}'. Posibles opciones: {e.options}. Por favor, proporciona un título más específico."
723
- except IndexError:
724
- return f"Error scrape_wikipedia_table: No se pudo encontrar la tabla con índice {table_index} en la sección especificada."
725
- except Exception as e:
726
- return f"Error inesperado en scrape_wikipedia_table: {e}"
727
-
728
- # NUEVA HERRAMIENTA: Lector de Excel
729
- def read_excel_data(file_path: str, sheet_name=0) -> str:
730
- """
731
- Lee un archivo Excel (.xls o .xlsx) desde una ruta de archivo local (file_path)
732
- y una hoja específica (sheet_name, por defecto la primera, índice 0).
733
- Devuelve los datos de la tabla como una cadena en formato CSV.
734
- Esta herramienta SÓLO funciona si el archivo Excel está accesible en el sistema de archivos local
735
- donde se ejecuta el agente y se proporciona la ruta correcta.
736
- No puede acceder a archivos adjuntos de correos o subidos a chats directamente.
737
- """
738
- try:
739
- # Para archivos remotos, necesitaríamos requests y pasar BytesIO(response.content)
740
- # if file_path.startswith('http://') or file_path.startswith('https://'):
741
- # response = requests.get(file_path)
742
- # response.raise_for_status() # Asegura que la descarga fue exitosa
743
- # excel_content = BytesIO(response.content)
744
- # df = pd.read_excel(excel_content, sheet_name=sheet_name)
745
- # else:
746
- # df = pd.read_excel(file_path, sheet_name=sheet_name)
747
-
748
- # Simplificamos: solo rutas locales por ahora, como es más probable en el entorno de prueba.
749
- if not os.path.exists(file_path):
750
- return f"Error read_excel_data: El archivo local '{file_path}' no fue encontrado."
751
-
752
- df = pd.read_excel(file_path, sheet_name=sheet_name)
753
-
754
- # Limpiar NaNs que pueden causar problemas al convertir a CSV o ser interpretados por el LLM
755
- df_cleaned = df.fillna('') # Reemplazar NaN con string vacío
756
-
757
- csv_output = df_cleaned.to_csv(index=False)
758
- if not csv_output.strip(): # Si el CSV está vacío (solo cabeceras vacías o nada)
759
- return "El archivo Excel o la hoja especificada parece estar vacía o no contiene datos procesables."
760
-
761
- return f"Contenido del archivo Excel '{os.path.basename(file_path)}' (hoja '{sheet_name}') en formato CSV:\n{csv_output}"
762
- except FileNotFoundError: # Esto es redundante si usamos os.path.exists, pero por si acaso.
763
- return f"Error read_excel_data: El archivo local '{file_path}' no fue encontrado."
764
- except ValueError as ve: # Errores comunes de pandas al leer formatos incorrectos
765
- if "Excel file format cannot be determined" in str(ve) or "File is not a zip file" in str(ve):
766
- return f"Error read_excel_data: El archivo '{file_path}' no parece ser un archivo Excel válido (.xls, .xlsx)."
767
- return f"Error read_excel_data: Problema al leer el archivo Excel (ValueError): {ve}"
768
- except Exception as e:
769
- return f"Error crítico en read_excel_data al procesar '{file_path}': {e}"
770
-
771
-
772
- # Encapsular herramientas
773
- search_tool = FunctionTool.from_defaults(fn=buscar_web, name="web_search", description="Realiza una búsqueda en la web (DuckDuckGo) con una consulta y devuelve un resumen de los principales resultados (título, enlace, cuerpo). Útil para encontrar información general, noticias, o datos específicos que no estén en Wikipedia.")
774
- reverse_tool = FunctionTool.from_defaults(fn=reverse_text, name="reverse_text", description="Invierte una cadena de texto. Útil para manipulaciones simples de strings o tareas específicas que lo requieran.")
775
- table_tool = FunctionTool.from_defaults(fn=analyze_table, name="analyze_markdown_table", description="Procesa una tabla que YA ESTÁ en es ideal modificar atributos privados, pero para un reinicio simple...
776
- # Mejor aún: crear una nueva instancia o un método reset() en el handler.
777
- # Por simplicidad, y dado que el callback manager del LLM es compartido, esto es complicado.
778
- # Una mejor práctica sería pasar un nuevo callback manager o handlers reseteados por pregunta si es necesario.
779
- # Para GAIA, donde se evalúa pregunta por pregunta, el estado acumulado de las métricas puede ser útil al final.
780
- # Si se quiere métricas por pregunta, el handler necesitaría ser instanciado/reseteado por llamada.
781
-
782
  try:
783
  resp = alfred_agent.query(question)
784
- # Imprimir métricas después de cada respuesta (opcional, para depuración)
785
- # print("--- Métricas de Herramientas para esta pregunta ---")
786
- # print(metrics_handler.get_metrics())
787
- # print("-------------------------------------------------")
788
- return resp.response or "No se generó respuesta."
789
  except Exception as e:
790
- return f"Error crítico del agente: {e}"
791
-
792
- if __name__ == "__main__":
793
- # Ejemplo de cómo obtener las métricas acumuladas al final de una sesión
794
- # Esto es solo un ejemplo, en tu `app.py` lo harías después del bucle de preguntas.
795
-
796
- # Simulación de algunas preguntas
797
- print("Ejecutando prueba de agente (botánica)...")
798
- # Pregunta original de la lista de la compra
799
- q_botanica = "I'm making a grocery list for my mom, but she's a professor of botany and she's a real stickler when it comes to categorizing things. I need to add different foods to different categories on the grocery list, but if I make a mistake, she won't buy anything inserted in the wrong category. Here's the list I have so far: milk, eggs, flour, whole bean coffee, Oreos, sweet potatoes, fresh basil, plums, green beans, rice, corn, bell pepper, whole allspice, acorns, broccoli, celery, zucchini, lettuce, peanuts I need to make headings for the fruits and vegetables. Could you please create a list of just the vegetables from my list? If you could do that, then I can figure out how to categorize the rest of the list into the appropriate categories. But remember that my mom is a real stickler, so make sure that no botanical fruits end up on the vegetable list, or she won't get them when she's at the store. Please alphabetize the list of vegetables, and place each item in a comma separated list."
800
- response_botanica = basic_agent_response(q_botanica)
801
- print(f"Pregunta Botánica: {q_botanica}")
802
- print(f"Respuesta del Agente: {response_botanica}\n")
803
-
804
- print("Ejecutando prueba de agente (Excel)...")
805
- # OJO: Esta URL es un ejemplo, DEBE ser un enlace directo a un archivo .xlsx o .xls
806
- # Github raw links a veces no funcionan directamente con requests si no son el archivo real.
807
- # Necesitarías una URL pública directa a un archivo Excel.
808
- # Ejemplo (ficticio, necesitas una URL real): "https://example.com/test_sales.xlsx"
809
- # Para probar sin una URL real, puedes comentar esta prueba o usar una ruta local si ejecutas esto localmente.
810
- q_excel = "Por favor, lee el archivo Excel en 'https://github.com/datasciencedojo/datasets/raw/master/Sales%20Data/Sales%20Examples.xlsx' y dime el total de 'Total Revenue' de la primera hoja."
811
- # Este enlace anterior SÍ es un Excel real y público
812
- response_excel = basic_agent_response(q_excel)
813
- print(f"Pregunta Excel: {q_excel}")
814
- print(f"Respuesta del Agente: {response_excel}\n")
815
 
816
- # Al final de todas las ejecuciones, puedes imprimir las métricas acumuladas:
817
- print("\n--- Métricas Acumuladas de Herramientas (Ejemplo Final) ---")
818
- print(metrics_handler.get_metrics())
819
- print("----------------------------------------------------------")
 
 
 
1
  import os
2
  import math
3
  import time
 
5
  import subprocess
6
  import requests
7
  import pandas as pd
8
+ from io import StringIO, BytesIO
 
 
 
 
 
 
 
9
  from bs4 import BeautifulSoup
10
  from duckduckgo_search import DDGS
11
  import wikipedia
 
13
  import google.generativeai as genai
14
 
15
  # LlamaIndex imports
16
+ from llama_index.core.llms import ChatMessage, LLMMetadata, LLM, CompletionResponse
 
 
 
 
 
 
 
17
  from llama_index.core.tools import FunctionTool
18
  from llama_index.core.agent import ReActAgent
19
+ from llama_index.core.callbacks.llama_debug import LlamaDebugHandler
 
20
 
21
  # -------------------------------------------------------------------
22
+ # 1) GeminiLLM personalizado
 
 
 
 
 
23
  # -------------------------------------------------------------------
24
+ ChatMessage.message = property(lambda self: self.content)
25
  class GeminiLLM(LLM):
26
+ model_name: str = Field(default="models/gemini-1.5-flash-latest")
27
+ temperature: float = Field(default=0.0)
 
 
28
 
29
  class Config:
30
  extra = "allow"
31
 
32
  def __init__(self, **kwargs):
33
+ super().__init__(**kwargs)
 
 
 
34
  key = os.getenv("GEMINI_API_KEY")
35
  if not key:
36
  raise ValueError("GEMINI_API_KEY no configurada")
37
  genai.configure(api_key=key)
38
+ self._gen_cfg = genai.types.GenerationConfig(temperature=self.temperature)
 
 
39
  self._model = genai.GenerativeModel(
40
  model_name=self.model_name,
41
  generation_config=self._gen_cfg
42
  )
43
+ if not self.callback_manager.handlers:
44
+ self.callback_manager.add_handler(LlamaDebugHandler())
 
 
 
 
 
 
 
 
 
45
 
46
  @property
47
  def metadata(self):
48
  return LLMMetadata(
49
+ context_window=1048576,
 
 
50
  num_output=8192,
51
+ is_chat_model=True,
52
+ is_function_calling_model=True,
 
 
 
 
 
53
  model_name=self.model_name,
54
  )
55
 
56
+ def chat(self, messages, **kwargs):
 
 
57
  hist = []
58
  for m in messages[:-1]:
59
+ role = "user" if m.role == "user" else "model"
60
+ hist.append({"role": role, "parts": [{"text": str(m.content)}]})
61
+ last = str(messages[-1].content)
 
 
 
 
 
 
 
 
 
 
62
  session = self._model.start_chat(history=hist)
63
  try:
64
+ resp = session.send_message(last)
65
  return ChatMessage(role="assistant", content=resp.text)
66
+ except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
67
  return ChatMessage(role="assistant", content=f"Error Gemini: {e}")
68
 
69
  async def achat(self, messages, **kwargs):
70
  return await asyncio.to_thread(self.chat, messages, **kwargs)
71
 
72
+ def stream_complete(self, prompt, formatted=False, **kwargs):
73
+ stream = self._model.generate_content(str(prompt), stream=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  def gen():
75
  acc = ""
76
  for chunk in stream:
 
79
  delta = chunk.parts[0].text
80
  if delta:
81
  acc += delta
82
+ yield CompletionResponse(text=acc, delta=delta)
83
  return gen()
84
 
85
+ async def astream_complete(self, prompt, formatted=False, **kwargs):
86
+ return await asyncio.to_thread(self.stream_complete, prompt, formatted=formatted, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ def stream_chat(self, messages, **kwargs):
89
+ hist = []
90
+ for m in messages[:-1]:
91
+ role = "user" if m.role == "user" else "model"
92
+ hist.append({"role": role, "parts": [{"text": str(m.content)}]})
93
+ last = str(messages[-1].content)
94
+ session = self._model.start_chat(history=hist)
95
+ stream = session.send_message(last, stream=True)
96
  def gen():
97
  acc = ""
98
  for chunk in stream:
 
101
  delta = chunk.parts[0].text
102
  if delta:
103
  acc += delta
104
+ yield ChatMessage(role="assistant", content=acc, additional_kwargs={"delta": delta})
105
  return gen()
106
 
107
+ def complete(self, prompt, formatted=False, **kwargs):
108
+ try:
109
+ resp = self._model.generate_content(str(prompt))
110
+ return CompletionResponse(text=resp.text)
111
+ except Exception as e:
112
+ return CompletionResponse(text=f"Error complete: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ async def acomplete(self, prompt, formatted=False, **kwargs):
115
+ return await asyncio.to_thread(self.complete, prompt, formatted=formatted, **kwargs)
 
116
 
 
117
  # -------------------------------------------------------------------
118
+ # 2) Herramientas
119
  # -------------------------------------------------------------------
120
+ HEADERS = {'User-Agent': 'Mozilla/5.0'}
 
 
 
 
 
121
 
122
+ # Web search tool
123
+ def buscar_web(query: str, max_attempts: int = 2, num_results: int = 5) -> str:
124
  for i in range(max_attempts):
125
  try:
 
 
 
 
126
  with DDGS(headers=HEADERS, timeout=25) as ddgs:
127
+ results = list(ddgs.text(query, region='es-es', safesearch='moderate', max_results=num_results))
 
 
 
 
 
128
  if results:
129
+ return "\n".join(f"Fuente {idx+1}: Título: {r['title']}\nEnlace: {r.get('href','N/A')}\nCuerpo: {r['body']}" for idx, r in enumerate(results))
130
+ return "No se encontraron resultados relevantes."
 
131
  except Exception as e:
132
+ if i < max_attempts - 1:
133
+ time.sleep(2 * (i + 1))
 
 
 
 
 
 
 
134
  else:
135
+ return f"Error buscar_web tras {max_attempts} intentos: {e}"
136
 
137
+ # Reverse text tool
138
  def reverse_text(text: str) -> str:
 
139
  return text[::-1]
140
 
141
+ # Analyze markdown table
142
  def analyze_table(table_md: str, question: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  try:
144
+ lines = [l for l in table_md.splitlines() if l.strip() and '---' not in l]
145
+ rows = [ [c.strip() for c in l.strip().strip('|').split('|')] for l in lines ]
146
+ if len(rows) < 2:
147
+ return "Tabla Markdown mal formateada o vacía."
148
+ df = pd.DataFrame(rows[1:], columns=rows[0])
149
+ if 'conmut' in question.lower():
150
+ cols = df.columns.tolist()[1:]
151
+ counter = set()
152
+ for x in cols:
153
+ for y in cols:
 
 
 
 
154
  try:
155
+ if df.loc[df[rows[0][0]]==x, y].iat[0] != df.loc[df[rows[0][0]]==y, x].iat[0]:
156
+ counter.update([x, y])
157
+ except:
158
+ continue
159
+ return ', '.join(sorted(counter)) or 'Conmutativa'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  return df.to_csv(index=False)
 
 
 
 
 
 
 
 
161
  except Exception as e:
162
+ return f"Error analyze_table: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ # Execute code tool
165
  def execute_code(code: str) -> str:
 
 
 
166
  try:
167
+ allowed_globals = {'__builtins__': None, 'math': math}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  try:
169
+ val = eval(code, allowed_globals, {})
170
+ return str(val)
171
+ except:
172
+ res = subprocess.run(["python", "-S", "-c", code], capture_output=True, text=True, timeout=10)
173
+ if res.returncode != 0:
174
+ return f"Error código: {res.stderr.strip()}"
175
+ return res.stdout.strip() or "(sin salida)"
176
+ except subprocess.TimeoutExpired:
177
+ return "Error ejecutar código: timeout"
178
+ except Exception as e:
179
+ return f"Error crítico: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ # Read Excel tool
182
+ def read_excel_data(file_path: str, sheet_name=0) -> str:
183
+ try:
184
+ if file_path.startswith(('http://','https://')):
185
+ resp = requests.get(file_path, headers=HEADERS, timeout=30)
186
+ resp.raise_for_status()
187
+ df = pd.read_excel(BytesIO(resp.content), sheet_name=sheet_name)
188
  else:
189
+ if not os.path.exists(file_path):
190
+ return f"Error read_excel_data: archivo '{file_path}' no encontrado"
191
+ df = pd.read_excel(file_path, sheet_name=sheet_name)
192
+ df = df.fillna('')
193
+ return df.to_csv(index=False)
194
+ except Exception as e:
195
+ return f"Error read_excel_data: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
+ # Botanical classification tool
198
+ def classify_botanical(items_list_str: str) -> str:
199
+ fruits = {"tomate","pepino","calabacín","berenjena","pimiento","aguacate","calabaza","guisante","judía verde","maíz"}
200
+ vegetables = {"zanahoria","patata","batata","cebolla","ajo","puerro","apio","lechuga","espinaca","brócoli"}
201
+ items = [i.strip().lower() for i in items_list_str.split(',')] if items_list_str else []
202
+ vegs = [i for i in items if i in vegetables]
203
+ fruits_found = [i for i in items if i in fruits]
204
+ others = [i for i in items if i not in fruits and i not in vegetables]
205
+ return f"Verduras: {', '.join(sorted(set(vegs)))}\nFrutas: {', '.join(sorted(set(fruits_found)))}\nOtros: {', '.join(sorted(set(others)))}"
206
+
207
+ # Wikipedia table scraper
208
+ def scrape_wikipedia_table(page_title: str, section: str, table_index: int = 0) -> str:
209
  try:
210
+ wikipedia.set_lang("es")
211
+ page = wikipedia.page(page_title, auto_suggest=False)
212
+ soup = BeautifulSoup(page.html(), 'html.parser')
213
+ header = next((h for h in soup.find_all(['h2','h3']) if section.lower() in h.get_text(strip=True).lower()), None)
214
+ if not header:
215
+ return f"Sección '{section}' no encontrada en '{page_title}'"
216
+ tables = []
217
+ for sib in header.find_next_siblings():
218
+ if sib.name in ['h2','h3']: break
219
+ if sib.name=='table' and 'wikitable' in sib.get('class',[]): tables.append(sib)
220
+ if table_index>=len(tables): return f"Tabla índice {table_index} fuera de rango (solo {len(tables)} tablas)."
221
+ df = pd.read_html(str(tables[table_index]))[0]
222
+ return df.to_csv(index=False)
223
+ except Exception as e:
224
+ return f"Error scrape_wiki_table: {e}"
225
+
226
+ # Wrap tools
227
+ search_tool = FunctionTool.from_defaults(fn=buscar_web, name="web_search", description="Búsqueda DuckDuckGo.")
228
+ reverse_tool = FunctionTool.from_defaults(fn=reverse_text, name="reverse_text", description="Invierte texto.")
229
+ table_tool = FunctionTool.from_defaults(fn=analyze_table, name="analyze_markdown_table", description="Procesa tabla Markdown.")
230
+ code_tool = FunctionTool.from_defaults(fn=execute_code, name="execute_code", description="Ejecuta Python.")
231
+ excel_tool = FunctionTool.from_defaults(fn=read_excel_data, name="read_excel_data", description="Lee Excel.")
232
+ botanical_tool = FunctionTool.from_defaults(fn=classify_botanical, name="classify_botanical_foods", description="Clasifica botánicamente alimentos.")
233
+ scrape_tool = FunctionTool.from_defaults(fn=scrape_wikipedia_table, name="scrape_wiki_table", description="Scrapea tabla Wikipedia.")
234
+ fallback_tool= FunctionTool.from_defaults(fn=lambda q: "Procedo con conocimiento interno.", name="no_tool_solution", description="Fallback.")
235
+ all_tools = [search_tool, scrape_tool, table_tool, code_tool, excel_tool, botanical_tool, reverse_tool, fallback_tool]
236
+
237
+ # System prompt
238
+ tool_descriptions = "\n".join([t.description for t in all_tools])
239
+ #system_prompt = f"Eres Alfred...\nHerramientas:\n{tool_desc}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  # Prompt de sistema REFINADO
241
  system_prompt = (
242
  "Eres Alfred, un agente ReAct eficiente y preciso. Tu objetivo es responder preguntas de forma correcta.\n"
 
255
  "Herramientas disponibles (usa SOLO estas y con los nombres exactos):\n{tool_descriptions}"
256
  )
257
 
258
+ # Agent init
259
+ llm = GeminiLLM()
260
+ alfred_agent = ReActAgent.from_tools(tools=all_tools, llm=llm, system_prompt=system_prompt, verbose=True, max_iterations=15, callback_manager=llm.callback_manager)
 
 
 
 
 
 
 
 
 
261
 
 
262
  def basic_agent_response(question: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  try:
264
  resp = alfred_agent.query(question)
265
+ return resp.response or "No response."
 
 
 
 
266
  except Exception as e:
267
+ return f"Error crítico: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268