Nancy1906 commited on
Commit
d490cbf
·
verified ·
1 Parent(s): 3837bb8

xxxxxxxxxxx

Browse files
Files changed (1) hide show
  1. my_tools.py +180 -136
my_tools.py CHANGED
@@ -3,172 +3,216 @@ import math
3
  import pandas as pd
4
  from duckduckgo_search import DDGS
5
  import wikipedia
6
- import llama_index # <--- AÑADIR ESTA LÍNEA
7
  from llama_index.core.tools import FunctionTool
8
  from llama_index.core.agent import ReActAgent
9
- # --- CORREGIR ESTA IMPORTACIÓN ---
10
- # from llama_index.core.llms.types import ChatMessage, LLMMetadata, LLM
11
- from llama_index.core.llms import ChatMessage, LLMMetadata, LLM # <--- NUEVA IMPORTACIÓN
12
- from llama_index.core.callbacks import CallbackManager # LlamaDebugHandler no se usa directamente aquí, pero sí en el property
13
- from llama_index.core.callbacks.llama_debug import LlamaDebugHandler # Importación completa si se usa directamente
14
  import google.generativeai as genai
 
15
 
 
 
16
 
17
 
18
  # --- Gemini LLM personalizado ---
19
  class GeminiLLM(LLM):
20
- def __init__(self, model="models/gemini-1.5-flash-latest"):
21
- super().__init__() # <--- Buena práctica llamar al __init__ de la clase base
22
  gemini_api_key = os.getenv("GEMINI_API_KEY")
23
  if not gemini_api_key:
24
  raise ValueError("GEMINI_API_KEY environment variable not set.")
25
  genai.configure(api_key=gemini_api_key)
26
- self.model = genai.GenerativeModel(model)
27
- # El callback_manager se puede inicializar aquí si siempre será el mismo
28
- self._callback_manager = CallbackManager([LlamaDebugHandler()])
29
-
30
-
31
- def chat(self, messages: list[ChatMessage], **kwargs):
32
- # Construir el prompt compatible con Gemini
33
- # Gemini espera una lista de mensajes con roles 'user' y 'model' (para historial)
34
- # El último mensaje de 'user' es el prompt actual.
35
- gemini_messages = []
36
- system_prompt = "" # LlamaIndex podría pasar un system_prompt vía kwargs o primer mensaje
37
-
38
- prompt_parts = []
39
- for m in messages:
40
- # Adaptar roles si es necesario, Gemini usa 'user' y 'model'
41
- role = "user" if m.role == "user" else "model" # Asumimos que 'assistant' mapea a 'model'
42
- prompt_parts.append(f"{role}: {m.content}")
43
 
44
- # El prompt final para generate_content usualmente es solo el último mensaje del usuario,
45
- # con el historial implícito si el modelo es conversacional.
46
- # Para el modelo genai.GenerativeModel, el historial se maneja a través de start_chat.
47
- # Si solo hacemos una llamada, el prompt concatenado está bien, pero es mejor usar la API de chat.
48
-
49
- # Usando start_chat para mantener el contexto si es una conversación más larga
50
- # Esto es más robusto si el agente hace múltiples llamadas al LLM con historial
51
- chat_session = self.model.start_chat(
52
- history=[{'role': msg.role if msg.role in ['user', 'model'] else ('user' if msg.role == 'assistant' else 'user'), 'parts': [msg.content]} for msg in messages[:-1]]
53
  )
54
- resp = chat_session.send_message(messages[-1].content)
55
-
56
- # Alternativa simple si solo es un turno (como en el prompt original construido)
57
- # prompt_str = "\n".join(prompt_parts) + "\nAssistant:" # O model:
58
- # resp = self.model.generate_content(prompt_str)
59
-
60
- return ChatMessage(role="assistant", content=resp.text)
61
 
62
  @property
63
  def metadata(self) -> LLMMetadata:
 
 
 
64
  return LLMMetadata(
65
- context_window=8192, # Consultar la documentación de gemini-1.5-flash para el valor exacto
66
- num_output=1024, # Consultar la documentación de gemini-1.5-flash para el valor exacto
67
  is_chat_model=True,
68
- is_function_calling_model=False, # Gemini tiene su propio sistema de tools/function calling
69
- model_name="gemini-1.5-flash-latest"
70
  )
71
 
72
  @property
73
- def callback_manager(self): # Propiedad para acceder al callback_manager
74
  return self._callback_manager
