{"id":51002562,"url":"https://github.com/chenhaodev/agentmem","last_synced_at":"2026-06-20T16:32:37.119Z","repository":{"id":364309218,"uuid":"1267347955","full_name":"chenhaodev/agentmem","owner":"chenhaodev","description":"Switchable agent-memory middleware: short/long-term separation, pluggable long-term backends (vector, mem0, LightRAG, Letta), powered by DeepSeek-V4-Flash","archived":false,"fork":false,"pushed_at":"2026-06-12T13:47:39.000Z","size":72,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T15:13:17.688Z","etag":null,"topics":["agent-memory","ai-agents","deepseek","knowledge-graph","letta","lightrag","llm","mem0","middleware","python","rag","vector-search"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chenhaodev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-12T13:07:11.000Z","updated_at":"2026-06-12T14:24:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chenhaodev/agentmem","commit_stats":null,"previous_names":["chenhaodev/agentmem"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/chenhaodev/agentmem","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenhaodev%2Fagentmem","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenhaodev%2Fagentmem/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenhaodev%2Fagentmem/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenhaodev%2Fagentmem/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chenhaodev","download_url":"https://codeload.github.com/chenhaodev/agentmem/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenhaodev%2Fagentmem/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34578089,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-memory","ai-agents","deepseek","knowledge-graph","letta","lightrag","llm","mem0","middleware","python","rag","vector-search"],"created_at":"2026-06-20T16:32:36.349Z","updated_at":"2026-06-20T16:32:37.108Z","avatar_url":"https://github.com/chenhaodev.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# agentmem — a switchable agent-memory middleware\n\n[![CI](https://github.com/chenhaodev/agentmem/actions/workflows/ci.yml/badge.svg)](https://github.com/chenhaodev/agentmem/actions/workflows/ci.yml)\n\nOne interface over multiple agent-memory frameworks, with **short-term** and\n**long-term** memory cleanly separated. Powered by **DeepSeek-V4-Flash**\n(OpenAI-compatible). Switch the long-term backend with a single env var.\n\n```python\nfrom agentmem import MemoryManager, Message\n\nmm = MemoryManager()                       # long-term backend via LONG_TERM_BACKEND (default: vector)\n\nmm.add_turn(\"s1\", \"alice\", Message(\"user\", \"I'm vegetarian and I love hiking in the Alps\"))\nmm.end_session(\"s1\", \"alice\")              # consolidate short-term turns into long-term memory\n\nprint(mm.recall(\"alice\", \"what food does she eat?\"))\n# [MemoryItem(score=0.52, text='User is vegetarian'), ...]\n\n# build a context block for your next LLM prompt: recent turns + relevant memories\nctx = mm.build_context(\"s1\", \"alice\", query=\"trip ideas\")\nprint(ctx.as_prompt_block())\n```\n\nSwitch backends with one env var — `LONG_TERM_BACKEND=mem0` (or `lightrag`,\n`letta`, `vector+mem0` to fan out across several). Nothing else changes.\n\n## Why this shape\n\n- **Short-term ≠ long-term.** Short-term is the recent raw-turn buffer — kept\n  lightweight and framework-agnostic (no LLM on the hot path), living in a shared\n  namespace so multiple agents/handlers in a process see the same recent context.\n- **Long-term is pluggable.** The heavy frameworks (mem0, Letta, …) only matter\n  here. They sit behind one `LongTermBackend` contract, so swapping them is a\n  config change, not a code change.\n- **Common interface, not common features.** The contract is the lowest common\n  denominator (`add / search / get_all / delete` over `{text, metadata, score}`).\n  Backend-specific superpowers ride along in `MemoryItem.metadata`.\n\n```\nMemoryManager (facade)\n ├─ ShortTermMemory   pluggable, shared, no LLM                 (short_term.py)\n │    ├─ memory   in-process ring buffer (zero deps)  ← default\n │    └─ redis    Redis list per session — shared across processes/machines\n └─ LongTermBackend   pluggable, LLM-backed extraction          (backends/)\n      ├─ vector    plain numpy cosine + pluggable embedder + disk persistence ← default\n      ├─ mem0      mem0 library, DeepSeek as its extraction LLM\n      ├─ lightrag  knowledge-graph memory; builds its own entity/relation graph\n      ├─ letta     Letta archival memory (needs a Letta server)\n      └─ router    fan-out writes + merged reads across several of the above (a+b)\n └─ DeepSeekLLM     one OpenAI-compatible client, injected where extraction is needed\n```\n\nEmbedders for the vector backend (`EMBEDDING_PROVIDER`): `auto` (local\nsentence-transformers → hashing fallback), `hash` (offline, zero-dep),\n`sentence_transformers`, or `openai` (any OpenAI-compatible `/embeddings`\nendpoint via `EMBEDDING_BASE_URL`). `sentence_transformers` runs on CPU — no\nGPU needed. On older macOS see `requirements-local-cpu.txt` for the pinned,\nverified-working dependency set.\n\nWrite path: `add_turn` → short-term (always) → consolidate to long-term every\n`CONSOLIDATE_EVERY` user turns. Read path: `build_context` → recent raw turns\n**+** relevance-ranked memories.\n\n### Async consolidation\n\nConsolidation runs the LLM (fact extraction / graph building), which is slow. Set\n`CONSOLIDATION_ASYNC=1` to run it on a background worker so `add_turn` returns\nimmediately (verified: ~0–1 ms vs seconds inline):\n\n```python\ncfg = Config.from_env(); cfg.consolidation_async = True\nwith MemoryManager(cfg) as mm:                 # context manager stops the worker\n    mm.add_turn(\"s\", \"u\", Message(\"user\", \"...\"))   # returns instantly\n    mm.flush()                                  # wait for background extraction\n    mm.recall(\"u\", \"...\")                       # now searchable\n    # end_session() also drains automatically; mm.consolidation_errors surfaces failures\n```\n\nA single worker thread serializes all long-term access (the backends aren't\nthread-safe), so reads and the background write never race. **Run one long-term\nbackend per process** — mem0 and LightRAG are heavy ML stacks that interfere when\nsharing a process (the opt-in live tests isolate each in its own subprocess).\n\n## Quickstart\n\n```bash\npip install -r requirements.txt\ncp .env.example .env        # add your DEEPSEEK_API_KEY\npython demo.py              # default: vector backend, zero extra deps\n```\n\n```python\nfrom agentmem import MemoryManager, Message\n\nmm = MemoryManager()                                   # backend from env\nmm.add_turn(\"sess1\", \"alice\", Message(\"user\", \"I'm vegetarian and love the Alps\"))\nmm.end_session(\"sess1\", \"alice\")                       # flush short-\u003elong\nprint(mm.recall(\"alice\", \"what food does she eat?\"))\nctx = mm.build_context(\"sess1\", \"alice\", query=\"trip ideas\")\nprompt = ctx.as_prompt_block()                         # feed into your next LLM call\n```\n\n## Switching backends\n\n```bash\nLONG_TERM_BACKEND=vector python demo.py   # default, no extra install\nLONG_TERM_BACKEND=mem0     python demo.py   # pip install \"mem0ai\u003e=2.0\" qdrant-client sentence-transformers\nLONG_TERM_BACKEND=lightrag python demo.py   # pip install \"lightrag-hku\u003e=1.0\"  (graph memory)\nLONG_TERM_BACKEND=letta    python demo.py   # Letta server — see \"Letta setup\" below\n```\n\nBoth `mem0` and `lightrag` are verified live against DeepSeek-V4-Flash\n(mem0 2.0.5 = DeepSeek + HuggingFace MiniLM embeddings + embedded Qdrant;\nlightrag-hku 1.5.2 = knowledge graph). Reproduce with the opt-in live tests:\n\n```bash\nset -a \u0026\u0026 . ./.env \u0026\u0026 set +a\nexport SSL_CERT_FILE=$(python3 -c \"import certifi; print(certifi.where())\")\nexport TOKENIZERS_PARALLELISM=false RUN_LIVE=1\npython tests/test_live.py\n\n# Share short-term memory across processes (verified live):\ndocker run -d --name agentmem-redis -p 6379:6379 redis:7-alpine   # pip install redis\nSHORT_TERM_STORE=redis REDIS_URL=redis://localhost:6379/0 python demo.py\n\n# Store long memory in several frameworks at once (cross-backend routing):\nLONG_TERM_BACKEND=vector+mem0 python demo.py\n```\n\n### Cross-backend routing\n\nSet `LONG_TERM_BACKEND` to a `+`-list to wrap several backends in a router that\n**fans out writes** and **merges reads** — \"long memory in different frameworks\nat once\". A router *is* a `LongTermBackend`, so nothing else changes.\n\n- **Write:** fans out to all children (`ROUTER_WRITE=first` to write only the\n  first). Route a single write to one child with `add(..., metadata={\"backend\":\n  \"mem0\"})`. A child failing doesn't lose the write to the others.\n- **Read:** queries every child and merges. Scores aren't comparable across\n  heterogeneous backends, so the default `ROUTER_MERGE=interleave` (round-robin)\n  gives balanced results; `ROUTER_MERGE=score` sorts by raw score. Results are\n  deduped by text and tagged with `metadata[\"backend\"]` for provenance.\n- Verified live: `vector+mem0` fans out and returns merged, provenance-tagged\n  memories. (Don't combine mem0 + lightrag in one process — see below.)\n\n### Letta setup (verified live)\n\nLetta is a stateful agent server; we use its archival memory. It needs an LLM\n**and** an embedding model — DeepSeek has no embeddings, so use a local Ollama\nembedder. The adapter auto-creates one agent per `user_id`.\n\n```bash\npip install \"letta-client\u003e=1.0\"\n\n# 1. Ollama embedder (Ollama's OpenAI-compatible embeddings live at /v1)\nollama pull nomic-embed-text\n\n# 2. Letta server with DeepSeek as the LLM + reach to Ollama\ndocker run -d --name letta -p 8283:8283 \\\n  -e OPENAI_API_KEY=$DEEPSEEK_API_KEY \\\n  -e OPENAI_API_BASE=https://api.deepseek.com/v1 \\\n  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \\\n  letta/letta:latest\n\n# 3. point agentmem at it\nexport LONG_TERM_BACKEND=letta\nexport LETTA_MODEL=openai-proxy/deepseek-v4-flash\nexport LETTA_EMBEDDING_ENDPOINT=http://host.docker.internal:11434/v1   # NOTE the /v1\n```\n\nGotcha: Letta routes Ollama embeddings through its OpenAI client, so the\nendpoint must end in `/v1` (`LETTA_EMBEDDING_ENDPOINT`) — without it Ollama\nreturns 404. Verified with Letta v0.16.8 / letta-client 1.12.1.\n\n## Install\n\n```bash\npip install -e .            # core only (vector backend)\npip install -e \".[mem0]\"   # or [lightrag] / [letta] / [redis] / [embeddings] / [all]\nagentmem-demo              # console entry point (== python -m agentmem)\n```\n\n## Benchmark\n\n`benchmark.py` runs a shared dataset through each backend in isolated\nsubprocesses. Two datasets:\n\n```bash\npython benchmark.py            # EASY set — latency comparison (recall saturates)\npython benchmark.py --hard     # HARD set — retrieval QUALITY (discriminating)\npython benchmark.py --hard vector mem0   # subset\n```\n\nThe **hard** set (15 facts / 8 queries) is built to break naive retrieval:\ntemporal updates (Porto supersedes Lisbon), distractors (sister's allergy vs the\nuser's), multi-hop relations (mentor → their lab), and negations (quit coffee →\ntea now). Metrics over the top-5 retrieved: `hit@1`, `hit@3`, `MRR`, and `clean`\n(answer retrieved **and** no stale/wrong fact ranked above it).\n\nIndicative run (this machine, CPU, DeepSeek-V4-Flash):\n\n| backend  | write (s) | query (ms) | hit@1 | hit@3 | clean | MRR  |\n|----------|----------:|-----------:|------:|------:|------:|-----:|\n| vector   |     10.4  |        27  |  0.38 |  1.0  |  0.38 | 0.67 |\n| mem0     |      9.9  |        26  |  0.25 |  0.75 |  0.5  | 0.54 |\n| lightrag |     77.6  |      3670  |  1.0  |  1.0  |  0.38 | 1.0  |\n| letta    |     48.3  |      2470  |  0.62 |  1.0  |  0.5  | 0.79 |\n\nHow to read it:\n\n- **lightrag** `hit@1`/`MRR = 1.0` is an artifact: it returns one graph-context\n  blob containing everything, so the answer is always \"rank 1\". But `clean =\n  0.38` shows the blob also carries the contradictions — it offloads\n  disambiguation to the downstream LLM rather than discriminating at retrieval.\n- **letta** has the best `clean` (0.5): semantic passage retrieval is best at not\n  surfacing a stale fact above the right one.\n- **vector** finds the answer in the top-3 every time (`hit@3 = 1.0`) but often\n  ranks a distractor first (`hit@1 = 0.38`).\n- **mem0** rephrases/merges facts on extraction (trimming volume) and ties Letta\n  for the best `clean` (0.5) — proper top-k ranking keeps stale facts from\n  outranking the answer. It's also the highest-variance backend: across runs its\n  `hit@1` swung 0.25↔0.75. The dataset deliberately pits stale vs current facts,\n  which pure vector similarity still can't fully resolve (`clean` tops out at 0.5).\n\nCaveats: numbers vary run-to-run (LLM extraction is non-deterministic — mem0\nmost, e.g. `hit@1` 0.25↔0.75 above); latency is single-run. `stored` (not shown) differs by design: extracted\nfacts vs raw passages vs one graph doc. Rules of thumb: speed/personalization →\nmem0/vector; relational/multi-hop reasoning → lightrag; long-running agents with\nprecise recall → letta.\n\n## Tests\n\n```bash\npython tests/test_smoke.py     # fully offline; also discoverable via `pytest tests/`\n```\n\nCI (`.github/workflows/ci.yml`) byte-compiles everything and runs the offline\nsuite on Python 3.10 / 3.11 / 3.12 for every push and PR — core deps only, no\nbackend servers. The live suite (`tests/test_live.py`) self-skips without\n`RUN_LIVE=1` and is not run in CI. (The CI badge above renders publicly once the\nrepo is made public.)\n\n## Configuration (env / `.env`)\n\n| Var | Default | Meaning |\n|-----|---------|---------|\n| `DEEPSEEK_API_KEY` | — | DeepSeek key (OpenAI-compatible endpoint) |\n| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model id |\n| `DEEPSEEK_API_BASE` | `https://api.deepseek.com` | base url |\n| `LONG_TERM_BACKEND` | `vector` | `vector` \\| `mem0` \\| `lightrag` \\| `letta` \\| a `+`-list e.g. `vector+mem0` |\n| `ROUTER_MERGE` | `interleave` | cross-backend read merge: `interleave` \\| `score` |\n| `ROUTER_WRITE` | `all` | cross-backend write fan-out: `all` \\| `first` |\n| `LIGHTRAG_WORKING_DIR` | `.data/lightrag` | per-user graph dir (lightrag backend) |\n| `SHORT_TERM_MAX_TURNS` | `12` | raw turns kept per session |\n| `CONSOLIDATE_EVERY` | `4` | promote short→long every N user turns |\n| `CONSOLIDATION_ASYNC` | `0` | run consolidation on a background worker (`1` to enable) |\n\n## Notes \u0026 current limits\n\n- **Offline-friendly:** with no network/key, fact extraction falls back to raw\n  user turns and the vector backend uses a deterministic **hashing embedder**\n  (lexical, not semantic — so some recall scores are ~0). Install\n  `sentence-transformers` or supply a live `DEEPSEEK_API_KEY` for real quality.\n- The `vector` backend is in-memory by default; set `VECTOR_PERSIST_PATH` to\n  persist to disk (atomic JSON write, auto-loaded on startup).\n- Short-term sharing across processes: set `SHORT_TERM_STORE=redis` (verified\n  live — a write in one process is seen by another via a Redis list per session,\n  ring-buffer capped by `SHORT_TERM_MAX_TURNS`). Within a single process the\n  default `memory` store already shares across agents.\n- The `lightrag` backend builds a knowledge graph and does its own extraction,\n  so it supports **add + search** (search returns graph-aware context). It does\n  **not** support `get_all`/`delete` (no flat memory list) — reset by removing a\n  user's dir under `LIGHTRAG_WORKING_DIR`.\n\n## Adding a backend\n\nImplement the `LongTermBackend` Protocol (`backends/base.py`) — `add`, `search`,\n`get_all`, `delete` returning `MemoryItem`s — and register it in\n`backends/__init__.py::build_backend`. That's the whole extension surface.\n\n## Layout\n\n```\nsrc/agentmem/\n  manager.py        facade: short+long orchestration, consolidation policy\n  short_term.py     in-process shared recency buffer\n  llm.py            DeepSeek-V4-Flash client + fact extraction\n  embeddings.py     hashing (default) / sentence-transformers embedders\n  config.py         env-driven config\n  types.py          Message, MemoryItem\n  backends/\n    base.py         LongTermBackend Protocol\n    vector.py       plain numpy backend (default)\n    mem0_backend.py mem0 adapter\n    letta_backend.py Letta adapter\ndemo.py             runnable end-to-end demo\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenhaodev%2Fagentmem","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchenhaodev%2Fagentmem","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenhaodev%2Fagentmem/lists"}