Initial commit - Captain Claude multiagent system
- Core captain_claude.py orchestrator - Context manager with SQL schemas - Provider adapters (Anthropic, OpenAI) - Execution scripts
This commit is contained in:
25
context-manager/src/__init__.py
Normal file
25
context-manager/src/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Context Manager - Sistema local de gestión de contexto para IA
|
||||
|
||||
Características:
|
||||
- Log inmutable (tabla de referencia no editable)
|
||||
- Gestor de contexto mejorable
|
||||
- Agnóstico al modelo de IA
|
||||
- Sistema de métricas para mejora continua
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "TZZR System"
|
||||
|
||||
from .database import Database
|
||||
from .context_selector import ContextSelector
|
||||
from .models import Session, Message, ContextBlock, Algorithm
|
||||
|
||||
__all__ = [
|
||||
"Database",
|
||||
"ContextSelector",
|
||||
"Session",
|
||||
"Message",
|
||||
"ContextBlock",
|
||||
"Algorithm",
|
||||
]
|
||||
508
context-manager/src/context_selector.py
Normal file
508
context-manager/src/context_selector.py
Normal file
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
Selector de Contexto - Motor principal
|
||||
|
||||
Selecciona el contexto óptimo para enviar al modelo de IA
|
||||
basándose en el algoritmo activo y las fuentes disponibles.
|
||||
"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any, Callable
|
||||
|
||||
from .models import (
|
||||
Session, Message, MessageRole, ContextBlock, Memory,
|
||||
Knowledge, Algorithm, AmbientContext, ContextItem,
|
||||
SelectedContext, ContextSource
|
||||
)
|
||||
from .database import Database
|
||||
|
||||
|
||||
class ContextSelector:
|
||||
"""
|
||||
Motor de selección de contexto.
|
||||
|
||||
Características:
|
||||
- Agnóstico al modelo de IA
|
||||
- Basado en algoritmo configurable
|
||||
- Métricas de rendimiento
|
||||
- Soporte para múltiples fuentes
|
||||
"""
|
||||
|
||||
def __init__(self, db: Database):
|
||||
self.db = db
|
||||
self._custom_selectors: Dict[str, Callable] = {}
|
||||
|
||||
def register_selector(self, name: str, selector_fn: Callable):
|
||||
"""Registra un selector de contexto personalizado"""
|
||||
self._custom_selectors[name] = selector_fn
|
||||
|
||||
def select_context(
|
||||
self,
|
||||
session: Session,
|
||||
user_message: str,
|
||||
algorithm: Algorithm = None,
|
||||
max_tokens: int = None
|
||||
) -> SelectedContext:
|
||||
"""
|
||||
Selecciona el contexto óptimo para la sesión actual.
|
||||
|
||||
Args:
|
||||
session: Sesión activa
|
||||
user_message: Mensaje del usuario
|
||||
algorithm: Algoritmo a usar (o el activo por defecto)
|
||||
max_tokens: Límite de tokens (sobreescribe config del algoritmo)
|
||||
|
||||
Returns:
|
||||
SelectedContext con los items seleccionados
|
||||
"""
|
||||
# Obtener algoritmo
|
||||
if algorithm is None:
|
||||
algorithm = self.db.get_active_algorithm()
|
||||
|
||||
if algorithm is None:
|
||||
# Algoritmo por defecto si no hay ninguno
|
||||
algorithm = Algorithm(
|
||||
code="DEFAULT",
|
||||
name="Default",
|
||||
config={
|
||||
"max_tokens": 4000,
|
||||
"sources": {
|
||||
"system_prompts": True,
|
||||
"context_blocks": True,
|
||||
"memory": True,
|
||||
"knowledge": True,
|
||||
"history": True,
|
||||
"ambient": True
|
||||
},
|
||||
"weights": {"priority": 0.4, "relevance": 0.3, "recency": 0.2, "frequency": 0.1},
|
||||
"history_config": {"max_messages": 20, "summarize_after": 10, "include_system": False},
|
||||
"memory_config": {"max_items": 15, "min_importance": 30},
|
||||
"knowledge_config": {"max_items": 5, "require_keyword_match": True}
|
||||
}
|
||||
)
|
||||
|
||||
config = algorithm.config
|
||||
token_budget = max_tokens or config.get("max_tokens", 4000)
|
||||
sources = config.get("sources", {})
|
||||
|
||||
# Verificar si hay selector personalizado
|
||||
if algorithm.selector_code and algorithm.code in self._custom_selectors:
|
||||
return self._custom_selectors[algorithm.code](
|
||||
session, user_message, config, self.db
|
||||
)
|
||||
|
||||
# Selección estándar
|
||||
context = SelectedContext(algorithm_id=algorithm.id)
|
||||
composition = {}
|
||||
|
||||
# 1. System prompts y bloques de contexto
|
||||
if sources.get("context_blocks", True):
|
||||
blocks = self._select_context_blocks(user_message, config)
|
||||
for block in blocks:
|
||||
if context.total_tokens + block.tokens_estimated <= token_budget:
|
||||
context.items.append(ContextItem(
|
||||
source=ContextSource.DATASET,
|
||||
content=block.content,
|
||||
tokens=block.tokens_estimated,
|
||||
priority=block.priority,
|
||||
metadata={"block_code": block.code, "category": block.category}
|
||||
))
|
||||
context.total_tokens += block.tokens_estimated
|
||||
|
||||
composition["context_blocks"] = {
|
||||
"count": len([i for i in context.items if i.source == ContextSource.DATASET]),
|
||||
"tokens": sum(i.tokens for i in context.items if i.source == ContextSource.DATASET)
|
||||
}
|
||||
|
||||
# 2. Memoria a largo plazo
|
||||
if sources.get("memory", True):
|
||||
memory_config = config.get("memory_config", {})
|
||||
memories = self._select_memories(
|
||||
user_message,
|
||||
min_importance=memory_config.get("min_importance", 30),
|
||||
max_items=memory_config.get("max_items", 15)
|
||||
)
|
||||
|
||||
memory_tokens = 0
|
||||
memory_count = 0
|
||||
for mem in memories:
|
||||
tokens = len(mem.content) // 4
|
||||
if context.total_tokens + tokens <= token_budget:
|
||||
context.items.append(ContextItem(
|
||||
source=ContextSource.MEMORY,
|
||||
content=f"[Memoria - {mem.type}]: {mem.content}",
|
||||
tokens=tokens,
|
||||
priority=mem.importance,
|
||||
metadata={"memory_type": mem.type, "importance": mem.importance}
|
||||
))
|
||||
context.total_tokens += tokens
|
||||
memory_tokens += tokens
|
||||
memory_count += 1
|
||||
|
||||
composition["memory"] = {"count": memory_count, "tokens": memory_tokens}
|
||||
|
||||
# 3. Base de conocimiento
|
||||
if sources.get("knowledge", True):
|
||||
knowledge_config = config.get("knowledge_config", {})
|
||||
keywords = self._extract_keywords(user_message)
|
||||
|
||||
if keywords or not knowledge_config.get("require_keyword_match", True):
|
||||
knowledge_items = self.db.search_knowledge(
|
||||
keywords=keywords if knowledge_config.get("require_keyword_match", True) else None,
|
||||
limit=knowledge_config.get("max_items", 5)
|
||||
)
|
||||
|
||||
knowledge_tokens = 0
|
||||
knowledge_count = 0
|
||||
for item in knowledge_items:
|
||||
if context.total_tokens + item.tokens_estimated <= token_budget:
|
||||
context.items.append(ContextItem(
|
||||
source=ContextSource.KNOWLEDGE,
|
||||
content=f"[Conocimiento - {item.title}]: {item.content}",
|
||||
tokens=item.tokens_estimated,
|
||||
priority=item.priority,
|
||||
metadata={"title": item.title, "category": item.category}
|
||||
))
|
||||
context.total_tokens += item.tokens_estimated
|
||||
knowledge_tokens += item.tokens_estimated
|
||||
knowledge_count += 1
|
||||
|
||||
composition["knowledge"] = {"count": knowledge_count, "tokens": knowledge_tokens}
|
||||
|
||||
# 4. Contexto ambiental
|
||||
if sources.get("ambient", True):
|
||||
ambient = self.db.get_latest_ambient_context()
|
||||
if ambient:
|
||||
ambient_content = self._format_ambient_context(ambient)
|
||||
tokens = len(ambient_content) // 4
|
||||
if context.total_tokens + tokens <= token_budget:
|
||||
context.items.append(ContextItem(
|
||||
source=ContextSource.AMBIENT,
|
||||
content=ambient_content,
|
||||
tokens=tokens,
|
||||
priority=30,
|
||||
metadata={"captured_at": ambient.captured_at.isoformat()}
|
||||
))
|
||||
context.total_tokens += tokens
|
||||
composition["ambient"] = {"count": 1, "tokens": tokens}
|
||||
|
||||
# 5. Historial de conversación (al final para llenar espacio restante)
|
||||
if sources.get("history", True):
|
||||
history_config = config.get("history_config", {})
|
||||
history = self.db.get_session_history(
|
||||
session.id,
|
||||
limit=history_config.get("max_messages", 20),
|
||||
include_system=history_config.get("include_system", False)
|
||||
)
|
||||
|
||||
history_tokens = 0
|
||||
history_count = 0
|
||||
for msg in history:
|
||||
tokens = len(msg.content) // 4
|
||||
if context.total_tokens + tokens <= token_budget:
|
||||
context.items.append(ContextItem(
|
||||
source=ContextSource.HISTORY,
|
||||
content=msg.content,
|
||||
tokens=tokens,
|
||||
priority=10,
|
||||
metadata={"role": msg.role.value, "sequence": msg.sequence_num}
|
||||
))
|
||||
context.total_tokens += tokens
|
||||
history_tokens += tokens
|
||||
history_count += 1
|
||||
|
||||
composition["history"] = {"count": history_count, "tokens": history_tokens}
|
||||
|
||||
context.composition = composition
|
||||
return context
|
||||
|
||||
def _select_context_blocks(
|
||||
self,
|
||||
user_message: str,
|
||||
config: Dict[str, Any]
|
||||
) -> List[ContextBlock]:
|
||||
"""Selecciona bloques de contexto relevantes"""
|
||||
blocks = self.db.get_active_context_blocks()
|
||||
relevant_blocks = []
|
||||
|
||||
keywords = self._extract_keywords(user_message)
|
||||
|
||||
for block in blocks:
|
||||
rules = block.activation_rules
|
||||
|
||||
# Siempre incluir
|
||||
if rules.get("always", False):
|
||||
relevant_blocks.append(block)
|
||||
continue
|
||||
|
||||
# Verificar keywords
|
||||
block_keywords = rules.get("keywords", [])
|
||||
if block_keywords:
|
||||
if any(kw.lower() in user_message.lower() for kw in block_keywords):
|
||||
relevant_blocks.append(block)
|
||||
continue
|
||||
|
||||
# Verificar categoría system (siempre incluir)
|
||||
if block.category == "system":
|
||||
relevant_blocks.append(block)
|
||||
|
||||
# Ordenar por prioridad
|
||||
relevant_blocks.sort(key=lambda b: b.priority, reverse=True)
|
||||
return relevant_blocks
|
||||
|
||||
def _select_memories(
|
||||
self,
|
||||
user_message: str,
|
||||
min_importance: int = 30,
|
||||
max_items: int = 15
|
||||
) -> List[Memory]:
|
||||
"""Selecciona memorias relevantes"""
|
||||
memories = self.db.get_memories(
|
||||
min_importance=min_importance,
|
||||
limit=max_items * 2 # Obtener más para filtrar
|
||||
)
|
||||
|
||||
# Filtrar por relevancia al mensaje
|
||||
keywords = self._extract_keywords(user_message)
|
||||
if keywords:
|
||||
scored_memories = []
|
||||
for mem in memories:
|
||||
score = sum(1 for kw in keywords if kw.lower() in mem.content.lower())
|
||||
scored_memories.append((mem, score + mem.importance / 100))
|
||||
|
||||
scored_memories.sort(key=lambda x: x[1], reverse=True)
|
||||
return [m[0] for m in scored_memories[:max_items]]
|
||||
|
||||
return memories[:max_items]
|
||||
|
||||
def _extract_keywords(self, text: str) -> List[str]:
|
||||
"""Extrae keywords de un texto"""
|
||||
# Palabras comunes a ignorar
|
||||
stopwords = {
|
||||
"el", "la", "los", "las", "un", "una", "unos", "unas",
|
||||
"de", "del", "al", "a", "en", "con", "por", "para",
|
||||
"que", "qué", "como", "cómo", "donde", "dónde", "cuando", "cuándo",
|
||||
"es", "son", "está", "están", "ser", "estar", "tener", "hacer",
|
||||
"y", "o", "pero", "si", "no", "me", "te", "se", "nos",
|
||||
"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
||||
"have", "has", "had", "do", "does", "did", "will", "would",
|
||||
"can", "could", "should", "may", "might", "must",
|
||||
"and", "or", "but", "if", "then", "else", "when", "where",
|
||||
"what", "which", "who", "whom", "this", "that", "these", "those",
|
||||
"i", "you", "he", "she", "it", "we", "they", "my", "your", "his", "her"
|
||||
}
|
||||
|
||||
# Extraer palabras
|
||||
words = re.findall(r'\b\w+\b', text.lower())
|
||||
|
||||
# Filtrar
|
||||
keywords = [
|
||||
w for w in words
|
||||
if len(w) > 2 and w not in stopwords
|
||||
]
|
||||
|
||||
return list(set(keywords))
|
||||
|
||||
def _format_ambient_context(self, ambient: AmbientContext) -> str:
|
||||
"""Formatea el contexto ambiental como texto"""
|
||||
lines = ["[Contexto del sistema]"]
|
||||
|
||||
env = ambient.environment
|
||||
if env:
|
||||
if env.get("timezone"):
|
||||
lines.append(f"- Zona horaria: {env['timezone']}")
|
||||
if env.get("working_directory"):
|
||||
lines.append(f"- Directorio: {env['working_directory']}")
|
||||
if env.get("git_branch"):
|
||||
lines.append(f"- Git branch: {env['git_branch']}")
|
||||
if env.get("active_project"):
|
||||
lines.append(f"- Proyecto activo: {env['active_project']}")
|
||||
|
||||
state = ambient.system_state
|
||||
if state:
|
||||
if state.get("servers"):
|
||||
servers = state["servers"]
|
||||
online = [k for k, v in servers.items() if v == "online"]
|
||||
if online:
|
||||
lines.append(f"- Servidores online: {', '.join(online)}")
|
||||
|
||||
if state.get("alerts"):
|
||||
alerts = state["alerts"]
|
||||
if alerts:
|
||||
lines.append(f"- Alertas activas: {len(alerts)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class ContextManager:
|
||||
"""
|
||||
Gestor completo de contexto.
|
||||
|
||||
Combina el selector con el logging y métricas.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db: Database = None,
|
||||
host: str = None,
|
||||
port: int = None,
|
||||
database: str = None,
|
||||
user: str = None,
|
||||
password: str = None
|
||||
):
|
||||
if db:
|
||||
self.db = db
|
||||
else:
|
||||
self.db = Database(
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
user=user,
|
||||
password=password
|
||||
)
|
||||
|
||||
self.selector = ContextSelector(self.db)
|
||||
self._current_session: Optional[Session] = None
|
||||
|
||||
def start_session(
|
||||
self,
|
||||
user_id: str = None,
|
||||
instance_id: str = None,
|
||||
model_provider: str = None,
|
||||
model_name: str = None,
|
||||
metadata: Dict[str, Any] = None
|
||||
) -> Session:
|
||||
"""Inicia una nueva sesión"""
|
||||
algorithm = self.db.get_active_algorithm()
|
||||
self._current_session = self.db.create_session(
|
||||
user_id=user_id,
|
||||
instance_id=instance_id,
|
||||
model_provider=model_provider,
|
||||
model_name=model_name,
|
||||
algorithm_id=algorithm.id if algorithm else None,
|
||||
metadata=metadata
|
||||
)
|
||||
return self._current_session
|
||||
|
||||
def get_context_for_message(
|
||||
self,
|
||||
message: str,
|
||||
max_tokens: int = None,
|
||||
session: Session = None
|
||||
) -> SelectedContext:
|
||||
"""Obtiene el contexto para un mensaje"""
|
||||
session = session or self._current_session
|
||||
if not session:
|
||||
raise ValueError("No hay sesión activa. Llama a start_session() primero.")
|
||||
|
||||
return self.selector.select_context(
|
||||
session=session,
|
||||
user_message=message,
|
||||
max_tokens=max_tokens
|
||||
)
|
||||
|
||||
def log_user_message(
|
||||
self,
|
||||
content: str,
|
||||
context: SelectedContext = None,
|
||||
session: Session = None
|
||||
) -> uuid.UUID:
|
||||
"""Registra un mensaje del usuario en el log inmutable"""
|
||||
session = session or self._current_session
|
||||
if not session:
|
||||
raise ValueError("No hay sesión activa.")
|
||||
|
||||
return self.db.insert_log_entry(
|
||||
session_id=session.id,
|
||||
role=MessageRole.USER,
|
||||
content=content,
|
||||
model_provider=session.model_provider,
|
||||
model_name=session.model_name,
|
||||
context_snapshot=context.to_dict() if context else None,
|
||||
context_algorithm_id=context.algorithm_id if context else None,
|
||||
context_tokens_used=context.total_tokens if context else None
|
||||
)
|
||||
|
||||
def log_assistant_message(
|
||||
self,
|
||||
content: str,
|
||||
tokens_input: int = None,
|
||||
tokens_output: int = None,
|
||||
latency_ms: int = None,
|
||||
model_provider: str = None,
|
||||
model_name: str = None,
|
||||
model_params: Dict[str, Any] = None,
|
||||
session: Session = None
|
||||
) -> uuid.UUID:
|
||||
"""Registra una respuesta del asistente en el log inmutable"""
|
||||
session = session or self._current_session
|
||||
if not session:
|
||||
raise ValueError("No hay sesión activa.")
|
||||
|
||||
return self.db.insert_log_entry(
|
||||
session_id=session.id,
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=content,
|
||||
model_provider=model_provider or session.model_provider,
|
||||
model_name=model_name or session.model_name,
|
||||
model_params=model_params,
|
||||
tokens_input=tokens_input,
|
||||
tokens_output=tokens_output,
|
||||
latency_ms=latency_ms
|
||||
)
|
||||
|
||||
def record_metric(
|
||||
self,
|
||||
context: SelectedContext,
|
||||
log_entry_id: uuid.UUID,
|
||||
tokens_budget: int,
|
||||
latency_ms: int = None,
|
||||
model_tokens_input: int = None,
|
||||
model_tokens_output: int = None,
|
||||
session: Session = None
|
||||
) -> uuid.UUID:
|
||||
"""Registra una métrica de uso del algoritmo"""
|
||||
session = session or self._current_session
|
||||
if not session or not context.algorithm_id:
|
||||
return None
|
||||
|
||||
return self.db.record_metric(
|
||||
algorithm_id=context.algorithm_id,
|
||||
session_id=session.id,
|
||||
log_entry_id=log_entry_id,
|
||||
tokens_budget=tokens_budget,
|
||||
tokens_used=context.total_tokens,
|
||||
context_composition=context.composition,
|
||||
latency_ms=latency_ms,
|
||||
model_tokens_input=model_tokens_input,
|
||||
model_tokens_output=model_tokens_output
|
||||
)
|
||||
|
||||
def rate_response(
|
||||
self,
|
||||
metric_id: uuid.UUID,
|
||||
relevance: float = None,
|
||||
quality: float = None,
|
||||
satisfaction: float = None
|
||||
):
|
||||
"""Evalúa una respuesta (feedback manual)"""
|
||||
self.db.update_metric_evaluation(
|
||||
metric_id=metric_id,
|
||||
relevance=relevance,
|
||||
quality=quality,
|
||||
satisfaction=satisfaction,
|
||||
method="user_feedback"
|
||||
)
|
||||
|
||||
def verify_session_integrity(self, session: Session = None) -> Dict[str, Any]:
|
||||
"""Verifica la integridad de la sesión"""
|
||||
session = session or self._current_session
|
||||
if not session:
|
||||
raise ValueError("No hay sesión activa.")
|
||||
|
||||
return self.db.verify_chain_integrity(session.id)
|
||||
|
||||
def close(self):
|
||||
"""Cierra las conexiones"""
|
||||
self.db.close()
|
||||
621
context-manager/src/database.py
Normal file
621
context-manager/src/database.py
Normal file
@@ -0,0 +1,621 @@
|
||||
"""
|
||||
Conexión a base de datos PostgreSQL
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from contextlib import contextmanager
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor, Json
|
||||
from psycopg2 import pool
|
||||
HAS_PSYCOPG2 = True
|
||||
except ImportError:
|
||||
HAS_PSYCOPG2 = False
|
||||
|
||||
from .models import (
|
||||
Session, Message, MessageRole, ContextBlock, Memory,
|
||||
Knowledge, Algorithm, AlgorithmStatus, AlgorithmMetric,
|
||||
AmbientContext
|
||||
)
|
||||
|
||||
|
||||
class Database:
|
||||
"""Gestión de conexión a PostgreSQL"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = None,
|
||||
port: int = None,
|
||||
database: str = None,
|
||||
user: str = None,
|
||||
password: str = None,
|
||||
min_connections: int = 1,
|
||||
max_connections: int = 10
|
||||
):
|
||||
if not HAS_PSYCOPG2:
|
||||
raise ImportError("psycopg2 no está instalado. Ejecuta: pip install psycopg2-binary")
|
||||
|
||||
self.host = host or os.getenv("PGHOST", "localhost")
|
||||
self.port = port or int(os.getenv("PGPORT", "5432"))
|
||||
self.database = database or os.getenv("PGDATABASE", "context_manager")
|
||||
self.user = user or os.getenv("PGUSER", "postgres")
|
||||
self.password = password or os.getenv("PGPASSWORD", "")
|
||||
|
||||
self._pool = pool.ThreadedConnectionPool(
|
||||
min_connections,
|
||||
max_connections,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
database=self.database,
|
||||
user=self.user,
|
||||
password=self.password
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def get_connection(self):
|
||||
"""Obtiene una conexión del pool"""
|
||||
conn = self._pool.getconn()
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
self._pool.putconn(conn)
|
||||
|
||||
@contextmanager
|
||||
def get_cursor(self, dict_cursor: bool = True):
|
||||
"""Obtiene un cursor"""
|
||||
with self.get_connection() as conn:
|
||||
cursor_factory = RealDictCursor if dict_cursor else None
|
||||
with conn.cursor(cursor_factory=cursor_factory) as cur:
|
||||
yield cur
|
||||
|
||||
def close(self):
|
||||
"""Cierra el pool de conexiones"""
|
||||
self._pool.closeall()
|
||||
|
||||
# ==========================================
|
||||
# SESIONES
|
||||
# ==========================================
|
||||
|
||||
def create_session(
|
||||
self,
|
||||
user_id: str = None,
|
||||
instance_id: str = None,
|
||||
model_provider: str = None,
|
||||
model_name: str = None,
|
||||
algorithm_id: uuid.UUID = None,
|
||||
metadata: Dict[str, Any] = None
|
||||
) -> Session:
|
||||
"""Crea una nueva sesión"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT create_session(%s, %s, %s, %s, %s, %s) as id
|
||||
""",
|
||||
(user_id, instance_id, model_provider, model_name,
|
||||
str(algorithm_id) if algorithm_id else None,
|
||||
Json(metadata or {}))
|
||||
)
|
||||
session_id = cur.fetchone()["id"]
|
||||
|
||||
return Session(
|
||||
id=session_id,
|
||||
user_id=user_id,
|
||||
instance_id=instance_id,
|
||||
model_provider=model_provider,
|
||||
model_name=model_name,
|
||||
algorithm_id=algorithm_id,
|
||||
metadata=metadata or {}
|
||||
)
|
||||
|
||||
def get_session(self, session_id: uuid.UUID) -> Optional[Session]:
|
||||
"""Obtiene una sesión por ID"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM sessions WHERE id = %s",
|
||||
(str(session_id),)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return Session(
|
||||
id=row["id"],
|
||||
user_id=row["user_id"],
|
||||
instance_id=row["instance_id"],
|
||||
model_provider=row["initial_model_provider"],
|
||||
model_name=row["initial_model_name"],
|
||||
algorithm_id=row["initial_context_algorithm_id"],
|
||||
metadata=row["metadata"] or {},
|
||||
started_at=row["started_at"],
|
||||
ended_at=row["ended_at"],
|
||||
total_messages=row["total_messages"],
|
||||
total_tokens_input=row["total_tokens_input"],
|
||||
total_tokens_output=row["total_tokens_output"]
|
||||
)
|
||||
return None
|
||||
|
||||
# ==========================================
|
||||
# LOG INMUTABLE
|
||||
# ==========================================
|
||||
|
||||
def insert_log_entry(
|
||||
self,
|
||||
session_id: uuid.UUID,
|
||||
role: MessageRole,
|
||||
content: str,
|
||||
model_provider: str = None,
|
||||
model_name: str = None,
|
||||
model_params: Dict[str, Any] = None,
|
||||
context_snapshot: Dict[str, Any] = None,
|
||||
context_algorithm_id: uuid.UUID = None,
|
||||
context_tokens_used: int = None,
|
||||
tokens_input: int = None,
|
||||
tokens_output: int = None,
|
||||
latency_ms: int = None,
|
||||
source_ip: str = None,
|
||||
user_agent: str = None
|
||||
) -> uuid.UUID:
|
||||
"""Inserta una entrada en el log inmutable"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT insert_log_entry(
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||
) as id
|
||||
""",
|
||||
(
|
||||
str(session_id),
|
||||
role.value,
|
||||
content,
|
||||
model_provider,
|
||||
model_name,
|
||||
Json(model_params or {}),
|
||||
Json(context_snapshot) if context_snapshot else None,
|
||||
str(context_algorithm_id) if context_algorithm_id else None,
|
||||
context_tokens_used,
|
||||
tokens_input,
|
||||
tokens_output,
|
||||
latency_ms,
|
||||
source_ip,
|
||||
user_agent
|
||||
)
|
||||
)
|
||||
return cur.fetchone()["id"]
|
||||
|
||||
def get_session_history(
|
||||
self,
|
||||
session_id: uuid.UUID,
|
||||
limit: int = None,
|
||||
include_system: bool = False
|
||||
) -> List[Message]:
|
||||
"""Obtiene el historial de una sesión"""
|
||||
with self.get_cursor() as cur:
|
||||
query = """
|
||||
SELECT * FROM immutable_log
|
||||
WHERE session_id = %s
|
||||
"""
|
||||
params = [str(session_id)]
|
||||
|
||||
if not include_system:
|
||||
query += " AND role != 'system'"
|
||||
|
||||
query += " ORDER BY sequence_num DESC"
|
||||
|
||||
if limit:
|
||||
query += " LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
messages = []
|
||||
for row in reversed(rows):
|
||||
messages.append(Message(
|
||||
id=row["id"],
|
||||
session_id=row["session_id"],
|
||||
sequence_num=row["sequence_num"],
|
||||
role=MessageRole(row["role"]),
|
||||
content=row["content"],
|
||||
hash=row["hash"],
|
||||
hash_anterior=row["hash_anterior"],
|
||||
model_provider=row["model_provider"],
|
||||
model_name=row["model_name"],
|
||||
model_params=row["model_params"] or {},
|
||||
context_snapshot=row["context_snapshot"],
|
||||
context_algorithm_id=row["context_algorithm_id"],
|
||||
context_tokens_used=row["context_tokens_used"],
|
||||
tokens_input=row["tokens_input"],
|
||||
tokens_output=row["tokens_output"],
|
||||
latency_ms=row["latency_ms"],
|
||||
created_at=row["created_at"]
|
||||
))
|
||||
return messages
|
||||
|
||||
def verify_chain_integrity(self, session_id: uuid.UUID) -> Dict[str, Any]:
|
||||
"""Verifica la integridad de la cadena de hashes"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM verify_chain_integrity(%s)",
|
||||
(str(session_id),)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return {
|
||||
"is_valid": row["is_valid"],
|
||||
"broken_at_sequence": row["broken_at_sequence"],
|
||||
"expected_hash": row["expected_hash"],
|
||||
"actual_hash": row["actual_hash"]
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# BLOQUES DE CONTEXTO
|
||||
# ==========================================
|
||||
|
||||
def create_context_block(self, block: ContextBlock) -> uuid.UUID:
|
||||
"""Crea un bloque de contexto"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO context_blocks
|
||||
(code, name, description, content, category, priority, scope, project_id, activation_rules, active)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
block.code, block.name, block.description, block.content,
|
||||
block.category, block.priority, block.scope,
|
||||
str(block.project_id) if block.project_id else None,
|
||||
Json(block.activation_rules), block.active
|
||||
)
|
||||
)
|
||||
return cur.fetchone()["id"]
|
||||
|
||||
def get_active_context_blocks(
|
||||
self,
|
||||
category: str = None,
|
||||
scope: str = None,
|
||||
project_id: uuid.UUID = None
|
||||
) -> List[ContextBlock]:
|
||||
"""Obtiene bloques de contexto activos"""
|
||||
with self.get_cursor() as cur:
|
||||
query = "SELECT * FROM context_blocks WHERE active = true"
|
||||
params = []
|
||||
|
||||
if category:
|
||||
query += " AND category = %s"
|
||||
params.append(category)
|
||||
if scope:
|
||||
query += " AND scope = %s"
|
||||
params.append(scope)
|
||||
if project_id:
|
||||
query += " AND (project_id = %s OR project_id IS NULL)"
|
||||
params.append(str(project_id))
|
||||
|
||||
query += " ORDER BY priority DESC"
|
||||
|
||||
cur.execute(query, params)
|
||||
return [
|
||||
ContextBlock(
|
||||
id=row["id"],
|
||||
code=row["code"],
|
||||
name=row["name"],
|
||||
description=row["description"],
|
||||
content=row["content"],
|
||||
content_hash=row["content_hash"],
|
||||
category=row["category"],
|
||||
priority=row["priority"],
|
||||
tokens_estimated=row["tokens_estimated"],
|
||||
scope=row["scope"],
|
||||
project_id=row["project_id"],
|
||||
activation_rules=row["activation_rules"] or {},
|
||||
active=row["active"],
|
||||
version=row["version"]
|
||||
)
|
||||
for row in cur.fetchall()
|
||||
]
|
||||
|
||||
# ==========================================
|
||||
# MEMORIA
|
||||
# ==========================================
|
||||
|
||||
def get_memories(
|
||||
self,
|
||||
type: str = None,
|
||||
min_importance: int = 0,
|
||||
limit: int = 20
|
||||
) -> List[Memory]:
|
||||
"""Obtiene memorias activas"""
|
||||
with self.get_cursor() as cur:
|
||||
query = """
|
||||
SELECT * FROM memory
|
||||
WHERE active = true
|
||||
AND importance >= %s
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
"""
|
||||
params = [min_importance]
|
||||
|
||||
if type:
|
||||
query += " AND type = %s"
|
||||
params.append(type)
|
||||
|
||||
query += " ORDER BY importance DESC, last_used_at DESC NULLS LAST LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
cur.execute(query, params)
|
||||
return [
|
||||
Memory(
|
||||
id=row["id"],
|
||||
type=row["type"],
|
||||
category=row["category"],
|
||||
content=row["content"],
|
||||
summary=row["summary"],
|
||||
importance=row["importance"],
|
||||
confidence=float(row["confidence"]) if row["confidence"] else 1.0,
|
||||
uses=row["uses"],
|
||||
last_used_at=row["last_used_at"],
|
||||
verified=row["verified"]
|
||||
)
|
||||
for row in cur.fetchall()
|
||||
]
|
||||
|
||||
def save_memory(self, memory: Memory) -> uuid.UUID:
|
||||
"""Guarda una memoria"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO memory
|
||||
(type, category, content, summary, extracted_from_session, importance, confidence, expires_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
memory.type, memory.category, memory.content, memory.summary,
|
||||
str(memory.extracted_from_session) if memory.extracted_from_session else None,
|
||||
memory.importance, memory.confidence, memory.expires_at
|
||||
)
|
||||
)
|
||||
return cur.fetchone()["id"]
|
||||
|
||||
# ==========================================
|
||||
# CONOCIMIENTO
|
||||
# ==========================================
|
||||
|
||||
def search_knowledge(
|
||||
self,
|
||||
keywords: List[str] = None,
|
||||
category: str = None,
|
||||
tags: List[str] = None,
|
||||
limit: int = 5
|
||||
) -> List[Knowledge]:
|
||||
"""Busca en la base de conocimiento"""
|
||||
with self.get_cursor() as cur:
|
||||
query = "SELECT * FROM knowledge_base WHERE active = true"
|
||||
params = []
|
||||
|
||||
if category:
|
||||
query += " AND category = %s"
|
||||
params.append(category)
|
||||
|
||||
if tags:
|
||||
query += " AND tags && %s"
|
||||
params.append(tags)
|
||||
|
||||
if keywords:
|
||||
# Búsqueda simple por contenido
|
||||
keyword_conditions = []
|
||||
for kw in keywords:
|
||||
keyword_conditions.append("content ILIKE %s")
|
||||
params.append(f"%{kw}%")
|
||||
query += f" AND ({' OR '.join(keyword_conditions)})"
|
||||
|
||||
query += " ORDER BY priority DESC, access_count DESC LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
cur.execute(query, params)
|
||||
return [
|
||||
Knowledge(
|
||||
id=row["id"],
|
||||
title=row["title"],
|
||||
category=row["category"],
|
||||
tags=row["tags"] or [],
|
||||
content=row["content"],
|
||||
tokens_estimated=row["tokens_estimated"],
|
||||
priority=row["priority"],
|
||||
access_count=row["access_count"]
|
||||
)
|
||||
for row in cur.fetchall()
|
||||
]
|
||||
|
||||
# ==========================================
|
||||
# CONTEXTO AMBIENTAL
|
||||
# ==========================================
|
||||
|
||||
def get_latest_ambient_context(self) -> Optional[AmbientContext]:
|
||||
"""Obtiene el contexto ambiental más reciente"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT * FROM ambient_context
|
||||
WHERE expires_at > NOW()
|
||||
ORDER BY captured_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return AmbientContext(
|
||||
id=row["id"],
|
||||
captured_at=row["captured_at"],
|
||||
expires_at=row["expires_at"],
|
||||
environment=row["environment"] or {},
|
||||
system_state=row["system_state"] or {},
|
||||
active_resources=row["active_resources"] or []
|
||||
)
|
||||
return None
|
||||
|
||||
def save_ambient_context(self, context: AmbientContext) -> int:
|
||||
"""Guarda un snapshot de contexto ambiental"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO ambient_context
|
||||
(environment, system_state, active_resources, expires_at)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
Json(context.environment),
|
||||
Json(context.system_state),
|
||||
Json(context.active_resources),
|
||||
context.expires_at
|
||||
)
|
||||
)
|
||||
return cur.fetchone()["id"]
|
||||
|
||||
# ==========================================
|
||||
# ALGORITMOS
|
||||
# ==========================================
|
||||
|
||||
def get_active_algorithm(self) -> Optional[Algorithm]:
|
||||
"""Obtiene el algoritmo activo"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT * FROM context_algorithms
|
||||
WHERE status = 'active'
|
||||
ORDER BY activated_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return Algorithm(
|
||||
id=row["id"],
|
||||
code=row["code"],
|
||||
name=row["name"],
|
||||
description=row["description"],
|
||||
version=row["version"],
|
||||
status=AlgorithmStatus(row["status"]),
|
||||
config=row["config"] or {},
|
||||
selector_code=row["selector_code"],
|
||||
times_used=row["times_used"],
|
||||
avg_tokens_used=float(row["avg_tokens_used"]) if row["avg_tokens_used"] else None,
|
||||
avg_relevance_score=float(row["avg_relevance_score"]) if row["avg_relevance_score"] else None,
|
||||
avg_response_quality=float(row["avg_response_quality"]) if row["avg_response_quality"] else None,
|
||||
parent_algorithm_id=row["parent_algorithm_id"],
|
||||
activated_at=row["activated_at"]
|
||||
)
|
||||
return None
|
||||
|
||||
def get_algorithm(self, algorithm_id: uuid.UUID) -> Optional[Algorithm]:
|
||||
"""Obtiene un algoritmo por ID"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM context_algorithms WHERE id = %s",
|
||||
(str(algorithm_id),)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return Algorithm(
|
||||
id=row["id"],
|
||||
code=row["code"],
|
||||
name=row["name"],
|
||||
description=row["description"],
|
||||
version=row["version"],
|
||||
status=AlgorithmStatus(row["status"]),
|
||||
config=row["config"] or {},
|
||||
selector_code=row["selector_code"],
|
||||
times_used=row["times_used"]
|
||||
)
|
||||
return None
|
||||
|
||||
def fork_algorithm(
|
||||
self,
|
||||
source_id: uuid.UUID,
|
||||
new_code: str,
|
||||
new_name: str,
|
||||
reason: str = None
|
||||
) -> uuid.UUID:
|
||||
"""Clona un algoritmo para experimentación"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT fork_algorithm(%s, %s, %s, %s) as id",
|
||||
(str(source_id), new_code, new_name, reason)
|
||||
)
|
||||
return cur.fetchone()["id"]
|
||||
|
||||
def activate_algorithm(self, algorithm_id: uuid.UUID) -> bool:
|
||||
"""Activa un algoritmo"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT activate_algorithm(%s) as success",
|
||||
(str(algorithm_id),)
|
||||
)
|
||||
return cur.fetchone()["success"]
|
||||
|
||||
# ==========================================
|
||||
# MÉTRICAS
|
||||
# ==========================================
|
||||
|
||||
def record_metric(
|
||||
self,
|
||||
algorithm_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
log_entry_id: uuid.UUID,
|
||||
tokens_budget: int,
|
||||
tokens_used: int,
|
||||
context_composition: Dict[str, Any],
|
||||
latency_ms: int = None,
|
||||
model_tokens_input: int = None,
|
||||
model_tokens_output: int = None
|
||||
) -> uuid.UUID:
|
||||
"""Registra una métrica de uso"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT record_algorithm_metric(
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||
) as id
|
||||
""",
|
||||
(
|
||||
str(algorithm_id), str(session_id), str(log_entry_id),
|
||||
tokens_budget, tokens_used, Json(context_composition),
|
||||
latency_ms, model_tokens_input, model_tokens_output
|
||||
)
|
||||
)
|
||||
return cur.fetchone()["id"]
|
||||
|
||||
def update_metric_evaluation(
|
||||
self,
|
||||
metric_id: uuid.UUID,
|
||||
relevance: float = None,
|
||||
quality: float = None,
|
||||
satisfaction: float = None,
|
||||
method: str = "manual"
|
||||
) -> bool:
|
||||
"""Actualiza la evaluación de una métrica"""
|
||||
with self.get_cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT update_metric_evaluation(%s, %s, %s, %s, %s) as success",
|
||||
(str(metric_id), relevance, quality, satisfaction, method)
|
||||
)
|
||||
return cur.fetchone()["success"]
|
||||
|
||||
def get_algorithm_performance(self, algorithm_id: uuid.UUID = None) -> List[Dict[str, Any]]:
|
||||
"""Obtiene estadísticas de rendimiento de algoritmos"""
|
||||
with self.get_cursor() as cur:
|
||||
query = "SELECT * FROM algorithm_performance"
|
||||
params = []
|
||||
|
||||
if algorithm_id:
|
||||
query += " WHERE id = %s"
|
||||
params.append(str(algorithm_id))
|
||||
|
||||
cur.execute(query, params)
|
||||
return [dict(row) for row in cur.fetchall()]
|
||||
309
context-manager/src/models.py
Normal file
309
context-manager/src/models.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Modelos de datos para Context Manager
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
import uuid
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
|
||||
class MessageRole(Enum):
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
SYSTEM = "system"
|
||||
TOOL = "tool"
|
||||
|
||||
|
||||
class ContextSource(Enum):
|
||||
MEMORY = "memory"
|
||||
KNOWLEDGE = "knowledge"
|
||||
HISTORY = "history"
|
||||
AMBIENT = "ambient"
|
||||
DATASET = "dataset"
|
||||
|
||||
|
||||
class AlgorithmStatus(Enum):
|
||||
DRAFT = "draft"
|
||||
TESTING = "testing"
|
||||
ACTIVE = "active"
|
||||
DEPRECATED = "deprecated"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
"""Sesión de conversación"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
user_id: Optional[str] = None
|
||||
instance_id: Optional[str] = None
|
||||
model_provider: Optional[str] = None
|
||||
model_name: Optional[str] = None
|
||||
algorithm_id: Optional[uuid.UUID] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
started_at: datetime = field(default_factory=datetime.now)
|
||||
ended_at: Optional[datetime] = None
|
||||
total_messages: int = 0
|
||||
total_tokens_input: int = 0
|
||||
total_tokens_output: int = 0
|
||||
|
||||
@property
|
||||
def hash(self) -> str:
|
||||
content = f"{self.id}{self.started_at.isoformat()}"
|
||||
return hashlib.sha256(content.encode()).hexdigest()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
"""Mensaje en el log inmutable"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
session_id: uuid.UUID = None
|
||||
sequence_num: int = 0
|
||||
role: MessageRole = MessageRole.USER
|
||||
content: str = ""
|
||||
hash: str = ""
|
||||
hash_anterior: Optional[str] = None
|
||||
|
||||
# Modelo
|
||||
model_provider: Optional[str] = None
|
||||
model_name: Optional[str] = None
|
||||
model_params: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Contexto
|
||||
context_snapshot: Optional[Dict[str, Any]] = None
|
||||
context_algorithm_id: Optional[uuid.UUID] = None
|
||||
context_tokens_used: Optional[int] = None
|
||||
|
||||
# Respuesta
|
||||
tokens_input: Optional[int] = None
|
||||
tokens_output: Optional[int] = None
|
||||
latency_ms: Optional[int] = None
|
||||
|
||||
# Metadata
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
source_ip: Optional[str] = None
|
||||
user_agent: Optional[str] = None
|
||||
|
||||
def compute_hash(self) -> str:
|
||||
"""Calcula el hash del mensaje (blockchain-style)"""
|
||||
content = (
|
||||
(self.hash_anterior or "") +
|
||||
str(self.session_id) +
|
||||
str(self.sequence_num) +
|
||||
self.role.value +
|
||||
self.content
|
||||
)
|
||||
return hashlib.sha256(content.encode()).hexdigest()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextBlock:
|
||||
"""Bloque de contexto reutilizable"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
code: str = ""
|
||||
name: str = ""
|
||||
description: Optional[str] = None
|
||||
content: str = ""
|
||||
content_hash: Optional[str] = None
|
||||
category: str = "general"
|
||||
priority: int = 50
|
||||
tokens_estimated: int = 0
|
||||
scope: str = "global"
|
||||
project_id: Optional[uuid.UUID] = None
|
||||
activation_rules: Dict[str, Any] = field(default_factory=dict)
|
||||
active: bool = True
|
||||
version: int = 1
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def __post_init__(self):
|
||||
self.content_hash = hashlib.sha256(self.content.encode()).hexdigest()
|
||||
self.tokens_estimated = len(self.content) // 4
|
||||
|
||||
|
||||
@dataclass
|
||||
class Memory:
|
||||
"""Memoria a largo plazo"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
type: str = "fact"
|
||||
category: Optional[str] = None
|
||||
content: str = ""
|
||||
summary: Optional[str] = None
|
||||
content_hash: Optional[str] = None
|
||||
extracted_from_session: Optional[uuid.UUID] = None
|
||||
importance: int = 50
|
||||
confidence: float = 1.0
|
||||
uses: int = 0
|
||||
last_used_at: Optional[datetime] = None
|
||||
expires_at: Optional[datetime] = None
|
||||
active: bool = True
|
||||
verified: bool = False
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Knowledge:
|
||||
"""Base de conocimiento"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
title: str = ""
|
||||
category: str = ""
|
||||
tags: List[str] = field(default_factory=list)
|
||||
content: str = ""
|
||||
content_hash: Optional[str] = None
|
||||
tokens_estimated: int = 0
|
||||
source_type: Optional[str] = None
|
||||
source_ref: Optional[str] = None
|
||||
priority: int = 50
|
||||
access_count: int = 0
|
||||
active: bool = True
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Algorithm:
|
||||
"""Algoritmo de selección de contexto"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
code: str = ""
|
||||
name: str = ""
|
||||
description: Optional[str] = None
|
||||
version: str = "1.0.0"
|
||||
status: AlgorithmStatus = AlgorithmStatus.DRAFT
|
||||
config: Dict[str, Any] = field(default_factory=lambda: {
|
||||
"max_tokens": 4000,
|
||||
"sources": {
|
||||
"system_prompts": True,
|
||||
"context_blocks": True,
|
||||
"memory": True,
|
||||
"knowledge": True,
|
||||
"history": True,
|
||||
"ambient": True
|
||||
},
|
||||
"weights": {
|
||||
"priority": 0.4,
|
||||
"relevance": 0.3,
|
||||
"recency": 0.2,
|
||||
"frequency": 0.1
|
||||
},
|
||||
"history_config": {
|
||||
"max_messages": 20,
|
||||
"summarize_after": 10,
|
||||
"include_system": False
|
||||
},
|
||||
"memory_config": {
|
||||
"max_items": 15,
|
||||
"min_importance": 30
|
||||
},
|
||||
"knowledge_config": {
|
||||
"max_items": 5,
|
||||
"require_keyword_match": True
|
||||
}
|
||||
})
|
||||
selector_code: Optional[str] = None
|
||||
times_used: int = 0
|
||||
avg_tokens_used: Optional[float] = None
|
||||
avg_relevance_score: Optional[float] = None
|
||||
avg_response_quality: Optional[float] = None
|
||||
parent_algorithm_id: Optional[uuid.UUID] = None
|
||||
fork_reason: Optional[str] = None
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
activated_at: Optional[datetime] = None
|
||||
deprecated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlgorithmMetric:
|
||||
"""Métrica de rendimiento de algoritmo"""
|
||||
id: uuid.UUID = field(default_factory=uuid.uuid4)
|
||||
algorithm_id: uuid.UUID = None
|
||||
session_id: Optional[uuid.UUID] = None
|
||||
log_entry_id: Optional[uuid.UUID] = None
|
||||
tokens_budget: int = 0
|
||||
tokens_used: int = 0
|
||||
token_efficiency: float = 0.0
|
||||
context_composition: Dict[str, Any] = field(default_factory=dict)
|
||||
latency_ms: Optional[int] = None
|
||||
model_tokens_input: Optional[int] = None
|
||||
model_tokens_output: Optional[int] = None
|
||||
relevance_score: Optional[float] = None
|
||||
response_quality: Optional[float] = None
|
||||
user_satisfaction: Optional[float] = None
|
||||
auto_evaluated: bool = False
|
||||
evaluation_method: Optional[str] = None
|
||||
recorded_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AmbientContext:
|
||||
"""Contexto ambiental del sistema"""
|
||||
id: int = 0
|
||||
captured_at: datetime = field(default_factory=datetime.now)
|
||||
expires_at: Optional[datetime] = None
|
||||
environment: Dict[str, Any] = field(default_factory=dict)
|
||||
system_state: Dict[str, Any] = field(default_factory=dict)
|
||||
active_resources: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextItem:
|
||||
"""Item individual de contexto seleccionado"""
|
||||
source: ContextSource
|
||||
content: str
|
||||
tokens: int
|
||||
priority: int
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelectedContext:
|
||||
"""Contexto seleccionado para enviar al modelo"""
|
||||
items: List[ContextItem] = field(default_factory=list)
|
||||
total_tokens: int = 0
|
||||
algorithm_id: Optional[uuid.UUID] = None
|
||||
composition: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_messages(self) -> List[Dict[str, str]]:
|
||||
"""Convierte el contexto a formato de mensajes para la API"""
|
||||
messages = []
|
||||
|
||||
# System context primero
|
||||
system_content = []
|
||||
for item in self.items:
|
||||
if item.source in [ContextSource.MEMORY, ContextSource.KNOWLEDGE, ContextSource.AMBIENT]:
|
||||
system_content.append(item.content)
|
||||
|
||||
if system_content:
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": "\n\n".join(system_content)
|
||||
})
|
||||
|
||||
# History messages
|
||||
for item in self.items:
|
||||
if item.source == ContextSource.HISTORY:
|
||||
role = item.metadata.get("role", "user")
|
||||
messages.append({
|
||||
"role": role,
|
||||
"content": item.content
|
||||
})
|
||||
|
||||
return messages
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serializa el contexto para snapshot"""
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"source": item.source.value,
|
||||
"content": item.content[:500] + "..." if len(item.content) > 500 else item.content,
|
||||
"tokens": item.tokens,
|
||||
"priority": item.priority,
|
||||
"metadata": item.metadata
|
||||
}
|
||||
for item in self.items
|
||||
],
|
||||
"total_tokens": self.total_tokens,
|
||||
"algorithm_id": str(self.algorithm_id) if self.algorithm_id else None,
|
||||
"composition": self.composition
|
||||
}
|
||||
18
context-manager/src/providers/__init__.py
Normal file
18
context-manager/src/providers/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Adaptadores para proveedores de IA
|
||||
|
||||
Permite usar el Context Manager con cualquier modelo de IA.
|
||||
"""
|
||||
|
||||
from .base import BaseProvider, ProviderResponse
|
||||
from .anthropic import AnthropicProvider
|
||||
from .openai import OpenAIProvider
|
||||
from .ollama import OllamaProvider
|
||||
|
||||
__all__ = [
|
||||
"BaseProvider",
|
||||
"ProviderResponse",
|
||||
"AnthropicProvider",
|
||||
"OpenAIProvider",
|
||||
"OllamaProvider",
|
||||
]
|
||||
110
context-manager/src/providers/anthropic.py
Normal file
110
context-manager/src/providers/anthropic.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Adaptador para Anthropic (Claude)
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from .base import BaseProvider, ProviderResponse
|
||||
from ..models import SelectedContext, ContextSource
|
||||
|
||||
try:
|
||||
import anthropic
|
||||
HAS_ANTHROPIC = True
|
||||
except ImportError:
|
||||
HAS_ANTHROPIC = False
|
||||
|
||||
|
||||
class AnthropicProvider(BaseProvider):
|
||||
"""Proveedor para modelos de Anthropic (Claude)"""
|
||||
|
||||
provider_name = "anthropic"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str = None,
|
||||
model: str = "claude-sonnet-4-20250514",
|
||||
max_tokens: int = 4096,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(api_key=api_key, model=model, **kwargs)
|
||||
|
||||
if not HAS_ANTHROPIC:
|
||||
raise ImportError("anthropic no está instalado. Ejecuta: pip install anthropic")
|
||||
|
||||
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
||||
self.model = model
|
||||
self.max_tokens = max_tokens
|
||||
self.client = anthropic.Anthropic(api_key=self.api_key)
|
||||
|
||||
def format_context(self, context: SelectedContext) -> tuple:
|
||||
"""
|
||||
Formatea el contexto para la API de Anthropic.
|
||||
|
||||
Returns:
|
||||
Tuple de (system_prompt, messages)
|
||||
"""
|
||||
system_parts = []
|
||||
messages = []
|
||||
|
||||
for item in context.items:
|
||||
if item.source in [ContextSource.MEMORY, ContextSource.KNOWLEDGE,
|
||||
ContextSource.AMBIENT, ContextSource.DATASET]:
|
||||
system_parts.append(item.content)
|
||||
elif item.source == ContextSource.HISTORY:
|
||||
role = item.metadata.get("role", "user")
|
||||
messages.append({
|
||||
"role": role,
|
||||
"content": item.content
|
||||
})
|
||||
|
||||
system_prompt = "\n\n".join(system_parts) if system_parts else None
|
||||
return system_prompt, messages
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
message: str,
|
||||
context: SelectedContext = None,
|
||||
system_prompt: str = None,
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> ProviderResponse:
|
||||
"""Envía mensaje a Claude"""
|
||||
|
||||
# Formatear contexto
|
||||
context_system, context_messages = self.format_context(context) if context else (None, [])
|
||||
|
||||
# Combinar system prompts
|
||||
final_system = ""
|
||||
if system_prompt:
|
||||
final_system = system_prompt
|
||||
if context_system:
|
||||
final_system = f"{final_system}\n\n{context_system}" if final_system else context_system
|
||||
|
||||
# Construir mensajes
|
||||
messages = context_messages.copy()
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
# Llamar a la API
|
||||
response, latency_ms = self._measure_latency(
|
||||
self.client.messages.create,
|
||||
model=self.model,
|
||||
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
||||
system=final_system if final_system else anthropic.NOT_GIVEN,
|
||||
messages=messages,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
return ProviderResponse(
|
||||
content=response.content[0].text,
|
||||
model=response.model,
|
||||
tokens_input=response.usage.input_tokens,
|
||||
tokens_output=response.usage.output_tokens,
|
||||
latency_ms=latency_ms,
|
||||
finish_reason=response.stop_reason,
|
||||
raw_response={
|
||||
"id": response.id,
|
||||
"type": response.type,
|
||||
"role": response.role
|
||||
}
|
||||
)
|
||||
85
context-manager/src/providers/base.py
Normal file
85
context-manager/src/providers/base.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Clase base para proveedores de IA
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import time
|
||||
|
||||
from ..models import SelectedContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProviderResponse:
|
||||
"""Respuesta de un proveedor de IA"""
|
||||
content: str
|
||||
model: str
|
||||
tokens_input: int = 0
|
||||
tokens_output: int = 0
|
||||
latency_ms: int = 0
|
||||
finish_reason: str = "stop"
|
||||
raw_response: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class BaseProvider(ABC):
|
||||
"""
|
||||
Clase base para adaptadores de proveedores de IA.
|
||||
|
||||
Cada proveedor debe implementar:
|
||||
- send_message(): Enviar mensaje y recibir respuesta
|
||||
- format_context(): Formatear contexto al formato del proveedor
|
||||
"""
|
||||
|
||||
provider_name: str = "base"
|
||||
|
||||
def __init__(self, api_key: str = None, model: str = None, **kwargs):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.extra_config = kwargs
|
||||
|
||||
@abstractmethod
|
||||
def send_message(
|
||||
self,
|
||||
message: str,
|
||||
context: SelectedContext = None,
|
||||
system_prompt: str = None,
|
||||
**kwargs
|
||||
) -> ProviderResponse:
|
||||
"""
|
||||
Envía un mensaje al modelo y retorna la respuesta.
|
||||
|
||||
Args:
|
||||
message: Mensaje del usuario
|
||||
context: Contexto seleccionado
|
||||
system_prompt: Prompt de sistema adicional
|
||||
**kwargs: Parámetros adicionales del modelo
|
||||
|
||||
Returns:
|
||||
ProviderResponse con la respuesta
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def format_context(self, context: SelectedContext) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Formatea el contexto al formato de mensajes del proveedor.
|
||||
|
||||
Args:
|
||||
context: Contexto seleccionado
|
||||
|
||||
Returns:
|
||||
Lista de mensajes en el formato del proveedor
|
||||
"""
|
||||
pass
|
||||
|
||||
def estimate_tokens(self, text: str) -> int:
|
||||
"""Estimación simple de tokens (4 caracteres por token)"""
|
||||
return len(text) // 4
|
||||
|
||||
def _measure_latency(self, func, *args, **kwargs):
|
||||
"""Mide la latencia de una función"""
|
||||
start = time.time()
|
||||
result = func(*args, **kwargs)
|
||||
latency_ms = int((time.time() - start) * 1000)
|
||||
return result, latency_ms
|
||||
120
context-manager/src/providers/openai.py
Normal file
120
context-manager/src/providers/openai.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Adaptador para OpenAI (GPT)
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from .base import BaseProvider, ProviderResponse
|
||||
from ..models import SelectedContext, ContextSource
|
||||
|
||||
try:
|
||||
import openai
|
||||
HAS_OPENAI = True
|
||||
except ImportError:
|
||||
HAS_OPENAI = False
|
||||
|
||||
|
||||
class OpenAIProvider(BaseProvider):
|
||||
"""Proveedor para modelos de OpenAI (GPT)"""
|
||||
|
||||
provider_name = "openai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str = None,
|
||||
model: str = "gpt-4",
|
||||
max_tokens: int = 4096,
|
||||
base_url: str = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(api_key=api_key, model=model, **kwargs)
|
||||
|
||||
if not HAS_OPENAI:
|
||||
raise ImportError("openai no está instalado. Ejecuta: pip install openai")
|
||||
|
||||
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
|
||||
self.model = model
|
||||
self.max_tokens = max_tokens
|
||||
self.client = openai.OpenAI(
|
||||
api_key=self.api_key,
|
||||
base_url=base_url
|
||||
)
|
||||
|
||||
def format_context(self, context: SelectedContext) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Formatea el contexto para la API de OpenAI.
|
||||
|
||||
Returns:
|
||||
Lista de mensajes en formato OpenAI
|
||||
"""
|
||||
messages = []
|
||||
system_parts = []
|
||||
|
||||
for item in context.items:
|
||||
if item.source in [ContextSource.MEMORY, ContextSource.KNOWLEDGE,
|
||||
ContextSource.AMBIENT, ContextSource.DATASET]:
|
||||
system_parts.append(item.content)
|
||||
elif item.source == ContextSource.HISTORY:
|
||||
role = item.metadata.get("role", "user")
|
||||
messages.append({
|
||||
"role": role,
|
||||
"content": item.content
|
||||
})
|
||||
|
||||
# Insertar system message al inicio
|
||||
if system_parts:
|
||||
messages.insert(0, {
|
||||
"role": "system",
|
||||
"content": "\n\n".join(system_parts)
|
||||
})
|
||||
|
||||
return messages
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
message: str,
|
||||
context: SelectedContext = None,
|
||||
system_prompt: str = None,
|
||||
temperature: float = 1.0,
|
||||
**kwargs
|
||||
) -> ProviderResponse:
|
||||
"""Envía mensaje a GPT"""
|
||||
|
||||
# Formatear contexto
|
||||
messages = self.format_context(context) if context else []
|
||||
|
||||
# Añadir system prompt adicional
|
||||
if system_prompt:
|
||||
if messages and messages[0]["role"] == "system":
|
||||
messages[0]["content"] = f"{system_prompt}\n\n{messages[0]['content']}"
|
||||
else:
|
||||
messages.insert(0, {"role": "system", "content": system_prompt})
|
||||
|
||||
# Añadir mensaje del usuario
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
# Llamar a la API
|
||||
response, latency_ms = self._measure_latency(
|
||||
self.client.chat.completions.create,
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
choice = response.choices[0]
|
||||
|
||||
return ProviderResponse(
|
||||
content=choice.message.content,
|
||||
model=response.model,
|
||||
tokens_input=response.usage.prompt_tokens,
|
||||
tokens_output=response.usage.completion_tokens,
|
||||
latency_ms=latency_ms,
|
||||
finish_reason=choice.finish_reason,
|
||||
raw_response={
|
||||
"id": response.id,
|
||||
"created": response.created,
|
||||
"system_fingerprint": response.system_fingerprint
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user