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:
ARCHITECT
2026-01-01 22:41:20 +00:00
parent 05d21976ca
commit d1b2c16bd0
5 changed files with 728 additions and 107 deletions

14
cm Executable file
View 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" "$@"

View File

@@ -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;

View 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
View 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
View 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()