Nancy1906 commited on
Commit
3d04920
·
verified ·
1 Parent(s): 18f8f98

Update my

Browse files
Files changed (1) hide show
  1. my_tools.py +210 -346
my_tools.py CHANGED
@@ -1,409 +1,273 @@
 
 
1
  import os
2
  import math
3
- import pandas as pd # No se usa directamente aquí pero podría ser útil para el DataFrame en app.py
 
 
 
 
4
  from duckduckgo_search import DDGS
5
  import wikipedia
6
- import llama_index
 
7
  from llama_index.core.tools import FunctionTool
8
  from llama_index.core.agent import ReActAgent
9
- from llama_index.core.llms import ChatMessage, LLMMetadata, LLM, CompletionResponse
 
10
  from llama_index.core.callbacks import CallbackManager
11
  from llama_index.core.callbacks.llama_debug import LlamaDebugHandler
 
12
  import google.generativeai as genai
13
- import asyncio
14
- from pydantic import Field # Para declarar campos si fuera necesario, aunque no para _generation_config
15
- from llama_index.core.llms import ChatMessage
16
- import time # Para reintentos
17
-
18
- # --- Intento mejorado para obtener la versión de LlamaIndex ---
19
- try:
20
- from importlib import metadata
21
- try:
22
- llama_index_version = metadata.version('llama-index')
23
- except metadata.PackageNotFoundError:
24
- try:
25
- llama_index_version = metadata.version('llama-index-core')
26
- except metadata.PackageNotFoundError:
27
- llama_index_version = "No se pudo determinar (con importlib.metadata)"
28
- except ImportError:
29
- try:
30
- from llama_index.core import __version__ as llama_index_core_version
31
- llama_index_version = llama_index_core_version
32
- except ImportError:
33
- llama_index_version = "No se pudo determinar (fallback a __version__ falló)"
34
-
35
- print(f"LlamaIndex version detectada: {llama_index_version}")
36
 
37
- class PatchedChatMessage(ChatMessage):
38
- @property
39
- def message(self):
40
- return self
41
-
42
- # --- Gemini LLM personalizado ---
43
  class GeminiLLM(LLM):
44
- model_name: str = Field(default="models/gemini-1.5-flash-latest", description="The Gemini model to use.")
45
- temperature: float = Field(default=0.7, description="The temperature to use for generation.")
46
-
47
- # Atributos privados que no queremos que Pydantic valide como campos del modelo directamente
48
- # pero que necesitamos para la lógica interna. Los inicializaremos en __init__.
49
- _model_instance: genai.GenerativeModel = None
50
- _generation_config_instance: genai.types.GenerationConfig = None
51
-
52
- # Para Pydantic v1, si la clase base lo es, permitir atributos extra
53
- # Para Pydantic v2, esto sería model_config = {"extra": "allow"}
54
  class Config:
55
- extra = "allow" # Permite atributos que no están definidos explícitamente como campos
56
-
57
- def __init__(self, model_name: str = "models/gemini-1.5-flash-latest", temperature: float = 0.7, **kwargs):
58
- # Llamar a super().__init__() con los campos definidos y **kwargs
59
- # Esto es importante para que Pydantic inicialice correctamente
60
- super().__init__(model_name=model_name, temperature=temperature, **kwargs) # Pasar kwargs a la clase base
61
-
62
- gemini_api_key = os.getenv("GEMINI_API_KEY")
63
- if not gemini_api_key:
64
- raise ValueError("GEMINI_API_KEY environment variable not set.")
65
- genai.configure(api_key=gemini_api_key)
66
-
67
- # Usar self.temperature y self.model_name que Pydantic ya ha asignado
68
- self._generation_config_instance = genai.types.GenerationConfig(
69
- temperature=self.temperature
70
- )
71
- self._model_instance = genai.GenerativeModel(
72
  model_name=self.model_name,
73
- generation_config=self._generation_config_instance
74
  )
