Nancy1906 commited on
Commit
46b58aa
·
verified ·
1 Parent(s): d490cbf
Files changed (1) hide show
  1. my_tools.py +209 -93
my_tools.py CHANGED
@@ -1,88 +1,117 @@
1
  import os
2
  import math
3
- import pandas as pd
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 # <--- AÑADIR 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 # <--- AÑADIR para asyncio.to_thread
 
14
 
15
- # ... (código para obtener la versión de LlamaIndex) ...
16
- # print(f"LlamaIndex version detectada: {llama_index_version}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
 
19
  # --- Gemini LLM personalizado ---
20
  class GeminiLLM(LLM):
21
- def __init__(self, model_name="models/gemini-1.5-flash-latest", temperature: float = 0.7): # Añadido temperature
22
- super().__init__()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  gemini_api_key = os.getenv("GEMINI_API_KEY")
24
  if not gemini_api_key:
25
  raise ValueError("GEMINI_API_KEY environment variable not set.")
26
  genai.configure(api_key=gemini_api_key)
27
 
28
- # Configuración para la generación, incluida la temperatura
29
- self._generation_config = genai.types.GenerationConfig(
30
- # candidate_count=1, # Ya es 1 por defecto
31
- # stop_sequences=stop_sequences, # Podríamos añadir esto si es necesario
32
- # max_output_tokens=max_output_tokens, # Controlado por LlamaIndex via num_output
33
- temperature=temperature
34
  )
35
- self.model = genai.GenerativeModel(
36
- model_name=model_name,
37
- generation_config=self._generation_config
38
- # safety_settings=... # Podríamos añadir configuraciones de seguridad aquí
39
  )
40
- self._callback_manager = CallbackManager([LlamaDebugHandler(print_trace=True)]) # print_trace para más detalle
 
 
 
 
 
 
 
41
 
42
  @property
43
  def metadata(self) -> LLMMetadata:
44
- # Estos valores deben ser precisos para el modelo específico
45
- # gemini-1.5-flash tiene hasta 1M de tokens de contexto.
46
- # num_output puede ser configurado o es inherentemente grande.
47
  return LLMMetadata(
48
- context_window=1048576, # Para gemini-1.5-flash
49
- num_output=8192, # Max output tokens para gemini-1.5-flash
50
  is_chat_model=True,
51
- is_function_calling_model=True, # Gemini sí soporta function calling (declarar herramientas)
52
- model_name=self.model.model_name # Usar el nombre del modelo configurado
53
  )
54
 
55
- @property
56
- def callback_manager(self):
57
- return self._callback_manager
 
 
58
 
59
  # --- Implementación de Chat ---
60
  def chat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
61
  gemini_history = []
62
- for msg in messages[:-1]: # Todos excepto el último
63
  role = "user" if msg.role == "user" else "model"
64
  gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
65
 
66
  last_user_message = messages[-1].content
67
 
68
- chat_session = self.model.start_chat(history=gemini_history)
69
  try:
70
  response = chat_session.send_message(last_user_message)
71
  return ChatMessage(role="assistant", content=response.text)
72
  except Exception as e:
73
- # Podríamos manejar errores específicos de Gemini aquí, como bloqueos de contenido
74
  print(f"Error en Gemini chat: {e}")
75
- # Devolver un mensaje de error coherente o re-lanzar
76
  return ChatMessage(role="assistant", content=f"Error al generar respuesta: {e}")
77
 
78
-
79
  async def achat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
80
- # Para SDK síncrona, usar asyncio.to_thread
81
  return await asyncio.to_thread(self.chat, messages, **kwargs)
82
 
83
  def stream_chat(self, messages: list[ChatMessage], **kwargs):
84
- # El SDK de Gemini v1 para Python con genai.GenerativeModel().generate_content(..., stream=True)
85
- # o chat_session.send_message(..., stream=True) soporta streaming.
86
  gemini_history = []
87
  for msg in messages[:-1]:
88
  role = "user" if msg.role == "user" else "model"
@@ -90,33 +119,26 @@ class GeminiLLM(LLM):
90
 
91
  last_user_message = messages[-1].content
92
 
93
- chat_session = self.model.start_chat(history=gemini_history)
94
  response_stream = chat_session.send_message(last_user_message, stream=True)
95
 
96
  def gen():
