Spaces:
Runtime error
Runtime error
fff
Browse files- my_tools.py +74 -47
my_tools.py
CHANGED
|
@@ -5,14 +5,18 @@ import math
|
|
| 5 |
import time
|
| 6 |
import asyncio
|
| 7 |
import subprocess
|
|
|
|
| 8 |
import pandas as pd
|
|
|
|
|
|
|
| 9 |
from duckduckgo_search import DDGS
|
| 10 |
import wikipedia
|
| 11 |
from pydantic import Field
|
| 12 |
import google.generativeai as genai
|
| 13 |
|
|
|
|
| 14 |
from llama_index.core.llms import ChatMessage, LLMMetadata, LLM, CompletionResponse
|
| 15 |
-
# Monkey-patch
|
| 16 |
ChatMessage.message = property(lambda self: self)
|
| 17 |
from llama_index.core.tools import FunctionTool
|
| 18 |
from llama_index.core.agent import ReActAgent
|
|
@@ -80,7 +84,7 @@ class GeminiLLM(LLM):
|
|
| 80 |
acc = ""
|
| 81 |
for chunk in stream:
|
| 82 |
delta = getattr(chunk, "text", "")
|
| 83 |
-
if not delta and chunk.parts:
|
| 84 |
delta = chunk.parts[0].text
|
| 85 |
if delta:
|
| 86 |
acc += delta
|
|
@@ -103,7 +107,7 @@ class GeminiLLM(LLM):
|
|
| 103 |
acc = ""
|
| 104 |
for chunk in stream:
|
| 105 |
delta = getattr(chunk, "text", "")
|
| 106 |
-
if not delta and chunk.parts:
|
| 107 |
delta = chunk.parts[0].text
|
| 108 |
if delta:
|
| 109 |
acc += delta
|
|
@@ -113,9 +117,7 @@ class GeminiLLM(LLM):
|
|
| 113 |
async def astream_complete(self, prompt, formatted=False, **kwargs):
|
| 114 |
return await asyncio.to_thread(self.stream_complete, prompt, formatted=formatted, **kwargs)
|
| 115 |
|
| 116 |
-
# <<< METODO ADICIONAL PARA CUMPLIR con LLM.abstractmethod >>>
|
| 117 |
def astream_chat(self, messages: list[ChatMessage], **kwargs):
|
| 118 |
-
# reutilizamos stream_chat existente
|
| 119 |
return self.stream_chat(messages, **kwargs)
|
| 120 |
|
| 121 |
# -------------------------------------------------------------------
|
|
@@ -137,34 +139,49 @@ def buscar_web(query: str, max_attempts: int = 2) -> str:
|
|
| 137 |
else:
|
| 138 |
return f"Error buscar_web tras {max_attempts} intentos: {e}"
|
| 139 |
|
|
|
|
| 140 |
def reverse_text(text: str) -> str:
|
| 141 |
return text[::-1]
|
| 142 |
|
|
|
|
| 143 |
def analyze_table(table_md: str, question: str) -> str:
|
| 144 |
try:
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
df = pd.DataFrame(rows[1:], columns=rows[0])
|
| 149 |
-
if
|
| 150 |
-
S =
|
| 151 |
counter = set()
|
| 152 |
for x in S:
|
| 153 |
for y in S:
|
| 154 |
-
a = df.loc[df[rows[0][0]]==x, y].
|
| 155 |
-
b = df.loc[df[rows[0][0]]==y, x].
|
| 156 |
if a != b:
|
| 157 |
counter.update([x, y])
|
| 158 |
-
return
|
| 159 |
return df.to_csv(index=False)
|
| 160 |
except Exception as e:
|
| 161 |
return f"Error analyze_table: {e}"
|
| 162 |
|
|
|
|
| 163 |
def execute_code(code: str) -> str:
|
| 164 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
res = subprocess.run(
|
| 166 |
-
["python", "-c", code],
|
| 167 |
-
capture_output=True, text=True, timeout=5
|
| 168 |
)
|
| 169 |
if res.stderr:
|
| 170 |
return f"Error código: {res.stderr.strip()}"
|
|
@@ -172,46 +189,58 @@ def execute_code(code: str) -> str:
|
|
| 172 |
except Exception as e:
|
| 173 |
return f"Error ejecutar código: {e}"
|
| 174 |
|
|
|
|
| 175 |
def no_tool_solution(query: str) -> str:
|
| 176 |
return "Procedo con conocimiento interno."
|
| 177 |
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
#
|
| 193 |
system_prompt = (
|
| 194 |
"Eres Alfred, un agente ReAct que usa pasos de pensamiento claros y herramientas especializadas.\n"
|
| 195 |
-
"
|
| 196 |
-
"
|
| 197 |
-
"
|
| 198 |
-
"
|
| 199 |
-
"
|
| 200 |
-
"
|
| 201 |
-
"6) Para cálculos complejos, usa execute_code.\n"
|
| 202 |
-
"7) Si nada ayuda, llama no_tool_solution y responde con tu conocimiento.\n"
|
| 203 |
-
"8) Si la pregunta involucra audio, video o imágenes no procesables, informa que no puedes acceder.\n"
|
| 204 |
-
"9) Proporciona siempre una respuesta final clara al usuario.\n"
|
| 205 |
-
"\nHerramientas disponibles:\n{tool_descriptions}\n"
|
| 206 |
-
"No muestres tus pensamientos al usuario; solo la respuesta final."
|
| 207 |
-
|
| 208 |
)
|
| 209 |
|
| 210 |
-
#
|
| 211 |
-
# 5) Inicializar agente con GeminiLLM
|
| 212 |
-
# -------------------------------------------------------------------
|
| 213 |
llm = GeminiLLM(model_name="models/gemini-1.5-flash-latest", temperature=0.0)
|
| 214 |
-
|
| 215 |
alfred_agent = ReActAgent.from_tools(
|
| 216 |
tools=all_tools,
|
| 217 |
llm=llm,
|
|
@@ -220,9 +249,7 @@ alfred_agent = ReActAgent.from_tools(
|
|
| 220 |
max_iterations=20
|
| 221 |
)
|
| 222 |
|
| 223 |
-
#
|
| 224 |
-
# 6) Función pública
|
| 225 |
-
# -------------------------------------------------------------------
|
| 226 |
def basic_agent_response(question: str) -> str:
|
| 227 |
try:
|
| 228 |
resp = alfred_agent.query(question)
|
|
|
|
| 5 |
import time
|
| 6 |
import asyncio
|
| 7 |
import subprocess
|
| 8 |
+
import requests
|
| 9 |
import pandas as pd
|
| 10 |
+
from io import StringIO
|
| 11 |
+
from bs4 import BeautifulSoup
|
| 12 |
from duckduckgo_search import DDGS
|
| 13 |
import wikipedia
|
| 14 |
from pydantic import Field
|
| 15 |
import google.generativeai as genai
|
| 16 |
|
| 17 |
+
# LlamaIndex imports
|
| 18 |
from llama_index.core.llms import ChatMessage, LLMMetadata, LLM, CompletionResponse
|
| 19 |
+
# Monkey-patch ChatMessage to satisfy .message
|
| 20 |
ChatMessage.message = property(lambda self: self)
|
| 21 |
from llama_index.core.tools import FunctionTool
|
| 22 |
from llama_index.core.agent import ReActAgent
|
|
|
|
| 84 |
acc = ""
|
| 85 |
for chunk in stream:
|
| 86 |
delta = getattr(chunk, "text", "")
|
| 87 |
+
if not delta and hasattr(chunk, 'parts') and chunk.parts:
|
| 88 |
delta = chunk.parts[0].text
|
| 89 |
if delta:
|
| 90 |
acc += delta
|
|
|
|
| 107 |
acc = ""
|
| 108 |
for chunk in stream:
|
| 109 |
delta = getattr(chunk, "text", "")
|
| 110 |
+
if not delta and hasattr(chunk, 'parts') and chunk.parts:
|
| 111 |
delta = chunk.parts[0].text
|
| 112 |
if delta:
|
| 113 |
acc += delta
|
|
|
|
| 117 |
async def astream_complete(self, prompt, formatted=False, **kwargs):
|
| 118 |
return await asyncio.to_thread(self.stream_complete, prompt, formatted=formatted, **kwargs)
|
| 119 |
|
|
|
|
| 120 |
def astream_chat(self, messages: list[ChatMessage], **kwargs):
|
|
|
|
| 121 |
return self.stream_chat(messages, **kwargs)
|
| 122 |
|
| 123 |
# -------------------------------------------------------------------
|
|
|
|
| 139 |
else:
|
| 140 |
return f"Error buscar_web tras {max_attempts} intentos: {e}"
|
| 141 |
|
| 142 |
+
|
| 143 |
def reverse_text(text: str) -> str:
|
| 144 |
return text[::-1]
|
| 145 |
|
| 146 |
+
|
| 147 |
def analyze_table(table_md: str, question: str) -> str:
|
| 148 |
try:
|
| 149 |
+
# Remove alignment row and parse header/body
|
| 150 |
+
lines = [l for l in table_md.splitlines() if l.strip()]
|
| 151 |
+
# Filter out divider line
|
| 152 |
+
lines = [l for l in lines if not l.strip().startswith('|') or '---' not in l]
|
| 153 |
+
# Convert markdown to CSV-like
|
| 154 |
+
rows = []
|
| 155 |
+
for l in lines:
|
| 156 |
+
parts = [c.strip() for c in l.strip().strip('|').split('|')]
|
| 157 |
+
rows.append(parts)
|
| 158 |
df = pd.DataFrame(rows[1:], columns=rows[0])
|
| 159 |
+
if 'conmut' in question.lower():
|
| 160 |
+
S = rows[0][1:]
|
| 161 |
counter = set()
|
| 162 |
for x in S:
|
| 163 |
for y in S:
|
| 164 |
+
a = df.loc[df[rows[0][0]]==x, y].iat[0]
|
| 165 |
+
b = df.loc[df[rows[0][0]]==y, x].iat[0]
|
| 166 |
if a != b:
|
| 167 |
counter.update([x, y])
|
| 168 |
+
return ', '.join(sorted(counter)) or 'No hay contraejemplos'
|
| 169 |
return df.to_csv(index=False)
|
| 170 |
except Exception as e:
|
| 171 |
return f"Error analyze_table: {e}"
|
| 172 |
|
| 173 |
+
|
| 174 |
def execute_code(code: str) -> str:
|
| 175 |
try:
|
| 176 |
+
# Try eval for simple expressions
|
| 177 |
+
try:
|
| 178 |
+
val = eval(code, {'__builtins__': None, 'math': math}, math.__dict__)
|
| 179 |
+
return str(val)
|
| 180 |
+
except:
|
| 181 |
+
pass
|
| 182 |
+
# Fallback to subprocess
|
| 183 |
res = subprocess.run(
|
| 184 |
+
["python", "-c", code], capture_output=True, text=True, timeout=5
|
|
|
|
| 185 |
)
|
| 186 |
if res.stderr:
|
| 187 |
return f"Error código: {res.stderr.strip()}"
|
|
|
|
| 189 |
except Exception as e:
|
| 190 |
return f"Error ejecutar código: {e}"
|
| 191 |
|
| 192 |
+
|
| 193 |
def no_tool_solution(query: str) -> str:
|
| 194 |
return "Procedo con conocimiento interno."
|
| 195 |
|
| 196 |
|
| 197 |
+
def scrape_wikipedia_table(query: str, section: str) -> str:
|
| 198 |
+
"""
|
| 199 |
+
Busca una página de Wikipedia y extrae la tabla HTML bajo la sección dada.
|
| 200 |
+
Devuelve CSV de la tabla.
|
| 201 |
+
"""
|
| 202 |
+
try:
|
| 203 |
+
page = wikipedia.page(query)
|
| 204 |
+
html = page.html()
|
| 205 |
+
soup = BeautifulSoup(html, 'html.parser')
|
| 206 |
+
sec = None
|
| 207 |
+
for header in soup.find_all(['h2','h3']):
|
| 208 |
+
if section.lower() in header.get_text().lower():
|
| 209 |
+
sec = header
|
| 210 |
+
break
|
| 211 |
+
if not sec:
|
| 212 |
+
return f"Sección '{section}' no encontrada en '{query}'."
|
| 213 |
+
table = sec.find_next('table', {'class':'wikitable'})
|
| 214 |
+
if not table:
|
| 215 |
+
return "No se encontró tabla wikitable después de la sección."
|
| 216 |
+
df = pd.read_html(str(table))[0]
|
| 217 |
+
return df.to_csv(index=False)
|
| 218 |
+
except Exception as e:
|
| 219 |
+
return f"Error scrape_wikipedia_table: {e}"
|
| 220 |
|
| 221 |
+
# Encapsular herramientas
|
| 222 |
+
search_tool = FunctionTool.from_defaults(fn=buscar_web, name="web_search", description="Búsqueda DDG (3 resultados).")
|
| 223 |
+
reverse_tool = FunctionTool.from_defaults(fn=reverse_text, name="reverse_text", description="Invierte texto.")
|
| 224 |
+
table_tool = FunctionTool.from_defaults(fn=analyze_table, name="analyze_table", description="Procesa tablas Markdown y verifica conmutatividad.")
|
| 225 |
+
code_tool = FunctionTool.from_defaults(fn=execute_code, name="execute_code", description="Ejecuta Python para cálculos.")
|
| 226 |
+
fallback_tool = FunctionTool.from_defaults(fn=no_tool_solution, name="no_tool_solution", description="Fallback: conocimiento interno.")
|
| 227 |
+
scrape_tool = FunctionTool.from_defaults(fn=scrape_wikipedia_table,name="scrape_wiki_table", description="Extrae tabla de Wikipedia por sección.")
|
| 228 |
|
| 229 |
+
all_tools = [search_tool, reverse_tool, table_tool, code_tool, scrape_tool, fallback_tool]
|
| 230 |
+
|
| 231 |
+
# Prompt de sistema
|
| 232 |
system_prompt = (
|
| 233 |
"Eres Alfred, un agente ReAct que usa pasos de pensamiento claros y herramientas especializadas.\n"
|
| 234 |
+
"1) Analiza la pregunta. 2) Decide la herramienta más adecuada.\n"
|
| 235 |
+
"3) Usa web_search, reverse_text, analyze_table, execute_code o scrape_wiki_table.\n"
|
| 236 |
+
"4) Si nada aplica, usa no_tool_solution.\n"
|
| 237 |
+
"5) Si la pregunta involucra audio/video/imágenes no procesables, informa que no puedes acceder.\n"
|
| 238 |
+
"6) Responde final claro.\n"
|
| 239 |
+
"Herramientas:\n{tool_descriptions}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
)
|
| 241 |
|
| 242 |
+
# Inicializar agente
|
|
|
|
|
|
|
| 243 |
llm = GeminiLLM(model_name="models/gemini-1.5-flash-latest", temperature=0.0)
|
|
|
|
| 244 |
alfred_agent = ReActAgent.from_tools(
|
| 245 |
tools=all_tools,
|
| 246 |
llm=llm,
|
|
|
|
| 249 |
max_iterations=20
|
| 250 |
)
|
| 251 |
|
| 252 |
+
# Función pública
|
|
|
|
|
|
|
| 253 |
def basic_agent_response(question: str) -> str:
|
| 254 |
try:
|
| 255 |
resp = alfred_agent.query(question)
|