75
- # El callback_manager se hereda de la clase base LLM, podemos configurarlo si es necesario.
76
- # self.callback_manager = kwargs.get('callback_manager', CallbackManager([LlamaDebugHandler(print_trace=True)]))
77
- # Si la clase base LLM ya inicializa callback_manager, no necesitamos reasignarlo a menos que queramos uno específico.
78
- # Por defecto, llama_index.core.llms.LLM inicializa self.callback_manager = callback_manager or CallbackManager([])
79
- # Si queremos el LlamaDebugHandler, podemos pasarlo o reconfigurarlo
80
- if not self.callback_manager.handlers: # Si no hay manejadores, añadir el nuestro
81
- self.callback_manager.add_handler(LlamaDebugHandler())
82
-
83
 
84
  @property
85
- def metadata(self) -> LLMMetadata:
86
  return LLMMetadata(
87
- context_window=1048576,
88
- num_output=8192,
89
  is_chat_model=True,
90
- is_function_calling_model=True,
91
- model_name=self.model_name
92
  )
93
 
94
- # callback_manager ya es una propiedad en la clase base LLM.
95
- # No necesitamos redefinirla a menos que la lógica de acceso sea diferente.
96
- # @property
97
- # def callback_manager(self):
98
- # return self._callback_manager
99
-
100
- # --- Implementación de Chat ---
101
- '''
102
- def chat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
103
- gemini_history = []
104
- for msg in messages[:-1]:
105
- role = "user" if msg.role == "user" else "model"
106
- gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
107
-
108
- last_user_message = messages[-1].content
109
-
110
- chat_session = self._model_instance.start_chat(history=gemini_history)
111
  try:
112
- response = chat_session.send_message(last_user_message)
113
- return ChatMessage(role="assistant", content=response.text)
114
  except Exception as e:
115
- print(f"Error en Gemini chat: {e}")
116
- return ChatMessage(role="assistant", content=f"Error al generar respuesta: {e}")
117
- '''
118
- def chat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
119
- gemini_history = []
120
- for msg in messages[:-1]:
121
- role = "user" if msg.role == "user" else "model"
122
- gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
123
-
124
- last_user_message = messages[-1].content
125
-
126
- chat_session = self._model_instance.start_chat(history=gemini_history)
127
- try:
128
- response = chat_session.send_message(last_user_message)
129
- return PatchedChatMessage(role="assistant", content=response.text)
130
- except Exception as e:
131
- print(f"Error en Gemini chat: {e}")
132
- return PatchedChatMessage(role="assistant", content=f"Error al generar respuesta: {e}")
133
 
134
- async def achat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
135
  return await asyncio.to_thread(self.chat, messages, **kwargs)
136
 
137
- def stream_chat(self, messages: list[ChatMessage], **kwargs):
138
- gemini_history = []
139
- for msg in messages[:-1]:
140
- role = "user" if msg.role == "user" else "model"
141
- gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
142
-
143
- last_user_message = messages[-1].content
144
-
145
- chat_session = self._model_instance.start_chat(history=gemini_history)
146
- response_stream = chat_session.send_message(last_user_message, stream=True)
147
-
148
  def gen():
149
- accumulated_text = ""
150
- for chunk in response_stream:
151
- delta = ""
152
- if hasattr(chunk, 'text') and chunk.text:
153
- delta = chunk.text
154
- # Podríamos necesitar revisar la estructura exacta del chunk para obtener el delta correcto.
155
- # A veces es chunk.parts[0].text
156
- elif chunk.parts and hasattr(chunk.parts[0], 'text'):
157
- delta = chunk.parts[0].text
158
-
159
- if delta:
160
- accumulated_text += delta
161
- yield ChatMessage(role="assistant", content=accumulated_text, additional_kwargs={"delta": delta})
162
- return gen()
163
-
164
- async def astream_chat(self, messages: list[ChatMessage], **kwargs):
165
- loop = asyncio.get_event_loop()
166
-
167
- gemini_history = []
168
- for msg in messages[:-1]:
169
- role = "user" if msg.role == "user" else "model"
170
- gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
171
- last_user_message = messages[-1].content
172
-
173
- def get_stream_iterator():
174
- chat_session = self._model_instance.start_chat(history=gemini_history)
175
- return chat_session.send_message(last_user_message, stream=True)
176
-
177
- response_stream = await loop.run_in_executor(None, get_stream_iterator)
178
-
179
- async def gen():
180
- accumulated_text = ""
181
- all_chunks_text = []
182
- for chunk in response_stream:
183
- delta = ""
184
- if hasattr(chunk, 'text') and chunk.text:
185
- delta = chunk.text
186
- elif chunk.parts and hasattr(chunk.parts[0], 'text'):
187
  delta = chunk.parts[0].text
