Primer commit

This commit is contained in:
2025-05-05 02:21:55 +02:00
commit 7b6f525809
62 changed files with 78661 additions and 0 deletions
+122
View File
@@ -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])
+65
View File
@@ -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.")
View File
+36
View File
@@ -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
+52
View File
@@ -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()
+30
View File
@@ -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
+64
View File
@@ -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
+86
View File
@@ -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]
View File
View File