97
  accumulated_text = ""
98
  for chunk in response_stream:
99
- delta = chunk.text # Asumiendo que el chunk tiene .text con el delta
100
- accumulated_text += delta
101
- yield ChatMessage(role="assistant", content=accumulated_text, additional_kwargs={"delta": delta})
 
 
 
 
 
 
 
 
102
  return gen()
103
 
104
  async def astream_chat(self, messages: list[ChatMessage], **kwargs):
105
- # Similar a stream_chat pero con manejo async si la SDK lo permite,
106
- # o envolviendo la lógica de streaming síncrona.
107
- # Por simplicidad, si la SDK no tiene un `asend_message` o similar,
108
- # podemos hacer esto bloqueante o intentar adaptarlo.
109
- # Dado que send_message(stream=True) devuelve un iterador,
110
- # necesitamos una forma de iterar asíncronamente o usar to_thread.
111
-
112
- # Este es un placeholder más complejo de implementar correctamente de forma no bloqueante
113
- # sin una API async nativa en la SDK para streaming.
114
- # Por ahora, una simulación básica como la anterior:
115
-
116
- # De manera simple, podemos hacer que devuelva el resultado completo en un solo chunk.
117
- # O, si queremos que funcione con `async for`, tenemos que adaptar el generador.
118
-
119
- # Este es un enfoque un poco más avanzado para iterar sobre el stream en un hilo separado:
120
  loop = asyncio.get_event_loop()
121
 
122
  gemini_history = []
@@ -125,94 +147,188 @@ class GeminiLLM(LLM):
125
  gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
126
  last_user_message = messages[-1].content
127
 
128
- # La función que se ejecutará en el hilo
129
  def get_stream_iterator():
130
- chat_session = self.model.start_chat(history=gemini_history)
131
  return chat_session.send_message(last_user_message, stream=True)
132
 
133
  response_stream = await loop.run_in_executor(None, get_stream_iterator)
134
 
135
  async def gen():
136
  accumulated_text = ""
137
- # Necesitamos iterar sobre el stream de forma que no bloquee el bucle de eventos
138
- # Esto puede ser complejo si el iterador es bloqueante.
139
- # Una forma es obtener todos los chunks en el hilo y luego producirlos.
140
  all_chunks_text = []
141
- for chunk in response_stream: # Esto podría seguir siendo bloqueante si response_stream es un iterador síncrono
142
- all_chunks_text.append(chunk.text)
 
 
 
 
 
 
143
 
144
  for text_delta in all_chunks_text:
145
  accumulated_text += text_delta
146
  yield ChatMessage(role="assistant", content=accumulated_text, additional_kwargs={"delta": text_delta})
147
- await asyncio.sleep(0) # Ceder control brevemente
148
-
149
  return gen()
150
 
151
- # --- Implementación de Complete (requerido por la clase base LLM) ---
152
  def complete(self, prompt: str, formatted: bool = False, **kwargs) -> CompletionResponse:
153
- # `formatted` es una pista de LlamaIndex, podemos ignorarla si no aplica.
154
- # Usar generate_content para una sola finalización
155
  try:
156
- response = self.model.generate_content(prompt)
157
  return CompletionResponse(text=response.text)
158
  except Exception as e:
159
  print(f"Error en Gemini complete: {e}")
160
  return CompletionResponse(text=f"Error al generar completion: {e}")
161
 
162
-
163
  async def acomplete(self, prompt: str, formatted: bool = False, **kwargs) -> CompletionResponse:
164
  return await asyncio.to_thread(self.complete, prompt, formatted=formatted, **kwargs)
165
 
166
  def stream_complete(self, prompt: str, formatted: bool = False, **kwargs):
167
- # Usar generate_content con stream=True
168
- response_stream = self.model.generate_content(prompt, stream=True)
169
 
170
  def gen():
171
  accumulated_text = ""
172
  for chunk in response_stream:
173
- # Asegurarse de que el chunk tiene 'text' y no es un error de prompt feedback, etc.
174
- if hasattr(chunk, 'text'):
175
  delta = chunk.text
 
 
 
 
176
  accumulated_text += delta
177
  yield CompletionResponse(text=accumulated_text, delta=delta)
178
  elif hasattr(chunk, 'prompt_feedback'):