188
  if delta:
189
- all_chunks_text.append(delta)
190
-
191
- for text_delta in all_chunks_text:
192
- accumulated_text += text_delta
193
- yield ChatMessage(role="assistant", content=accumulated_text, additional_kwargs={"delta": text_delta})
194
- await asyncio.sleep(0)
195
  return gen()
196
 
197
- # --- Implementación de Complete ---
198
- def complete(self, prompt: str, formatted: bool = False, **kwargs) -> CompletionResponse:
199
  try:
200
- response = self._model_instance.generate_content(prompt)
201
- return CompletionResponse(text=response.text)
202
  except Exception as e:
203
- print(f"Error en Gemini complete: {e}")
204
- return CompletionResponse(text=f"Error al generar completion: {e}")
205
 
206
- async def acomplete(self, prompt: str, formatted: bool = False, **kwargs) -> CompletionResponse:
207
  return await asyncio.to_thread(self.complete, prompt, formatted=formatted, **kwargs)
208
 
209
- def stream_complete(self, prompt: str, formatted: bool = False, **kwargs):
210
- response_stream = self._model_instance.generate_content(prompt, stream=True)
211
-
212
  def gen():
213
- accumulated_text = ""
214
- for chunk in response_stream:
215
- delta = ""
216
- if hasattr(chunk, 'text') and chunk.text:
217
- delta = chunk.text
218
- elif chunk.parts and hasattr(chunk.parts[0], 'text'):
219
  delta = chunk.parts[0].text
220
-
221
  if delta:
222
- accumulated_text += delta
223
- yield CompletionResponse(text=accumulated_text, delta=delta)
224
- elif hasattr(chunk, 'prompt_feedback'):
225
- print(f"Feedback del prompt en stream_complete: {chunk.prompt_feedback}")
226
  return gen()
227
 
228
- async def astream_complete(self, prompt: str, formatted: bool = False, **kwargs):
229
- loop = asyncio.get_event_loop()
230
-
231
- def get_stream_iterator():
232
- return self._model_instance.generate_content(prompt, stream=True)
233
-
234
- response_stream = await loop.run_in_executor(None, get_stream_iterator)
235
-
236
- async def gen():
237
- accumulated_text = ""
238
- all_chunks_data = []
239
- for chunk in response_stream:
240
- delta = ""
241
- feedback = None
242
- if hasattr(chunk, 'text') and chunk.text:
243
- delta = chunk.text
244
- elif chunk.parts and hasattr(chunk.parts[0], 'text'):
245
- delta = chunk.parts[0].text
246
-
247
- if hasattr(chunk, 'prompt_feedback'):
248
- feedback = chunk.prompt_feedback
249
-
250
- if delta:
251
- all_chunks_data.append({'delta': delta})
252
- if feedback:
253
- all_chunks_data.append({'feedback': feedback})
254
-
255
-
256
- for data in all_chunks_data:
257
- if 'delta' in data:
258
- delta_val = data['delta']
259
- accumulated_text += delta_val
260
- yield CompletionResponse(text=accumulated_text, delta=delta_val)
261
- elif 'feedback' in data:
262
- print(f"Feedback del prompt en astream_complete: {data['feedback']}")
263
- await asyncio.sleep(0)
264
- return gen()
265
 
266
- llm = GeminiLLM()
 
 
267
 
268
- 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'}
269
 
270
- def buscar_web(query: str, max_attempts: int = 2) -> str: # Añadido max_attempts
271
- """Busca en la web utilizando DuckDuckGo y devuelve los 3 primeros resultados.
272
- Intenta ser robusto contra errores de red o rate limits de DDG."""
273
- print(f"Herramienta buscar_web: Buscando '{query}'")
274
- for attempt in range(max_attempts):
275
  try:
276
- # Aumentar timeout y añadir headers
277
- with DDGS(headers=HEADERS, timeout=25) as ddgs: # Timeout de 25 segundos
278
  results = list(ddgs.text(query, region='es-es', safesearch='moderate', timelimit='y', max_results=3))
279
  if results:
280
- return "\n".join([f"Título: {r['title']}, Cuerpo: {r['body']}" for r in results])
281
- return "No se encontraron resultados en la web."
282
  except Exception as e:
