Nancy1906 commited on
Commit
9d1f396
·
verified ·
1 Parent(s): cfdc147
Files changed (1) hide show
  1. 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 para evitar el error de .message faltante
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
- lines = [l.strip() for l in table_md.strip().splitlines() if l.strip()]
146
- content = [l for l in lines if not set(l) <= set("|- ")]
147
- rows = [[c.strip() for c in r.split("|")[1:-1]] for r in content]
 
 
 
 
 
 
148
  df = pd.DataFrame(rows[1:], columns=rows[0])
149
- if "no conmut" in question.lower():
150
- S = df.columns.tolist()
151
  counter = set()
152
  for x in S:
153
  for y in S:
154
- a = df.loc[df[rows[0][0]]==x, y].values[0]
155
- b = df.loc[df[rows[0][0]]==y, x].values[0]
156
  if a != b:
157
  counter.update([x, y])
158
- return ", ".join(sorted(counter)) or "No hay contraejemplos"
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
- # 3) Encapsular como FunctionTool
181
- # -------------------------------------------------------------------
182
- search_tool = FunctionTool.from_defaults(fn=buscar_web, name="web_search", description="Búsqueda DDG (3 resultados).")
183
- reverse_tool = FunctionTool.from_defaults(fn=reverse_text, name="reverse_text", description="Invierte texto.")
184
- table_tool = FunctionTool.from_defaults(fn=analyze_table, name="analyze_table", description="Parses Markdown tables y responde conmutatividad.")
185
- code_tool = FunctionTool.from_defaults(fn=execute_code, name="execute_code", description="Ejecuta Python para cálculos.")
186
- fallback_tool = FunctionTool.from_defaults(fn=no_tool_solution,name="no_tool_solution", description="Fallback: conocimiento interno.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
- all_tools = [search_tool, reverse_tool, table_tool, code_tool, fallback_tool]
 
 
 
 
 
 
189
 
190
- # -------------------------------------------------------------------
191
- # 4) Prompt de sistema
192
- # -------------------------------------------------------------------
193
  system_prompt = (
194
  "Eres Alfred, un agente ReAct que usa pasos de pensamiento claros y herramientas especializadas.\n"
195
- "Sigue este flujo:\n"
196
- "1) Analiza la pregunta.\n"
197
- "2) Piensa paso a paso (internamente), luego decide qué acción tomar.\n"
198
- "3) Si necesitas datos externos, usa web_search.\n"
199
- "4) Si hay texto invertido, usa reverse_text.\n"
200
- "5) Si hay tablas Markdown, usa analyze_table.\n"
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)