Développer la mémoire à long terme des agents avec EverOS et Milvus
EverOS est un système de mémoire « Markdown-first » destiné aux agents IA. Il extrait des souvenirs durables à partir de conversations, conserve le Markdown comme source de référence et construit un index dérivé consultable.
Dans ce tutoriel, nous allons créer un assistant de projet capable de mémoriser les décisions de lancement issues de conversations distinctes. Nous ajouterons des conversations concernant le lancement du projet Atlas aux côtés de conversations sans rapport avec d’autres projets. EverOS utilisera un LLM pour extraire les mémoires, tandis que Milvus stockera les index BM25 et vectoriels utilisés pour la recherche hybride.
Conversations
|
v
EverOS + LLM ------> Markdown memory files
|
| embedding model
v
Milvus ------> BM25 + vector hybrid search
Le LLM et le modèle d’embedding ont des rôles distincts. Le LLM transforme une conversation en souvenirs structurés. Le modèle d’embedding convertit ces souvenirs, ainsi que les requêtes de recherche ultérieures, en vecteurs. La recherche hybride de base présentée dans ce tutoriel ne nécessite pas de modèle de reclassement.
Prérequis
Vous devez disposer de :
- Python 3.12 ou version ultérieure
uv- Un serveur Milvus en cours d’exécution
- Une clé API OpenAI
Ce tutoriel se connecte au serveur Milvus à l'adresse http://localhost:19530. EverOS prend également en charge Zilliz Cloud via les mêmes paramètres d'URI et de jeton. Son backend Milvus attend un point de terminaison distant et n'accepte pas de chemin d'accès à un fichier Milvus Lite.
Installez EverOS
Créez un projet local et installez EverOS avec ses dépendances Milvus facultatives :
mkdir everos-milvus-demo
cd everos-milvus-demo
uv init --bare --python 3.12
uv add "everos[milvus]"
La commande ne spécifie délibérément pas de version, de sorte qu’une nouvelle installation installe la dernière version compatible d’EverOS.
Initialisez une racine mémoire distincte pour ce tutoriel :
export EVEROS_ROOT="$PWD/everos-data"
uv run everos init --root "$EVEROS_ROOT"
EverOS crée les fichiers everos.toml et ome.toml dans ce répertoire. Il y enregistrera également les mémoires extraites.
Configurer OpenAI et Milvus
Définissez la clé API OpenAI et configurez EverOS à l’aide de variables d’environnement :
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 utilise OpenAI à la fois pour l’extraction de mémoire et pour les représentations vectorielles. text-embedding-3-small renvoie par défaut des dimensions 1536, mais EverOS transmet la valeur configurée dimensions à OpenAI. Ce tutoriel demande des dimensions 1024 afin de correspondre aux schémas Milvus gérés par EverOS.
Le mode de mémoire « chat » permet de centrer cet exemple sur les mémoires utilisateur. EverOS gère les collections Milvus et leurs schémas ; vous n’avez donc pas besoin de les créer vous-même.
Démarrez EverOS
Démarrez le serveur HTTP EverOS :
uv run everos server start --root "$EVEROS_ROOT"
Laissez ce terminal ouvert. EverOS se connecte à Milvus et crée sept collections d’index dérivés avec le préfixe configuré lors du démarrage.
Ouvrez un autre terminal dans le même répertoire de projet et vérifiez le service :
curl http://127.0.0.1:8000/health
Exemple de sortie :
{
"status": "ok",
"version": "1.3.0",
"capabilities": {
"llm": true,
"embed": true,
"rerank": false,
"multimodal_llm": false,
"parser": true
},
"cascade": {
"healthy": true,
"pending": 0
}
}
La réponse contient des champs supplémentaires relatifs à l'état de santé. Les valeurs importantes pour ce tutoriel sont status: "ok", llm: true, embed: true et cascade.healthy: true.
Ajouter des conversations de projet
Le programme Python suivant envoie dix conversations indépendantes à EverOS. Atlas dispose de discussions distinctes sur le lancement et la restauration. Huit conversations concernant d’autres projets servent de distracteurs, de sorte que la recherche ultérieure doive identifier les souvenirs de projet corrects.
Enregistrez le code suivant sous le nom « 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']}")
Exécutez-le depuis le répertoire du projet :
uv run python add_memories.py
Résultats de référence :
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
En définissant ` defer_extraction ` sur ` true `, chaque conversation est stockée dans le tampon durable sans demander au LLM de détecter une limite. L'appel suivant à ` /flush ` marque la fin de cette session et déclenche une extraction. EverOS écrit ensuite l'épisode extrait au format Markdown et l'intègre de manière asynchrone à l'index Milvus.
Inspecter la mémoire Markdown
Le fichier d’épisode généré est stocké au niveau de l’application, du projet et de l’utilisateur :
find "$EVEROS_ROOT/project-assistant/launch-planning/users/maya/episodes" \
-type f -name "*.md"
Sortie de référence (la date du nom de fichier correspond à la date à laquelle vous exécutez l’exemple) :
everos-data/project-assistant/launch-planning/users/maya/episodes/episode-2026-09-08.md
Ouvrez le fichier pour voir les souvenirs extraits par le LLM. En voici un extrait abrégé :
## 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 formulation exacte, les identifiants et les horodatages peuvent varier, car le souvenir est extrait par le LLM. Les fichiers Markdown d’origine restent la source de référence fiable ; l’index Milvus peut être reconstruit à partir de ceux-ci.
Rechercher dans les souvenirs
Utilisez la recherche hybride pour déterminer ce qui doit être mémorisé avant la mise en service d’Atlas. Enregistrez le code suivant sous le nom « 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']}")
Lancez la recherche :
uv run python search_memories.py
Résultats de référence (les scores et la formulation peuvent varier) :
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
Les deux conversations Atlas apparaissent avant les huit conversations sans rapport. EverOS envoie la requête au point de terminaison d’embedding d’OpenAI, demande à Milvus des candidats BM25 et vectoriels dans le cadre de l’application et du projet de Maya, puis fusionne les deux listes de résultats.
Examinez les collections Milvus
EverOS crée une collection pour chaque type de mémoire dérivée pris en charge. Utilisez MilvusClient pour répertorier le nombre de lignes de chacune d’entre elles :
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()
Référence à la sortie de l’exécution validée :
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
Le nombre exact de faits atomiques peut varier en fonction de la sortie du LLM. Les dix lignes d'épisodes correspondent aux dix conversations vidées. Les autres collections sont disponibles pour les modes de mémoire et les fonctionnalités d'EverOS que cet exemple ciblé n'utilise pas.
Utiliser un autre déploiement Milvus
Pour utiliser un autre point de terminaison Milvus Server ou Zilliz Cloud, mettez à jour EVEROS_MILVUS__URI. Définissez EVEROS_MILVUS__TOKEN lorsque le point de terminaison nécessite une authentification. Le code d’ingestion et de recherche reste inchangé.
Conclusion
En combinant EverOS et Milvus, vous pouvez transformer les conversations en souvenirs durables et les récupérer à l’aide de mots-clés et de signaux sémantiques. Vous pouvez adapter ce même modèle pour doter les assistants et autres applications agentiques d’une mémoire à long terme pour vos propres utilisateurs, projets et workflows.