179
- # Manejar el caso donde el prompt es bloqueado, etc.
180
  print(f"Feedback del prompt en stream_complete: {chunk.prompt_feedback}")
181
- # Podríamos lanzar una excepción o devolver un mensaje de error especial.
182
- # Por ahora, solo lo imprimimos y el stream podría detenerse o continuar vacío.
183
- pass # O `break` si queremos detener el stream ante un feedback negativo
184
-
185
  return gen()
186
 
187
-
188
  async def astream_complete(self, prompt: str, formatted: bool = False, **kwargs):
189
- # Similar a astream_chat, la implementación async de un stream síncrono es un poco más compleja.
190
  loop = asyncio.get_event_loop()
191
 
192
  def get_stream_iterator():
193
- return self.model.generate_content(prompt, stream=True)
194
 
195
  response_stream = await loop.run_in_executor(None, get_stream_iterator)
196
 
197
  async def gen():
198
  accumulated_text = ""
199
- all_chunks_data = [] # Para recolectar en el hilo y luego generar
200
- for chunk in response_stream: # Esto es bloqueante en el hilo executor
201
- if hasattr(chunk, 'text'):
202
- all_chunks_data.append({'delta': chunk.text})
203
- elif hasattr(chunk, 'prompt_feedback'):
204
- all_chunks_data.append({'feedback': chunk.prompt_feedback})
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  for data in all_chunks_data:
207
  if 'delta' in data:
208
- delta = data['delta']
209
- accumulated_text += delta
210
- yield CompletionResponse(text=accumulated_text, delta=delta)
211
  elif 'feedback' in data:
212
  print(f"Feedback del prompt en astream_complete: {data['feedback']}")
213
- await asyncio.sleep(0) # Ceder control
214
  return gen()
215
 
216
- llm = GeminiLLM() # Esto ya no debería dar error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- # ... (resto del código de my_tools.py: herramientas, agente, basic_agent_response) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
16
+ # --- Intento mejorado para obtener la versión de LlamaIndex ---
17
+ try:
18
+ from importlib import metadata
19
+ try:
20
+ llama_index_version = metadata.version('llama-index')
21
+ except metadata.PackageNotFoundError:
22
+ try:
23
+ llama_index_version = metadata.version('llama-index-core')
24
+ except metadata.PackageNotFoundError:
25
+ llama_index_version = "No se pudo determinar (con importlib.metadata)"
26
+ except ImportError:
27
+ try:
28
+ from llama_index.core import __version__ as llama_index_core_version
29
+ llama_index_version = llama_index_core_version
30
+ except ImportError:
31
+ llama_index_version = "No se pudo determinar (fallback a __version__ falló)"
32
+
33
+ print(f"LlamaIndex version detectada: {llama_index_version}")
34
 
35
 
36
  # --- Gemini LLM personalizado ---
37
  class GeminiLLM(LLM):
38
+ model_name: str = Field(default="models/gemini-1.5-flash-latest", description="The Gemini model to use.")
39
+ temperature: float = Field(default=0.7, description="The temperature to use for generation.")
40
+
41
+ # Atributos privados que no queremos que Pydantic valide como campos del modelo directamente
42
+ # pero que necesitamos para la lógica interna. Los inicializaremos en __init__.
43
+ _model_instance: genai.GenerativeModel = None
44
+ _generation_config_instance: genai.types.GenerationConfig = None
45
+
46
+ # Para Pydantic v1, si la clase base lo es, permitir atributos extra
47
+ # Para Pydantic v2, esto sería model_config = {"extra": "allow"}
48
+ class Config:
49
+ extra = "allow" # Permite atributos que no están definidos explícitamente como campos
50
+
51
+ def __init__(self, model_name: str = "models/gemini-1.5-flash-latest", temperature: float = 0.7, **kwargs):
52
+ # Llamar a super().__init__() con los campos definidos y **kwargs
53
+ # Esto es importante para que Pydantic inicialice correctamente
54
+ super().__init__(model_name=model_name, temperature=temperature, **kwargs) # Pasar kwargs a la clase base
55
+
56
  gemini_api_key = os.getenv("GEMINI_API_KEY")
57
  if not gemini_api_key:
58
  raise ValueError("GEMINI_API_KEY environment variable not set.")
59
  genai.configure(api_key=gemini_api_key)
60
 