283
- print(f"Error en herramienta buscar_web (intento {attempt + 1}/{max_attempts}): {type(e).__name__} - {e}")
284
- if "ratelimit" in str(e).lower() or "timed out" in str(e).lower():
285
- if attempt < max_attempts - 1: # Si no es el último intento
286
- wait_time = (attempt + 1) * 5 # Backoff simple: 5s, 10s
287
- print(f"Esperando {wait_time}s antes de reintentar búsqueda web...")
288
- time.sleep(wait_time)
289
- else: # Último intento fallido
290
- return f"Error persistente al buscar en la web después de {max_attempts} intentos: {e}"
291
- else: # Otro tipo de error, no reintentar
292
- return f"Error al buscar en la web: {e}"
293
- return f"La búsqueda web falló después de {max_attempts} intentos." # Si el bucle termina
294
-
295
- search_tool = FunctionTool.from_defaults(
296
- fn=buscar_web,
297
- name="web_search",
298
- description="Busca en la web utilizando DuckDuckGo para obtener información actualizada o temas generales. Devuelve pequeños snippets de texto de los resultados. Úsalo cuando Wikipedia no sea suficiente o la información sea muy reciente."
299
- )
300
 
301
- def get_wikipedia_summary(query: str) -> str:
302
- """Busca un resumen conciso de un tema específico en Wikipedia.
303
- Intenta primero en español, y si no encuentra la página, intenta en inglés.
304
- Devuelve las primeras 3 frases del artículo. Ideal para definiciones, hechos históricos."""
305
- print(f"Herramienta wikipedia_lookup: Buscando '{query}'")
306
- try:
307
- wikipedia.set_lang("es")
308
- summary = wikipedia.summary(query, sentences=3, auto_suggest=False, redirect=True)
309
- return summary
310
- except wikipedia.exceptions.PageError:
311
- print(f"Página '{query}' no encontrada en Wikipedia en español. Intentando en inglés...")
312
- try:
313
- wikipedia.set_lang("en")
314
- summary_en = wikipedia.summary(query, sentences=3, auto_suggest=False, redirect=True)
315
- # Indicar que la info es de Wikipedia en inglés puede ser útil para el LLM
316
- return f"[Información de Wikipedia en Inglés]: {summary_en}"
317
- except wikipedia.exceptions.PageError:
318
- return f"La página '{query}' no se encontró ni en Wikipedia en español ni en inglés."
319
- except wikipedia.exceptions.DisambiguationError as e_en:
320
- options_str_en = ", ".join(e_en.options[:3])
321
- return f"La búsqueda '{query}' en Wikipedia en inglés es ambigua. Opciones posibles: {options_str_en}. Por favor, sé más específico."
322
- except Exception as e_gen_en:
323
- return f"Error al buscar '{query}' en Wikipedia en inglés: {type(e_gen_en).__name__} {e_gen_en}"
324
- except wikipedia.exceptions.DisambiguationError as e_es:
325
- options_str_es = ", ".join(e_es.options[:3])
326
- return f"La búsqueda '{query}' en Wikipedia en español es ambigua. Opciones posibles: {options_str_es}. Por favor, sé más específico."
327
- except Exception as e_gen_es:
328
- return f"Error al buscar '{query}' en Wikipedia en español: {type(e_gen_es).__name__} {e_gen_es}"
329
-
330
- wikipedia_tool = FunctionTool.from_defaults(
331
- fn=get_wikipedia_summary,
332
- name="wikipedia_lookup",
333
- description="Busca un resumen conciso de un tema específico en Wikipedia (primero en español, luego en inglés si es necesario). Ideal para definiciones, hechos históricos, biografías. Ejemplo de input: {'query': 'Albert Einstein'}"
334
- )
335
 
336
- def calcular_expresion(expr: str) -> str:
337
  """
338
- Evalúa expresiones matemáticas de forma segura.
339
- Ejemplos: '2+2', 'math.sqrt(16)', 'pow(2,3)', '37 * 19'.
340
- Funciones math disponibles: sqrt, pow, sin, cos, tan, log, log10, pi, e, etc.
341
  """
342
  try:
343
- # Entorno seguro para eval()
344
- allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
345
- # Permitir acceso directo a funciones de math sin el prefijo 'math.'
346
- # y también con el prefijo 'math.' para consistencia con la descripción.
347
- safe_env = allowed_names.copy()
348
- safe_env["math"] = math
349
-
350
- result = eval(expr, {"__builtins__": {}}, safe_env)
351
- return str(result)
352
- except NameError as e:
353
- return f"Error de cálculo: '{e}'. Asegúrate de usar funciones matemáticas válidas (ej: sqrt, pow, log) y constantes (ej: pi, e)."
354
- except SyntaxError as e:
355
- return f"Error de sintaxis en la expresión matemática: '{expr}'. Verifica la expresión."
 
 
 
 
 
 
 
356
  except Exception as e:
357
- return f"Error de cálculo al evaluar '{expr}': {type(e).__name__} {e}"
358
 
359
- calculator_tool = FunctionTool.from_defaults(
360
- fn=calcular_expresion,
361
- name="calculadora",
362
- description="Calculadora para expresiones matemáticas. Puede usar funciones como sqrt(), pow(), log(), sin(), cos(), tan() y constantes como pi, e. Ejemplo: 'sqrt(25) + pow(2,3)' o '37*19'."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  )
364
 
365
- custom_react_system_prompt = """
366
- Eres un asistente IA llamado Alfred, diseñado para responder preguntas y completar tareas usando un conjunto de herramientas.
367
- Sigue estrictamente el formato Thought/Action/Action Input/Observation.
368
- Cuando respondas, hazlo en el mismo idioma que la pregunta original del usuario.
369
- Piensa paso a paso. Considera si la información necesaria ya está en la pregunta o en el historial de conversación antes de usar una herramienta.
370
- Si la pregunta contiene datos tabulares o listas directamente en el prompt, analiza esos datos cuidadosamente para responder. No necesitas herramientas para información ya proporcionada.
371
- Si una herramienta devuelve un error o no encuentra información útil después de un par de intentos, considera si puedes responder de otra manera o si debes indicar que no puedes obtener la respuesta.
372
- No inventes respuestas si no tienes la información.
373
- Para tareas de clasificación que requieren conocimiento especializado (ej. botánica), si no estás seguro de una clasificación, indica que necesitas más información o que la clasificación puede ser compleja.
374
- """
375
-
376
- # --- AGENTE RESTAURADO ---
377
  alfred_agent = ReActAgent.from_tools(
378
- tools=[search_tool, wikipedia_tool, calculator_tool],
379
- llm=llm,
380
- verbose=True, # Mantener verbose=True para depuración
381
- max_iterations=15, # <--- AUMENTAR A 15 (o incluso 20)
382
- system_prompt=custom_react_system_prompt
383
  )
384
 
 
 
 
385
 
386
- # --- FUNCIÓN DE RESPUESTA DEL AGENTE RESTAURADA ---
387
  def basic_agent_response(question: str) -> str:
388
- print(f"🤖 Alfred (ReAct Agent) recibió la pregunta: {question}")
389
  try:
390
- # alfred_agent.query() devuelve un objeto AgentChatResponse
391
- agent_chat_response_object = alfred_agent.query(question)
392
-
393
- # El texto de la respuesta final del agente está en el atributo .response de AgentChatResponse
394
- final_response_text = agent_chat_response_object.response
395
-
396
- # Verificar si es None o vacío, aunque el agente debería generar algo.
397
- if final_response_text is None:
398
- print("⚠️ La respuesta final de Alfred fue None.")
399
- return "El agente no generó una respuesta textual."
400
-
401
- print(f"📝 Respuesta final de Alfred: {final_response_text}")
402
- return str(final_response_text) # Asegurar que es una cadena
403
-
404
  except Exception as e:
405
- print(f"💥 Error crítico en Alfred al procesar la pregunta '{question}': {type(e).__name__} - {e}")
406
- import traceback
407
- traceback.print_exc()
408
- # Devolver el mensaje de error como respuesta para la evaluación
409
- return f"Error del agente al procesar la pregunta: {type(e).__name__} - {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # alfred_agent.py
2
+
3
  import os
4
  import math
5
+ import time
6
+ import asyncio
7
+ import subprocess
8
+ import pandas as pd
9
+ from io import StringIO
10
  from duckduckgo_search import DDGS
