Gestión de Contexto Persistente para Agentes IA (OpenViking)
Sistema de memoria jerárquica para agentes IA basado en sistema de archivos: organiza contexto por proyecto, tarea y subtarea con persistencia entre sesiones y capacidad de autoevolución. Incluye implementación Python lista para integrar con LangChain u otros frameworks de agentes.
Incluida en el Pase · para Python, LangChain, Claude Code
""" NutriFlow Agent — Memoria Persistente con OpenViking ContextManager
Python 3.9+ compatible (uses List/Tuple from typing for older runtimes)
Cliente: NutriFlow SaaS (clínicas y dietistas) Stack: Next.js 14 · PostgreSQL 15 · Stripe · Vercel Agente: Asistente dev con memoria jerárquica entre sesiones
Este módulo inicializa el árbol de contexto para el agente de NutriFlow, define las herramientas LangChain para que el agente pueda leer/escribir/buscar contexto, y muestra cómo el agente aprende de sus errores (self-evolution).
Uso: python resultado.py
Dependencias (core no necesita nada externo): pip install langchain-core langchain-openai # sólo para integración LangChain """
from future import annotations
import os import json from pathlib import Path from datetime import datetime, timezone from typing import Optional, List, Tuple
─────────────────────────────────────────────
CORE: ContextManager
─────────────────────────────────────────────
class ContextManager: """Gestión de contexto jerárquico basado en sistema de archivos."""
def __init__(self, root: str = "./context"):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def read(self, path: str) -> Optional[str]:
full = self.root / path
return full.read_text(encoding="utf-8") if full.is_file() else None
def write(self, path: str, content: str, metadata: Optional[dict] = None):
full = self.root / path
full.parent.mkdir(parents=True, exist_ok=True)
header = ""
if metadata:
meta = {**metadata, "updated": datetime.now(timezone.utc).isoformat()}
header = f"<!-- meta: {json.dumps(meta)} -->\n\n"
full.write_text(header + content, encoding="utf-8")
def list(self, path: str = "") -> List[str]:
full = self.root / path
if not full.is_dir():
return []
return [str(p.relative_to(self.root)) for p in sorted(full.rglob("*")) if p.is_file()]
def delete(self, path: str):
full = self.root / path
if full.is_file():
trash = self.root / ".trash" / path
trash.parent.mkdir(parents=True, exist_ok=True)
full.rename(trash)
def search(self, query: str, path: str = "") -> List[Tuple[str, str]]:
results = []
for filepath in self.list(path):
content = self.read(filepath)
if content and query.lower() in content.lower():
idx = content.lower().index(query.lower())
snippet = content[max(0, idx - 50):idx + len(query) + 50]
results.append((filepath, snippet))
return results
─────────────────────────────────────────────
SCOPED CONTEXT DELIVERY
─────────────────────────────────────────────
class ScopedContext: """Entrega contexto según el alcance actual del agente."""
SCOPE_RULES = {
"project": [
"project/README.md",
"memory/facts.md",
"memory/preferences.md",
"skills/coding-style.md",
],
"task": ["task/current.md"],
"subtask": [],
}
def __init__(self, ctx: ContextManager):
self.ctx = ctx
def get_context(self, scope: str = "task", subtask_id: Optional[str] = None) -> str:
parts = []
for path in self.SCOPE_RULES["project"]:
content = self.ctx.read(path)
if content:
parts.append(f"## {path}\n{content}")
if scope in ("task", "subtask"):
for path in self.SCOPE_RULES["task"]:
content = self.ctx.read(path)
if content:
parts.append(f"## {path}\n{content}")
if scope == "subtask" and subtask_id:
content = self.ctx.read(f"task/subtasks/{subtask_id}.md")
if content:
parts.append(f"## Subtask: {subtask_id}\n{content}")
lessons = self.ctx.read("memory/lessons.md")
if lessons:
parts.append(f"## Lecciones Aprendidas\n{lessons}")
return "\n\n---\n\n".join(parts)
─────────────────────────────────────────────
SELF-EVOLVING: el agente actualiza su memoria
─────────────────────────────────────────────
class EvolvingAgent: """Agente que actualiza su contexto tras cada tarea completada."""
def __init__(self, ctx: ContextManager):
self.ctx = ctx
def complete_task(self, task: str, result: str, success: bool, lesson: str = "", facts: str = ""):
"""Cierra una tarea, archiva el contexto y guarda lo aprendido."""
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M")
# Archivar tarea actual
current = self.ctx.read("task/current.md")
if current:
self.ctx.write(f"task/history/{timestamp}.md", current,
metadata={"task": task, "success": success})
# Registrar lección si falló
if not success and lesson:
existing = self.ctx.read("memory/lessons.md") or ""
self.ctx.write("memory/lessons.md",
f"{existing}\n\n### {timestamp} — FALLO\n**Tarea:** {task}\n**Lección:** {lesson}")
# Guardar nuevos hechos descubiertos
if facts and "NONE" not in facts.upper():
existing = self.ctx.read("memory/facts.md") or ""
self.ctx.write("memory/facts.md",
f"{existing}\n\n### Descubierto {timestamp}\n{facts}")
print(f" ✔ Tarea archivada: {timestamp}.md | éxito={success}")
─────────────────────────────────────────────
LANGCHAIN TOOLS (sin LLM real, sólo definición)
─────────────────────────────────────────────
def build_langchain_tools(ctx: ContextManager):
"""
Devuelve las tres herramientas LangChain que el agente NutriFlow usará.
Para registrarlas: agent = create_react_agent(llm, tools=[...])
"""
try:
from langchain_core.tools import tool
@tool
def read_context(path: str) -> str:
"""Lee un archivo de contexto para recordar info del proyecto, decisiones o lecciones."""
return ctx.read(path) or f"Sin contexto en {path}"
@tool
def write_context(path: str, content: str) -> str:
"""Guarda aprendizajes, decisiones o hechos en el contexto persistente."""
ctx.write(path, content, metadata={"source": "agent"})
return f"Guardado en {path}"
@tool
def search_context(query: str) -> str:
"""Busca en todos los archivos de contexto información relevante."""
results = ctx.search(query)
return "\n".join(f"[{p}] ...{s}..." for p, s in results[:5]) or "Sin resultados."
return [read_context, write_context, search_context]
except ImportError:
print(" ⚠ langchain-core no instalado — herramientas omitidas (core funciona sin él)")
return []
─────────────────────────────────────────────
DEMO: Inicialización del contexto de NutriFlow
─────────────────────────────────────────────
def init_nutriflow_context(ctx: ContextManager): """Carga el árbol de contexto inicial para el agente de NutriFlow."""
ctx.write("project/README.md", """\
NutriFlow SaaS
Plataforma B2B de planes nutricionales para clínicas y dietistas.
Stack
- Frontend: Next.js 14 (App Router, React Server Components)
- Base de datos: PostgreSQL 15 con Row Level Security habilitado
- Pagos: Stripe Billing (suscripciones mensuales/anuales)
- Email: Resend + React Email templates
- Deploy: Vercel (preview + prod), Railway (BD)
Arquitectura
API Routes en /app/api/*
Autenticación: Clerk (no NextAuth)
ORM: Drizzle (no Prisma)
Validación: Zod en client + server """, metadata={"source": "init"})
ctx.write("memory/facts.md", """\
Hechos conocidos sobre NutriFlow
Base de datos
- Usar UUIDs v7 (no v4) — función
gen_uuid_v7()disponible como extensión - Row Level Security activo en todas las tablas de clientes
- Índices en
clinic_idycreated_atpara la mayoría de queries - Bug conocido: endpoint
/api/plansdevuelve fechas en local time en lugar de UTC
Infraestructura
- Variables de entorno en Vercel: nunca en
.env.localen producción - Edge Runtime NO soportado — usar Node.js runtime para Drizzle
- Límite de ejecución en Vercel: 10s en Hobby, 60s en Pro
Equipo
3 desarrolladores: Ana (lead), Marcos (backend), Lucia (frontend)
Reuniones de sprint los lunes a las 10h
Tickets en Linear, no Jira """, metadata={"source": "init"})
ctx.write("memory/preferences.md", """\
Preferencias del equipo NutriFlow
Testing
- Vitest (no Jest) para unit tests
- Playwright para e2e
- Coverage mínimo: 80% en lógica de negocio
Código
- TypeScript strict mode obligatorio
- Server Components por defecto;
use clientsólo si hay interactividad - Fetch nativo (no axios)
- Error handling con
Result<T, E>pattern (no try/catch en lógica)
Revisiones
PRs con descripción en español
Squash merge en main
Rama feature/, fix/, chore/* """, metadata={"source": "init"})
ctx.write("skills/coding-style.md", """\
Estilo de código NutriFlow
Naming
- camelCase para variables y funciones
- PascalCase para componentes y tipos
- SCREAMING_SNAKE_CASE para constantes de entorno
SQL
- Siempre filtrar por
clinic_idantes de otros predicados (aprovecha RLS) - EXPLAIN ANALYZE en cualquier query que toque >10k filas
- Usar CTEs para queries de >3 joins
Estructura de archivos
/app/api/[recurso]/route.ts— endpoints/lib/db/[recurso].ts— queries Drizzle/lib/schemas/[recurso].ts— esquemas Zod """, metadata={"source": "init"})ctx.write("task/current.md", """\
Tarea Actual: Optimizar query de historial de planes
Descripción
La query en /lib/db/plans.ts:getPatientHistory() tarda >2s en clínicas con >500 pacientes.
Contexto relevante
- Tabla
plan_historytiene 4M filas en producción - Índice actual sólo en
patient_id, falta índice compuesto conclinic_id - RLS filtra por clinic_id pero DESPUÉS del seq scan
Objetivo
Reducir p95 a <200ms añadiendo índice compuesto y reescribiendo el CTE.
Estado
-
EXPLAIN ANALYZE de la query actual
-
Proponer nuevo índice
-
Reescribir CTE
-
Medir mejora en staging """, metadata={"source": "sprint-23"})
print(" ✔ Árbol de contexto NutriFlow inicializado")
─────────────────────────────────────────────
ENTRYPOINT
─────────────────────────────────────────────
def main(): base = Path(file).parent / "context_nutriflow" ctx = ContextManager(str(base)) scoped = ScopedContext(ctx) agent = EvolvingAgent(ctx)
print("\n" + "═" * 60)
print(" NutriFlow Agent — OpenViking Context Demo")
print("═" * 60)
# 1. Inicializar contexto del proyecto
print("\n[1] Inicializando contexto del proyecto…")
init_nutriflow_context(ctx)
# 2. Listar todos los archivos de contexto
print("\n[2] Árbol de contexto generado:")
for f in ctx.list():
print(f" 📄 {f}")
# 3. Obtener contexto con scope 'task'
print("\n[3] Contexto cargado para scope=task (extracto):")
task_ctx = scoped.get_context(scope="task")
lines = task_ctx.split("\n")
for line in lines[:30]:
print(f" {line}")
print(f" … (+{len(lines) - 30} líneas más)")
# 4. Búsqueda de contexto
print("\n[4] Búsqueda: 'UUID'")
results = ctx.search("UUID")
for path, snippet in results:
print(f" [{path}] …{snippet.strip()}…")
# 5. El agente completa la tarea con aprendizajes
print("\n[5] Agente completa la tarea de optimización:")
agent.complete_task(
task="Optimizar query getPatientHistory()",
result="Añadido índice compuesto (clinic_id, patient_id, created_at DESC). P95 bajó de 2.1s a 87ms.",
success=True,
facts="- Índice compuesto (clinic_id, patient_id, created_at DESC) reduce el seq scan 96% en plan_history\n- RLS en Postgres aplica AFTER el plan de ejecución; los índices deben incluir la columna RLS"
)
# 6. Mostrar hechos actualizados
print("\n[6] Hechos actualizados en memory/facts.md:")
facts = ctx.read("memory/facts.md")
if facts:
for line in facts.split("\n")[-12:]:
print(f" {line}")
# 7. Herramientas LangChain disponibles
print("\n[7] Herramientas LangChain disponibles para el agente:")
tools = build_langchain_tools(ctx)
if tools:
for t in tools:
print(f" 🔧 {t.name}: {t.description[:60]}…")
else:
print(" (instalar langchain-core para activar herramientas)")
print("\n" + "═" * 60)
print(f" Contexto guardado en: {base.resolve()}")
print(" El agente recordará todo esto en la próxima sesión.")
print("═" * 60 + "\n")
if name == "main": main()
// qué_hace
Gestiona el contexto de agentes IA de forma jerárquica y persistente usando un sistema de archivos, con scoping por niveles (proyecto, tarea, subtarea) y autoactualización de memoria.
// cómo_lo_hace
Implementa un ContextManager en Python puro que lee/escribe/busca archivos de contexto estructurados en árbol, con integración LangChain como herramientas de agente y lógica de autoevolución que extrae lecciones y hechos tras cada tarea.
// ejemplo_de_uso
Para agentes IA de larga duración que pierden contexto entre sesiones y necesitan recordar decisiones previas. Ej.: un agente de análisis financiero recuerda qué empresas ya procesó, las lecciones aprendidas y el estado de cada subtarea entre reinicios del servidor.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…