Crea una memoria a largo plazo para agentes con EverOS y Milvus

EverOS es un sistema de memoria basado en Markdown para agentes de IA. Extrae recuerdos duraderos de las conversaciones, mantiene Markdown como fuente de referencia y crea un índice derivado en el que se pueden realizar búsquedas.

En este tutorial, crearemos un asistente de proyectos que recuerde las decisiones de lanzamiento a lo largo de conversaciones independientes. Añadiremos conversaciones sobre el lanzamiento del Proyecto Atlas junto con conversaciones no relacionadas sobre otros proyectos. EverOS utilizará un modelo de lenguaje grande (LLM) para extraer los recuerdos, mientras que Milvus almacenará los índices BM25 y vectoriales utilizados para la búsqueda híbrida.

Conversations
      |
      v
EverOS + LLM ------> Markdown memory files
      |
      | embedding model
      v
Milvus ------> BM25 + vector hybrid search

El LLM y el modelo de incrustación tienen funciones diferentes. El LLM convierte una conversación en recuerdos estructurados. El modelo de incrustación convierte esos recuerdos y las consultas de búsqueda posteriores en vectores. La búsqueda híbrida básica de este tutorial no requiere un modelo de reordenación.

Requisitos previos

Necesitas:

Este tutorial se conecta al servidor Milvus en http://localhost:19530. EverOS también es compatible con Zilliz Cloud mediante la misma configuración de URI y token. Su backend de Milvus espera un punto final remoto y no acepta una ruta de archivo de Milvus Lite.

Instala EverOS

Crea un proyecto local e instala EverOS con sus dependencias opcionales de Milvus:

mkdir everos-milvus-demo
cd everos-milvus-demo

uv init --bare --python 3.12
uv add "everos[milvus]"

El comando no especifica intencionadamente una versión concreta, por lo que una nueva instalación instalará la última versión compatible de EverOS.

Inicializa una raíz de memoria independiente para el tutorial:

export EVEROS_ROOT="$PWD/everos-data"
uv run everos init --root "$EVEROS_ROOT"

EverOS crea « everos.toml » y « ome.toml » en este directorio. También escribirá aquí las memorias extraídas.

Configurar OpenAI y Milvus

Establece la clave de la API de OpenAI y configura EverOS mediante variables de entorno:

export OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
export MILVUS_URI="http://localhost:19530"

export EVEROS_INDEX__BACKEND="milvus"
export EVEROS_MILVUS__URI="$MILVUS_URI"
export EVEROS_MILVUS__COLLECTION_PREFIX="everos_bootcamp"

export EVEROS_LLM__MODEL="gpt-5.4-mini"
export EVEROS_LLM__API_KEY="$OPENAI_API_KEY"
export EVEROS_LLM__BASE_URL="https://api.openai.com/v1"

export EVEROS_EMBEDDING__MODEL="text-embedding-3-small"
export EVEROS_EMBEDDING__API_KEY="$OPENAI_API_KEY"
export EVEROS_EMBEDDING__BASE_URL="https://api.openai.com/v1"
export EVEROS_EMBEDDING__DIMENSIONS="1024"

export EVEROS_MEMORIZE__MODE="chat"

EverOS utiliza OpenAI tanto para la extracción de memoria como para las representaciones. text-embedding-3-small devuelve por defecto las dimensiones 1536, pero EverOS reenvía el valor configurado de dimensions a OpenAI. Este tutorial solicita las dimensiones 1024 para que coincidan con los esquemas de Milvus gestionados por EverOS.

El modo de memoria « chat » centra este ejemplo en las memorias de los usuarios. EverOS gestiona las colecciones de Milvus y sus esquemas, por lo que no es necesario que las crees tú mismo.

Inicia EverOS

Inicia el servidor HTTP de EverOS:

uv run everos server start --root "$EVEROS_ROOT"