61
+ # Usar self.temperature y self.model_name que Pydantic ya ha asignado
62
+ self._generation_config_instance = genai.types.GenerationConfig(
63
+ temperature=self.temperature
 
 
 
64
  )
65
+ self._model_instance = genai.GenerativeModel(
66
+ model_name=self.model_name,
67
+ generation_config=self._generation_config_instance
 
68
  )
69
+ # El callback_manager se hereda de la clase base LLM, podemos configurarlo si es necesario.
70
+ # self.callback_manager = kwargs.get('callback_manager', CallbackManager([LlamaDebugHandler(print_trace=True)]))
71
+ # Si la clase base LLM ya inicializa callback_manager, no necesitamos reasignarlo a menos que queramos uno específico.
72
+ # Por defecto, llama_index.core.llms.LLM inicializa self.callback_manager = callback_manager or CallbackManager([])
73
+ # Si queremos el LlamaDebugHandler, podemos pasarlo o reconfigurarlo
74
+ if not self.callback_manager.handlers: # Si no hay manejadores, añadir el nuestro
75
+ self.callback_manager.add_handler(LlamaDebugHandler(print_trace=True))
76
+
77
 
78
  @property
79
  def metadata(self) -> LLMMetadata:
 
 
 
80
  return LLMMetadata(
81
+ context_window=1048576,
82
+ num_output=8192,
83
  is_chat_model=True,
84
+ is_function_calling_model=True,
85
+ model_name=self.model_name
86
  )
87
 
88
+ # callback_manager ya es una propiedad en la clase base LLM.
89
+ # No necesitamos redefinirla a menos que la lógica de acceso sea diferente.
90
+ # @property
91
+ # def callback_manager(self):
92
+ # return self._callback_manager
93
 
94
  # --- Implementación de Chat ---
95
  def chat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
96
  gemini_history = []
97
+ for msg in messages[:-1]:
98
  role = "user" if msg.role == "user" else "model"
99
  gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
100
 
101
  last_user_message = messages[-1].content
102
 
103
+ chat_session = self._model_instance.start_chat(history=gemini_history)
104
  try:
105
  response = chat_session.send_message(last_user_message)
106
  return ChatMessage(role="assistant", content=response.text)
107
  except Exception as e:
 
108
  print(f"Error en Gemini chat: {e}")
 
109
  return ChatMessage(role="assistant", content=f"Error al generar respuesta: {e}")
110
 
 
111
  async def achat(self, messages: list[ChatMessage], **kwargs) -> ChatMessage:
 
112
  return await asyncio.to_thread(self.chat, messages, **kwargs)
113
 
114
  def stream_chat(self, messages: list[ChatMessage], **kwargs):
 
 
115
  gemini_history = []
116
  for msg in messages[:-1]:
117
  role = "user" if msg.role == "user" else "model"
 
119
 
120
  last_user_message = messages[-1].content
121
 
122
+ chat_session = self._model_instance.start_chat(history=gemini_history)
123
  response_stream = chat_session.send_message(last_user_message, stream=True)
124
 
125
  def gen():
126
  accumulated_text = ""
127
  for chunk in response_stream:
128
+ delta = ""
129
+ if hasattr(chunk, 'text') and chunk.text:
130
+ delta = chunk.text
131
+ # Podríamos necesitar revisar la estructura exacta del chunk para obtener el delta correcto.
132
+ # A veces es chunk.parts[0].text
133
+ elif chunk.parts and hasattr(chunk.parts[0], 'text'):
134
+ delta = chunk.parts[0].text
135
+
136
+ if delta:
137
+ accumulated_text += delta
138
+ yield ChatMessage(role="assistant", content=accumulated_text, additional_kwargs={"delta": delta})
139
  return gen()
140
 
141
  async def astream_chat(self, messages: list[ChatMessage], **kwargs):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  loop = asyncio.get_event_loop()
143
 
144
  gemini_history = []
 
147
  gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
148
  last_user_message = messages[-1].content
149
 
 
150
  def get_stream_iterator():
151
+ chat_session = self._model_instance.start_chat(history=gemini_history)
152
  return chat_session.send_message(last_user_message, stream=True)
153
 
154
  response_stream = await loop.run_in_executor(None, get_stream_iterator)
155
 
156
  async def gen():
157
  accumulated_text = ""
 
 
 
158
  all_chunks_text = []
