feat: Implement main application shell with navigation and color scheme toggle
- Added Appshell component with responsive navbar and main content area - Integrated ColorSchemeToggle for light/dark mode switching - Created Welcome component with styled title and introductory text - Developed ChatPage for LLM interaction with WebSocket support - Implemented Biblioteca for managing notes with rich text editor - Added LoginPage for user authentication with error handling - Introduced MessageList and MessageBubble components for chat messages - Styled components with CSS modules for consistent design
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||
from domains.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 domains.Logger.logger_db import LoggerDB, logger
|
||||
LoggerDB(db_credencial, "logger_agentes", created_by="sistema")
|
||||
|
||||
|
||||
|
||||
|
||||
class AgenteAI:
|
||||
def __init__(
|
||||
self,
|
||||
modelo: ModeloABC,
|
||||
nombre: str,
|
||||
descripcion: str,
|
||||
system_prompt: str,
|
||||
rol: str,
|
||||
objetivos: List[str],
|
||||
max_iterations: int = 1,
|
||||
memoria: Optional[MemoryConvABC] = None,
|
||||
version: str = "1.0.0",
|
||||
mcp: Optional[ClientRegistry] = None,
|
||||
output_schema: Optional[dict] = None,
|
||||
):
|
||||
self.modelo = modelo
|
||||
self.memoria = memoria
|
||||
self.output_schema = output_schema
|
||||
|
||||
self.nombre = nombre
|
||||
self.descripcion = descripcion
|
||||
self.system_prompt = system_prompt
|
||||
self.max_iterations = max_iterations
|
||||
self.rol = rol
|
||||
self.objetivos = objetivos
|
||||
self.version = version
|
||||
|
||||
self.created_at = datetime.now()
|
||||
self.updated_at = self.created_at
|
||||
self.numero_interacciones = 0
|
||||
self.mcp = mcp # <-- Aquí guardamos el registry
|
||||
|
||||
|
||||
|
||||
def actualizar_configuracion(self, **kwargs):
|
||||
for clave, valor in kwargs.items():
|
||||
if hasattr(self, clave):
|
||||
setattr(self, clave, valor)
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
|
||||
|
||||
async def generar_system_prompt(self) -> str:
|
||||
|
||||
info = f"""Eres un agente de texto y te llamas {self.nombre}
|
||||
|
||||
### Descripción:
|
||||
{self.descripcion}
|
||||
|
||||
### Rol:
|
||||
{self.rol}
|
||||
|
||||
### Objetivos:
|
||||
{chr(10).join(f"- {o}" for o in self.objetivos)}
|
||||
|
||||
### System Prompt:
|
||||
{self.system_prompt}
|
||||
|
||||
Siempre estructura tus respuestas con claridad, y termina con <END> cuando hayas completado la tarea principal del usuario.
|
||||
""".strip()
|
||||
|
||||
return info
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def construir_prompt_usuario(self, prompt_usuario: str) -> str:
|
||||
bloques = []
|
||||
|
||||
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.**
|
||||
- Solo usa `<END>` 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.
|
||||
|
||||
---
|
||||
|
||||
✅ 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."
|
||||
|
||||
try:
|
||||
resultado = await self.mcp.listar_tools_por_cliente()
|
||||
tools_por_cliente = resultado.get("tools", {})
|
||||
errores = resultado.get("errores", {})
|
||||
|
||||
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
|
||||
|
||||
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}"
|
||||
|
||||
|
||||
|
||||
### Ejecutar VARIOS bloques MCP
|
||||
|
||||
async def ejecutar_multiples_bloques_mcp(self, respuesta: str) -> Optional[List[str]]:
|
||||
logger.info("Buscando múltiples bloques MCP en la respuesta.")
|
||||
|
||||
patron = r"```mcp\s*(\{.*?\})\s*```"
|
||||
matches = re.finditer(patron, respuesta, re.DOTALL)
|
||||
|
||||
resultados = []
|
||||
hubo_bloques = False
|
||||
|
||||
for match in matches:
|
||||
hubo_bloques = True
|
||||
bloque_json_str = match.group(1)
|
||||
try:
|
||||
bloque = json.loads(bloque_json_str)
|
||||
server_name = bloque["server"]
|
||||
tool_name = bloque["tool"]
|
||||
input_args = bloque.get("input", {})
|
||||
|
||||
logger.info(f"Ejecutando bloque MCP: servidor={server_name}, herramienta={tool_name}")
|
||||
|
||||
try:
|
||||
cliente_mcp = self.mcp.get(server_name)
|
||||
except KeyError:
|
||||
msg = f"No se encontró el cliente MCP para el servidor '{server_name}'"
|
||||
logger.warning(msg)
|
||||
resultados.append(msg)
|
||||
continue
|
||||
|
||||
async with cliente_mcp:
|
||||
resultado = await cliente_mcp.call_tool(tool_name, input_args)
|
||||
resultado_str = f"[{server_name}.{tool_name}] → {resultado}"
|
||||
resultados.append(resultado_str)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error al procesar bloque MCP: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
resultados.append(error_msg)
|
||||
|
||||
if not hubo_bloques:
|
||||
logger.info("No se encontró ningún bloque MCP en la respuesta.")
|
||||
return None
|
||||
|
||||
return resultados
|
||||
|
||||
|
||||
|
||||
|
||||
###----------- Funcion para interactuar
|
||||
|
||||
async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]:
|
||||
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.generar_system_prompt(),
|
||||
stream=stream
|
||||
)
|
||||
|
||||
return respuesta
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###----------- Funcion para interactuar en bucle
|
||||
|
||||
async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]:
|
||||
respuestas = [] if not stream else None
|
||||
respuesta_anterior = ""
|
||||
resultado_mcp_anterior = None # <-- Guarda último resultado del MCP
|
||||
iteration = 0
|
||||
prompt_original = prompt.strip()
|
||||
|
||||
async def generador():
|
||||
nonlocal iteration, respuesta_anterior, resultado_mcp_anterior
|
||||
|
||||
while self.max_iterations == 0 or iteration < self.max_iterations:
|
||||
instruccion_fin = (
|
||||
"\n\nIMPORTANTE: Cuando hayas respondido completamente a la pregunta original del usuario y no requieras más pasos, "
|
||||
"escribe <END> para indicar que has terminado."
|
||||
)
|
||||
|
||||
if iteration == 0:
|
||||
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"
|
||||
f"{instruccion_fin}"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
respuesta = await self.modelo.responder(
|
||||
prompt=prompt_final,
|
||||
system_prompt=await self.generar_system_prompt(),
|
||||
stream=stream
|
||||
)
|
||||
|
||||
if stream:
|
||||
buffer_respuesta = ""
|
||||
async for token in respuesta:
|
||||
buffer_respuesta += token
|
||||
yield token
|
||||
respuesta_anterior = buffer_respuesta
|
||||
else:
|
||||
respuestas.append(respuesta)
|
||||
respuesta_anterior = respuesta
|
||||
|
||||
# Revisar y ejecutar bloque MCP si existe
|
||||
resultado_mcp_anterior = None
|
||||
if "```mcp" in respuesta_anterior:
|
||||
resultados_mcp = await self.ejecutar_multiples_bloques_mcp(respuesta_anterior)
|
||||
if resultados_mcp:
|
||||
resultado_mcp_anterior = "\n".join(resultados_mcp)
|
||||
|
||||
if stream:
|
||||
yield "\n" + resultado_mcp_anterior
|
||||
else:
|
||||
respuestas.append(resultado_mcp_anterior)
|
||||
|
||||
# Guardar historial si hay memoria
|
||||
if self.memoria:
|
||||
self.memoria.guardar_turno("user", prompt_actual)
|
||||
self.memoria.guardar_turno("assistant", respuesta_anterior)
|
||||
|
||||
self.numero_interacciones += 1
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
if "<end>" in respuesta_anterior.lower() and "```mcp" not in respuesta_anterior.lower():
|
||||
break
|
||||
|
||||
iteration += 1
|
||||
|
||||
return generador() if stream else await generador_to_list(generador())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Helper para consumir generador asincrónico si no es stream
|
||||
async def generador_to_list(gen: AsyncGenerator[str, None]) -> List[str]:
|
||||
buffer = ""
|
||||
async for chunk in gen:
|
||||
buffer += chunk
|
||||
return [buffer]
|
||||
@@ -0,0 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
class EmbedderABC(ABC):
|
||||
@abstractmethod
|
||||
def encoder(self, text: str) -> List[float]:
|
||||
"""Genera los embeddings para un texto dado."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def dimension_number(self) -> int:
|
||||
"""Devuelve la dimensión del modelo de embedding."""
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
from typing import List
|
||||
from domains.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
|
||||
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
|
||||
class OpenAIEmbedder(EmbedderABC):
|
||||
def __init__(self, credencial: OpenAICredencial,
|
||||
model: str,
|
||||
id: str = None):
|
||||
self.model = model
|
||||
self.client = OpenAICliente(credencial)
|
||||
self._dimension = None # Lazy loading
|
||||
self.id = id if id is not None else GeneradorIDUnico("OAMB").generar()
|
||||
|
||||
def encoder(self, text: str) -> List[float]:
|
||||
"""
|
||||
Genera los embeddings para un texto dado utilizando el modelo de OpenAI.
|
||||
"""
|
||||
response = self.client.embedding(model=self.model, input=text)
|
||||
embedding = response.data[0].embedding
|
||||
if self._dimension is None:
|
||||
self._dimension = len(embedding)
|
||||
return embedding
|
||||
|
||||
def dimension_number(self) -> int:
|
||||
"""
|
||||
Devuelve la dimensión del modelo de embedding, generando un embedding si no se ha calculado aún.
|
||||
"""
|
||||
if self._dimension is None:
|
||||
_ = self.encoder("dimension_check")
|
||||
return self._dimension
|
||||
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy import Column, String, ForeignKey
|
||||
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from domains.Llms.Embedders.Openai_embedder import OpenAIEmbedder
|
||||
from domains.ApiKeys.openai_apikey import OpenAICredencial
|
||||
|
||||
# ----------------------
|
||||
# Cargar configuración desde .env si se requiere
|
||||
# ----------------------
|
||||
from entrypoint import ENV_PATH
|
||||
load_dotenv(ENV_PATH)
|
||||
|
||||
# ----------------------
|
||||
# MODELO (SQLAlchemy)
|
||||
# ----------------------
|
||||
|
||||
class OpenAIEmbedderModel(Base, Model_base):
|
||||
__tablename__ = "openai_embedders"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
|
||||
api_key_id = Column(String, ForeignKey("openai_credenciales.id"), nullable=False)
|
||||
model = Column(String, nullable=False)
|
||||
|
||||
# ----------------------
|
||||
# MAPPER
|
||||
# ----------------------
|
||||
|
||||
class OpenAIEmbedderMapper(Mapper_base[OpenAIEmbedder, OpenAIEmbedderModel]):
|
||||
|
||||
@staticmethod
|
||||
def to_model(obj: OpenAIEmbedder) -> OpenAIEmbedderModel:
|
||||
return OpenAIEmbedderModel(
|
||||
id=obj.id,
|
||||
api_key_id=obj.client.credencial.id,
|
||||
model=obj.model
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_model(model: OpenAIEmbedderModel, credencial: OpenAICredencial) -> OpenAIEmbedder:
|
||||
return OpenAIEmbedder(
|
||||
id=model.id,
|
||||
credencial=credencial,
|
||||
model=model.model
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_dict(obj: OpenAIEmbedder) -> dict:
|
||||
return {
|
||||
"id": obj.id,
|
||||
"api_key_id": obj.client.credencial.id,
|
||||
"model": obj.model
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict, credencial: OpenAICredencial) -> OpenAIEmbedder:
|
||||
return OpenAIEmbedder(
|
||||
id=data["id"],
|
||||
credencial=credencial,
|
||||
model=data["model"]
|
||||
)
|
||||
|
||||
# ----------------------
|
||||
# REPO
|
||||
# ----------------------
|
||||
|
||||
class OpenAIEmbedderRepo(Repo_base[OpenAIEmbedderModel, OpenAIEmbedder]):
|
||||
def __init__(self, conexion: ConexionBase):
|
||||
super().__init__(
|
||||
session=conexion.get_session(),
|
||||
modelo=OpenAIEmbedderModel,
|
||||
mapper=OpenAIEmbedderMapper
|
||||
)
|
||||
|
||||
def get_by_id(self, id_: str, credencial: OpenAICredencial) -> OpenAIEmbedder | None:
|
||||
model = self.session.get(self.Modelo, id_)
|
||||
return self.Mapper.from_model(model, credencial) if model else None
|
||||
|
||||
def get_all(self, credencial_loader: callable) -> list[OpenAIEmbedder]:
|
||||
"""
|
||||
:param credencial_loader: función que recibe un api_key_id y devuelve una instancia de OpenAICredencial
|
||||
"""
|
||||
models = self.session.query(self.Modelo).all()
|
||||
return [
|
||||
self.Mapper.from_model(m, credencial_loader(m.api_key_id))
|
||||
for m in models
|
||||
]
|
||||
@@ -0,0 +1,100 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
from pydantic import AnyUrl
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.client.transports import (
|
||||
StreamableHttpTransport,
|
||||
PythonStdioTransport,
|
||||
ClientTransport,
|
||||
)
|
||||
from mcp.types import *
|
||||
from fastmcp.exceptions import ClientError
|
||||
import asyncio
|
||||
|
||||
|
||||
class MCPClient:
|
||||
def __init__(self, name: str, client: Client):
|
||||
self.name = name
|
||||
self.client = client
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ClientWrapper(name={self.name})>"
|
||||
|
||||
@classmethod
|
||||
def from_http(cls, name: str, url: str | AnyUrl) -> "MCPClient":
|
||||
transport = StreamableHttpTransport(url=str(url))
|
||||
client = Client(transport=transport)
|
||||
return cls(name=name, client=client)
|
||||
|
||||
@classmethod
|
||||
def from_stdio(
|
||||
cls,
|
||||
name: str,
|
||||
script_path: Union[str, Path],
|
||||
args: Optional[list[str]] = None,
|
||||
cwd: Optional[Union[str, Path]] = None,
|
||||
env: Optional[dict[str, str]] = None,
|
||||
) -> "MCPClient":
|
||||
transport = PythonStdioTransport(
|
||||
script_path=script_path, args=args, cwd=cwd, env=env
|
||||
)
|
||||
client = Client(transport=transport)
|
||||
return cls(name=name, client=client)
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self.client.is_connected()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.client.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.client.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
# Delegación MCP
|
||||
|
||||
async def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> list[TextContent | ImageContent | EmbeddedResource]:
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self.client.call_tool(name, arguments), timeout=10
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise RuntimeError(f"Timeout al ejecutar herramienta '{name}'")
|
||||
|
||||
async def get_prompt(
|
||||
self, name: str, arguments: dict[str, str] | None = None
|
||||
) -> GetPromptResult:
|
||||
return await self.client.get_prompt(name, arguments)
|
||||
|
||||
async def list_tools(self) -> list[Tool]:
|
||||
return await self.client.list_tools()
|
||||
|
||||
async def list_prompts(self) -> list[Prompt]:
|
||||
return await self.client.list_prompts()
|
||||
|
||||
async def list_resources(self) -> list[Resource]:
|
||||
return await self.client.list_resources()
|
||||
|
||||
async def list_resource_templates(self) -> list[ResourceTemplate]:
|
||||
return await self.client.list_resource_templates()
|
||||
|
||||
async def read_resource(
|
||||
self, uri: AnyUrl | str
|
||||
) -> list[TextResourceContents | BlobResourceContents]:
|
||||
return await self.client.read_resource(uri)
|
||||
|
||||
async def complete(
|
||||
self,
|
||||
ref: ResourceReference | PromptReference,
|
||||
argument: dict[str, str],
|
||||
) -> Completion:
|
||||
return await self.client.complete(ref, argument)
|
||||
|
||||
async def ping(self) -> bool:
|
||||
return await self.client.ping()
|
||||
|
||||
async def set_logging_level(self, level: LoggingLevel) -> None:
|
||||
return await self.client.set_logging_level(level)
|
||||
|
||||
async def send_roots_list_changed(self) -> None:
|
||||
return await self.client.send_roots_list_changed()
|
||||
@@ -0,0 +1,56 @@
|
||||
from domains.Llms.MCPs.McpClient import MCPClient
|
||||
from typing import Any
|
||||
|
||||
class ClientRegistry:
|
||||
def __init__(self):
|
||||
self._clients: dict[str, MCPClient] = {}
|
||||
|
||||
def add(self, name: str, wrapper: MCPClient) -> None:
|
||||
self._clients[name] = wrapper
|
||||
|
||||
def get(self, name: str) -> MCPClient:
|
||||
if name not in self._clients:
|
||||
raise KeyError(f"Cliente '{name}' no encontrado en el registro.")
|
||||
return self._clients[name]
|
||||
|
||||
def all(self) -> dict[str, MCPClient]:
|
||||
return self._clients
|
||||
|
||||
def list_names(self) -> list[str]:
|
||||
return list(self._clients.keys())
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return name in self._clients
|
||||
|
||||
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["tools"][name] = await wrapper.list_tools()
|
||||
except Exception as e:
|
||||
resultado["errores"][name] = str(e)
|
||||
resultado["tools"][name] = []
|
||||
return 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["prompts"][name] = await wrapper.list_prompts()
|
||||
except Exception as e:
|
||||
resultado["errores"][name] = str(e)
|
||||
resultado["prompts"][name] = []
|
||||
return 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["resources"][name] = await wrapper.list_resources()
|
||||
except Exception as e:
|
||||
resultado["errores"][name] = str(e)
|
||||
resultado["resources"][name] = []
|
||||
return resultado
|
||||
@@ -0,0 +1,48 @@
|
||||
# server_runner.py
|
||||
import subprocess
|
||||
import asyncio
|
||||
import socket
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
async def wait_for_port(host: str, port: int, timeout: float = 10.0):
|
||||
for _ in range(int(timeout * 10)):
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=0.5):
|
||||
return True
|
||||
except (OSError, ConnectionRefusedError):
|
||||
await asyncio.sleep(0.1)
|
||||
raise TimeoutError(f"No se pudo conectar al servidor en {host}:{port}")
|
||||
|
||||
class MCPServerRunner:
|
||||
def __init__(self, server_script_path: str, python_path: str = "python"):
|
||||
self.server_script_path = server_script_path
|
||||
self.python_path = python_path
|
||||
self.port: int = self._extraer_puerto()
|
||||
self.process: subprocess.Popen | None = None
|
||||
|
||||
def _extraer_puerto(self) -> int:
|
||||
contenido = Path(self.server_script_path).read_text(encoding="utf-8")
|
||||
coincidencias = re.findall(r"port\s*=\s*(\d+)", contenido)
|
||||
if not coincidencias:
|
||||
raise ValueError(f"No se pudo detectar el puerto en {self.server_script_path}")
|
||||
return int(coincidencias[0])
|
||||
|
||||
async def start(self):
|
||||
if self.process is None or self.process.poll() is not None:
|
||||
self.process = subprocess.Popen(
|
||||
[self.python_path, self.server_script_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
await wait_for_port("127.0.0.1", self.port)
|
||||
print(f"🟢 Servidor MCP iniciado en puerto {self.port}")
|
||||
|
||||
async def stop(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
print("🔴 Servidor MCP detenido")
|
||||
@@ -0,0 +1,133 @@
|
||||
from fastmcp import FastMCP
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# Directorio base seguro
|
||||
SANDBOX_DIR = Path("./sandbox").resolve()
|
||||
SANDBOX_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def safe_path(requested_path: str) -> Path:
|
||||
"""Siempre interpreta la ruta como relativa al SANDBOX_DIR, incluso si empieza con '/'."""
|
||||
# Normaliza la ruta quitando el primer '/'
|
||||
normalized = requested_path.strip().lstrip("/")
|
||||
full_path = (SANDBOX_DIR / normalized).resolve()
|
||||
|
||||
if not full_path.is_relative_to(SANDBOX_DIR):
|
||||
raise ValueError("Ruta fuera del directorio permitido.")
|
||||
return full_path
|
||||
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.tool(description="Lee y devuelve el contenido de un archivo de texto ubicado en el sistema de archivos seguro. El archivo debe estar dentro del sandbox.")
|
||||
def read_file(path: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
if not file_path.is_file():
|
||||
raise FileNotFoundError(f"Archivo '{path}' no encontrado.")
|
||||
return file_path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
return f"⚠️ Error al leer archivo '{path}': {str(e)}"
|
||||
|
||||
|
||||
@mcp.tool(description="Escribe contenido de texto en un archivo dentro del sandbox. Si el archivo ya existe, será sobrescrito.")
|
||||
def write_file(path: str, content: str) -> str:
|
||||
file_path = safe_path(path)
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
return "Archivo guardado correctamente."
|
||||
|
||||
@mcp.tool(description="Elimina de forma segura un archivo ubicado dentro del sandbox.")
|
||||
def delete_file(path: str) -> str:
|
||||
file_path = safe_path(path)
|
||||
if not file_path.is_file():
|
||||
raise FileNotFoundError("Archivo no encontrado.")
|
||||
file_path.unlink()
|
||||
return "Archivo eliminado."
|
||||
|
||||
@mcp.tool(description="Crea una carpeta (y sus carpetas padre si es necesario) dentro del sandbox.")
|
||||
def create_folder(path: str) -> str:
|
||||
folder_path = safe_path(path)
|
||||
folder_path.mkdir(parents=True, exist_ok=True)
|
||||
return "Carpeta creada."
|
||||
|
||||
|
||||
|
||||
@mcp.tool(description="Lista archivos y carpetas dentro de una ruta del sandbox.")
|
||||
def list_directory(path: str = ".") -> list[str]:
|
||||
folder = safe_path(path)
|
||||
if not folder.is_dir():
|
||||
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
|
||||
return sorted(str(p.relative_to(SANDBOX_DIR)) for p in folder.iterdir())
|
||||
|
||||
@mcp.tool(description="Muestra la estructura de carpetas y archivos como un árbol, desde una ruta dentro del sandbox.")
|
||||
def tree(path: str = ".", depth: int = 3) -> str:
|
||||
base = safe_path(path)
|
||||
if not base.is_dir():
|
||||
raise NotADirectoryError("Ruta no corresponde a una carpeta.")
|
||||
|
||||
tree_output = []
|
||||
|
||||
def walk(dir_path: Path, prefix: str = "", level: int = 0):
|
||||
if level > depth:
|
||||
return
|
||||
entries = sorted(dir_path.iterdir())
|
||||
for i, entry in enumerate(entries):
|
||||
connector = "└── " if i == len(entries) - 1 else "├── "
|
||||
tree_output.append(f"{prefix}{connector}{entry.name}")
|
||||
if entry.is_dir():
|
||||
extension = " " if i == len(entries) - 1 else "│ "
|
||||
walk(entry, prefix + extension, level + 1)
|
||||
|
||||
tree_output.append(f"{base.name}/")
|
||||
walk(base)
|
||||
return "\n".join(tree_output)
|
||||
|
||||
@mcp.tool(description="Devuelve información detallada sobre un archivo: tamaño en bytes, fecha de modificación y tipo.")
|
||||
def file_info(path: str) -> dict:
|
||||
fpath = safe_path(path)
|
||||
if not fpath.exists():
|
||||
raise FileNotFoundError("Archivo no encontrado.")
|
||||
return {
|
||||
"nombre": fpath.name,
|
||||
"tipo": "carpeta" if fpath.is_dir() else "archivo",
|
||||
"tamaño_bytes": fpath.stat().st_size,
|
||||
"última_modificación": datetime.fromtimestamp(fpath.stat().st_mtime).isoformat(),
|
||||
"relativo": str(fpath.relative_to(SANDBOX_DIR))
|
||||
}
|
||||
|
||||
@mcp.tool(description="Copia un archivo o carpeta dentro del sandbox a otra ruta.")
|
||||
def copy_file(src: str, dest: str) -> str:
|
||||
src_path = safe_path(src)
|
||||
dest_path = safe_path(dest)
|
||||
if src_path.is_dir():
|
||||
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
|
||||
else:
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src_path, dest_path)
|
||||
return "Copia completada."
|
||||
|
||||
@mcp.tool(description="Mueve o renombra un archivo o carpeta dentro del sandbox.")
|
||||
def move_file(src: str, dest: str) -> str:
|
||||
src_path = safe_path(src)
|
||||
dest_path = safe_path(dest)
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
src_path.rename(dest_path)
|
||||
return "Movimiento completado."
|
||||
|
||||
@mcp.tool(description="Elimina todos los archivos y subcarpetas dentro de una carpeta del sandbox.")
|
||||
def clear_folder(path: str) -> str:
|
||||
folder_path = safe_path(path)
|
||||
if not folder_path.is_dir():
|
||||
raise NotADirectoryError("La ruta no es una carpeta.")
|
||||
for item in folder_path.iterdir():
|
||||
if item.is_file() or item.is_symlink():
|
||||
item.unlink()
|
||||
elif item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
return "Carpeta vaciada."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4201, path="/fs")
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.tool(description="Suma dos números enteros.")
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
@mcp.tool(description="Resta dos números enteros.")
|
||||
def subtract(a: int, b: int) -> int:
|
||||
return a - b
|
||||
|
||||
@mcp.tool(description="Multiplica dos números enteros.")
|
||||
def multiply(a: int, b: int) -> int:
|
||||
return a * b
|
||||
|
||||
@mcp.tool(description="Divide dos números y devuelve el resultado flotante.")
|
||||
def divide(a: float, b: float) -> float:
|
||||
if b == 0:
|
||||
raise ValueError("No se puede dividir entre cero.")
|
||||
return a / b
|
||||
|
||||
@mcp.tool(description="Calcula el módulo de dos números enteros.")
|
||||
def modulo(a: int, b: int) -> int:
|
||||
return a % b
|
||||
|
||||
@mcp.tool(description="Concatena dos cadenas de texto.")
|
||||
def concat(a: str, b: str) -> str:
|
||||
return a + b
|
||||
|
||||
@mcp.tool(description="Devuelve la longitud de una cadena.")
|
||||
def string_length(s: str) -> int:
|
||||
return len(s)
|
||||
|
||||
@mcp.tool(description="Convierte una cadena a mayúsculas.")
|
||||
def to_upper(s: str) -> str:
|
||||
return s.upper()
|
||||
|
||||
@mcp.tool(description="Convierte una cadena a minúsculas.")
|
||||
def to_lower(s: str) -> str:
|
||||
return s.lower()
|
||||
|
||||
@mcp.tool(description="Devuelve la suma de todos los elementos en una lista de enteros.")
|
||||
def sum_list(numbers: list[int]) -> int:
|
||||
return sum(numbers)
|
||||
|
||||
@mcp.tool(description="Devuelve el valor máximo en una lista de enteros.")
|
||||
def max_in_list(numbers: list[int]) -> int:
|
||||
return max(numbers)
|
||||
|
||||
@mcp.tool(description="Verifica si un número es par.")
|
||||
def is_even(n: int) -> bool:
|
||||
return n % 2 == 0
|
||||
|
||||
@mcp.tool(description="Verifica si una cadena es un palíndromo.")
|
||||
def is_palindrome(s: str) -> bool:
|
||||
return s == s[::-1]
|
||||
|
||||
@mcp.tool(description="Calcula el factorial de un número entero positivo.")
|
||||
def factorial(n: int) -> int:
|
||||
if n < 0:
|
||||
raise ValueError("El factorial no está definido para negativos.")
|
||||
if n == 0:
|
||||
return 1
|
||||
result = 1
|
||||
for i in range(1, n + 1):
|
||||
result *= i
|
||||
return result
|
||||
|
||||
@mcp.tool(description="Devuelve los primeros n números de Fibonacci.")
|
||||
def fibonacci(n: int) -> list[int]:
|
||||
if n <= 0:
|
||||
return []
|
||||
seq = [0, 1]
|
||||
while len(seq) < n:
|
||||
seq.append(seq[-1] + seq[-2])
|
||||
return seq[:n]
|
||||
|
||||
@mcp.tool(description="Devuelve si un número es primo.")
|
||||
def is_prime(n: int) -> bool:
|
||||
if n <= 1:
|
||||
return False
|
||||
for i in range(2, int(n**0.5) + 1):
|
||||
if n % i == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
|
||||
|
||||
# mcp.run(transport="stdio")
|
||||
@@ -0,0 +1,69 @@
|
||||
from fastmcp import FastMCP
|
||||
import uuid
|
||||
import datetime
|
||||
import socket
|
||||
import platform
|
||||
import os
|
||||
|
||||
mcp = FastMCP()
|
||||
|
||||
@mcp.tool(description="Genera un UUID versión 4.")
|
||||
def generate_uuid() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
@mcp.tool(description="Devuelve la fecha y hora actuales en formato ISO 8601.")
|
||||
def current_datetime() -> str:
|
||||
return datetime.datetime.now().isoformat()
|
||||
|
||||
@mcp.tool(description="Devuelve solo la fecha actual.")
|
||||
def current_date() -> str:
|
||||
return datetime.date.today().isoformat()
|
||||
|
||||
@mcp.tool(description="Devuelve el nombre del host actual.")
|
||||
def get_hostname() -> str:
|
||||
return socket.gethostname()
|
||||
|
||||
@mcp.tool(description="Devuelve el sistema operativo actual.")
|
||||
def get_os() -> str:
|
||||
return platform.system()
|
||||
|
||||
@mcp.tool(description="Devuelve el nombre del usuario actual del sistema.")
|
||||
def get_current_user() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
@mcp.tool(description="Invierte un valor booleano.")
|
||||
def invert_boolean(flag: bool) -> bool:
|
||||
return not flag
|
||||
|
||||
# @mcp.tool(description="Devuelve los archivos y carpetas del directorio actual.")
|
||||
# def list_current_directory() -> list[str]:
|
||||
# return os.listdir()
|
||||
|
||||
# @mcp.tool(description="Crea un archivo con un nombre dado.")
|
||||
# def create_file(filename: str) -> str:
|
||||
# with open(filename, "w") as f:
|
||||
# f.write("")
|
||||
# return f"Archivo '{filename}' creado."
|
||||
|
||||
# @mcp.tool(description="Lee el contenido de un archivo de texto dado.")
|
||||
# def read_file(filename: str) -> str:
|
||||
# with open(filename, "r") as f:
|
||||
# return f.read()
|
||||
|
||||
# @mcp.tool(description="Escribe contenido a un archivo, sobrescribiéndolo.")
|
||||
# def write_file(filename: str, content: str) -> str:
|
||||
# with open(filename, "w") as f:
|
||||
# f.write(content)
|
||||
# return f"Contenido escrito en '{filename}'."
|
||||
|
||||
@mcp.tool(description="Devuelve el número de CPUs disponibles en el sistema.")
|
||||
def get_cpu_count() -> int:
|
||||
return os.cpu_count()
|
||||
|
||||
@mcp.tool(description="Devuelve el timestamp actual (UNIX).")
|
||||
def current_timestamp() -> float:
|
||||
return datetime.datetime.now().timestamp()
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Literal
|
||||
|
||||
class MemoryConvABC(ABC):
|
||||
"""
|
||||
Interfaz abstracta para memorias de conversación multi-turno.
|
||||
"""
|
||||
|
||||
def __init__(self, k: int = 10):
|
||||
"""
|
||||
:param k: Número máximo de turnos (pares user/assistant) a retornar.
|
||||
"""
|
||||
self.k = k
|
||||
|
||||
@abstractmethod
|
||||
def guardar_turno(self, rol: Literal["user", "assistant"], contenido: str) -> None:
|
||||
"""
|
||||
Guarda un mensaje de un turno de conversación.
|
||||
Se guarda secuencialmente y se asume que el orden es respetado.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cargar_historial_chat(self) -> list[dict]:
|
||||
"""
|
||||
Devuelve los últimos `k*2` mensajes como lista de dicts tipo chat:
|
||||
[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def limpiar(self) -> None:
|
||||
"""
|
||||
Limpia completamente la memoria.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,52 @@
|
||||
from sqlalchemy import Table, Column, Integer, String, MetaData, insert, select, delete
|
||||
from typing import Literal
|
||||
|
||||
from domains.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from domains.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
|
||||
from domains.Llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||
|
||||
|
||||
class MemoryConvPostgres(MemoryConvABC):
|
||||
def __init__(self, nombre_tabla: str, k: int, credencial: PostgresCredencial):
|
||||
"""
|
||||
:param nombre_tabla: Nombre de la tabla en PostgreSQL.
|
||||
:param k: Número de pares user/assistant a recuperar.
|
||||
:param credencial: Credencial para conectar a PostgreSQL.
|
||||
"""
|
||||
super().__init__(k=k)
|
||||
self.conexion = PostgresConexion(credencial) # Se instancia directamente con la credencial
|
||||
self.nombre_tabla = nombre_tabla
|
||||
|
||||
self.metadata = MetaData()
|
||||
self.tabla = Table(
|
||||
self.nombre_tabla,
|
||||
self.metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("rol", String, nullable=False),
|
||||
Column("contenido", String, nullable=False),
|
||||
)
|
||||
|
||||
# Crea la tabla si no existe
|
||||
self.metadata.create_all(self.conexion._engine)
|
||||
|
||||
def guardar_turno(self, rol: Literal["user", "assistant"], contenido: str) -> None:
|
||||
stmt = insert(self.tabla).values(rol=rol, contenido=contenido)
|
||||
with self.conexion.get_session() as session:
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
|
||||
def cargar_historial_chat(self) -> list[dict]:
|
||||
stmt = (
|
||||
select(self.tabla.c.rol, self.tabla.c.contenido)
|
||||
.order_by(self.tabla.c.id.desc())
|
||||
.limit(self.k * 2)
|
||||
)
|
||||
with self.conexion.get_session() as session:
|
||||
result = session.execute(stmt).fetchall()
|
||||
return [{"role": r.rol, "content": r.contenido} for r in reversed(result)]
|
||||
|
||||
def limpiar(self) -> None:
|
||||
stmt = delete(self.tabla)
|
||||
with self.conexion.get_session() as session:
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
@@ -0,0 +1,30 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class ModeloABC(ABC):
|
||||
"""
|
||||
Clase base para definir la interfaz de un modelo conversacional.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
temperature: float = 0.7,
|
||||
top_p: float = 1.0,
|
||||
top_k: int = None,
|
||||
frecuencia_penalizacion: float = 0.0,
|
||||
num_tokens_maximos: int = 512
|
||||
):
|
||||
self.model = model
|
||||
self.temperature = temperature
|
||||
self.top_p = top_p
|
||||
self.top_k = top_k
|
||||
self.frecuencia_penalizacion = frecuencia_penalizacion
|
||||
self.num_tokens_maximos = num_tokens_maximos
|
||||
|
||||
@abstractmethod
|
||||
async def responder(self, prompt: str, system_prompt: str = "", stream: bool = False, **kwargs) -> str:
|
||||
"""
|
||||
Devuelve una respuesta a partir de un prompt y configuración del modelo.
|
||||
Este método debe implementarse de forma asíncrona en las subclases.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,67 @@
|
||||
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
from typing import AsyncGenerator, Union
|
||||
from domains.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
|
||||
@@ -0,0 +1,90 @@
|
||||
from domains.Llms.Modelos.Base_model import ModeloABC
|
||||
from domains.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
from domains.Security.GenerarIDs import GeneradorIDUnico
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, Union
|
||||
|
||||
class ModeloOpenAI(ModeloABC):
|
||||
def __init__(
|
||||
self,
|
||||
cliente: OpenAICliente,
|
||||
model: str = "gpt-4o",
|
||||
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,
|
||||
use_legacy: bool = False
|
||||
):
|
||||
# Generar ID con prefijo MOPA si no fue proporcionado
|
||||
self.id = id if id is not None else GeneradorIDUnico("MOPA").generar()
|
||||
|
||||
# Inicializar resto del modelo base
|
||||
super().__init__(
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
top_k=top_k,
|
||||
frecuencia_penalizacion=frecuencia_penalizacion,
|
||||
num_tokens_maximos=num_tokens_maximos
|
||||
)
|
||||
|
||||
# Asignar cliente e indicadores adicionales
|
||||
self.cliente = cliente
|
||||
self.use_legacy = use_legacy
|
||||
|
||||
async def responder(
|
||||
self,
|
||||
prompt: str,
|
||||
system_prompt: str = "",
|
||||
stream: bool = False,
|
||||
**kwargs
|
||||
) -> Union[str, AsyncGenerator[str, None]]:
|
||||
if self.use_legacy:
|
||||
if stream:
|
||||
raise NotImplementedError("El modo legacy no soporta streaming.")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
respuesta = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self.cliente.completion(
|
||||
model=self.model,
|
||||
prompt=prompt,
|
||||
temperature=self.temperature,
|
||||
top_p=self.top_p,
|
||||
max_tokens=self.num_tokens_maximos,
|
||||
frequency_penalty=self.frecuencia_penalizacion,
|
||||
**kwargs
|
||||
)
|
||||
)
|
||||
return respuesta.choices[0].text.strip()
|
||||
|
||||
# Construcción de mensajes estilo Chat
|
||||
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
|
||||
@@ -0,0 +1,122 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean
|
||||
|
||||
from domains.ArquitectureLayer.Mapper import Mapper_base
|
||||
from domains.ArquitectureLayer.Model import Model_base
|
||||
from domains.ArquitectureLayer.Repo import Repo_base
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from domains.ConexionSql.Base_conexion import ConexionBase
|
||||
from domains.base import Base
|
||||
from domains.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
|
||||
|
||||
# ----------------------
|
||||
# Cargar clave maestra
|
||||
# ----------------------
|
||||
from entrypoint import ENV_PATH
|
||||
load_dotenv(ENV_PATH)
|
||||
pssword = os.getenv('MASTER_PASSWORD')
|
||||
if pssword is None:
|
||||
raise ValueError("MASTER_PASSWORD no está definida en el archivo .env")
|
||||
|
||||
# ----------------------
|
||||
# MODELO (SQLAlchemy)
|
||||
# ----------------------
|
||||
|
||||
class ModeloOpenAIConfigModel(Base, Model_base):
|
||||
__tablename__ = 'modelo_openai_configs'
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
|
||||
model = Column(String, nullable=False)
|
||||
temperature = Column(Float, default=0.7, nullable=False)
|
||||
top_p = Column(Float, default=1.0, nullable=False)
|
||||
top_k = Column(Integer, nullable=True)
|
||||
|
||||
frecuencia_penalizacion = Column(Float, default=0.0, nullable=False)
|
||||
num_tokens_maximos = Column(Integer, default=512, nullable=False)
|
||||
|
||||
use_legacy = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# ----------------------
|
||||
# MAPPER
|
||||
# ----------------------
|
||||
|
||||
class ModeloOpenAIConfigMapper(Mapper_base[ModeloOpenAI, ModeloOpenAIConfigModel]):
|
||||
|
||||
@staticmethod
|
||||
def to_model(obj: ModeloOpenAI) -> ModeloOpenAIConfigModel:
|
||||
return ModeloOpenAIConfigModel(
|
||||
id=obj.id,
|
||||
model=obj.model,
|
||||
temperature=obj.temperature,
|
||||
top_p=obj.top_p,
|
||||
top_k=obj.top_k,
|
||||
frecuencia_penalizacion=obj.frecuencia_penalizacion,
|
||||
num_tokens_maximos=obj.num_tokens_maximos,
|
||||
use_legacy=obj.use_legacy
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_model(model: ModeloOpenAIConfigModel, cliente: Optional[object] = None) -> ModeloOpenAI:
|
||||
return ModeloOpenAI(
|
||||
id=model.id,
|
||||
cliente=cliente,
|
||||
model=model.model,
|
||||
temperature=model.temperature,
|
||||
top_p=model.top_p,
|
||||
top_k=model.top_k,
|
||||
frecuencia_penalizacion=model.frecuencia_penalizacion,
|
||||
num_tokens_maximos=model.num_tokens_maximos,
|
||||
use_legacy=model.use_legacy
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_dict(obj: ModeloOpenAI) -> dict:
|
||||
return {
|
||||
"id": obj.id,
|
||||
"model": obj.model,
|
||||
"temperature": obj.temperature,
|
||||
"top_p": obj.top_p,
|
||||
"top_k": obj.top_k,
|
||||
"frecuencia_penalizacion": obj.frecuencia_penalizacion,
|
||||
"num_tokens_maximos": obj.num_tokens_maximos,
|
||||
"use_legacy": obj.use_legacy
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict, cliente: Optional[object] = None) -> ModeloOpenAI:
|
||||
return ModeloOpenAI(
|
||||
id=data["id"],
|
||||
cliente=cliente,
|
||||
model=data["model"],
|
||||
temperature=data["temperature"],
|
||||
top_p=data["top_p"],
|
||||
top_k=data["top_k"],
|
||||
frecuencia_penalizacion=data["frecuencia_penalizacion"],
|
||||
num_tokens_maximos=data["num_tokens_maximos"],
|
||||
use_legacy=data["use_legacy"]
|
||||
)
|
||||
|
||||
# ----------------------
|
||||
# REPO
|
||||
# ----------------------
|
||||
|
||||
class ModeloOpenAIConfigRepo(Repo_base[ModeloOpenAIConfigModel, ModeloOpenAI]):
|
||||
def __init__(self, conexion: ConexionBase, cliente: object):
|
||||
super().__init__(
|
||||
session=conexion.get_session(),
|
||||
modelo=ModeloOpenAIConfigModel,
|
||||
mapper=ModeloOpenAIConfigMapper
|
||||
)
|
||||
self.cliente = cliente # Necesario para construir el dominio con lógica
|
||||
|
||||
def get_by_id(self, id_: str) -> ModeloOpenAI | None:
|
||||
model = self.session.get(self.Modelo, id_)
|
||||
return self.Mapper.from_model(model, self.cliente) if model else None
|
||||
|
||||
def get_all(self) -> list[ModeloOpenAI]:
|
||||
models = self.session.query(self.Modelo).all()
|
||||
return [self.Mapper.from_model(m, self.cliente) for m in models]
|
||||
Reference in New Issue
Block a user