Creare una memoria a lungo termine per gli agenti con EverOS e Milvus
EverOS è un sistema di memoria "Markdown-first" per agenti di intelligenza artificiale. Estrae memorie durature dalle conversazioni, mantiene il Markdown come fonte di verità e crea un indice derivato ricercabile.
In questo tutorial, realizzeremo un assistente di progetto in grado di ricordare le decisioni relative al rilascio tra conversazioni separate. Aggiungeremo conversazioni sul lancio di Project Atlas insieme a conversazioni non correlate su altri progetti. EverOS utilizzerà un LLM per estrarre i ricordi, mentre Milvus memorizzerà gli indici BM25 e vettoriali utilizzati per la ricerca ibrida.
Conversations
|
v
EverOS + LLM ------> Markdown memory files
|
| embedding model
v
Milvus ------> BM25 + vector hybrid search
L’LLM e il modello di embedding hanno responsabilità diverse. L’LLM trasforma una conversazione in memorie strutturate. Il modello di embedding converte tali memorie e le successive query di ricerca in vettori. La ricerca ibrida di base in questo tutorial non richiede un modello di riclassificazione.
Prerequisiti
È necessario:
- Python 3.12 o versioni successive
uv- Un server Milvus in esecuzione
- Una chiave API OpenAI
Questo tutorial si connette al server Milvus all'indirizzo http://localhost:19530. EverOS supporta anche Zilliz Cloud tramite le stesse impostazioni URI e token. Il suo backend Milvus richiede un endpoint remoto e non accetta un percorso di file Milvus Lite.
Installare EverOS
Creare un progetto locale e installare EverOS con le dipendenze opzionali di Milvus:
mkdir everos-milvus-demo
cd everos-milvus-demo
uv init --bare --python 3.12
uv add "everos[milvus]"
Il comando non specifica intenzionalmente una versione, quindi una nuova installazione installerà l’ultima versione compatibile di EverOS.
Inizializza una radice di memoria separata per il tutorial:
export EVEROS_ROOT="$PWD/everos-data"
uv run everos init --root "$EVEROS_ROOT"
EverOS crea everos.toml e ome.toml in questa directory. Scriverà qui anche le memorie estratte.
Configurare OpenAI e Milvus
Impostare la chiave API di OpenAI e configurare EverOS tramite variabili d’ambiente:
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 utilizza OpenAI sia per l’estrazione della memoria che per gli embedding. Per impostazione predefinita, text-embedding-3-small restituisce dimensioni 1536, ma EverOS inoltra a OpenAI il valore configurato per dimensions. Questo tutorial richiede dimensioni 1024 per allinearsi agli schemi Milvus gestiti da EverOS.
La modalità di memoria chat mantiene questo esempio incentrato sulle memorie utente. EverOS gestisce le collezioni Milvus e i relativi schemi, quindi non è necessario crearli manualmente.
Avvia EverOS
Avvia il server HTTP di EverOS:
uv run everos server start --root "$EVEROS_ROOT"
Lascia aperto questo terminale. Durante l’avvio, EverOS si connette a Milvus e crea sette collezioni di indici derivati con il prefisso configurato.
Aprire un altro terminale nella stessa directory del progetto e verificare il servizio:
curl http://127.0.0.1:8000/health
Output di riferimento:
{
"status": "ok",
"version": "1.3.0",
"capabilities": {
"llm": true,
"embed": true,
"rerank": false,
"multimodal_llm": false,
"parser": true
},
"cascade": {
"healthy": true,
"pending": 0
}
}
La risposta contiene campi aggiuntivi relativi allo stato di salute. I valori importanti per questo tutorial sono status: "ok", llm: true, embed: true e cascade.healthy: true.
Aggiungere conversazioni relative al progetto
Il seguente programma Python invia dieci conversazioni indipendenti a EverOS. Atlas presenta discussioni separate relative al lancio e al rollback. Otto conversazioni su altri progetti fungono da distrattori, in modo che la ricerca successiva debba identificare le memorie relative al progetto corretto.
Salvare il codice seguente come 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']}")
Eseguirlo dalla directory del progetto:
uv run python add_memories.py
Output di riferimento:
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
Impostando ` defer_extraction ` su ` true `, ogni conversazione viene memorizzata nel buffer permanente senza richiedere all'LLM di rilevare un confine. La seguente chiamata ` /flush ` segna la fine di quella sessione e innesca un'estrazione. EverOS scrive quindi l'episodio estratto in Markdown e lo incorpora in modo asincrono nell'indice Milvus.
Esamina la memoria Markdown
Il file dell'episodio generato viene memorizzato nei contesti dell'applicazione, del progetto e dell'utente:
find "$EVEROS_ROOT/project-assistant/launch-planning/users/maya/episodes" \
-type f -name "*.md"
Output di riferimento (la data nel nome del file riflette il momento in cui si esegue l’esempio):
everos-data/project-assistant/launch-planning/users/maya/episodes/episode-2026-09-08.md
Apri il file per visualizzare i ricordi estratti dall’LLM. Un estratto abbreviato si presenta così:
## 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 formulazione esatta, gli identificatori e i timestamp possono variare poiché la memoria viene estratta dall’LLM. I file Markdown originali rimangono la fonte di riferimento definitiva; l’indice Milvus può essere ricostruito a partire da essi.
Cerca nei ricordi
Utilizza la ricerca ibrida per verificare cosa dovrebbe essere memorizzato prima che Atlas diventi operativo. Salva il codice seguente come 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']}")
Esegui la ricerca:
uv run python search_memories.py
Risultato di riferimento (i punteggi e la formulazione possono variare):
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
Entrambe le conversazioni di Atlas vengono restituite prima delle otto conversazioni non correlate. EverOS invia la query all’endpoint di embedding di OpenAI, richiede a Milvus i candidati BM25 e vettoriali all’interno dell’ambito dell’applicazione e del progetto di Maya, quindi fonde i due elenchi di risultati.
Esamina le collezioni di Milvus
EverOS crea una collezione per ogni tipo di memoria derivata supportato. Usa MilvusClient per elencare il numero di righe:
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()
Output di riferimento dall’esecuzione convalidata:
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
Il numero esatto di fatti atomici può variare a seconda dell’output dell’LLM. Le dieci righe di episodi corrispondono alle dieci conversazioni salvate. Le altre collezioni sono disponibili per le modalità di memoria e le funzionalità di EverOS che questo esempio specifico non utilizza.
Utilizzare un’altra distribuzione Milvus
Per utilizzare un altro endpoint di Milvus Server o Zilliz Cloud, aggiorna EVEROS_MILVUS__URI. Imposta EVEROS_MILVUS__TOKEN quando l’endpoint richiede l’autenticazione. Il codice di acquisizione e ricerca rimane invariato.
Conclusione
Combinando EverOS con Milvus, è possibile trasformare le conversazioni in memorie durature e recuperarle tramite parole chiave e segnali semantici. È possibile adattare lo stesso modello per dotare gli assistenti e altre applicazioni agentiche di una memoria a lungo termine per i propri utenti, progetti e flussi di lavoro.