11
  import wikipedia
12
+ from llama_index import ServiceContext, LLMPredictor
13
+ from llama_index.core.llms import ChatMessage, LLMMetadata, LLM, CompletionResponse
14
  from llama_index.core.tools import FunctionTool
15
  from llama_index.core.agent import ReActAgent
16
+ from llama_index.indices.object_index import ObjectIndex
17
+ from llama_index.indices.vector_store import VectorStoreIndex
18
  from llama_index.core.callbacks import CallbackManager
19
  from llama_index.core.callbacks.llama_debug import LlamaDebugHandler
20
+ from pydantic import Field
21
  import google.generativeai as genai
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ # -------------------------------------------------------------------
24
+ # 1) GeminiLLM: tu implementación personalizada
25
+ # -------------------------------------------------------------------
 
 
 
26
  class GeminiLLM(LLM):
27
+ model_name: str = Field(default="models/gemini-1.5-flash-latest")
28
+ temperature: float = Field(default=0.7)
29
+
 
 
 
 
 
 
 
30
  class Config:
31
+ extra = "allow"
32
+
33
+ def __init__(self, **kwargs):
34
+ super().__init__(**kwargs)
35
+ key = os.getenv("GEMINI_API_KEY")
36
+ if not key:
37
+ raise ValueError("GEMINI_API_KEY no configurada")
38
+ genai.configure(api_key=key)
39
+ self._gen_cfg = genai.types.GenerationConfig(temperature=self.temperature)
40
+ self._model = genai.GenerativeModel(
 
 
 
 
 
 
 
41
  model_name=self.model_name,
42
+ generation_config=self._gen_cfg
43
  )
44
+ if not self.callback_manager.handlers:
45
+ self.callback_manager.add_handler(LlamaDebugHandler())
 
 
 
 
 
 
46
 
47
  @property
48
+ def metadata(self):
49
  return LLMMetadata(
50
+ context_window=1048576,
51
+ num_output=8192,
52
  is_chat_model=True,
53
+ is_function_calling_model=True,
54
+ model_name=self.model_name,
55
  )
56
 
57
+ def chat(self, messages, **kwargs):
58
+ hist = []
59
+ for m in messages[:-1]:
60
+ role = "user" if m.role == "user" else "model"
61
+ hist.append({"role": role, "parts":[{"text": m.content}]})
62
+ last = messages[-1].content
63
+ session = self._model.start_chat(history=hist)
 
 
 
 
 
 
 
 
 
 
64
  try:
65
+ resp = session.send_message(last)
66
+ return ChatMessage(role="assistant", content=resp.text)
67
  except Exception as e:
