Spaces:
Runtime error
Runtime error
ffff
Browse files- 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
|
| 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
|
| 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 |
-
#
|
| 40 |
-
# -------------------------------------------------------------------
|
| 41 |
-
HEADERS = {'User- LlamaDebugHandler
|
| 42 |
-
|
| 43 |
-
# -------------------------------------------------------------------
|
| 44 |
-
# 1) GeminiLLM personalizado (SIN CAMBIOS - Asumimos que está bien)
|
| 45 |
# -------------------------------------------------------------------
|
|
|
|
| 46 |
class GeminiLLM(LLM):
|
| 47 |
-
|
| 48 |
-
|
| 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().
|
| 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=
|
| 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 |
-
|
| 72 |
-
|
| 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=
|
| 87 |
-
else:
|
| 88 |
-
return f, # Gemini 1.5 Flash tiene 1M de tokens
|
| 89 |
num_output=8192,
|
| 90 |
-
|
| 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,
|
| 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".
|
| 106 |
-
|
| 107 |
-
|
| 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(
|
| 121 |
return ChatMessage(role="assistant", content=resp.text)
|
| 122 |
-
except
|
| 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 |
-
|
| 140 |
-
|
| 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
|
| 184 |
return gen()
|
| 185 |
|
| 186 |
-
def
|
| 187 |
-
|
| 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
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 218 |
return gen()
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 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
|
| 260 |
-
|
| 261 |
-
return "Respuesta basada en conocimiento interno. El LLM procederá a responder directamente."
|
| 262 |
|
| 263 |
-
def scrape_wikipedia_table()
|
| 264 |
# -------------------------------------------------------------------
|
| 265 |
-
# 2) Herramientas
|
| 266 |
# -------------------------------------------------------------------
|
| 267 |
-
HEADERS = {'User-Agent':'Mozilla/5.0
|
| 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 |
-
|
| 275 |
-
|
| 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',
|
| 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}:
|
| 291 |
-
|
| 292 |
-
return "No se encontraron resultados relevantes en la web para esta consulta."
|
| 293 |
except Exception as e:
|
| 294 |
-
|
| 295 |
-
|
| 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
|
| 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 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 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 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 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 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
|
|
|
| 568 |
else:
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 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 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
try:
|
| 618 |
-
wikipedia.set_lang("es")
|
| 619 |
-
page = wikipedia.page(page_title, auto_suggest=False)
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
for
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 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 |
-
#
|
| 693 |
-
llm = GeminiLLM(
|
| 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 |
-
|
| 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
|
| 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 |
|
|
|
|
|
|
|
|
|
|
|
|