75
-
76
- # Necesario para LLM. LlamaIndex espera estos métodos async aunque no los usemos activamente para todo.
77
- async def achat(self, messages: list[ChatMessage], **kwargs):
78
- # Implementación asíncrona simple (puede ser igual a la síncrona si la SDK no es async nativa fácil)
79
- # o usar asyncio.to_thread si la SDK de Gemini es bloqueante
80
- return self.chat(messages, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  async def astream_chat(self, messages: list[ChatMessage], **kwargs):
83
- # Placeholder para streaming asíncrono
84
- # Por ahora, podemos hacer que devuelva un generador que produce la respuesta completa de una vez.
85
- response = self.chat(messages, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  async def gen():
87
- yield response
 
 
 
 
 
 
 
 
 
 
 
 
88
  return gen()
89
 
90
- def stream_chat(self, messages: list[ChatMessage], **kwargs):
91
- # Placeholder para streaming síncrono
92
- response = self.chat(messages, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  def gen():
94
- yield response
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  return gen()
96
 
97
 
98
- llm = GeminiLLM()
99
-
100
- # --- Herramientas ---
101
- def buscar_web(query: str) -> str:
102
- """Busca en la web utilizando DuckDuckGo y devuelve los 3 primeros resultados."""
103
- try:
104
- with DDGS() as ddgs:
105
- results = list(ddgs.text(query, region='es-es', safesearch='moderate', timelimit='y', max_results=3))
106
- if results:
107
- return "\n".join([f"{r['title']}: {r['body']}" for r in results])
108
- return "No se encontraron resultados."
109
- except Exception as e:
110
- return f"Error al buscar en la web: {e}"
111
-
112
-
113
- search_tool = FunctionTool.from_defaults(
114
- fn=buscar_web,
115
- name="web_search",
116
- description="Busca en la web utilizando DuckDuckGo para obtener información actualizada o temas generales."
117
- )
118
-
119
- def get_wikipedia_summary(query: str) -> str:
120
- """Busca un resumen breve de un tema en Wikipedia (primeras 3 frases)."""
121
- try:
122
- wikipedia.set_lang("es")
123
- return wikipedia.summary(query, sentences=3, auto_suggest=False)
124
- except wikipedia.exceptions.PageError:
125
- return f"La página '{query}' no existe en Wikipedia."
126
- except wikipedia.exceptions.DisambiguationError as e:
127
- return f"La búsqueda '{query}' es ambigua. Opciones posibles: {e.options[:5]}"
128
- except Exception as e:
129
- return f"Error al buscar en Wikipedia: {e}"
130
-
131
- wikipedia_tool = FunctionTool.from_defaults(
132
- fn=get_wikipedia_summary,
133
- name="wikipedia_lookup",
134
- description="Busca un resumen breve de un tema específico en Wikipedia."
135
- )
136
-
137
- def calcular_expresion(expr: str) -> str:
138
- """
139
- Evalúa expresiones matemáticas.
140
- Ejemplos: '2+2', 'math.sqrt(16)', 'pow(2,3)', '37 * 19'.
141
- Funciones math disponibles: sqrt, pow, sin, cos, tan, log, log10, etc.
142
- """
143
- try:
144
- # Un entorno seguro para eval(), permitiendo solo funciones de math
145
- allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
146
- allowed_names["math"] = math # Para poder usar math.sqrt() etc.
147
 
148
- result = eval(expr, {"__builtins__": {}}, allowed_names)
149
- return str(result)
150
- except Exception as e:
151
- return f"Error de cálculo al evaluar '{expr}': {e}"
152
-
153
- calculator_tool = FunctionTool.from_defaults(
154
- fn=calcular_expresion,
155
- name="calculadora",
156
- description="Resuelve expresiones matemáticas. Utiliza 'math.funcion()' para funciones como sqrt, pow, sin, etc. Ej: 'math.sqrt(16)', '37*19'."
157
- )
158
-
159
- # --- Agente ---
160
- alfred_agent = ReActAgent.from_tools(
161
- tools=[search_tool, wikipedia_tool, calculator_tool],
162
- llm=llm,
163
- verbose=True # <--- Poner a True para debugging inicial es muy útil
164
- )
165
-
166
- def basic_agent_response(question: str) -> str:
167
- print(f"🤖 Alfred recibió la pregunta: {question}")
168
- try:
169
- response = alfred_agent.query(question)
170
- print(f"📝 Respuesta de Alfred: {response}")
171
- return str(response) # <--- CORREGIDO: Eliminado el punto extra
172
- except Exception as e:
173
- print(f"💥 Error en Alfred al procesar la pregunta '{question}': {e}")
174
- return f"Error del agente al procesar la pregunta: {e}"
 
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 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"
89
+ gemini_history.append({'role': role, 'parts': [{'text': msg.content}]})
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 = []
123
+ for msg in messages[:-1]:
124
+ role = "user" if msg.role == "user" else "model"
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) ...