68
+ return ChatMessage(role="assistant", content=f"Error Gemini: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ async def achat(self, messages, **kwargs):
71
  return await asyncio.to_thread(self.chat, messages, **kwargs)
72
 
73
+ def stream_chat(self, messages, **kwargs):
74
+ hist = []
75
+ for m in messages[:-1]:
76
+ role = "user" if m.role=="user" else "model"
77
+ hist.append({"role": role, "parts":[{"text":m.content}]})
78
+ last = messages[-1].content
79
+ session = self._model.start_chat(history=hist)
80
+ stream = session.send_message(last, stream=True)
 
 
 
81
  def gen():
82
+ acc = ""
83
+ for chunk in stream:
84
+ delta = getattr(chunk, "text", "")
85
+ if not delta and chunk.parts:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  delta = chunk.parts[0].text
87
  if delta:
88
+ acc += delta
89
+ yield ChatMessage(role="assistant", content=acc, additional_kwargs={"delta":delta})
 
 
 
 
90
  return gen()
91
 
92
+ def complete(self, prompt, formatted=False, **kwargs):
 
93
  try:
94
+ resp = self._model.generate_content(prompt)
95
+ return CompletionResponse(text=resp.text)
96
  except Exception as e:
97
+ return CompletionResponse(text=f"Error complete: {e}")
 
98
 
99
+ async def acomplete(self, prompt, formatted=False, **kwargs):
100
  return await asyncio.to_thread(self.complete, prompt, formatted=formatted, **kwargs)
101
 
102
+ def stream_complete(self, prompt, formatted=False, **kwargs):
103
+ stream = self._model.generate_content(prompt, stream=True)
 
104
  def gen():
105
+ acc = ""
106
+ for chunk in stream:
107
+ delta = getattr(chunk, "text", "")
108
+ if not delta and chunk.parts:
 
 
109
  delta = chunk.parts[0].text
 
110
  if delta:
111
+ acc += delta
112
+ yield CompletionResponse(text=acc, delta=delta)
 
 
113
  return gen()
114
 
115
+ async def astream_complete(self, prompt, formatted=False, **kwargs):
116
+ return await asyncio.to_thread(self.stream_complete, prompt, formatted=formatted, **kwargs)
117
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ # -------------------------------------------------------------------
120
+ # 2) Funciones de herramienta
121
+ # -------------------------------------------------------------------
122
 
123
+ HEADERS = {'User-Agent':'Mozilla/5.0'}
124
 
125
+ def buscar_web(query: str, max_attempts: int = 2) -> str:
126
+ """Búsqueda real en DuckDuckGo con reintentos."""
127
+ for i in range(max_attempts):
 
 
128
  try:
129
+ with DDGS(headers=HEADERS, timeout=25) as ddgs:
 
130
  results = list(ddgs.text(query, region='es-es', safesearch='moderate', timelimit='y', max_results=3))
131
  if results:
132
+ return "\n".join(f"Título: {r['title']}\nCuerpo: {r['body']}" for r in results)
133
+ return "No se encontraron resultados."
134
  except Exception as e:
135
+ if i < max_attempts-1:
136
+ time.sleep(5*(i+1))
137
+ else:
138
+ return f"Error al 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
  """
145
+ Parsea tabla Markdown y responde preguntas sencillas.
146
+ Ejemplo específico: acciones no conmutativas.
 
147
  """
148
  try:
149
+ # Convertir Markdown a CSV-like
150
+ lines = [l.strip() for l in table_md.strip().splitlines() if l.strip()]
151
+ # Quitar las líneas de separador '---'
152
+ content = [l for l in lines if not set(l) <= set("|- ")]
153
+ # Extraer filas
154
+ rows = [ [c.strip() for c in r.split("|")[1:-1]] for r in content ]
155
+ df = pd.DataFrame(rows[1:], columns=rows[0])
156
+ # Si la pregunta menciona 'conmutativa', detectamos contrajemplos
157
+ if "no conmut" in question.lower():
158
+ S = list(df.columns)
159
+ counter = set()
160
+ for x in S:
161
+ for y in S:
162
+ a = df.loc[df[rows[0][0]]==x, y].values[0]
163
+ b = df.loc[df[rows[0][0]]==y, x].values[0]
164
+ if a != b:
165
+ counter.add(x); counter.add(y)
166
+ return ", ".join(sorted(counter)) or "No hay contraejemplos"
167
+ # Para otras preguntas, devolvemos la tabla en texto
168
+ return df.to_csv(index=False)
169
  except Exception as e:
170
+ return f"Error analyze_table: {e}"
171
 
172
+ def execute_code(code: str) -> str:
173
+ """Ejecuta código Python en un subproceso seguro."""
174
+ try:
175
+ # Prevenir imports faltantes
176
+ result = subprocess.run(["python","-c",code], capture_output=True, text=True, timeout=5)
177
+ if result.stderr:
178
+ return f"Error en código: {result.stderr.strip()}"
179
+ return result.stdout.strip() or "(sin salida)"
180
+ except Exception as e:
181
+ return f"Error ejecución código: {e}"
182
+
183
+ def no_tool_solution(query: str) -> str:
184
+ return "Procedo a responder con mi conocimiento interno."
185
+
186
+ # -------------------------------------------------------------------
187
+ # 3) Envolver como FunctionTool
188
+ # -------------------------------------------------------------------
189
+
190
+ search_tool = FunctionTool.from_defaults(fn=buscar_web, name="web_search",
191
+ description="Busca información en la web usando DuckDuckGo (3 resultados).")
192
+ reverse_tool = FunctionTool.from_defaults(fn=reverse_text, name="reverse_text",
193
+ description="Invierte el texto dado. Útil para decodificar cadenas al revés.")
194
+ table_tool = FunctionTool.from_defaults(fn=analyze_table, name="analyze_table",
195
+ description="Parsea tabla Markdown y responde preguntas sobre conmutatividad u otras sencillas.")
196
+ code_tool = FunctionTool.from_defaults(fn=execute_code, name="execute_code",
197
+ description="Ejecuta fragmentos de código Python para cálculos o procesamiento.")
198
+ fallback_tool = FunctionTool.from_defaults(fn=no_tool_solution, name="no_tool_solution",
199
+ description="Fallback: responde usando únicamente conocimiento interno si otras herramientas fallan.")
200
+
201
+ # -------------------------------------------------------------------
202
+ # 4) Índice de herramientas + ToolRetriever
203
+ # -------------------------------------------------------------------
204
+
205
+ all_tools = [search_tool, reverse_tool, table_tool, code_tool, fallback_tool]
206
+ obj_index = ObjectIndex.from_objects(all_tools, index_cls=VectorStoreIndex)
207
+ tool_retriever = obj_index.as_retriever(similarity_top_k=3)
208
+
209
+ # -------------------------------------------------------------------
210
+ # 5) Prompt de sistema
211
+ # -------------------------------------------------------------------
212
+
213
+ system_prompt = (
214
+ "Eres Alfred, un agente ReAct que usa pasos de pensamiento claros y herramientas especializadas.\n"
215
+ "Sigue este flujo:\n"
216
+ "1) Analiza la pregunta.\n"
217
+ "2) Piensa paso a paso (internamente), luego decide qué acción tomar.\n"
218
+ "3) Si necesitas datos externos, usa web_search.\n"
219
+ "4) Si hay texto invertido, usa reverse_text.\n"
220
+ "5) Si hay tablas Markdown, usa analyze_table.\n"
221
+ "6) Para cálculos complejos, usa execute_code.\n"
222
+ "7) Si nada ayuda, llama no_tool_solution y responde con tu conocimiento.\n"
223
+ "8) Si la pregunta involucra audio, video o imágenes no procesables, informa que no puedes acceder.\n"
224
+ "9) Proporciona siempre una respuesta final clara al usuario.\n"
225
+ "\nHerramientas disponibles:\n{tool_descriptions}\n"
226
+ "No muestres tus pensamientos al usuario; solo la respuesta final."
227
  )
228
 
229
+ # -------------------------------------------------------------------
230
+ # 6) Inicializar Agent con GeminiLLM
231
+ # -------------------------------------------------------------------
232
+
233
+ # Contexto de LLM
234
+ service_ctx = ServiceContext.from_defaults(
235
+ llm=GeminiLLM(model_name="models/gemini-1.5-flash-latest", temperature=0.0)
236
+ )
237
+
238
+ # Crear el agente ReAct
 
 
239
  alfred_agent = ReActAgent.from_tools(
240
+ tool_retriever=tool_retriever,
241
+ service_context=service_ctx,
242
+ system_prompt=system_prompt,
243
+ verbose=True, # mostrar pasos para depuración
244
+ max_iterations=20 # permitir suficiente iteraciones
245
  )
246
 
247
+ # -------------------------------------------------------------------
248
+ # 7) Función de invocación
249
+ # -------------------------------------------------------------------
250
 
 
251
  def basic_agent_response(question: str) -> str:
252
+ print(f"🤖 Alfred recibió: {question}")
253
  try:
254
+ resp_obj = alfred_agent.query(question)
255
+ return resp_obj.response or "No se generó respuesta."
 
 
 
 
 
 
 
 
 
 
 
 
256
  except Exception as e:
257
+ return f"Error crítico en el agente: {e}"
258
+
259
+ # -------------------------------------------------------------------
260
+ # 8) Prueba rápida (descomentar para test local)
261
+ # -------------------------------------------------------------------
262
+ if __name__ == "__main__":
263
+ tests = [
264
+ "¿Cuánto es 37 por 19?",
265
+ ".tfel",
266
+ "|*|a|b|\n|---|---|---|\n|a|b|c|\n|b|c|a|\nprovide the subset involved in non-commutative",
267
+ "I'm making a grocery list para mi mamá..., incluye milk, eggs, bell pepper, zucchini. Solo vegetales."
268
+ ]
269
+ for q in tests:
270
+ print("Q:", q)
271
+ print("A:", basic_agent_response(q))
272
+ print("-"*40)
273
+