Primer commit
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
from llms.Modelos.Base_model import ModeloABC
|
||||
from llms.Memory.Base_MemoryConv import MemoryConvABC
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Union
|
||||
|
||||
|
||||
class AgenteAI:
|
||||
def __init__(
|
||||
self,
|
||||
modelo: ModeloABC,
|
||||
nombre: str,
|
||||
descripcion: str,
|
||||
system_prompt: str, # <- pertenece al agente
|
||||
rol: str,
|
||||
objetivos: List[str],
|
||||
memoria: Optional[MemoryConvABC] = None,
|
||||
version: str = "1.0.0",
|
||||
tools: Optional[List] = None,
|
||||
output_schema: Optional[dict] = None, # <- nuevo parámetro
|
||||
):
|
||||
self.modelo = modelo
|
||||
self.memoria = memoria
|
||||
self.tools = tools or []
|
||||
self.output_schema = output_schema
|
||||
|
||||
self.nombre = nombre
|
||||
self.descripcion = descripcion
|
||||
self.system_prompt = system_prompt
|
||||
self.rol = rol
|
||||
self.objetivos = objetivos
|
||||
self.version = version
|
||||
|
||||
self.created_at = datetime.now()
|
||||
self.updated_at = self.created_at
|
||||
self.numero_interacciones = 0
|
||||
|
||||
def actualizar_configuracion(self, **kwargs):
|
||||
for clave, valor in kwargs.items():
|
||||
if hasattr(self, clave):
|
||||
setattr(self, clave, valor)
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
@property
|
||||
def full_system_prompt(self) -> str:
|
||||
partes = [
|
||||
f"Tu nombre es: {self.nombre}",
|
||||
f"Tu descripción: {self.descripcion}",
|
||||
f"Tu Rol: {self.rol}",
|
||||
f"Tus Objetivos: {', '.join(self.objetivos)}",
|
||||
""
|
||||
]
|
||||
|
||||
# Incluir herramientas disponibles
|
||||
herramientas = self._obtener_descripcion_tools()
|
||||
if herramientas:
|
||||
partes.append("Estas son tus herramientas disponibles:\n")
|
||||
partes.extend(herramientas)
|
||||
partes.append("Úsalas cuando creas oportuno.\n")
|
||||
|
||||
partes.append(self.system_prompt)
|
||||
|
||||
if self.output_schema:
|
||||
partes.append(
|
||||
"SIEMPRE formatea la respuesta final siguiendo estrictamente el siguiente esquema JSON:"
|
||||
)
|
||||
partes.append(f"```json\n{self.output_schema}\n```")
|
||||
|
||||
return "\n".join(partes)
|
||||
|
||||
def _obtener_descripcion_tools(self) -> List[str]:
|
||||
"""
|
||||
Devuelve una lista de strings con el nombre y descripción de cada herramienta
|
||||
recogida desde los servidores MCP conectados.
|
||||
"""
|
||||
descripciones = []
|
||||
if not hasattr(self, "mcp_servers"):
|
||||
return descripciones
|
||||
|
||||
for server in self.mcp_servers:
|
||||
if hasattr(server, "tools") and server.tools:
|
||||
for tool in server.tools:
|
||||
# tool puede ser string o dict, depende cómo lo expongas
|
||||
if isinstance(tool, str):
|
||||
descripciones.append(f"- {tool}: [sin descripción]")
|
||||
elif isinstance(tool, dict):
|
||||
nombre = tool.get("name", "¿?")
|
||||
descripcion = tool.get("description", "[sin descripción]")
|
||||
descripciones.append(f"- {nombre}: {descripcion}")
|
||||
elif hasattr(tool, "name"):
|
||||
descripcion = getattr(tool, "description", "[sin descripción]")
|
||||
descripciones.append(f"- {tool.name}: {descripcion}")
|
||||
return descripciones
|
||||
|
||||
async def interactuar(self, prompt: str) -> str:
|
||||
if not self.modelo:
|
||||
raise ValueError("El agente no tiene un modelo asignado.")
|
||||
|
||||
historial = []
|
||||
if self.memoria:
|
||||
historial = self.memoria.cargar_historial_chat()
|
||||
|
||||
contexto = historial + [{"role": "user", "content": prompt}]
|
||||
prompt_final = self._formatear_prompt(contexto)
|
||||
|
||||
# Espera la respuesta del modelo de forma asíncrona
|
||||
respuesta = await self.modelo.responder(
|
||||
prompt=prompt_final,
|
||||
system_prompt=self.full_system_prompt
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def _formatear_prompt(self, mensajes: List[dict]) -> str:
|
||||
return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes])
|
||||
@@ -0,0 +1,65 @@
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Optional, List, Dict
|
||||
from contextlib import AsyncExitStack
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.types import Tool
|
||||
|
||||
class MCPStdioServer:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
command: str,
|
||||
args: List[str],
|
||||
env: Optional[Dict[str, str]] = None
|
||||
):
|
||||
self.name = name
|
||||
self.command = command
|
||||
self.args = args
|
||||
self.env = env or os.environ.copy()
|
||||
self.exit_stack = AsyncExitStack()
|
||||
self.session: Optional[ClientSession] = None
|
||||
self.tools: List[Tool] = []
|
||||
|
||||
async def start(self):
|
||||
# Configurar el bucle de eventos Proactor en Windows si es necesario
|
||||
if os.name == "nt":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||
|
||||
server_params = StdioServerParameters(
|
||||
command=self.command,
|
||||
args=self.args,
|
||||
env=self.env
|
||||
)
|
||||
|
||||
# Iniciar el transporte y establecer la sesión
|
||||
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||
read, write = stdio_transport
|
||||
|
||||
self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
||||
await self.session.initialize()
|
||||
response = await self.session.list_tools()
|
||||
self.tools = response.tools
|
||||
|
||||
if self.tools:
|
||||
print(f"[{self.name}] Servidor iniciado con herramientas:")
|
||||
for tool in self.tools:
|
||||
nombre = getattr(tool, "name", "[sin nombre]")
|
||||
descripcion = getattr(tool, "description", "[sin descripción]")
|
||||
print(f" - {nombre} - {descripcion}")
|
||||
else:
|
||||
print(f"[{self.name}] Servidor iniciado, pero no se detectaron herramientas.")
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: Dict):
|
||||
if not self.session:
|
||||
raise RuntimeError("La sesión no está inicializada.")
|
||||
result = await self.session.call_tool(tool_name, arguments)
|
||||
return result.content
|
||||
|
||||
def get_tool_names(self) -> List[str]:
|
||||
return [tool.name for tool in self.tools]
|
||||
|
||||
async def stop(self):
|
||||
await self.exit_stack.aclose()
|
||||
print(f"[{self.name}] Servidor detenido.")
|
||||
@@ -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 backend.Credenciales.postgres_credencial import PostgresCredencial
|
||||
from backend.ConexionSql.Postgres_conexion import PostgresConexion # Usamos la clase específica
|
||||
from 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 = "", **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,64 @@
|
||||
from llms.Modelos.Base_model import ModeloABC
|
||||
from backend.ConexionApis.OpenAi_conexion import OpenAICliente
|
||||
|
||||
class ModeloOpenAI(ModeloABC):
|
||||
def __init__(
|
||||
self,
|
||||
cliente: OpenAICliente,
|
||||
model: str = "gpt-4o",
|
||||
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
|
||||
):
|
||||
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
|
||||
self.use_legacy = use_legacy
|
||||
|
||||
async def responder(self, prompt: str, system_prompt: str = "", **kwargs) -> str:
|
||||
import asyncio
|
||||
|
||||
if self.use_legacy:
|
||||
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()
|
||||
else:
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
respuesta = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: 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,
|
||||
**kwargs
|
||||
)
|
||||
)
|
||||
return respuesta.choices[0].message.content
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean
|
||||
|
||||
from backend.ConexionSql.Base_conexion import ConexionBase
|
||||
from backend.base import Base
|
||||
from 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):
|
||||
__tablename__ = 'modelo_openai_configs'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
model = Column(String, nullable=False)
|
||||
temperature = Column(Float, default=0.7)
|
||||
top_p = Column(Float, default=1.0)
|
||||
top_k = Column(Integer, nullable=True)
|
||||
frecuencia_penalizacion = Column(Float, default=0.0)
|
||||
num_tokens_maximos = Column(Integer, default=512)
|
||||
use_legacy = Column(Boolean, default=False)
|
||||
|
||||
# ----------------------
|
||||
# MAPPER
|
||||
# ----------------------
|
||||
|
||||
class ModeloOpenAIConfigMapper:
|
||||
@staticmethod
|
||||
def to_dict(obj: ModeloOpenAI) -> dict:
|
||||
return {
|
||||
"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: object) -> ModeloOpenAI:
|
||||
return ModeloOpenAI(
|
||||
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
|
||||
)
|
||||
|
||||
# ----------------------
|
||||
# REPO
|
||||
# ----------------------
|
||||
|
||||
class ModeloOpenAIConfigRepo:
|
||||
def __init__(self, conexion: ConexionBase, cliente: object):
|
||||
self.session = conexion.get_session()
|
||||
self.cliente = cliente # Necesario para crear ModeloOpenAI
|
||||
|
||||
def add(self, config: ModeloOpenAI) -> int:
|
||||
data = ModeloOpenAIConfigMapper.to_dict(config)
|
||||
model = ModeloOpenAIConfigModel(**data)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return model.id
|
||||
|
||||
def get_by_id(self, id_: int) -> ModeloOpenAI | None:
|
||||
model = self.session.get(ModeloOpenAIConfigModel, id_)
|
||||
return ModeloOpenAIConfigMapper.from_model(model, self.cliente) if model else None
|
||||
|
||||
def get_all(self) -> list[ModeloOpenAI]:
|
||||
models = self.session.query(ModeloOpenAIConfigModel).all()
|
||||
return [ModeloOpenAIConfigMapper.from_model(m, self.cliente) for m in models]
|
||||
Reference in New Issue
Block a user