Mantén abierto este terminal. EverOS se conecta a Milvus y crea siete colecciones de índices derivados con el prefijo configurado durante el inicio.

Abre otra terminal en el mismo directorio del proyecto y comprueba el servicio:

curl http://127.0.0.1:8000/health

Salida de referencia:

{
  "status": "ok",
  "version": "1.3.0",
  "capabilities": {
    "llm": true,
    "embed": true,
    "rerank": false,
    "multimodal_llm": false,
    "parser": true
  },
  "cascade": {
    "healthy": true,
    "pending": 0
  }
}

La respuesta contiene campos adicionales de estado. Los valores importantes para este tutorial son status: "ok", llm: true, embed: true y cascade.healthy: true.

Añadir conversaciones del proyecto

El siguiente programa en Python envía diez conversaciones independientes a EverOS. Atlas tiene conversaciones separadas sobre el lanzamiento y la reversión. Ocho conversaciones sobre otros proyectos sirven de distracciones, de modo que la búsqueda posterior tiene que identificar los recuerdos correctos del proyecto.

Guarda el siguiente código como « add_memories.py »:

import json
import time
from urllib.request import Request, urlopen


API_URL = "http://127.0.0.1:8000/api/v2/memory"
NOW = int(time.time() * 1000)

conversations = [
    (
        "atlas-release",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW,
                "content": (
                    "For Project Atlas, we decided to launch with a 10% canary "
                    "on September 30. Promote to all users only after the checkout "
                    "error rate stays below 1% for 30 minutes."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 1_000,
                "content": (
                    "Understood. I will remember the Atlas launch date, canary "
                    "percentage, and promotion gate."
                ),
            },
        ],
    ),
    (
        "atlas-rollback",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 10_000,
                "content": (
                    "The Atlas rollback owner is Priya. Roll back immediately if "
                    "checkout errors exceed 2% for five minutes, and keep the "
                    "previous container image available for 24 hours."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 11_000,
                "content": (
                    "Got it. Priya owns rollback, with the 2% five-minute trigger "
                    "and a 24-hour image retention window."
                ),
            },
        ],
    ),
    (
        "orion-pricing",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 20_000,
                "content": (
                    "Project Orion will test annual billing with the education "
                    "segment. The pricing review is scheduled for October 12."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 21_000,
                "content": (
                    "I will remember Orion's annual billing experiment and October "
                    "pricing review."
                ),
            },
        ],
    ),
    (
        "vega-mobile",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 30_000,
                "content": (
                    "For Project Vega, the mobile team chose offline drafts as the "
                    "next milestone. Elena will review the interaction design on "
                    "October 18."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 31_000,
                "content": (
                    "Noted. Vega's next milestone is offline drafts, followed by "
                    "Elena's design review."
                ),
            },
        ],
    ),
    (
        "nova-warehouse",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 40_000,
                "content": (
                    "Project Nova will migrate the analytics warehouse to Iceberg. "
                    "Marcus owns the checksum rehearsal scheduled for October 22."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 41_000,
                "content": (
                    "I will remember Nova's warehouse migration and Marcus's "
                    "checksum rehearsal."
                ),
            },
        ],
    ),
    (
        "helios-support",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 50_000,
                "content": (
                    "Project Helios needs weekend support coverage for the APAC "
                    "region. Imani will publish the rotation schedule on November 1."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 51_000,
                "content": (
                    "Noted. Helios needs APAC weekend coverage, and Imani owns the "
                    "rotation schedule."
                ),
            },
        ],
    ),
    (
        "luna-onboarding",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 60_000,
                "content": (
                    "Project Luna will replace the onboarding tour with a checklist. "
                    "The localized copy is due from the content team on October 25."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 61_000,
                "content": (
                    "I will remember Luna's checklist approach and the localization "
                    "deadline."
                ),
            },
        ],
    ),
    (
        "aurora-observability",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 70_000,
                "content": (
                    "Project Aurora will retain detailed telemetry for 30 days. "
                    "The operations team should alert after three consecutive "
                    "heartbeat misses."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 71_000,
                "content": (
                    "Understood. Aurora keeps 30 days of telemetry and alerts after "
                    "three missed heartbeats."
                ),
            },
        ],
    ),
    (
        "comet-invoices",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 80_000,
                "content": (
                    "Project Comet will add downloadable invoice PDFs for enterprise "
                    "accounts. Finance will approve the tax-field layout on October 28."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 81_000,
                "content": (
                    "Noted. Comet covers enterprise invoice PDFs and an October tax "
                    "layout review."
                ),
            },
        ],
    ),
    (
        "solstice-research",
        [
            {
                "sender_id": "maya",
                "sender_name": "Maya",
                "role": "user",
                "timestamp": NOW + 90_000,
                "content": (
                    "Project Solstice is prototyping voice notes for field researchers. "
                    "The research team will interview 12 participants in November."
                ),
            },
            {
                "sender_id": "assistant",
                "role": "assistant",
                "timestamp": NOW + 91_000,
                "content": (
                    "I will remember Solstice's voice-note prototype and the planned "
                    "participant interviews."
                ),
            },
        ],
    ),
]


