From 6b491a9a41fd3b3732287f39658e277004b39a94 Mon Sep 17 00:00:00 2001 From: egutierrez Date: Mon, 19 May 2025 22:57:01 +0200 Subject: [PATCH] =?UTF-8?q?Implementaci=C3=B3n=20del=20cliente=20Ollama=20?= =?UTF-8?q?y=20su=20credencial,=20integraci=C3=B3n=20de=20logging=20en=20b?= =?UTF-8?q?ase=20de=20datos,=20y=20mejoras=20en=20la=20gesti=C3=B3n=20de?= =?UTF-8?q?=20herramientas=20MCP.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prueba_loop_agente.py | 81 +++-- snippets/utilizar_logger.py | 8 + src/ConexionApis/Ollama_cliente.py | 62 ++++ src/Credenciales/ollama_credencial.py | 20 ++ src/Llms/Agente.py | 378 ++++++++++++++++------- src/Llms/MCPs/McpClient_Registry.py | 30 +- src/Llms/MCPs/McpServers/server_math.py | 4 +- src/Llms/MCPs/McpServers/server_utils.py | 1 + src/Llms/Modelos/Ollama_model.py | 67 ++++ src/Logger/logger_db.py | 60 ++++ 10 files changed, 544 insertions(+), 167 deletions(-) create mode 100644 snippets/utilizar_logger.py create mode 100644 src/ConexionApis/Ollama_cliente.py create mode 100644 src/Credenciales/ollama_credencial.py create mode 100644 src/Llms/Modelos/Ollama_model.py create mode 100644 src/Logger/logger_db.py diff --git a/prueba_loop_agente.py b/prueba_loop_agente.py index ca7b397..76573f3 100644 --- a/prueba_loop_agente.py +++ b/prueba_loop_agente.py @@ -11,29 +11,43 @@ from fastmcp.client.transports import StreamableHttpTransport from fastmcp.client import Client from src.Llms.MCPs.McpClient import MCPClient # ya tienes esta clase from src.Llms.MCPs.McpClient_Registry import ClientRegistry # o ajusta según tu estructura +from src.Credenciales.ollama_credencial import OllamaCredencial +from src.ConexionApis.Ollama_cliente import OllamaCliente +from src.Llms.Modelos.Ollama_model import ModeloOllama import asyncio async def main(): - # Usar Credencial openai + # # Usar Credencial openai - conexion_admin = PostgresConexion(db_credencial) - repo = OpenAICredencialRepo(conexion_admin) - credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014") - cliente = OpenAICliente(credencial_openai) + # conexion_admin = PostgresConexion(db_credencial) + # repo = OpenAICredencialRepo(conexion_admin) + # credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014") + # if credencial_openai is None: + # raise ValueError("No se encontró la credencial OpenAI con el ID proporcionado.") + # cliente = OpenAICliente(credencial_openai) + # Usar Credencial ollama + credencial_ollama = OllamaCredencial(titulo="Ollama") - # crea el modelo (openai) + cliente = OllamaCliente(credencial_ollama) - modelo = ModeloOpenAI( + modelo = ModeloOllama( cliente=cliente, - model="gpt-4o", - temperature=1, - top_p=1.0 - ) + model="llama3.1:8b") + + + + # # crea el modelo (openai) + + # modelo = ModeloOpenAI( + # cliente=cliente, + # model="gpt-4o", + # temperature=1 + # ) # Le otorga memoria @@ -70,20 +84,20 @@ async def main(): agente2 = AgenteAI( modelo=modelo, nombre="Asistente Inteligente", - descripcion="Un asistente conversacional versátil, capaz de resolver problemas, acceder a herramientas y proporcionar respuestas útiles.", - system_prompt=( - "Eres un asistente inteligente que ayuda al usuario a resolver tareas, responder preguntas y usar herramientas disponibles si es necesario. " - "Debes razonar paso a paso, y si se detecta que una herramienta MCP es útil, actúa generando el bloque MCP apropiado sin dar más explicaciones. " - "Siempre estructura tus respuestas con claridad, y termina con cuando creas haber completado la tarea." - ), + descripcion="", + system_prompt="", + # system_prompt = """ + # Eres un asistente general. No tienes acceso a herramientas externas ni herramientas MCP. No debes mencionar herramientas MCP, servidores ni bloques de código. + # Responde de forma clara y amigable a cualquier pregunta general del usuario. + # """.strip(), rol="asistente", objetivos=[ - "Resolver tareas del usuario", - "Usar herramientas MCP si es útil", - "Responder de forma clara y útil" + # "Resolver tareas del usuario", + # "Usar herramientas MCP si es útil", + # "Responder de forma clara y útil" ], - # max_iterations=3, + max_iterations=0, # memoria=memoria, mcp=registry # ← ✅ Integración del cliente MCP @@ -92,22 +106,29 @@ async def main(): # --- FUNCIÓN DE EJECUCIÓN --- async def probar_interaccion_stream(): - # # 🔌 Conectar a los servidores MCP registrados - # await mcp_client.connect_all() + print("🧠 Agente iniciado. Escribe 'salir' para terminar.\n") - print("Respuesta en streaming:\n") - respuesta_gen = await agente2.interactuar_en_bucle( - "¿Cuál es mi nombre de usuario en este sistema?", - stream=True - ) + while True: + prompt = input("\n📝 Escribe tu pregunta: ") + if prompt.strip().lower() in ("salir", "exit", "quit"): + print("\n👋 Hasta pronto.") + break - async for token in respuesta_gen: - print(token, end="", flush=True) + print("\n💬 Respuesta en streaming:\n") + respuesta_gen = await agente2.interactuar_en_bucle( + prompt=prompt, + stream=True + ) + + async for token in respuesta_gen: + print(token, end="", flush=True) await probar_interaccion_stream() + + # Ejecutar if __name__ == "__main__": asyncio.run(main()) diff --git a/snippets/utilizar_logger.py b/snippets/utilizar_logger.py new file mode 100644 index 0000000..caaa542 --- /dev/null +++ b/snippets/utilizar_logger.py @@ -0,0 +1,8 @@ +from entrypoint.init_db import db_credencial +from src.Logger.logger_db import LoggerDB, logger + + +LoggerDB(db_credencial, "logger_eventos", created_by="sistema_agente") + +logger.info("Esto solo se verá en la base de datos") +logger.error("No aparecerá en la terminal") \ No newline at end of file diff --git a/src/ConexionApis/Ollama_cliente.py b/src/ConexionApis/Ollama_cliente.py new file mode 100644 index 0000000..ab77238 --- /dev/null +++ b/src/ConexionApis/Ollama_cliente.py @@ -0,0 +1,62 @@ +import requests +from src.Credenciales.ollama_credencial import OllamaCredencial + +class OllamaCliente: + def __init__(self, credencial: OllamaCredencial): + """ + Inicializa el cliente de Ollama con una instancia de OllamaCredencial. + """ + self.credencial = credencial + self.base_url = self.credencial.base_url + + # --- Chat Completions --- + def chat_completion(self, model: str, messages: list, stream: bool = False, **kwargs): + url = f"{self.base_url}/api/chat" + payload = { + "model": model, + "messages": messages, + "stream": stream, + **kwargs + } + response = requests.post(url, json=payload, stream=stream) + response.raise_for_status() + + return self._handle_stream(response) if stream else response.json() + + def _handle_stream(self, response): + for line in response.iter_lines(): + if line: + try: + parsed = line.decode("utf-8") + # Extraer contenido si está en JSON como {"message":{"content":"..."},...} + if parsed.startswith("{"): + import json + data = json.loads(parsed) + if "message" in data and "content" in data["message"]: + yield data["message"]["content"] + except Exception: + continue + + # --- Text Completion (legacy) --- + def completion(self, model: str, prompt: str, **kwargs): + url = f"{self.base_url}/api/generate" + payload = { + "model": model, + "prompt": prompt, + **kwargs + } + response = requests.post(url, json=payload) + response.raise_for_status() + return response.json() + + # --- Embeddings --- + def embedding(self, model: str, input: str | list[str], **kwargs): + url = f"{self.base_url}/api/embeddings" + payload = { + "model": model, + "prompt": input, + **kwargs + } + response = requests.post(url, json=payload) + response.raise_for_status() + return response.json() diff --git a/src/Credenciales/ollama_credencial.py b/src/Credenciales/ollama_credencial.py new file mode 100644 index 0000000..d37052a --- /dev/null +++ b/src/Credenciales/ollama_credencial.py @@ -0,0 +1,20 @@ +from src.Security.GenerarIDs import GeneradorIDUnico + +class OllamaCredencial: + def __init__(self, titulo: str, base_url: str = "http://localhost:11434", id: str = None): + """ + :param titulo: Nombre descriptivo para esta credencial de Ollama. + :param base_url: URL base donde está corriendo el servidor de Ollama (por defecto: localhost). + """ + self.id = id if id is not None else GeneradorIDUnico("OLLA").generar() + self.titulo = titulo + self.base_url = base_url.rstrip("/") + + def get_headers(self) -> dict: + """ + Retorna encabezados para autenticación si se requiere en el futuro. + Por defecto, Ollama local no usa headers especiales. + """ + return { + "Content-Type": "application/json" + } \ No newline at end of file diff --git a/src/Llms/Agente.py b/src/Llms/Agente.py index adf6842..cb92220 100644 --- a/src/Llms/Agente.py +++ b/src/Llms/Agente.py @@ -3,6 +3,14 @@ from src.Llms.Memory.Base_MemoryConv import MemoryConvABC from src.Llms.MCPs.McpClient_Registry import ClientRegistry from datetime import datetime from typing import Optional, List, Union, AsyncGenerator +import re +import json + +from entrypoint.init_db import db_credencial +from src.Logger.logger_db import LoggerDB, logger +LoggerDB(db_credencial, "logger_agentes", created_by="sistema") + + class AgenteAI: @@ -17,7 +25,7 @@ class AgenteAI: max_iterations: int = 1, memoria: Optional[MemoryConvABC] = None, version: str = "1.0.0", - mcp: ClientRegistry = None, + mcp: Optional[ClientRegistry] = None, output_schema: Optional[dict] = None, ): self.modelo = modelo @@ -48,219 +56,349 @@ class AgenteAI: - @property - async def full_system_prompt(self) -> str: - tools_str = await self._obtener_herramientas_disponibles_str() - return f""" -Eres un agente conversacional con acceso a herramientas MCP (Model Context Protocol). + async def generar_system_prompt(self) -> str: -Tu comportamiento sigue este flujo: + info = f"""Eres un agente de texto y te llamas {self.nombre} -1. **Piensa** para razonar tu decisión. -2. **Decide** si: - - puedes responder tú mismo, - - necesitas más información del usuario, - - o necesitas una herramienta MCP. -3. **Actúa**: - - Cuando uses MCP, termina **solo** con un bloque de código MCP y **nada más**. - - Ten en cuenta EXACTAMENTE los parámetros especificados. - - **No expliques, no hables después del bloque. Termina tu turno.** + ### Descripción: + {self.descripcion} ---- + ### Rol: + {self.rol} -# Formato MCP + ### Objetivos: + {chr(10).join(f"- {o}" for o in self.objetivos)} -```mcp -{{ - "tool": "", - "input": {{ - "clave": "valor" - }} -}} -Reglas clave: + ### System Prompt: + {self.system_prompt} -Razonas antes de actuar. + Siempre estructura tus respuestas con claridad, y termina con cuando hayas completado la tarea principal del usuario. + """.strip() -Nunca hables después de un bloque MCP. - -No combines respuestas y herramientas. - -Piensa. Decide. Actúa. - -Herramientas disponibles para usar con MCP: -{tools_str} - -""".strip() + return info + async def construir_prompt_usuario(self, prompt_usuario: str) -> str: + bloques = [] - # Conseguir las herramientas disponibles + if self.mcp: + tools_str = await self._obtener_herramientas_disponibles_str() + bloques.append(f"### Herramientas disponibles (MCP):\n{tools_str}") + bloques.append("""### Instrucciones para actuar con herramientas MCP: + Eres un agente conversacional con acceso a herramientas MCP. Cuando el usuario te haga una solicitud, sigue este proceso paso a paso: + --- + 🧠 **Piensa**: + Reflexiona en voz alta. Explica claramente qué crees que se necesita hacer y por qué. + 🎯 **Decide**: + Elige si puedes resolverlo tú solo, si necesitas más información del usuario, o si una herramienta MCP sería útil. + ⚙️ **Actúa**: + Si decides usar una herramienta, **escribe el bloque MCP justo después**, sin ningún texto extra después del bloque. + --- + ### Formato MCP: + + ```mcp + { + "server": "tools", + "tool": "get_current_user", + "input": {} + } + ``` + + --- + + ### ❗ REGLAS IMPORTANTES: + + - **Puedes pensar y decidir con texto normal**, pero: + - El **bloque MCP debe ser lo último** que aparece en tu mensaje. + - **NO escribas nada después del bloque MCP.** + - **NO pongas `` justo después de usar MCP.** + - Solo usa `` cuando: + - hayas terminado completamente la tarea del usuario, + - e interpretado la salida de las herramientas que usaste. + - Puedes hacer múltiples pasos si es necesario: usar una herramienta, esperar su salida, analizarla, usar otra, etc. + - Si decides no usar herramientas, simplemente responde como lo harías normalmente. + - Si no estás seguro de algo, **pide aclaraciones al usuario** antes de actuar. + + --- + + ### ⚠️ Ejemplos comunes de errores y cómo evitarlos: + + ❌ Incorrecto: + ``` + + { + "server": "tools", + "tool": "generate_uuid", + "input": {} + } + + ```` + + 🔴 Este bloque no funcionará. Le falta indicar que es un bloque MCP. + + ✅ Correcto: + ```mcp + { + "server": "tools", + "tool": "generate_uuid", + "input": {} + } + ```` + 🔵 Siempre usa ` ```mcp ` (con triple backtick y la palabra `mcp`) antes del JSON. No escribas nada después del bloque. + ```` + --- + + ### ✅ Ejemplo correcto: + + Necesito generar un identificador único para el usuario. + Para eso usaré la herramienta `generate_uuid` disponible. + + ```mcp + { + "server": "tools", + "tool": "generate_uuid", + "input": {} + } + + """) + + if self.memoria: + historial = self.memoria.cargar_historial_chat() + if historial: + memoria_str = "\n".join( + [f"{msg['role']}: {msg['content']}" for msg in historial] + ) + bloques.append(f"### Memoria del chat:\n{memoria_str}") + + if self.output_schema: + schema_str = str(self.output_schema) + bloques.append(f"### Salida esperada:\n{schema_str}") + + bloques.append(f"### Prompt del usuario:\n{prompt_usuario}") + + return "\n\n".join(bloques) + + + + + +### Conseguir las herramientas disponibles async def _obtener_herramientas_disponibles_str(self) -> str: + logger.info("Inicio de obtención de herramientas disponibles") + if not self.mcp: + logger.warning("No se ha definido el cliente MCP.") return "No se han definido herramientas disponibles." - herramientas = [] - tools_por_cliente = await self.mcp.listar_tools_por_cliente() + try: + resultado = await self.mcp.listar_tools_por_cliente() + tools_por_cliente = resultado.get("tools", {}) + errores = resultado.get("errores", {}) - for name, tools in tools_por_cliente.items(): - if not tools: - continue - herramientas.append(f"\n🔌 Cliente: {name}") - for tool in tools: - props = tool.inputSchema.get("properties", {}) - parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items()) - herramientas.append(f"""Nombre: {tool.name} - Descripción: {tool.description} - Parámetros: - {parametros} - """) - return "\n".join(herramientas) or "No hay herramientas disponibles actualmente." + logger.debug(f"Tools obtenidas: {list(tools_por_cliente.keys())}") + logger.debug(f"Errores detectados: {list(errores.keys())}") + + herramientas = [] + + for name, tools in tools_por_cliente.items(): + if not tools: + logger.info(f"Servidor {name} no tiene herramientas disponibles.") + continue + + herramientas.append(f"\n🔌 Server: {name}") + for tool in tools: + props = tool.inputSchema.get("properties", {}) + parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items()) + herramientas.append(f"""Nombre: {tool.name} + Descripción: {tool.description} + Parámetros: + {parametros} + """) + logger.debug(f"Herramienta agregada: {tool.name} del servidor {name}") + + if errores: + herramientas.append("\n⚠️ Los siguientes servidores no están disponibles:") + for name, error in errores.items(): + herramientas.append(f"- {name}: {error}") + logger.warning(f"Servidor con error: {name} -> {error}") + + logger.info("Finalización de obtención de herramientas exitosamente.") + return "\n".join(herramientas) or "No hay herramientas disponibles actualmente." + + except Exception as e: + logger.error(f"Error inesperado al obtener herramientas: {str(e)}", exc_info=True) + return "Se produjo un error al obtener las herramientas disponibles." - - - - - - - # Formatear prompt para agentes +### Formatear prompt para agentes def _formatear_prompt(self, mensajes: List[dict]) -> str: return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes]) +### Ejecutar codigo MCP + + async def ejecutar_bloque_mcp(self, respuesta: str) -> Optional[str]: + logger.info("Iniciando ejecución de bloque MCP.") + + patron = r"```mcp\s*(\{.*?\})\s*```" + match = re.search(patron, respuesta, re.DOTALL) + + if not match: + patron_incorrecto = r"```[\s]*\{.*?\}[\s]*```" + if re.search(patron_incorrecto, respuesta, re.DOTALL): + logger.warning("Bloque detectado sin especificador `mcp`.") + return "Advertencia: Usaste un bloque de herramienta MCP pero olvidaste indicar el lenguaje `mcp`. Corrige el bloque a: ```mcp { ... } ```" + logger.info("No se encontró ningún bloque MCP en la respuesta.") + return None + + try: + bloque_json_str = match.group(1) + logger.debug(f"Bloque MCP detectado: {bloque_json_str}") + + bloque = json.loads(bloque_json_str) + + server_name = bloque["server"] + tool_name = bloque["tool"] + input_args = bloque.get("input", {}) + + logger.info(f"Bloque MCP válido. Servidor: {server_name}, Herramienta: {tool_name}") + logger.debug(f"Parámetros de entrada: {input_args}") + + except Exception as e: + logger.error(f"Error al interpretar el bloque MCP: {e}", exc_info=True) + return f"Error al interpretar el bloque MCP: {e}" + + try: + cliente_mcp = self.mcp.get(server_name) + except KeyError: + logger.warning(f"No se encontró el cliente MCP para el servidor '{server_name}'.") + return f"No se encontró el cliente MCP para el servidor '{server_name}'" + + try: + logger.info(f"Ejecutando herramienta '{tool_name}' en servidor '{server_name}' con argumentos: {json.dumps(input_args, ensure_ascii=False)}") + + async with cliente_mcp: + resultado = await cliente_mcp.call_tool(tool_name, input_args) + logger.info(f"Ejecución completada exitosamente. Resultado: {resultado}") + return str(resultado) + + except Exception as e: + logger.error(f"Error al ejecutar herramienta '{tool_name}' en servidor '{server_name}': {e}", exc_info=True) + return f"Error al ejecutar herramienta '{tool_name}' en servidor '{server_name}': {e}" - ###----------- Funcion para interactuar +###----------- Funcion para interactuar async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]: - historial = self.memoria.cargar_historial_chat() if self.memoria else [] - contexto = historial + [{"role": "user", "content": prompt}] + mensaje_usuario = await self.construir_prompt_usuario(prompt) + contexto = [{"role": "user", "content": mensaje_usuario}] prompt_final = self._formatear_prompt(contexto) respuesta = await self.modelo.responder( prompt=prompt_final, - system_prompt=await self.full_system_prompt, # ✅ correcto + system_prompt=await self.generar_system_prompt(), stream=stream ) - if stream: - async def wrapper(): - buffer_respuesta = "" - async for token in respuesta: - buffer_respuesta += token - yield token - if self.memoria: - self.memoria.guardar_turno("user", prompt) - self.memoria.guardar_turno("assistant", buffer_respuesta) - self.numero_interacciones += 1 - self.updated_at = datetime.now() - return wrapper() - else: - if self.memoria: - self.memoria.guardar_turno("user", prompt) - self.memoria.guardar_turno("assistant", respuesta) - self.numero_interacciones += 1 - self.updated_at = datetime.now() - return respuesta + return respuesta - - - ###----------- Funcion para interactuar en bucle +###----------- Funcion para interactuar en bucle async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]: - print("🚀 [interactuar_en_bucle] Iniciando interacción") - historial = self.memoria.cargar_historial_chat() if self.memoria else [] - print(f"📜 [interactuar_en_bucle] Historial cargado: {historial}") respuestas = [] if not stream else None - respuesta_anterior = None + respuesta_anterior = "" + resultado_mcp_anterior = None # <-- Guarda último resultado del MCP iteration = 0 prompt_original = prompt.strip() - print(f"✏️ [interactuar_en_bucle] Prompt original: {prompt_original}") async def generador(): - nonlocal iteration, respuesta_anterior - prompt_actual = prompt_original + nonlocal iteration, respuesta_anterior, resultado_mcp_anterior while self.max_iterations == 0 or iteration < self.max_iterations: - print(f"\n🔁 [generador] Iteración: {iteration}") + instruccion_fin = ( + "\n\nIMPORTANTE: Cuando hayas respondido completamente a la pregunta original del usuario y no requieras más pasos, " + "escribe para indicar que has terminado." + ) if iteration == 0: - prompt_actual += ( - "\n\nIMPORTANTE:\n" - "Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, " - "di alguna de estas frases: " - ) + prompt_actual = prompt_original + instruccion_fin else: prompt_actual = ( f"Esta es la pregunta original:\n{prompt_original}\n\n" - f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n" - "\n\nIMPORTANTE:\n" - "Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, " - "di alguna de estas frases: " - "En caso contrario, responde a la pregunta original " - "y añade información relevante que no hayas mencionado antes.\n\n" + f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n\n" + f"{instruccion_fin}" ) - contexto = historial + [{"role": "user", "content": prompt_actual}] + if resultado_mcp_anterior: + prompt_actual += ( + "\n\nEsta fue la salida de la herramienta que usaste:\n" + f"{resultado_mcp_anterior}\n\n" + "Úsala para seguir resolviendo el problema o tomar una nueva decisión." + ) + + mensaje_usuario = await self.construir_prompt_usuario(prompt_actual) + contexto = [{"role": "user", "content": mensaje_usuario}] prompt_final = self._formatear_prompt(contexto) - print(f"📨 [generador] Prompt final enviado al modelo:\n{prompt_final}") - - print("🤖 [generador] Esperando respuesta del modelo...") respuesta = await self.modelo.responder( prompt=prompt_final, - system_prompt=await self.full_system_prompt, + system_prompt=await self.generar_system_prompt(), stream=stream ) - print("✅ [generador] Respuesta recibida") if stream: buffer_respuesta = "" async for token in respuesta: buffer_respuesta += token - # print(f"🔹 [stream] Token: {token}") yield token respuesta_anterior = buffer_respuesta - # print(f"📦 [stream] Respuesta completa:\n{respuesta_anterior}") else: respuestas.append(respuesta) respuesta_anterior = respuesta - # print(f"📦 [generador] Respuesta completa:\n{respuesta_anterior}") + # Revisar y ejecutar bloque MCP si existe + resultado_mcp_anterior = None + if "```mcp" in respuesta_anterior: + resultado_mcp = await self.ejecutar_bloque_mcp(respuesta_anterior) + if resultado_mcp: + resultado_mcp_anterior = resultado_mcp + + if stream: + yield "\n" + resultado_mcp + else: + respuestas.append(resultado_mcp) + + # Guardar historial si hay memoria if self.memoria: - print("💾 [memoria] Guardando turno en la memoria...") self.memoria.guardar_turno("user", prompt_actual) self.memoria.guardar_turno("assistant", respuesta_anterior) self.numero_interacciones += 1 self.updated_at = datetime.now() - print(f"📊 [generador] Interacción #{self.numero_interacciones} registrada") - if "" in respuesta_anterior.lower(): - print("🛑 [generador] Detectado . Terminando bucle.") + if "" in respuesta_anterior.lower() and "```mcp" not in respuesta_anterior.lower(): break iteration += 1 - prompt_actual = "" - return generador() if stream else await generador_to_list(generador) + return generador() if stream else await generador_to_list(generador()) diff --git a/src/Llms/MCPs/McpClient_Registry.py b/src/Llms/MCPs/McpClient_Registry.py index db1e90d..d97aec5 100644 --- a/src/Llms/MCPs/McpClient_Registry.py +++ b/src/Llms/MCPs/McpClient_Registry.py @@ -22,35 +22,35 @@ class ClientRegistry: def __contains__(self, name: str) -> bool: return name in self._clients - async def listar_tools_por_cliente(self) -> dict[str, list[Any]]: - resultado = {} + async def listar_tools_por_cliente(self) -> dict[str, Any]: + resultado = {"tools": {}, "errores": {}} for name, wrapper in self._clients.items(): try: async with wrapper: - resultado[name] = await wrapper.list_tools() + resultado["tools"][name] = await wrapper.list_tools() except Exception as e: - print(f"[TOOLS] ❌ Error en '{name}': {e}") - resultado[name] = [] + resultado["errores"][name] = str(e) + resultado["tools"][name] = [] return resultado - async def listar_prompts_por_cliente(self) -> dict[str, list[Any]]: - resultado = {} + async def listar_prompts_por_cliente(self) -> dict[str, Any]: + resultado = {"prompts": {}, "errores": {}} for name, wrapper in self._clients.items(): try: async with wrapper: - resultado[name] = await wrapper.list_prompts() + resultado["prompts"][name] = await wrapper.list_prompts() except Exception as e: - print(f"[PROMPTS] ❌ Error en '{name}': {e}") - resultado[name] = [] + resultado["errores"][name] = str(e) + resultado["prompts"][name] = [] return resultado - async def listar_resources_por_cliente(self) -> dict[str, list[Any]]: - resultado = {} + async def listar_resources_por_cliente(self) -> dict[str, Any]: + resultado = {"resources": {}, "errores": {}} for name, wrapper in self._clients.items(): try: async with wrapper: - resultado[name] = await wrapper.list_resources() + resultado["resources"][name] = await wrapper.list_resources() except Exception as e: - print(f"[RESOURCES] ❌ Error en '{name}': {e}") - resultado[name] = [] + resultado["errores"][name] = str(e) + resultado["resources"][name] = [] return resultado \ No newline at end of file diff --git a/src/Llms/MCPs/McpServers/server_math.py b/src/Llms/MCPs/McpServers/server_math.py index 55a870a..56b908c 100644 --- a/src/Llms/MCPs/McpServers/server_math.py +++ b/src/Llms/MCPs/McpServers/server_math.py @@ -87,6 +87,6 @@ def is_prime(n: int) -> bool: if __name__ == "__main__": - # mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math") + mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math") - mcp.run(transport="stdio") \ No newline at end of file + # mcp.run(transport="stdio") \ No newline at end of file diff --git a/src/Llms/MCPs/McpServers/server_utils.py b/src/Llms/MCPs/McpServers/server_utils.py index 94000c9..ba50ba6 100644 --- a/src/Llms/MCPs/McpServers/server_utils.py +++ b/src/Llms/MCPs/McpServers/server_utils.py @@ -66,3 +66,4 @@ def current_timestamp() -> float: if __name__ == "__main__": mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools") + diff --git a/src/Llms/Modelos/Ollama_model.py b/src/Llms/Modelos/Ollama_model.py new file mode 100644 index 0000000..bb6cf46 --- /dev/null +++ b/src/Llms/Modelos/Ollama_model.py @@ -0,0 +1,67 @@ +from src.Llms.Modelos.Base_model import ModeloABC +from src.Security.GenerarIDs import GeneradorIDUnico +from typing import AsyncGenerator, Union +from src.ConexionApis.Ollama_cliente import OllamaCliente # Asegúrate de importar correctamente +import asyncio + +class ModeloOllama(ModeloABC): + def __init__( + self, + cliente: OllamaCliente, + model: str = "llama3", + id: str = None, + temperature: float = 0.7, + top_p: float = 1.0, + top_k: int = None, + frecuencia_penalizacion: float = 0.0, + num_tokens_maximos: int = 512 + ): + if not isinstance(cliente, OllamaCliente): + raise TypeError("El parámetro 'cliente' debe ser una instancia de OllamaCliente") + + + self.id = id if id else GeneradorIDUnico("MOOL").generar() + super().__init__( + model=model, + temperature=temperature, + top_p=top_p, + top_k=top_k, + frecuencia_penalizacion=frecuencia_penalizacion, + num_tokens_maximos=num_tokens_maximos + ) + self.cliente = cliente + + async def responder( + self, + prompt: str, + system_prompt: str = "", + stream: bool = False, + **kwargs + ) -> Union[str, AsyncGenerator[str, None]]: + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + def sync_call(): + return self.cliente.chat_completion( + model=self.model, + messages=messages, + temperature=self.temperature, + top_p=self.top_p, + max_tokens=self.num_tokens_maximos, + frequency_penalty=self.frecuencia_penalizacion, + stream=stream, + **kwargs + ) + + loop = asyncio.get_event_loop() + resultado = await loop.run_in_executor(None, sync_call) + + if stream: + async def generador(): + for token in resultado: + yield token + return generador() + else: + return resultado.choices[0].message.content diff --git a/src/Logger/logger_db.py b/src/Logger/logger_db.py new file mode 100644 index 0000000..a201611 --- /dev/null +++ b/src/Logger/logger_db.py @@ -0,0 +1,60 @@ +from loguru import logger +from sqlalchemy import Column, Integer, String, Text, TIMESTAMP +from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import SQLAlchemyError + +from src.ArquitectureLayer.Model import Model_base +from src.ConexionSql.Postgres_conexion import PostgresConexion +from src.Credenciales.postgres_credencial import PostgresCredencial + +class LoggerDB: + _sink_removido = False # ← evita múltiples remove() si se crean varias instancias + + def __init__(self, credencial: PostgresCredencial, nombre_tabla: str, created_by: str = None): + if not LoggerDB._sink_removido: + logger.remove() # 🧹 elimina impresión en terminal + LoggerDB._sink_removido = True + + self.conexion = PostgresConexion(credencial) + self.engine = self.conexion.get_engine() + self.Session = sessionmaker(bind=self.engine) + self.nombre_tabla = nombre_tabla + self.created_by = created_by + + self.modelo_logger = self._generar_modelo_logger() + self._crear_tabla_si_no_existe() + logger.add(self._sink, level="DEBUG") + + def _generar_modelo_logger(self): + class LoggerTable(Model_base): + __tablename__ = self.nombre_tabla + id = Column(Integer, primary_key=True) + nivel = Column(String, nullable=False) + mensaje = Column(Text, nullable=False) + fecha = Column(TIMESTAMP(timezone=True), nullable=False) + modulo = Column(String, nullable=True) + funcion = Column(String, nullable=True) + linea = Column(Integer, nullable=True) + return LoggerTable + + def _crear_tabla_si_no_existe(self): + self.modelo_logger.__table__.create(self.engine, checkfirst=True) + + def _sink(self, message): + record = message.record + try: + session = self.Session() + log_entry = self.modelo_logger( + nivel=record["level"].name, + mensaje=record["message"], + fecha=record["time"], + modulo=record["module"], + funcion=record["function"], + linea=record["line"], + sys_created_by=self.created_by + ) + session.add(log_entry) + session.commit() + session.close() + except SQLAlchemyError as e: + print(f"[LoggerDB] Error guardando log en BD: {e}")