159
+ for chunk in response_stream:
160
+ delta = ""
161
+ if hasattr(chunk, 'text') and chunk.text:
162
+ delta = chunk.text
163
+ elif chunk.parts and hasattr(chunk.parts[0], 'text'):
164
+ delta = chunk.parts[0].text
165
+ if delta:
166
+ all_chunks_text.append(delta)
167
 
168
  for text_delta in all_chunks_text:
169
  accumulated_text += text_delta
170
  yield ChatMessage(role="assistant", content=accumulated_text, additional_kwargs={"delta": text_delta})
171
+ await asyncio.sleep(0)
 
172
  return gen()
173
 
174
+ # --- Implementación de Complete ---
175
  def complete(self, prompt: str, formatted: bool = False, **kwargs) -> CompletionResponse:
 
 
176
  try:
177
+ response = self._model_instance.generate_content(prompt)
178
  return CompletionResponse(text=response.text)
179
  except Exception as e:
180
  print(f"Error en Gemini complete: {e}")
181
  return CompletionResponse(text=f"Error al generar completion: {e}")
182
 
 
183
  async def acomplete(self, prompt: str, formatted: bool = False, **kwargs) -> CompletionResponse:
184
  return await asyncio.to_thread(self.complete, prompt, formatted=formatted, **kwargs)
185
 
186
  def stream_complete(self, prompt: str, formatted: bool = False, **kwargs):
187
+ response_stream = self._model_instance.generate_content(prompt, stream=True)
 
188
 
189
  def gen():
190
  accumulated_text = ""
191
  for chunk in response_stream:
192
+ delta = ""
193
+ if hasattr(chunk, 'text') and chunk.text:
194
  delta = chunk.text
195
+ elif chunk.parts and hasattr(chunk.parts[0], 'text'):
196
+ delta = chunk.parts[0].text
197
+
198
+ if delta:
199
  accumulated_text += delta
200
  yield CompletionResponse(text=accumulated_text, delta=delta)
201
  elif hasattr(chunk, 'prompt_feedback'):
 
202
  print(f"Feedback del prompt en stream_complete: {chunk.prompt_feedback}")
 
 
 
 
203
  return gen()
204
 
 
205
  async def astream_complete(self, prompt: str, formatted: bool = False, **kwargs):
 
206
  loop = asyncio.get_event_loop()
207
 
208
  def get_stream_iterator():
209
+ return self._model_instance.generate_content(prompt, stream=True)
210
 
211
  response_stream = await loop.run_in_executor(None, get_stream_iterator)
212
 
213
  async def gen():
214
  accumulated_text = ""
215
+ all_chunks_data = []
216
+ for chunk in response_stream:
217
+ delta = ""
218
+ feedback = None
219
+ if hasattr(chunk, 'text') and chunk.text:
220
+ delta = chunk.text
221
+ elif chunk.parts and hasattr(chunk.parts[0], 'text'):
222
+ delta = chunk.parts[0].text
223
+
224
+ if hasattr(chunk, 'prompt_feedback'):
225
+ feedback = chunk.prompt_feedback
226
+
227
+ if delta:
228
+ all_chunks_data.append({'delta': delta})
229
+ if feedback:
230
+ all_chunks_data.append({'feedback': feedback})
231
+
232
 
233
  for data in all_chunks_data:
234
  if 'delta' in data:
235
+ delta_val = data['delta']
236
+ accumulated_text += delta_val
237
+ yield CompletionResponse(text=accumulated_text, delta=delta_val)
238
  elif 'feedback' in data:
239
  print(f"Feedback del prompt en astream_complete: {data['feedback']}")
240
+ await asyncio.sleep(0)
241
  return gen()
242
 