def post(path, payload):
    request = Request(
        f"{API_URL}/{path}",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urlopen(request, timeout=300) as response:
        return json.load(response)["data"]


for session_id, messages in conversations:
    added = post(
        "add",
        {
            "session_id": session_id,
            "app_id": "project-assistant",
            "project_id": "launch-planning",
            "messages": messages,
            "defer_extraction": True,
        },
    )
    flushed = post(
        "flush",
        {
            "session_id": session_id,
            "app_id": "project-assistant",
            "project_id": "launch-planning",
        },
    )
    print(f"{session_id}: {added['status']} -> {flushed['status']}")

Ejecútalo desde el directorio del proyecto:

uv run python add_memories.py

Salida de referencia:

atlas-release: accumulated -> extracted
atlas-rollback: accumulated -> extracted
orion-pricing: accumulated -> extracted
vega-mobile: accumulated -> extracted
nova-warehouse: accumulated -> extracted
helios-support: accumulated -> extracted
luna-onboarding: accumulated -> extracted
aurora-observability: accumulated -> extracted
comet-invoices: accumulated -> extracted
solstice-research: accumulated -> extracted

Al establecer ` defer_extraction ` en ` true `, cada conversación se almacena en el búfer duradero sin pedir al LLM que detecte un límite. La siguiente llamada a ` /flush ` marca el final de esa sesión y desencadena una extracción. A continuación, EverOS escribe el episodio extraído en Markdown y lo integra de forma asíncrona en el índice de Milvus.

Inspeccionar la memoria Markdown

El archivo de episodio generado se almacena en los ámbitos de la aplicación, el proyecto y el usuario:

find "$EVEROS_ROOT/project-assistant/launch-planning/users/maya/episodes" \
  -type f -name "*.md"

Salida de referencia (la fecha del nombre del archivo refleja el momento en que se ejecuta el ejemplo):

everos-data/project-assistant/launch-planning/users/maya/episodes/episode-2026-09-08.md

Abre el archivo para ver los recuerdos extraídos por el LLM. Un extracto abreviado tiene este aspecto:

## ep_20260908_00000001

**owner_id**: maya
**session_id**: atlas-release
**sender_ids**: [maya, assistant]

### Subject
Maya's Project Atlas Launch Decision: September 30 Canary and Promotion Criteria

### Content
Maya decided that Project Atlas would launch with a 10% canary on September 30.
The promotion to all users would occur only after the checkout error rate remained
below 1% for 30 minutes.

La redacción exacta, los identificadores y las marcas de tiempo pueden variar, ya que el recuerdo es extraído por el LLM. Los archivos Markdown originales siguen siendo la fuente de información fidedigna y permanente; el índice de Milvus se puede reconstruir a partir de ellos.

Buscar en los recuerdos

Utiliza la búsqueda híbrida para preguntar qué se debe recordar antes de que Atlas entre en funcionamiento. Guarda el siguiente código como search_memories.py:

import json
import time
from urllib.request import Request, urlopen


URL = "http://127.0.0.1:8000/api/v2/memory/search"
payload = {
    "user_id": "maya",
    "app_id": "project-assistant",
    "project_id": "launch-planning",
    "query": "What should I remember before Atlas goes live?",
    "method": "hybrid",
    "top_k": 4,
}


def search():
    request = Request(
        URL,
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urlopen(request, timeout=300) as response:
        return json.load(response)["data"]["episodes"]


expected_sessions = {"atlas-release", "atlas-rollback"}

for _ in range(30):
    episodes = search()
    top_results = episodes[:2]
    if {episode["session_id"] for episode in top_results} == expected_sessions:
        break
    time.sleep(2)
else:
    raise RuntimeError("The expected Atlas memories were not indexed in time")

for rank, episode in enumerate(top_results, start=1):
    print(f"{rank}. {episode['session_id']} | score={episode['score']:.3f}")
    print(f"   {episode['subject']}")

Ejecuta la búsqueda:

uv run python search_memories.py

Resultado de referencia (las puntuaciones y la redacción pueden variar):

1. atlas-release | score=0.492
   Project Atlas Launch Plan: 10% Canary Rollout on September 30 with Error Rate Gate
2. atlas-rollback | score=0.400
   Atlas Rollback Plan Details: Priya as Owner, 2% Error Trigger, 24-Hour Image Retention

Ambas conversaciones de Atlas aparecen antes que las ocho conversaciones no relacionadas. EverOS envía la consulta al punto final de incrustación de OpenAI, solicita a Milvus candidatos BM25 y vectoriales dentro del ámbito de la aplicación y el proyecto de Maya, y fusiona las dos listas de resultados.

Examina las colecciones de Milvus

EverOS crea una colección para cada tipo de memoria derivada compatible. Utiliza MilvusClient para ver el número de filas de cada una:

import os

from pymilvus import MilvusClient


prefix = "everos_bootcamp"
client = MilvusClient(uri=os.environ.get("MILVUS_URI", "http://localhost:19530"))

memory_kinds = [
    "agent_case",
    "agent_skill",
    "atomic_fact",
    "episode",
    "foresight",
    "knowledge_topic",
    "user_profile",
]

for kind in memory_kinds:
    name = f"{prefix}_{kind}"
    if client.has_collection(collection_name=name):
        result = client.query(
            collection_name=name,
            filter="",
            output_fields=["count(*)"],
        )
        print(f"{kind}: {result[0]['count(*)']} rows")

client.close()

Salida de referencia de la ejecución validada:

agent_case: 0 rows
agent_skill: 0 rows
atomic_fact: 50 rows
episode: 10 rows
foresight: 0 rows
knowledge_topic: 0 rows
user_profile: 1 rows

El número exacto de hechos atómicos puede variar en función de la salida del LLM. Las diez filas de episodios corresponden a las diez conversaciones vaciadas. Las demás colecciones están disponibles para los modos de memoria y las funciones de EverOS que este ejemplo concreto no utiliza.

Utilizar otra implementación de Milvus

Para utilizar otro punto final de Milvus Server o Zilliz Cloud, actualice EVEROS_MILVUS__URI. Establezca EVEROS_MILVUS__TOKEN cuando el punto final requiera autenticación. El código de ingestión y búsqueda permanece sin cambios.

Conclusión

Al combinar EverOS con Milvus, puedes convertir las conversaciones en recuerdos duraderos y recuperarlos mediante palabras clave y señales semánticas. Puedes adaptar el mismo patrón para dotar a los asistentes y otras aplicaciones de tipo agente de una memoria a largo plazo para tus propios usuarios, proyectos y flujos de trabajo.