Add Context Manager client and log schema
- context_manager.py: Full stateless client for Anthropic API - Builds context from cto.blocks + cto.memory + log.messages - Logs all messages to immutable log.messages table - Interactive chat mode with CLI commands - context_bridge.py: PostgreSQL bridge for context queries - Peer auth support for local connections - get_all_relevant_blocks() single query optimization - build_system_prompt() from blocks - 04_log.sql: Immutable log schema - log.messages with hash chain integrity - Triggers preventing UPDATE/DELETE - 07_initial_blocks.sql: Initial context blocks - tzzr_base, rules_base, r2_storage - server_architect, server_deck, server_corp - cm: Launcher script with venv support
This commit is contained in:
14
cm
Executable file
14
cm
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Context Manager CLI
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "CONTEXT MANAGER - TZZR"
|
||||
echo "======================"
|
||||
|
||||
# Activar venv si existe
|
||||
if [ -d "$SCRIPT_DIR/venv" ]; then
|
||||
source "$SCRIPT_DIR/venv/bin/activate"
|
||||
fi
|
||||
|
||||
# Ejecutar
|
||||
exec python3 "$SCRIPT_DIR/context_manager.py" "$@"
|
||||
@@ -1,119 +1,60 @@
|
||||
-- ============================================
|
||||
-- SCHEMA LOG - Sistema TZZR
|
||||
-- Log inmutable de mensajes
|
||||
-- Versión: 2.0
|
||||
-- Fecha: 2026-01-01
|
||||
-- ============================================
|
||||
-- Schema LOG para Context Manager
|
||||
CREATE SCHEMA IF NOT EXISTS log;
|
||||
|
||||
DROP SCHEMA IF EXISTS log CASCADE;
|
||||
CREATE SCHEMA log;
|
||||
|
||||
-- Extensiones
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- Tipos
|
||||
CREATE TYPE log.ref_type AS ENUM ('context', 'accountant', 'secretary');
|
||||
|
||||
-- ============================================
|
||||
-- Tabla principal: messages
|
||||
-- ============================================
|
||||
CREATE TABLE log.messages (
|
||||
CREATE TABLE IF NOT EXISTS log.messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
hash CHAR(64) UNIQUE NOT NULL,
|
||||
session_hash CHAR(64) NOT NULL,
|
||||
thread_hash CHAR(64),
|
||||
owner_id CHAR(64) NOT NULL,
|
||||
players_id CHAR(64)[] NOT NULL DEFAULT '{}',
|
||||
owner_id CHAR(64),
|
||||
players_id CHAR(64)[],
|
||||
master_player CHAR(64),
|
||||
role TEXT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSONB DEFAULT '{}',
|
||||
prev_hash CHAR(64),
|
||||
hashtags CHAR(64)[] DEFAULT '{}',
|
||||
hashtags CHAR(64)[],
|
||||
flag_id CHAR(64),
|
||||
master_item_id CHAR(64),
|
||||
item_id CHAR(64)[] DEFAULT '{}',
|
||||
item_id CHAR(64)[],
|
||||
loc_id CHAR(64),
|
||||
ambient JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices messages
|
||||
CREATE INDEX idx_messages_session ON log.messages(session_hash);
|
||||
CREATE INDEX idx_messages_thread ON log.messages(thread_hash);
|
||||
CREATE INDEX idx_messages_owner ON log.messages(owner_id);
|
||||
CREATE INDEX idx_messages_players ON log.messages USING gin(players_id);
|
||||
CREATE INDEX idx_messages_master ON log.messages(master_player);
|
||||
CREATE INDEX idx_messages_prev ON log.messages(prev_hash);
|
||||
CREATE INDEX idx_messages_hashtags ON log.messages USING gin(hashtags);
|
||||
CREATE INDEX idx_messages_flag ON log.messages(flag_id);
|
||||
CREATE INDEX idx_messages_master_item ON log.messages(master_item_id);
|
||||
CREATE INDEX idx_messages_items ON log.messages USING gin(item_id);
|
||||
CREATE INDEX idx_messages_loc ON log.messages(loc_id);
|
||||
CREATE INDEX idx_messages_created ON log.messages(created_at);
|
||||
|
||||
-- ============================================
|
||||
-- Tabla relacional: message_refs
|
||||
-- Referencias a contexto y conocimiento
|
||||
-- ============================================
|
||||
CREATE TABLE log.message_refs (
|
||||
CREATE TABLE IF NOT EXISTS log.message_refs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
message_hash CHAR(64) NOT NULL,
|
||||
ref_hash CHAR(64) NOT NULL,
|
||||
ref_type log.ref_type NOT NULL,
|
||||
position INT NOT NULL,
|
||||
thread_hash CHAR(64),
|
||||
UNIQUE(message_hash, ref_hash, ref_type)
|
||||
ref_type VARCHAR(20) NOT NULL,
|
||||
position INT,
|
||||
thread_hash CHAR(64)
|
||||
);
|
||||
|
||||
-- Índices message_refs
|
||||
CREATE INDEX idx_refs_message ON log.message_refs(message_hash);
|
||||
CREATE INDEX idx_refs_ref ON log.message_refs(ref_hash);
|
||||
CREATE INDEX idx_refs_type ON log.message_refs(ref_type);
|
||||
CREATE INDEX idx_refs_thread ON log.message_refs(thread_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_log_session ON log.messages(session_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_log_thread ON log.messages(thread_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_log_owner ON log.messages(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_log_prev ON log.messages(prev_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_log_created ON log.messages(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_refs_message ON log.message_refs(message_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_refs_ref ON log.message_refs(ref_hash);
|
||||
|
||||
-- ============================================
|
||||
-- Funciones
|
||||
-- ============================================
|
||||
CREATE OR REPLACE FUNCTION log.sha256(data TEXT) RETURNS CHAR(64) AS $$
|
||||
CREATE OR REPLACE FUNCTION log.prevent_modification() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
RETURN encode(digest(data, 'sha256'), 'hex');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION log.prevent_update() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'UPDATE no permitido en %', TG_TABLE_NAME;
|
||||
RAISE EXCEPTION 'Log is immutable';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION log.prevent_delete() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'DELETE no permitido en %', TG_TABLE_NAME;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
DROP TRIGGER IF EXISTS no_update_messages ON log.messages;
|
||||
DROP TRIGGER IF EXISTS no_delete_messages ON log.messages;
|
||||
CREATE TRIGGER no_update_messages BEFORE UPDATE ON log.messages FOR EACH ROW EXECUTE FUNCTION log.prevent_modification();
|
||||
CREATE TRIGGER no_delete_messages BEFORE DELETE ON log.messages FOR EACH ROW EXECUTE FUNCTION log.prevent_modification();
|
||||
|
||||
-- ============================================
|
||||
-- Triggers de protección (inmutabilidad)
|
||||
-- ============================================
|
||||
CREATE TRIGGER protect_messages_update BEFORE UPDATE ON log.messages
|
||||
FOR EACH ROW EXECUTE FUNCTION log.prevent_update();
|
||||
CREATE TRIGGER protect_messages_delete BEFORE DELETE ON log.messages
|
||||
FOR EACH ROW EXECUTE FUNCTION log.prevent_delete();
|
||||
DROP TRIGGER IF EXISTS no_update_refs ON log.message_refs;
|
||||
DROP TRIGGER IF EXISTS no_delete_refs ON log.message_refs;
|
||||
CREATE TRIGGER no_update_refs BEFORE UPDATE ON log.message_refs FOR EACH ROW EXECUTE FUNCTION log.prevent_modification();
|
||||
CREATE TRIGGER no_delete_refs BEFORE DELETE ON log.message_refs FOR EACH ROW EXECUTE FUNCTION log.prevent_modification();
|
||||
|
||||
CREATE TRIGGER protect_refs_update BEFORE UPDATE ON log.message_refs
|
||||
FOR EACH ROW EXECUTE FUNCTION log.prevent_update();
|
||||
CREATE TRIGGER protect_refs_delete BEFORE DELETE ON log.message_refs
|
||||
FOR EACH ROW EXECUTE FUNCTION log.prevent_delete();
|
||||
|
||||
-- ============================================
|
||||
-- Permisos
|
||||
-- ============================================
|
||||
GRANT USAGE ON SCHEMA log TO tzzr;
|
||||
GRANT SELECT, INSERT ON log.messages TO tzzr;
|
||||
GRANT SELECT, INSERT ON log.message_refs TO tzzr;
|
||||
GRANT USAGE ON SEQUENCE log.messages_id_seq TO tzzr;
|
||||
GRANT USAGE ON SEQUENCE log.message_refs_id_seq TO tzzr;
|
||||
|
||||
-- Verificación
|
||||
SELECT 'Schema log v2.0: 2 tablas creadas' as status;
|
||||
GRANT USAGE ON SCHEMA log TO architect;
|
||||
GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA log TO architect;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA log TO architect;
|
||||
|
||||
79
schemas/07_initial_blocks.sql
Normal file
79
schemas/07_initial_blocks.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- Bloques iniciales para Context Manager
|
||||
|
||||
-- Permitir NULL en session_hash y owner_id si no está ya
|
||||
ALTER TABLE cto.blocks ALTER COLUMN session_hash DROP NOT NULL;
|
||||
ALTER TABLE cto.blocks ALTER COLUMN owner_id DROP NOT NULL;
|
||||
|
||||
-- BLOQUE GLOBAL: Info base TZZR
|
||||
INSERT INTO cto.blocks (hash, code, name, category, content, priority, scope, status)
|
||||
VALUES (encode(sha256('tzzr_base'::bytea), 'hex'), 'tzzr_base', 'TZZR Base', 'system',
|
||||
'# Sistema TZZR
|
||||
|
||||
## Servidores
|
||||
|
||||
| Servidor | IP | Propósito |
|
||||
|----------|-----|-----------|
|
||||
| ARCHITECT | 69.62.126.110 | Desarrollo |
|
||||
| DECK | 72.62.1.113 | Usuario |
|
||||
| CORP | 92.112.181.188 | Corporativo |
|
||||
|
||||
## Acceso SSH
|
||||
Usar clave ~/.ssh/tzzr para conexiones entre servidores.', 100, 'global', 'active')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- BLOQUE GLOBAL: Reglas de operación
|
||||
INSERT INTO cto.blocks (hash, code, name, category, content, priority, scope, status)
|
||||
VALUES (encode(sha256('rules_base'::bytea), 'hex'), 'rules_base', 'Reglas Base', 'rules',
|
||||
'## Reglas de Operación
|
||||
|
||||
- Ejecutar comandos sin pedir confirmación
|
||||
- No guardar documentos en servidor local, usar R2
|
||||
- Limpieza automática: subir a R2 y eliminar archivos temporales', 90, 'global', 'active')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- BLOQUE GLOBAL: R2 Storage
|
||||
INSERT INTO cto.blocks (hash, code, name, category, content, priority, scope, status)
|
||||
VALUES (encode(sha256('r2_storage'::bytea), 'hex'), 'r2_storage', 'R2 Storage', 'knowledge',
|
||||
'## Almacenamiento R2
|
||||
|
||||
Endpoint: https://7dedae6030f5554d99d37e98a5232996.r2.cloudflarestorage.com
|
||||
|
||||
### Bucket: architect
|
||||
- documentos adjuntos/ - Documentos para usuario
|
||||
- system/ - Configs y backups
|
||||
- auditorias/ - Logs', 80, 'global', 'active')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- BLOQUE SERVER: ARCHITECT
|
||||
INSERT INTO cto.blocks (hash, code, name, category, content, priority, scope, server_code, status)
|
||||
VALUES (encode(sha256('server_architect'::bytea), 'hex'), 'server_architect', 'Servidor ARCHITECT', 'config',
|
||||
'## ARCHITECT (69.62.126.110)
|
||||
|
||||
Servidor de desarrollo.
|
||||
|
||||
Servicios locales:
|
||||
- Gitea: http://localhost:3000
|
||||
- PostgreSQL: localhost:5432
|
||||
|
||||
SSH a otros servidores:
|
||||
- ssh -i ~/.ssh/tzzr root@72.62.1.113 (DECK)
|
||||
- ssh -i ~/.ssh/tzzr root@92.112.181.188 (CORP)', 70, 'server', 'ARCHITECT', 'active')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- BLOQUE SERVER: DECK
|
||||
INSERT INTO cto.blocks (hash, code, name, category, content, priority, scope, server_code, status)
|
||||
VALUES (encode(sha256('server_deck'::bytea), 'hex'), 'server_deck', 'Servidor DECK', 'config',
|
||||
'## DECK (72.62.1.113)
|
||||
|
||||
Servidor de usuario.', 70, 'server', 'DECK', 'active')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- BLOQUE SERVER: CORP
|
||||
INSERT INTO cto.blocks (hash, code, name, category, content, priority, scope, server_code, status)
|
||||
VALUES (encode(sha256('server_corp'::bytea), 'hex'), 'server_corp', 'Servidor CORP', 'config',
|
||||
'## CORP (92.112.181.188)
|
||||
|
||||
Servidor corporativo.', 70, 'server', 'CORP', 'active')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
SELECT code, category, scope, server_code, priority FROM cto.blocks ORDER BY priority DESC;
|
||||
236
src/context_bridge.py
Normal file
236
src/context_bridge.py
Normal file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Context Bridge - Puente entre Captain Claude y Context Manager
|
||||
Consulta cto.blocks y cto.memory para construir contexto dinámico.
|
||||
"""
|
||||
|
||||
import os
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ContextBridge:
|
||||
"""Puente para consultar el Context Manager desde PostgreSQL."""
|
||||
|
||||
def __init__(self,
|
||||
host: str = None,
|
||||
port: int = 5432,
|
||||
database: str = "postgres",
|
||||
user: str = None,
|
||||
password: str = None):
|
||||
# Usar variables de entorno o valores por defecto
|
||||
self.conn_params = {
|
||||
"database": os.environ.get("PGDATABASE", database),
|
||||
}
|
||||
|
||||
# Solo añadir host si no es local (para usar Unix socket con peer auth)
|
||||
pg_host = os.environ.get("PGHOST", host)
|
||||
if pg_host and pg_host not in ("localhost", "127.0.0.1", "::1", "", None):
|
||||
self.conn_params["host"] = pg_host
|
||||
self.conn_params["port"] = int(os.environ.get("PGPORT", port))
|
||||
|
||||
# Solo añadir user/password si están definidos
|
||||
pg_user = os.environ.get("PGUSER", user)
|
||||
pg_pass = os.environ.get("PGPASSWORD", password)
|
||||
if pg_user:
|
||||
self.conn_params["user"] = pg_user
|
||||
if pg_pass:
|
||||
self.conn_params["password"] = pg_pass
|
||||
|
||||
self._conn = None
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
"""Conexión lazy a PostgreSQL."""
|
||||
if self._conn is None or self._conn.closed:
|
||||
self._conn = psycopg2.connect(**self.conn_params)
|
||||
return self._conn
|
||||
|
||||
def close(self):
|
||||
"""Cerrar conexión."""
|
||||
if self._conn and not self._conn.closed:
|
||||
self._conn.close()
|
||||
|
||||
def get_active_blocks(self,
|
||||
scope: str = "global",
|
||||
server_code: Optional[str] = None,
|
||||
agent_code: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
Obtener bloques activos del Context Manager.
|
||||
|
||||
Args:
|
||||
scope: global, server, agent, session
|
||||
server_code: ARCHITECT, DECK, CORP
|
||||
agent_code: código del agente específico
|
||||
|
||||
Returns:
|
||||
Lista de bloques con code, name, content, category, priority
|
||||
"""
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT code, name, content, category, priority, token_count
|
||||
FROM cto.get_active_blocks(%s, %s, %s)
|
||||
ORDER BY priority DESC NULLS LAST
|
||||
""", (scope, server_code, agent_code))
|
||||
return cur.fetchall()
|
||||
|
||||
def get_all_relevant_blocks(self,
|
||||
server_code: Optional[str] = None,
|
||||
agent_code: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
Obtener todos los bloques relevantes (global + server + agent).
|
||||
La función SQL ya maneja la lógica de combinar scopes.
|
||||
"""
|
||||
# Una sola llamada - la función SQL devuelve global + server + agent
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT code, name, content, category, priority, token_count
|
||||
FROM cto.get_active_blocks(%s, %s, %s)
|
||||
ORDER BY priority DESC NULLS LAST
|
||||
""", ('global', server_code, agent_code))
|
||||
return cur.fetchall()
|
||||
|
||||
def build_system_prompt(self,
|
||||
server_code: Optional[str] = None,
|
||||
agent_code: Optional[str] = None,
|
||||
include_metadata: bool = False) -> str:
|
||||
"""
|
||||
Construir system prompt desde bloques de contexto.
|
||||
|
||||
Args:
|
||||
server_code: ARCHITECT, DECK, CORP
|
||||
agent_code: código del agente
|
||||
include_metadata: incluir comentarios con metadata
|
||||
|
||||
Returns:
|
||||
String con el system prompt concatenado
|
||||
"""
|
||||
blocks = self.get_all_relevant_blocks(server_code, agent_code)
|
||||
|
||||
if not blocks:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
for block in blocks:
|
||||
if include_metadata:
|
||||
header = f"<!-- {block.get('code', 'unknown')} | {block.get('category', 'general')} | priority:{block.get('priority', 0)} -->"
|
||||
parts.append(f"{header}\n{block['content']}")
|
||||
else:
|
||||
parts.append(block['content'])
|
||||
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
def generate_claude_md(self,
|
||||
output_path: str,
|
||||
server_code: Optional[str] = None,
|
||||
agent_code: Optional[str] = None) -> str:
|
||||
"""
|
||||
Generar archivo CLAUDE.md desde bloques de contexto.
|
||||
|
||||
Args:
|
||||
output_path: ruta donde guardar el archivo
|
||||
server_code: ARCHITECT, DECK, CORP
|
||||
agent_code: código del agente
|
||||
|
||||
Returns:
|
||||
Contenido generado
|
||||
"""
|
||||
content = self.build_system_prompt(server_code, agent_code, include_metadata=True)
|
||||
|
||||
header = f"""# Context Manager Generated
|
||||
# Server: {server_code or 'ALL'}
|
||||
# Agent: {agent_code or 'ALL'}
|
||||
# Generated: {datetime.now().isoformat()}
|
||||
|
||||
"""
|
||||
full_content = header + content
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(full_content)
|
||||
|
||||
return full_content
|
||||
|
||||
def get_algorithm_config(self, code: str = "default_v2") -> Optional[Dict]:
|
||||
"""Obtener configuración del algoritmo activo."""
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT code, name, version, config, status
|
||||
FROM cto.algorithms
|
||||
WHERE code = %s OR (status = 'active' AND %s IS NULL)
|
||||
LIMIT 1
|
||||
""", (code, code))
|
||||
return cur.fetchone()
|
||||
|
||||
def log_message(self,
|
||||
session_hash: str,
|
||||
owner_id: str,
|
||||
role: str,
|
||||
content: str,
|
||||
prev_hash: Optional[str] = None) -> str:
|
||||
"""
|
||||
Registrar mensaje en log inmutable.
|
||||
|
||||
Returns:
|
||||
Hash del mensaje registrado
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
# Calcular hash
|
||||
hash_input = f"{prev_hash or ''}{session_hash}{owner_id}{role}{content}"
|
||||
msg_hash = hashlib.sha256(hash_input.encode()).hexdigest()
|
||||
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
INSERT INTO log.messages (hash, session_hash, owner_id, role, content, prev_hash, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, NOW())
|
||||
ON CONFLICT (hash) DO NOTHING
|
||||
RETURNING hash
|
||||
""", (msg_hash, session_hash, owner_id, role, content, prev_hash))
|
||||
self.conn.commit()
|
||||
|
||||
return msg_hash
|
||||
|
||||
|
||||
# Singleton para uso fácil
|
||||
_bridge: Optional[ContextBridge] = None
|
||||
|
||||
def get_bridge() -> ContextBridge:
|
||||
"""Obtener instancia singleton del bridge."""
|
||||
global _bridge
|
||||
if _bridge is None:
|
||||
_bridge = ContextBridge()
|
||||
return _bridge
|
||||
|
||||
|
||||
def get_context(server_code: Optional[str] = None,
|
||||
agent_code: Optional[str] = None) -> str:
|
||||
"""Función helper para obtener contexto rápidamente."""
|
||||
return get_bridge().build_system_prompt(server_code, agent_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test básico
|
||||
bridge = ContextBridge()
|
||||
|
||||
print("=== Context Bridge Test ===\n")
|
||||
|
||||
# Probar obtener bloques
|
||||
blocks = bridge.get_active_blocks('global')
|
||||
print(f"Bloques globales: {len(blocks)}")
|
||||
|
||||
for block in blocks:
|
||||
print(f" - {block.get('code')}: {block.get('category')} (priority: {block.get('priority')})")
|
||||
|
||||
# Probar generar system prompt
|
||||
prompt = bridge.build_system_prompt(server_code='ARCHITECT')
|
||||
print(f"\nSystem prompt length: {len(prompt)} chars")
|
||||
|
||||
# Probar algoritmo
|
||||
algo = bridge.get_algorithm_config()
|
||||
if algo:
|
||||
print(f"\nAlgoritmo activo: {algo.get('code')} v{algo.get('version')}")
|
||||
|
||||
bridge.close()
|
||||
print("\n=== Test completado ===")
|
||||
351
src/context_manager.py
Executable file
351
src/context_manager.py
Executable file
@@ -0,0 +1,351 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Context Manager - Cliente completo para gestión de contexto TZZR.
|
||||
Maneja todo el contexto: cto.blocks, cto.memory, log.messages.
|
||||
Llama a API Anthropic directamente (stateless).
|
||||
"""
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
import anthropic
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
role: str
|
||||
content: str
|
||||
hash: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
hash: str
|
||||
owner_id: str
|
||||
server_code: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ContextManager:
|
||||
"""
|
||||
Context Manager completo.
|
||||
- Lee cto.blocks para contexto estático
|
||||
- Busca cto.memory para memorias relevantes
|
||||
- Lee/escribe log.messages para historial
|
||||
- Llama a Anthropic API directamente (stateless)
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
server_code: str = "ARCHITECT",
|
||||
model: str = "claude-sonnet-4-20250514",
|
||||
max_history: int = 20,
|
||||
max_memories: int = 10,
|
||||
token_budget: int = 8000):
|
||||
|
||||
self.server_code = server_code
|
||||
self.model = model
|
||||
self.max_history = max_history
|
||||
self.max_memories = max_memories
|
||||
self.token_budget = token_budget
|
||||
|
||||
# Conexión BD
|
||||
self.conn_params = {"database": os.environ.get("PGDATABASE", "postgres")}
|
||||
self._conn = None
|
||||
|
||||
# Cliente Anthropic
|
||||
self.anthropic = anthropic.Anthropic()
|
||||
|
||||
# Sesión actual
|
||||
self.session: Optional[Session] = None
|
||||
self.prev_hash: Optional[str] = None
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
if self._conn is None or self._conn.closed:
|
||||
self._conn = psycopg2.connect(**self.conn_params)
|
||||
return self._conn
|
||||
|
||||
def close(self):
|
||||
if self._conn and not self._conn.closed:
|
||||
self._conn.close()
|
||||
|
||||
# =========================================================================
|
||||
# SESIONES
|
||||
# =========================================================================
|
||||
|
||||
def start_session(self, owner_id: str = "user") -> Session:
|
||||
"""Iniciar nueva sesión."""
|
||||
session_hash = hashlib.sha256(
|
||||
f"{owner_id}{self.server_code}{datetime.now().isoformat()}".encode()
|
||||
).hexdigest()
|
||||
|
||||
self.session = Session(
|
||||
hash=session_hash,
|
||||
owner_id=owner_id,
|
||||
server_code=self.server_code,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
self.prev_hash = None
|
||||
return self.session
|
||||
|
||||
# =========================================================================
|
||||
# CONTEXTO: cto.blocks
|
||||
# =========================================================================
|
||||
|
||||
def get_blocks(self, agent_code: Optional[str] = None) -> List[Dict]:
|
||||
"""Obtener bloques activos desde cto.blocks."""
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT code, name, content, category, priority, token_count
|
||||
FROM cto.get_active_blocks(%s, %s, %s)
|
||||
ORDER BY priority DESC NULLS LAST
|
||||
""", ('global', self.server_code, agent_code))
|
||||
return cur.fetchall()
|
||||
|
||||
# =========================================================================
|
||||
# CONTEXTO: cto.memory
|
||||
# =========================================================================
|
||||
|
||||
def search_memories(self, query: str, limit: int = None) -> List[Dict]:
|
||||
"""Buscar memorias relevantes (por ahora sin embeddings, placeholder)."""
|
||||
limit = limit or self.max_memories
|
||||
|
||||
if not self.session:
|
||||
return []
|
||||
|
||||
# TODO: Implementar búsqueda con embeddings
|
||||
# Por ahora, obtener últimas memorias del owner
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT id, content, type, importance, summary
|
||||
FROM cto.memory
|
||||
WHERE owner_id = %s AND status = 'active'
|
||||
ORDER BY last_access DESC NULLS LAST, importance DESC
|
||||
LIMIT %s
|
||||
""", (self.session.owner_id, limit))
|
||||
return cur.fetchall()
|
||||
|
||||
# =========================================================================
|
||||
# CONTEXTO: log.messages (historial)
|
||||
# =========================================================================
|
||||
|
||||
def get_history(self, limit: int = None) -> List[Message]:
|
||||
"""Obtener últimos mensajes de la sesión desde log.messages."""
|
||||
limit = limit or self.max_history
|
||||
|
||||
if not self.session:
|
||||
return []
|
||||
|
||||
with self.conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT hash, role, content
|
||||
FROM log.messages
|
||||
WHERE session_hash = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
""", (self.session.hash, limit))
|
||||
rows = cur.fetchall()
|
||||
|
||||
# Invertir para orden cronológico
|
||||
messages = [
|
||||
Message(role=r['role'], content=r['content'], hash=r['hash'])
|
||||
for r in reversed(rows)
|
||||
]
|
||||
return messages
|
||||
|
||||
def log_message(self, role: str, content: str) -> str:
|
||||
"""Guardar mensaje en log.messages (inmutable)."""
|
||||
if not self.session:
|
||||
raise ValueError("No hay sesión activa")
|
||||
|
||||
# Calcular hash
|
||||
hash_input = f"{self.prev_hash or ''}{self.session.hash}{role}{content}"
|
||||
msg_hash = hashlib.sha256(hash_input.encode()).hexdigest()
|
||||
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
INSERT INTO log.messages (hash, session_hash, owner_id, role, content, prev_hash, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, NOW())
|
||||
ON CONFLICT (hash) DO NOTHING
|
||||
""", (msg_hash, self.session.hash, self.session.owner_id, role, content, self.prev_hash))
|
||||
self.conn.commit()
|
||||
|
||||
self.prev_hash = msg_hash
|
||||
return msg_hash
|
||||
|
||||
# =========================================================================
|
||||
# CONSTRUIR CONTEXTO COMPLETO
|
||||
# =========================================================================
|
||||
|
||||
def build_system_prompt(self, agent_code: Optional[str] = None) -> str:
|
||||
"""Construir system prompt desde blocks + memories."""
|
||||
parts = []
|
||||
|
||||
# 1. Bloques de contexto
|
||||
blocks = self.get_blocks(agent_code)
|
||||
for block in blocks:
|
||||
parts.append(f"<!-- {block.get('code')} -->\n{block['content']}")
|
||||
|
||||
# 2. Memorias relevantes (si hay)
|
||||
# memories = self.search_memories("") # TODO: usar query real
|
||||
# if memories:
|
||||
# parts.append("## Memorias Relevantes")
|
||||
# for mem in memories:
|
||||
# parts.append(f"- {mem.get('summary') or mem.get('content', '')[:100]}")
|
||||
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
def build_messages(self, user_message: str) -> List[Dict]:
|
||||
"""Construir lista de mensajes para API."""
|
||||
messages = []
|
||||
|
||||
# Historial de la sesión
|
||||
history = self.get_history()
|
||||
for msg in history:
|
||||
messages.append({"role": msg.role, "content": msg.content})
|
||||
|
||||
# Mensaje actual
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
return messages
|
||||
|
||||
# =========================================================================
|
||||
# ENVIAR MENSAJE
|
||||
# =========================================================================
|
||||
|
||||
def send_message(self, user_message: str, agent_code: Optional[str] = None) -> str:
|
||||
"""
|
||||
Enviar mensaje usando Context Manager completo.
|
||||
|
||||
1. Construye contexto desde BD
|
||||
2. Llama a Anthropic API (stateless)
|
||||
3. Guarda en log
|
||||
4. Retorna respuesta
|
||||
"""
|
||||
if not self.session:
|
||||
self.start_session()
|
||||
|
||||
# 1. Construir contexto
|
||||
system_prompt = self.build_system_prompt(agent_code)
|
||||
messages = self.build_messages(user_message)
|
||||
|
||||
# 2. Guardar mensaje usuario en log
|
||||
self.log_message("user", user_message)
|
||||
|
||||
# 3. Llamar a Anthropic API
|
||||
response = self.anthropic.messages.create(
|
||||
model=self.model,
|
||||
max_tokens=4096,
|
||||
system=system_prompt,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
assistant_message = response.content[0].text
|
||||
|
||||
# 4. Guardar respuesta en log
|
||||
self.log_message("assistant", assistant_message)
|
||||
|
||||
# 5. TODO: Extraer memorias async
|
||||
# self.extract_memories_async(user_message, assistant_message)
|
||||
|
||||
return assistant_message
|
||||
|
||||
# =========================================================================
|
||||
# EXTRACCIÓN DE MEMORIAS (TODO)
|
||||
# =========================================================================
|
||||
|
||||
def extract_memories(self, user_message: str, assistant_message: str):
|
||||
"""Extraer memorias del intercambio y guardar en cto.memory."""
|
||||
# TODO: Implementar extracción con LLM
|
||||
pass
|
||||
|
||||
# =========================================================================
|
||||
# CLI INTERACTIVO
|
||||
# =========================================================================
|
||||
|
||||
def chat(self):
|
||||
"""Modo chat interactivo."""
|
||||
print(f"\n{'='*60}")
|
||||
print("CONTEXT MANAGER - TZZR")
|
||||
print(f"Server: {self.server_code} | Model: {self.model}")
|
||||
print(f"{'='*60}")
|
||||
print("Comandos: /quit, /session, /blocks, /history")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
self.start_session()
|
||||
print(f"[Sesión: {self.session.hash[:16]}...]\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("Tú: ").strip()
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
if user_input == "/quit":
|
||||
print("Adiós.")
|
||||
break
|
||||
|
||||
if user_input == "/session":
|
||||
print(f"Sesión: {self.session.hash}")
|
||||
print(f"Owner: {self.session.owner_id}")
|
||||
print(f"Server: {self.server_code}")
|
||||
continue
|
||||
|
||||
if user_input == "/blocks":
|
||||
blocks = self.get_blocks()
|
||||
print(f"Bloques ({len(blocks)}):")
|
||||
for b in blocks:
|
||||
print(f" - {b['code']} ({b['category']}, p:{b['priority']})")
|
||||
continue
|
||||
|
||||
if user_input == "/history":
|
||||
history = self.get_history()
|
||||
print(f"Historial ({len(history)} mensajes):")
|
||||
for h in history:
|
||||
print(f" [{h.role}]: {h.content[:50]}...")
|
||||
continue
|
||||
|
||||
# Enviar mensaje
|
||||
response = self.send_message(user_input)
|
||||
print(f"\nAsistente: {response}\n")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nAdiós.")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
self.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Punto de entrada."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Context Manager TZZR")
|
||||
parser.add_argument("--server", default="ARCHITECT", help="Código de servidor")
|
||||
parser.add_argument("--model", default="claude-sonnet-4-20250514", help="Modelo Anthropic")
|
||||
parser.add_argument("--message", "-m", help="Enviar mensaje único")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
cm = ContextManager(server_code=args.server, model=args.model)
|
||||
|
||||
if args.message:
|
||||
cm.start_session()
|
||||
response = cm.send_message(args.message)
|
||||
print(response)
|
||||
cm.close()
|
||||
else:
|
||||
cm.chat()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user