243
+ llm = GeminiLLM()
244
+
245
+ # --- HERRAMIENTAS RESTAURADAS ---
246
+ def buscar_web(query: str) -> str:
247
+ """Busca en la web utilizando DuckDuckGo y devuelve los 3 primeros resultados."""
248
+ try:
249
+ with DDGS() as ddgs:
250
+ # Nota: ddgs.text devuelve un generador. Convertir a lista para obtener resultados.
251
+ results = list(ddgs.text(query, region='es-es', safesearch='moderate', timelimit='y', max_results=3))
252
+ if results:
253
+ return "\n".join([f"Título: {r['title']}, Cuerpo: {r['body']}" for r in results])
254
+ return "No se encontraron resultados en la web."
255
+ except Exception as e:
256
+ return f"Error al buscar en la web: {e}"
257
+
258
+ search_tool = FunctionTool.from_defaults(
259
+ fn=buscar_web,
260
+ name="web_search",
261
+ description="Útil para buscar información actual o general en internet. Proporciona un resumen de los resultados de búsqueda."
262
+ )
263
+
264
+ def get_wikipedia_summary(query: str) -> str:
265
+ """Busca un resumen breve de un tema en Wikipedia (primeras 3 frases)."""
266
+ try:
267
+ wikipedia.set_lang("es") # Asegurar el idioma
268
+ return wikipedia.summary(query, sentences=3, auto_suggest=False)
269
+ except wikipedia.exceptions.PageError:
270
+ return f"La página '{query}' no existe en Wikipedia en español."
271
+ except wikipedia.exceptions.DisambiguationError as e:
272
+ # Devolver algunas opciones para que el LLM pueda refinar la búsqueda si es necesario
273
+ options_str = ", ".join(e.options[:3])
274
+ return f"La búsqueda '{query}' es ambigua. Posibles opciones: {options_str}. Por favor, sé más específico."
275
+ except Exception as e:
276
+ return f"Error al buscar en Wikipedia: {e}"
277
+
278
+ wikipedia_tool = FunctionTool.from_defaults(
279
+ fn=get_wikipedia_summary,
280
+ name="wikipedia_lookup",
281
+ description="Busca un resumen conciso de un tema específico en Wikipedia. Ideal para definiciones, hechos históricos, biografías, etc."
282
+ )
283
+
284
+ def calcular_expresion(expr: str) -> str:
285
+ """
286
+ Evalúa expresiones matemáticas de forma segura.
287
+ Ejemplos: '2+2', 'math.sqrt(16)', 'pow(2,3)', '37 * 19'.
288
+ Funciones math disponibles: sqrt, pow, sin, cos, tan, log, log10, pi, e, etc.
289
+ """
290
+ try:
291
+ # Entorno seguro para eval()
292
+ allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
293
+ # Permitir acceso directo a funciones de math sin el prefijo 'math.'
294
+ # y también con el prefijo 'math.' para consistencia con la descripción.
295
+ safe_env = allowed_names.copy()
296
+ safe_env["math"] = math
297
+
298
+ result = eval(expr, {"__builtins__": {}}, safe_env)
299
+ return str(result)
300
+ except NameError as e:
301
+ 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)."
302
+ except SyntaxError as e:
303
+ return f"Error de sintaxis en la expresión matemática: '{expr}'. Verifica la expresión."
304
+ except Exception as e:
305
+ return f"Error de cálculo al evaluar '{expr}': {type(e).__name__} {e}"
306
+
307
+ calculator_tool = FunctionTool.from_defaults(
308
+ fn=calcular_expresion,
309
+ name="calculadora",
310
+ 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'."
311
+ )
312
+
313
+ # --- AGENTE RESTAURADO ---
314
+ alfred_agent = ReActAgent.from_tools(
315
+ tools=[search_tool, wikipedia_tool, calculator_tool],
316
+ llm=llm,
317
+ verbose=True # Mantener verbose=True para depuración
318
+ )
319
 
320
+ # --- FUNCIÓN DE RESPUESTA DEL AGENTE RESTAURADA ---
321
+ def basic_agent_response(question: str) -> str:
322
+ print(f"🤖 Alfred (ReAct Agent) recibió la pregunta: {question}")
323
+ try:
324
+ response = alfred_agent.query(question)
325
+ # response es un objeto AgentChatResponse, necesitamos su .response
326
+ response_text = str(response.response) if hasattr(response, 'response') else str(response)
327
+ print(f"📝 Respuesta final de Alfred: {response_text}")
328
+ return response_text
329
+ except Exception as e:
330
+ # Capturar errores específicos de la ejecución del agente si es posible
331
+ print(f"💥 Error crítico en Alfred al procesar la pregunta '{question}': {e}")
332
+ import traceback
333
+ traceback.print_exc() # Imprimir el traceback completo para más detalles
334
+ return f"Error del agente al procesar la pregunta: {type(e).__name__} - {e}"