diff --git a/code-examples/ai-memory.md b/code-examples/ai-memory.md index d9b3c8d..28ae704 100644 --- a/code-examples/ai-memory.md +++ b/code-examples/ai-memory.md @@ -12,28 +12,36 @@ Memgraph models three kinds of long-term memory as one unified graph: | Memory type | What it holds | How it is stored | | --- | --- | --- | -| **Semantic** | What the system **knows** (facts, preferences) | Entities with typed relationships | -| **Episodic** | What the system **experienced** (past interactions, time) | Interaction nodes encoding sequence and consequence | -| **Procedural** | What the system **knows how to do** (workflows) | Steps as nodes, transitions as edges | +| **Semantic** | What the system **knows** (facts, preferences) | `(:User)-[:HAS_MEMORY]->(:Memory)` | +| **Episodic** | What the system **experienced** (past interactions, time) | `(:Session)-[:HAS_ACTION]->(:Action)`, sequenced by `FOLLOWED_BY` | +| **Procedural** | What the system **knows how to do** (workflows) | `(:Session)-[:USED_SKILL]->(:Skill)` | -The value is the **interconnection**: *semantic fact → episodic event → -procedural response*. This example seeds all three for the page's own scenario -and answers it by traversal. +This example writes and reads all three through the actual +[Context Graph](https://github.com/memgraph/ai-toolkit/tree/main/context-graph) +packages a live coding-assistant plugin uses — `sessions-graph`, `actions-graph`, +`skills-graph` — instead of a hand-rolled schema. The `(:User)`/`(:Session)` +nodes those three packages share are the join key, so the payoff is a genuine +graph traversal, not three separate lookups glued together. ## High-level Plan -1. **Spin up** the memory store (Memgraph) and the MCP server your harness loads. -2. **Write** the three memory types for a client the assistant has worked with. -3. **Recall** each type, then all three together to answer *"Schedule a follow-up - with the client like last time."* +1. **Spin up** the memory store (Memgraph). +2. **Write** the three memory types for a client the assistant has worked with, + through `sessions-graph`/`actions-graph`/`skills-graph`. +3. **Recall** each type, then all three together to answer *"Schedule a + follow-up with the client like last time."* ## What You Need - **Docker**: https://docs.docker.com/get-docker/ +- **Python 3.10-3.13**: https://www.python.org/downloads/ (installs the three + Context Graph packages above from PyPI into a throwaway virtualenv — no + repository checkout needed) -That is it. No API keys and no Python: everything runs through Memgraph's own -images (`memgraph-mage`, `mcp-memgraph`, `mgconsole`). This is the "build custom" -path from the page (Cypher, MAGE, MCP). +No API keys: this example writes structured memory directly, the same way an +application would call these packages. Automatic, LLM-backed extraction from +raw conversation text is a separate, opt-in step — see +[Where to Go Next](#where-to-go-next). ## Run It @@ -56,10 +64,10 @@ If Windows blocks the script, allow local scripts for the session first: ## Step-by-step -### 1. Spin up Memgraph and the MCP server +### 1. Spin up Memgraph -Memgraph starts with schema info enabled (so the ontology is queryable), and the -MCP server, the tool your harness loads to read and write memory, is pointed at it: +Memgraph starts with schema info enabled, so the ontology is queryable once the +Context Graph packages have written into it: ```bash docker network create aimemory-net @@ -67,84 +75,87 @@ docker network create aimemory-net docker run -d --name aimemory-memgraph --network aimemory-net \ -p 7687:7687 -p 7444:7444 \ memgraph/memgraph-mage:3.12.0 --schema-info-enabled=True +``` + +### 2. Install the Context Graph memory packages -docker run -d --name aimemory-mcp --network aimemory-net \ - -p 8000:8000 --env MEMGRAPH_URL=bolt://aimemory-memgraph:7687 \ - memgraph/mcp-memgraph:0.2.0 +```bash +python3 -m venv .ai-memory-venv +.ai-memory-venv/bin/pip install sessions-graph actions-graph skills-graph memgraph-toolbox ``` -### 2. Write the three memory types +### 3. Write the three memory types + +The assistant has met a client before and knows how to schedule follow-ups. +That knowledge is split across three packages, glued together by a shared +`(:User {user_id})` and two `(:Session {session_id})` nodes +(`session-acme-kickoff`, `session-acme-followup`) — see `ai-memory.py`: + +```python +# Episodic: two real sessions, each with a ToolCall/ToolResult (actions-graph) +actions.create_session(Session(session_id="session-acme-kickoff", ...)) +actions.create_session(Session(session_id="session-acme-followup", ...)) +actions.record_tool_call(session_id=..., tool_name="schedule_meeting", tool_input={...}) +actions.record_tool_result(session_id=..., tool_use_id=..., tool_name="schedule_meeting", ...) + +# Semantic: a durable fact about the client (sessions-graph) +memories.save_memory( + user_id="acme-corp", + content="Acme Corp's contact is Dana Lee (timezone America/New_York); they prefer 30-minute meetings.", + session_id="session-acme-kickoff", +) + +# Procedural: a reusable skill, used during the follow-up session (skills-graph) +skills.add_skill(Skill(name="schedule-follow-up", description="...", content="1. Book a calendar slot...\n2. Send a calendar invite.")) +skills.record_skill_usage(session_id="session-acme-followup", skill_name="schedule-follow-up", action="used", timestamp=...) +``` -The assistant has met a client before and knows how to schedule follow-ups. That -knowledge is split across the three memories: +Run it: -```cypher -// Semantic: what the system KNOWS -MERGE (c:Client {name: "Acme Corp"}) SET c.contact = "Dana Lee", c.timezone = "America/New_York"; -MERGE (p:Preference {kind: "meeting_length", value: "30 min"}); -MATCH (c:Client {name:"Acme Corp"}), (p:Preference {kind:"meeting_length"}) MERGE (c)-[:PREFERS]->(p); - -// Episodic: what the system EXPERIENCED (with a sequence edge) -MERGE (m1:Interaction {id:"int-1", weekday:"Tuesday", duration:"30 min", when:"2026-06-30", summary:"kickoff"}); -MERGE (m2:Interaction {id:"int-2", weekday:"Tuesday", duration:"30 min", when:"2026-07-07", summary:"follow-up"}); -MATCH (m1:Interaction {id:"int-1"}), (m2:Interaction {id:"int-2"}) MERGE (m1)-[:NEXT]->(m2); - -// Procedural: what the system KNOWS HOW TO DO (steps + transitions) -MERGE (w:Workflow {name:"schedule_follow_up"}); -MERGE (s1:Step {name:"book calendar slot"}); MERGE (s2:Step {name:"send invite"}); -MATCH (w:Workflow {name:"schedule_follow_up"}), (s1:Step {name:"book calendar slot"}) MERGE (w)-[:STARTS_WITH]->(s1); -MATCH (s1:Step {name:"book calendar slot"}), (s2:Step {name:"send invite"}) MERGE (s1)-[:THEN]->(s2); +```bash +MEMGRAPH_URL=bolt://localhost:7687 .ai-memory-venv/bin/python ai-memory.py ``` -### 3. Recall +### 4. Recall -Each memory type is a small traversal: +Each memory type is a small, package-provided lookup: -```cypher --- Semantic: what do we know about the client? -MATCH (c:Client {name:"Acme Corp"})-[:PREFERS]->(p:Preference) -RETURN c.contact, c.timezone, p.value; - --- Episodic: what happened last time? -MATCH (i:Interaction)-[:WITH]->(:Client {name:"Acme Corp"}) -RETURN i.when, i.weekday, i.duration ORDER BY i.when DESC LIMIT 1; - --- Procedural: how do we schedule a follow-up? -MATCH (:Workflow {name:"schedule_follow_up"})-[:STARTS_WITH]->(first:Step) -MATCH p=(first)-[:THEN*0..]->(s:Step) -WITH s, length(p) AS ord ORDER BY ord RETURN collect(s.name) AS steps; +```python +memories.get_memories("acme-corp") # semantic +actions.list_sessions(limit=1) # episodic: most recent session +actions.get_session_actions(session.session_id) # ... and what happened in it +skills.get_skill("schedule-follow-up") # procedural ``` -The payoff is the **interconnected** recall, one traversal that joins all three to -answer *"schedule a follow-up with the client like last time"*: +The payoff is the **interconnected** recall: one Cypher traversal through the +shared `User`/`Session` nodes joins all three to answer *"schedule a follow-up +with the client like last time"*: ```cypher -MATCH (c:Client {name:"Acme Corp"}) -MATCH (last:Interaction)-[:WITH]->(c) -WITH c, last ORDER BY last.when DESC LIMIT 1 -MATCH (:Workflow {name:"schedule_follow_up"})-[:STARTS_WITH]->(f:Step) -MATCH pth=(f)-[:THEN*0..]->(st:Step) -WITH c, last, st, length(pth) AS o ORDER BY o -RETURN c.contact AS client, c.timezone AS timezone, - last.weekday AS like_last_time_day, last.duration AS duration, - collect(st.name) AS actions; +MATCH (u:User {user_id: "acme-corp"})-[:HAS_MEMORY]->(mem:Memory) +MATCH (u)-[:HAD_SESSION]->(s:Session)-[:HAS_ACTION]->(a:Action {tool_name: "schedule_meeting"}) +WITH u, mem, s, a ORDER BY s.started_at DESC LIMIT 1 +OPTIONAL MATCH (s)-[:USED_SKILL]->(sk:Skill) +RETURN mem.content AS client_facts, s.session_id AS last_session, + a.timestamp AS last_meeting_at, sk.name AS skill, sk.content AS how_to ``` -It returns *Dana Lee, America/New_York, Tuesday, 30 min, [book calendar slot, -send invite]*, everything needed for the assistant to reply *"Done. 30 min Tuesday -slot booked, invite sent."* +It returns *Dana Lee's Acme Corp facts, the `session-acme-followup` session, +the `schedule-follow-up` skill and its steps* — everything needed for the +assistant to reply *"Done. 30 min Tuesday slot booked, invite sent."* -### 4. Inspect the memory ontology +### 5. Inspect the memory ontology `SHOW SCHEMA INFO` returns the whole ontology (labels, relationship types, properties) in constant time, so an agent can learn the shape of memory before -querying it: +querying it — now the real `User`/`Session`/`Memory`/`Action`/`Skill` schema +the Context Graph packages created, not a demo-only schema: ```cypher SHOW SCHEMA INFO; ``` -### 5. Explore visually (optional) +### 6. Explore visually (optional) ```bash docker run -d --name aimemory-lab --network aimemory-net -p 3000:3000 \ @@ -155,24 +166,26 @@ docker run -d --name aimemory-lab --network aimemory-net -p 3000:3000 \ ## Wire It Into a Real Harness -The seeding above did by hand what your assistant should do automatically. Point -an MCP-capable harness (Claude Desktop, Cursor, VS Code, ...) at the running MCP -server and it can call `run_query`, `get_schema`, and the other tools to write new -semantic/episodic/procedural memory and recall it: - -```json -{ - "mcpServers": { - "memgraph-memory": { - "url": "http://localhost:8000/mcp/" - } - } -} +The seeding above did by hand what a real coding-assistant plugin does +automatically. The +[Context Graph](https://github.com/memgraph/ai-toolkit/tree/main/context-graph) +project ships that plugin for Claude Code and Codex — install it and point it +at this same Memgraph instance (its defaults, `bolt://localhost:7687` with no +auth and database `memgraph`, already match the container above): + +```bash +uv tool install agent-context-graph --with "skills-graph[agent-context-graph]" +agent-context-graph bootstrap --runtime claude-code \ + --connector skills-graph --connector actions-graph --connector sessions-graph +agent-context-graph config set identity.user_id "your-name" ``` -A hook in your harness that writes each session's facts, events, and workflows -through `run_query` on exit is the "plugin that collects sessions." On the next -session, the assistant reads that memory back before it starts. +Every real session then writes `Memory`/`Action`/`Skill` nodes automatically — +the same nodes `ai-memory.py` just wrote by hand — and the next session reads +that memory back before it starts. See the +[Context Graph guide](https://github.com/memgraph/ai-toolkit/blob/main/context-graph/README.md) +for the full walkthrough (Codex setup, reconciliation, cross-component +queries). ## Clean Up @@ -186,11 +199,16 @@ docker rm -f aimemory-lab - [Memgraph AI Memory](https://memgraph.com/ai-memory) (the three memory types and the graph-vs-vector argument). -- Add **semantic recall by similarity**: store an embedding per memory node and use - Memgraph [vector search](https://memgraph.com/docs/querying/vector-search) - (`search_node_vectors` is exposed by the MCP server) alongside traversal. +- Turn on **automatic, LLM-backed extraction**: this example wrote Memory nodes + by hand; `sessions-graph`'s reconciliation step instead extracts entities + from real session transcripts via `unstructured2graph` + LightRAG — see + [sessions-graph § reconciliation](https://github.com/memgraph/ai-toolkit/blob/main/context-graph/sessions-graph/README.md#session-reconciliation). +- Add **semantic recall by similarity**: `sessions-graph` already maintains a + full-text index over `Memory.content`; pair it with Memgraph + [vector search](https://memgraph.com/docs/querying/vector-search) for + embedding-based recall alongside traversal. - Retrieve memory with the same [GraphRAG](https://memgraph.com/graphrag) pipelines (Text2Cypher, pivot search, query-focused summarisation); see `agentic-graphrag.sh` in this folder. -- Read the [Memgraph MCP server](https://memgraph.com/blog/introducing-memgraph-mcp-server) - and [AI ecosystem](https://memgraph.com/docs/ai-ecosystem) docs. +- Read the [Context Graph](https://github.com/memgraph/ai-toolkit/tree/main/context-graph) + project docs and [AI ecosystem](https://memgraph.com/docs/ai-ecosystem) docs. diff --git a/code-examples/ai-memory.ps1 b/code-examples/ai-memory.ps1 index 6223674..d331388 100644 --- a/code-examples/ai-memory.ps1 +++ b/code-examples/ai-memory.ps1 @@ -18,14 +18,15 @@ # # The point is the interconnection: semantic fact -> episodic event -> procedural # response. This script seeds all three for the page's own example ("Schedule a -# follow-up with the client like last time") and recalls them by traversal. +# follow-up with the client like last time") and recalls them by traversal -- +# using the actual Context Graph packages a live coding-assistant plugin uses +# (github.com/memgraph/ai-toolkit/tree/main/context-graph), not a hand-rolled +# schema: +# - sessions-graph : semantic memory -- durable, user-owned facts +# - actions-graph : episodic memory -- timestamped session/action history +# - skills-graph : procedural memory -- named, reusable how-tos # -# Uses ONLY the Memgraph ecosystem (the "build custom" path from the page): -# - memgraph/memgraph-mage : the graph database that stores the memory -# - memgraph/mcp-memgraph : the MCP server your harness loads to read/write it -# - memgraph/mgconsole : Memgraph's CLI, used here to seed + query memory -# -# Requirements: Docker Desktop only. No API keys, no Python. +# Requirements: Docker Desktop + Python 3.10-3.13. # Docker Desktop: https://docs.docker.com/desktop/install/windows-install/ # # Usage (PowerShell 5.1 or PowerShell 7+): @@ -56,16 +57,15 @@ if (Test-Path variable:PSNativeCommandUseErrorActionPreference) { $OutputEncoding = New-Object System.Text.UTF8Encoding $false try { [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false } catch { } -# ---- Pinned versions (avoid ':latest' drift) -------------------------------- -$MageImage = 'memgraph/memgraph-mage:3.12.0' -$McpImage = 'memgraph/mcp-memgraph:0.2.0' -$MgconsoleImage = 'memgraph/mgconsole:1.6.0' +# ---- Pinned version (avoid ':latest' drift) --------------------------------- +$MageImage = 'memgraph/memgraph-mage:3.12.0' $Net = 'aimemory-net' $Db = 'aimemory-memgraph' -$Mcp = 'aimemory-mcp' $BoltPort = '7687' -$McpPort = '8000' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$Venv = Join-Path $ScriptDir '.ai-memory-venv' # ---- Helpers ---------------------------------------------------------------- function Write-Step { @@ -75,23 +75,25 @@ function Write-Step { } function Invoke-Cypher { - # Run one or more Cypher statements against Memgraph and show the result. + # Run one or more Cypher statements against Memgraph and show the result, + # reusing the mgconsole already bundled in the memgraph-mage image. param([Parameter(Mandatory = $true)][string]$Cypher) - $Cypher | docker run -i --rm --network $Net $MgconsoleImage --host $Db --port $BoltPort + $Cypher | docker exec -i $Db mgconsole --host 127.0.0.1 --port $BoltPort if ($LASTEXITCODE -ne 0) { throw "mgconsole exited with code $LASTEXITCODE" } } function Test-Cypher { # Same, but silent: used to poll until Memgraph accepts Bolt connections. param([string]$Cypher = 'RETURN 1;') - $Cypher | docker run -i --rm --network $Net $MgconsoleImage --host $Db --port $BoltPort *> $null + $Cypher | docker exec -i $Db mgconsole --host 127.0.0.1 --port $BoltPort *> $null return ($LASTEXITCODE -eq 0) } function Remove-Demo { - Write-Step 'Stopping and removing containers + network' - docker rm -f $Mcp $Db *> $null + Write-Step 'Stopping and removing container + network' + docker rm -f $Db *> $null docker network rm $Net *> $null + if (Test-Path $Venv) { Remove-Item -Recurse -Force $Venv } $global:LASTEXITCODE = 0 # nothing to remove is not a failure Write-Host 'Cleaned up.' } @@ -102,15 +104,36 @@ if ($Command -eq 'clean') { exit 0 } -# ---- 0. Prerequisite check -------------------------------------------------- +# ---- 0. Prerequisite checks -------------------------------------------------- if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Write-Error 'Docker is required: https://docs.docker.com/desktop/install/windows-install/' exit 1 } -# ---- 1a. Spin up Memgraph (the memory store) -------------------------------- +# Prefer the "py" launcher (the standard python.org install on Windows) asking +# it directly for an interpreter path, so $PyBin is always a single concrete +# executable regardless of which selector matched. +$PyBin = $null +if (Get-Command py -ErrorAction SilentlyContinue) { + foreach ($verFlag in @('-3.13', '-3.12', '-3.11', '-3.10')) { + $exe = & py $verFlag -c 'import sys; print(sys.executable)' 2>$null + if ($LASTEXITCODE -eq 0 -and $exe) { $PyBin = $exe; break } + } +} +if (-not $PyBin -and (Get-Command python -ErrorAction SilentlyContinue)) { + $verOut = & python -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>$null + if (@('3.10', '3.11', '3.12', '3.13') -contains $verOut) { + $PyBin = (Get-Command python).Source + } +} +if (-not $PyBin) { + Write-Error 'Python 3.10-3.13 is required: https://www.python.org/downloads/' + exit 1 +} + +# ---- 1. Spin up Memgraph (the memory store) ---------------------------------- Write-Step "Creating network + starting Memgraph ($MageImage)" -docker rm -f $Db $Mcp *> $null +docker rm -f $Db *> $null docker network create $Net *> $null $LASTEXITCODE = 0 @@ -119,89 +142,29 @@ docker run -d --name $Db --network $Net ` $MageImage --schema-info-enabled=True *> $null if ($LASTEXITCODE -ne 0) { throw 'Failed to start Memgraph.' } -Write-Step 'Waiting for Memgraph to accept Bolt connections' +Write-Step 'Waiting for Memgraph to be ready (Bolt-aware check)' while (-not (Test-Cypher)) { Start-Sleep -Seconds 1 } Write-Host "Memgraph is up on bolt://localhost:${BoltPort}" -# ---- 1b. Load the harness's memory tool (Memgraph MCP server) --------------- -# Your coding assistant / agent (the "harness") loads this MCP server as a tool. -# It then WRITES what it learns each session and READS it back later. Here we -# start the same server so a harness can attach; the seeding below simulates -# what the harness writes. -Write-Step "Starting the Memgraph MCP server ($McpImage), the memory tool your harness loads" -docker run -d --name $Mcp --network $Net ` - -p "${McpPort}:8000" ` - --env MEMGRAPH_URL="bolt://${Db}:7687" ` - $McpImage *> $null -if ($LASTEXITCODE -ne 0) { throw 'Failed to start the MCP server.' } -Write-Host "MCP server (streamable HTTP) at http://localhost:${McpPort}/mcp/" - -# ---- 2. Write the three memory types ---------------------------------------- -# Scenario from the page: the assistant has worked with a client before and knows -# how to schedule follow-ups. That knowledge is split across the three memories. -Write-Step 'Writing semantic, episodic, and procedural memory' -Invoke-Cypher @' -CREATE CONSTRAINT ON (c:Client) ASSERT c.name IS UNIQUE; - -// --- Semantic memory: what the system KNOWS (facts + preferences) --- -MERGE (c:Client {name: "Acme Corp"}) SET c.contact = "Dana Lee", c.timezone = "America/New_York"; -MERGE (p:Preference {kind: "meeting_length", value: "30 min"}); -MATCH (c:Client {name:"Acme Corp"}), (p:Preference {kind:"meeting_length"}) - MERGE (c)-[:PREFERS]->(p); - -// --- Episodic memory: what the system EXPERIENCED (interactions over time) --- -MERGE (m1:Interaction {id:"int-1", kind:"meeting", weekday:"Tuesday", duration:"30 min", when:"2026-06-30", summary:"kickoff"}); -MERGE (m2:Interaction {id:"int-2", kind:"meeting", weekday:"Tuesday", duration:"30 min", when:"2026-07-07", summary:"follow-up"}); -MATCH (m1:Interaction {id:"int-1"}), (c:Client {name:"Acme Corp"}) MERGE (m1)-[:WITH]->(c); -MATCH (m2:Interaction {id:"int-2"}), (c:Client {name:"Acme Corp"}) MERGE (m2)-[:WITH]->(c); -MATCH (m1:Interaction {id:"int-1"}), (m2:Interaction {id:"int-2"}) MERGE (m1)-[:NEXT]->(m2); // sequence - -// --- Procedural memory: what the system KNOWS HOW TO DO (a workflow) --- -MERGE (w:Workflow {name:"schedule_follow_up"}); -MERGE (s1:Step {name:"book calendar slot"}); -MERGE (s2:Step {name:"send invite"}); -MATCH (w:Workflow {name:"schedule_follow_up"}), (s1:Step {name:"book calendar slot"}) MERGE (w)-[:STARTS_WITH]->(s1); -MATCH (s1:Step {name:"book calendar slot"}), (s2:Step {name:"send invite"}) MERGE (s1)-[:THEN]->(s2); // transition -'@ -Write-Host 'Wrote semantic + episodic + procedural memory.' - -# ---- 3. Recall each memory type, then all three together -------------------- -Write-Step "Semantic recall: 'What do we know about the client?'" -Invoke-Cypher @' -MATCH (c:Client {name:"Acme Corp"})-[:PREFERS]->(p:Preference) -RETURN c.contact AS client, c.timezone AS timezone, p.value AS preferred_length; -'@ - -Write-Step "Episodic recall: 'What happened last time?' (most recent interaction)" -Invoke-Cypher @' -MATCH (i:Interaction)-[:WITH]->(:Client {name:"Acme Corp"}) -RETURN i.when AS date, i.weekday AS weekday, i.duration AS duration, i.summary AS summary -ORDER BY i.when DESC LIMIT 1; -'@ - -Write-Step "Procedural recall: 'How do we schedule a follow-up?' (steps in order)" -Invoke-Cypher @' -MATCH (:Workflow {name:"schedule_follow_up"})-[:STARTS_WITH]->(first:Step) -MATCH p=(first)-[:THEN*0..]->(s:Step) -WITH s, length(p) AS ord ORDER BY ord -RETURN collect(s.name) AS steps; -'@ - -Write-Step "Interconnected recall: 'Schedule a follow-up with the client like last time.'" -# One traversal joins semantic (client + timezone) + episodic (last meeting) + -# procedural (the workflow steps): semantic fact -> episodic event -> procedural response. -Invoke-Cypher @' -MATCH (c:Client {name:"Acme Corp"}) -MATCH (last:Interaction)-[:WITH]->(c) -WITH c, last ORDER BY last.when DESC LIMIT 1 -MATCH (:Workflow {name:"schedule_follow_up"})-[:STARTS_WITH]->(f:Step) -MATCH pth=(f)-[:THEN*0..]->(st:Step) -WITH c, last, st, length(pth) AS o ORDER BY o -RETURN c.contact AS client, c.timezone AS timezone, - last.weekday AS like_last_time_day, last.duration AS duration, - collect(st.name) AS actions; -'@ - +# ---- 2. Install the Context Graph memory packages ---------------------------- +$pyVersion = & $PyBin --version +Write-Step "Creating a Python virtualenv ($pyVersion) and installing sessions-graph, actions-graph, skills-graph" +if (Test-Path $Venv) { Remove-Item -Recurse -Force $Venv } +& $PyBin -m venv $Venv +$VenvPython = Join-Path $Venv 'Scripts\python.exe' +if (-not (Test-Path $VenvPython)) { $VenvPython = Join-Path $Venv 'bin/python' } +& $VenvPython -m pip install --quiet --upgrade pip +& $VenvPython -m pip install --quiet sessions-graph actions-graph skills-graph memgraph-toolbox + +# ---- 3. Write and recall the three memory types ------------------------------ +Write-Step 'Writing and recalling semantic, episodic, and procedural memory' +$env:MEMGRAPH_URL = "bolt://localhost:${BoltPort}" +& $VenvPython (Join-Path $ScriptDir 'ai-memory.py') +$pyExit = $LASTEXITCODE +Remove-Item Env:\MEMGRAPH_URL +if ($pyExit -ne 0) { throw 'ai-memory.py failed.' } + +# ---- 4. Inspect the memory ontology ------------------------------------------- Write-Step 'Memory ontology via SHOW SCHEMA INFO (returned in constant time)' try { Invoke-Cypher 'SHOW SCHEMA INFO;' @@ -213,27 +176,27 @@ try { Write-Host '' Write-Host "$([char]0x2713) AI memory is live." -ForegroundColor Green Write-Host @" -The assistant can now answer "like last time" by traversing: semantic (Acme Corp, -New York) -> episodic (last meeting: 30 min, Tuesday) -> procedural (book calendar -slot, send invite). +The assistant can now answer "like last time" by traversing: semantic (Acme +Corp, New York) -> episodic (last session: follow-up meeting) -> procedural +(schedule-follow-up skill). Explore the memory graph visually with Memgraph Lab: docker run -d --name aimemory-lab --network $Net -p 3000:3000 -e QUICK_CONNECT_MG_HOST=$Db -e QUICK_CONNECT_MG_PORT=7687 memgraph/lab:3.12.0 start http://localhost:3000 # then run: MATCH p=()-[]-() RETURN p; -Wire the memory into a real harness (so it collects sessions automatically). -Add this to your MCP client config (e.g. Claude Desktop / Cursor / VS Code): +Wire this into a REAL harness so it collects sessions automatically (no seeding +by hand): install the Context Graph plugin for Claude Code or Codex and point it +at this same Memgraph instance -- its defaults (bolt://localhost:7687, no auth, +database memgraph) already match this container. - { - "mcpServers": { - "memgraph-memory": { - "url": "http://localhost:${McpPort}/mcp/" - } - } - } + uv tool install agent-context-graph --with "skills-graph[agent-context-graph]" + agent-context-graph bootstrap --runtime claude-code `` + --connector skills-graph --connector actions-graph --connector sessions-graph + agent-context-graph config set identity.user_id "your-name" -Your assistant then calls the MCP tools (run_query, get_schema, ...) to write new -semantic/episodic/procedural memory and recall it, exactly as the seeding did. +Every real session then writes Memory/Action/Skill nodes automatically, the +same nodes ai-memory.py just wrote by hand. Full walkthrough: + https://github.com/memgraph/ai-toolkit/tree/main/context-graph Tear everything down when you are done: .\ai-memory.ps1 clean diff --git a/code-examples/ai-memory.py b/code-examples/ai-memory.py new file mode 100644 index 0000000..9e2735f --- /dev/null +++ b/code-examples/ai-memory.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""AI Memory with Memgraph: seed + recall using the real Context Graph packages. + +Unlike a hand-rolled schema, this writes semantic/episodic/procedural memory +through the same libraries a live coding-assistant plugin uses: + + - sessions-graph : semantic memory -- durable, user-owned facts (Memory nodes) + - actions-graph : episodic memory -- timestamped session/action history + - skills-graph : procedural memory -- named, reusable how-tos (Skill nodes) + +See https://github.com/memgraph/ai-toolkit/tree/main/context-graph for the +full Context Graph project these packages belong to. + +Connects via the same MEMGRAPH_URL / MEMGRAPH_USER / MEMGRAPH_PASSWORD / +MEMGRAPH_DATABASE env vars every package in that project reads (defaults to +bolt://localhost:7687, matching this demo's own container). +""" + +from actions_graph import ActionsGraph +from actions_graph.models import ActionStatus, Session +from memgraph_toolbox.api.memgraph import Memgraph +from sessions_graph import SessionsGraph +from skills_graph import Skill, SkillGraph + +USER_ID = "acme-corp" # the identity this durable memory belongs to +KICKOFF_SESSION = "session-acme-kickoff" +FOLLOWUP_SESSION = "session-acme-followup" +SKILL_NAME = "schedule-follow-up" + + +def log(msg: str) -> None: + print(f"\n\033[1;36m==> {msg}\033[0m") + + +def link_user_session(db: Memgraph, user_id: str, session_id: str) -> None: + # sessions-graph is the sole owner of (:User)-[:HAD_SESSION]->(:Session); + # this mirrors exactly what its SessionsGraphConnector does on SESSION_START. + db.query( + """ + MERGE (u:User {user_id: $user_id}) + MERGE (s:Session {session_id: $session_id}) + MERGE (u)-[:HAD_SESSION]->(s) + """, + params={"user_id": user_id, "session_id": session_id}, + ) + + +def seed(db: Memgraph) -> None: + actions = ActionsGraph(db) + memories = SessionsGraph(db) + skills = SkillGraph(db) + + actions.setup() + memories.setup() + skills.setup() + + # Sessions must exist before anything else references their session_id + # (save_memory's provenance MERGE and record_skill_usage's MERGE would + # otherwise race actions-graph's own unique constraint on Session). + log("Episodic: what the system EXPERIENCED (two real sessions, in order)") + actions.create_session( + Session( + session_id=KICKOFF_SESSION, + started_at="2026-06-30T15:00:00+00:00", + ended_at="2026-06-30T15:30:00+00:00", + status=ActionStatus.COMPLETED, + ) + ) + actions.create_session( + Session( + session_id=FOLLOWUP_SESSION, + started_at="2026-07-07T15:00:00+00:00", + ended_at="2026-07-07T15:30:00+00:00", + status=ActionStatus.COMPLETED, + ) + ) + link_user_session(db, USER_ID, KICKOFF_SESSION) + link_user_session(db, USER_ID, FOLLOWUP_SESSION) + + for session_id, when in ( + (KICKOFF_SESSION, "2026-06-30T15:05:00+00:00"), + (FOLLOWUP_SESSION, "2026-07-07T15:05:00+00:00"), + ): + call = actions.record_tool_call( + session_id=session_id, + tool_name="schedule_meeting", + tool_input={"client": "Acme Corp", "weekday": "Tuesday", "duration_min": 30}, + tool_use_id=f"{session_id}-call", + timestamp=when, + ) + actions.record_tool_result( + session_id=session_id, + tool_use_id=call.tool_use_id, + tool_name="schedule_meeting", + content="booked", + timestamp=when, + ) + print("Wrote 2 Sessions, 4 Actions (ToolCall + ToolResult each, FOLLOWED_BY sequenced).") + + log("Semantic: what the system KNOWS") + memories.save_memory( + user_id=USER_ID, + content="Acme Corp's contact is Dana Lee (timezone America/New_York); they prefer 30-minute meetings.", + session_id=KICKOFF_SESSION, + ) + print("Wrote 1 Memory.") + + log("Procedural: what the system KNOWS HOW TO DO") + skills.add_skill( + Skill( + name=SKILL_NAME, + description="Schedule a follow-up meeting with a client the assistant has met before.", + content=( + "1. Book a calendar slot matching the client's timezone and preferred meeting length.\n" + "2. Send a calendar invite." + ), + ) + ) + skills.record_skill_usage( + session_id=FOLLOWUP_SESSION, + skill_name=SKILL_NAME, + action="used", + timestamp="2026-07-07T15:06:00+00:00", + ) + print("Wrote 1 Skill, 1 USED_SKILL usage.") + + +def recall(db: Memgraph) -> None: + actions = ActionsGraph(db) + memories = SessionsGraph(db) + skills = SkillGraph(db) + + log("Semantic recall: what do we know about the client?") + for m in memories.get_memories(USER_ID): + print(f"- {m.content}") + + log("Episodic recall: what happened last time? (most recent session)") + last_session = actions.list_sessions(limit=1)[0] + print(f"- session {last_session.session_id} started_at={last_session.started_at}") + for a in actions.get_session_actions(last_session.session_id): + print(f" - {a.action_type.value} tool={getattr(a, 'tool_name', None)} at {a.timestamp}") + + log("Procedural recall: how do we schedule a follow-up?") + skill = skills.get_skill(SKILL_NAME) + print(skill.content) + + log("Interconnected recall: one traversal joining all three") + # semantic (User-HAS_MEMORY->Memory) + episodic (User-HAD_SESSION->Session + # -HAS_ACTION->Action) + procedural (Session-USED_SKILL->Skill), all through + # the shared User/Session nodes -- see context-graph/CONTEXT-MAP.md. + rows = db.query( + """ + MATCH (u:User {user_id: $user_id})-[:HAS_MEMORY]->(mem:Memory) + MATCH (u)-[:HAD_SESSION]->(s:Session)-[:HAS_ACTION]->(a:Action {tool_name: "schedule_meeting"}) + WITH u, mem, s, a ORDER BY s.started_at DESC LIMIT 1 + OPTIONAL MATCH (s)-[:USED_SKILL]->(sk:Skill) + RETURN mem.content AS client_facts, s.session_id AS last_session, + a.timestamp AS last_meeting_at, sk.name AS skill, sk.content AS how_to + """, + params={"user_id": USER_ID}, + ) + for row in rows: + print(f"client_facts : {row['client_facts']}") + print(f"last_session : {row['last_session']} (at {row['last_meeting_at']})") + print(f"skill : {row['skill']}") + print(f"how_to : {row['how_to']}") + + +if __name__ == "__main__": + db = Memgraph() + seed(db) + recall(db) diff --git a/code-examples/ai-memory.sh b/code-examples/ai-memory.sh index cf7757f..33ec352 100755 --- a/code-examples/ai-memory.sh +++ b/code-examples/ai-memory.sh @@ -16,14 +16,15 @@ # # The point is the interconnection: semantic fact -> episodic event -> procedural # response. This script seeds all three for the page's own example ("Schedule a -# follow-up with the client like last time") and recalls them by traversal. +# follow-up with the client like last time") and recalls them by traversal -- +# using the actual Context Graph packages a live coding-assistant plugin uses +# (github.com/memgraph/ai-toolkit/tree/main/context-graph), not a hand-rolled +# schema: +# - sessions-graph : semantic memory -- durable, user-owned facts +# - actions-graph : episodic memory -- timestamped session/action history +# - skills-graph : procedural memory -- named, reusable how-tos # -# Uses ONLY the Memgraph ecosystem (the "build custom" path from the page): -# - memgraph/memgraph-mage : the graph database that stores the memory -# - memgraph/mcp-memgraph : the MCP server your harness loads to read/write it -# - memgraph/mgconsole : Memgraph's CLI, used here to seed + query memory -# -# Requirements: Docker only. No API keys, no Python. +# Requirements: Docker + Python 3.10-3.13. # Docker: https://docs.docker.com/get-docker/ # # Usage: @@ -32,30 +33,30 @@ set -euo pipefail -# ---- Pinned versions (avoid ':latest' drift) -------------------------------- +# ---- Pinned version (avoid ':latest' drift) --------------------------------- MAGE_IMAGE="memgraph/memgraph-mage:3.12.0" -MCP_IMAGE="memgraph/mcp-memgraph:0.2.0" -MGCONSOLE_IMAGE="memgraph/mgconsole:1.6.0" NET="aimemory-net" DB="aimemory-memgraph" -MCP="aimemory-mcp" BOLT_PORT="7687" -MCP_PORT="8000" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV="$SCRIPT_DIR/.ai-memory-venv" # ---- Helpers ---------------------------------------------------------------- log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } query() { - # Run one or more Cypher statements (read from stdin) against Memgraph. - docker run -i --rm --network "$NET" "$MGCONSOLE_IMAGE" \ - --host "$DB" --port "$BOLT_PORT" + # Run one or more Cypher statements (read from stdin) against Memgraph, + # reusing the mgconsole already bundled in the memgraph-mage image. + docker exec -i "$DB" mgconsole --host 127.0.0.1 --port 7687 } teardown() { - log "Stopping and removing containers + network" - docker rm -f "$MCP" "$DB" >/dev/null 2>&1 || true + log "Stopping and removing container + network" + docker rm -f "$DB" >/dev/null 2>&1 || true docker network rm "$NET" >/dev/null 2>&1 || true + rm -rf "$VENV" echo "Cleaned up." } @@ -65,103 +66,63 @@ if [[ "${1:-}" == "clean" ]]; then exit 0 fi -# ---- 0. Prerequisite check -------------------------------------------------- +# ---- 0. Prerequisite checks -------------------------------------------------- command -v docker >/dev/null 2>&1 || { echo "Docker is required: https://docs.docker.com/get-docker/" >&2 exit 1 } -# ---- 1a. Spin up Memgraph (the memory store) -------------------------------- +PYBIN="" +for cand in python3.13 python3.12 python3.11 python3.10; do + command -v "$cand" >/dev/null 2>&1 && { PYBIN="$cand"; break; } +done +if [[ -z "$PYBIN" ]] && command -v python3 >/dev/null 2>&1; then + case "$(python3 -c 'import sys;print("%d.%d"%sys.version_info[:2])')" in + 3.10|3.11|3.12|3.13) PYBIN="python3" ;; + esac +fi +if [[ -z "$PYBIN" ]]; then + echo "Python 3.10-3.13 is required: https://www.python.org/downloads/" >&2 + exit 1 +fi + +# ---- 1. Spin up Memgraph (the memory store) --------------------------------- log "Creating network + starting Memgraph ($MAGE_IMAGE)" -docker rm -f "$DB" "$MCP" >/dev/null 2>&1 || true +docker rm -f "$DB" >/dev/null 2>&1 || true docker network create "$NET" >/dev/null 2>&1 || true docker run -d --name "$DB" --network "$NET" \ -p "${BOLT_PORT}:7687" -p 7444:7444 \ "$MAGE_IMAGE" --schema-info-enabled=True >/dev/null -log "Waiting for Memgraph to accept Bolt connections" -until echo "RETURN 1;" | query >/dev/null 2>&1; do sleep 1; done +log "Waiting for Memgraph to be ready (Bolt-aware check)" +ready=0 +for i in $(seq 1 30); do + if echo "RETURN 1;" | query >/dev/null 2>&1; then + ready=1 + break + fi + sleep 1 +done +if [[ "$ready" -ne 1 ]]; then + echo "Memgraph did not become ready in time" >&2 + docker logs "$DB" || true + exit 1 +fi echo "Memgraph is up on bolt://localhost:${BOLT_PORT}" -# ---- 1b. Load the harness's memory tool (Memgraph MCP server) --------------- -# Your coding assistant / agent (the "harness") loads this MCP server as a tool. -# It then WRITES what it learns each session and READS it back later. Here we -# start the same server so a harness can attach; the seeding below simulates -# what the harness writes. -log "Starting the Memgraph MCP server ($MCP_IMAGE), the memory tool your harness loads" -docker run -d --name "$MCP" --network "$NET" \ - -p "${MCP_PORT}:8000" \ - --env MEMGRAPH_URL="bolt://${DB}:7687" \ - "$MCP_IMAGE" >/dev/null -echo "MCP server (streamable HTTP) at http://localhost:${MCP_PORT}/mcp/" - -# ---- 2. Write the three memory types ---------------------------------------- -# Scenario from the page: the assistant has worked with a client before and knows -# how to schedule follow-ups. That knowledge is split across the three memories. -log "Writing semantic, episodic, and procedural memory" -query <<'CYPHER' -CREATE CONSTRAINT ON (c:Client) ASSERT c.name IS UNIQUE; - -// --- Semantic memory: what the system KNOWS (facts + preferences) --- -MERGE (c:Client {name: "Acme Corp"}) SET c.contact = "Dana Lee", c.timezone = "America/New_York"; -MERGE (p:Preference {kind: "meeting_length", value: "30 min"}); -MATCH (c:Client {name:"Acme Corp"}), (p:Preference {kind:"meeting_length"}) - MERGE (c)-[:PREFERS]->(p); - -// --- Episodic memory: what the system EXPERIENCED (interactions over time) --- -MERGE (m1:Interaction {id:"int-1", kind:"meeting", weekday:"Tuesday", duration:"30 min", when:"2026-06-30", summary:"kickoff"}); -MERGE (m2:Interaction {id:"int-2", kind:"meeting", weekday:"Tuesday", duration:"30 min", when:"2026-07-07", summary:"follow-up"}); -MATCH (m1:Interaction {id:"int-1"}), (c:Client {name:"Acme Corp"}) MERGE (m1)-[:WITH]->(c); -MATCH (m2:Interaction {id:"int-2"}), (c:Client {name:"Acme Corp"}) MERGE (m2)-[:WITH]->(c); -MATCH (m1:Interaction {id:"int-1"}), (m2:Interaction {id:"int-2"}) MERGE (m1)-[:NEXT]->(m2); // sequence - -// --- Procedural memory: what the system KNOWS HOW TO DO (a workflow) --- -MERGE (w:Workflow {name:"schedule_follow_up"}); -MERGE (s1:Step {name:"book calendar slot"}); -MERGE (s2:Step {name:"send invite"}); -MATCH (w:Workflow {name:"schedule_follow_up"}), (s1:Step {name:"book calendar slot"}) MERGE (w)-[:STARTS_WITH]->(s1); -MATCH (s1:Step {name:"book calendar slot"}), (s2:Step {name:"send invite"}) MERGE (s1)-[:THEN]->(s2); // transition -CYPHER -echo "Wrote semantic + episodic + procedural memory." - -# ---- 3. Recall each memory type, then all three together -------------------- -log "Semantic recall: 'What do we know about the client?'" -query <<'CYPHER' -MATCH (c:Client {name:"Acme Corp"})-[:PREFERS]->(p:Preference) -RETURN c.contact AS client, c.timezone AS timezone, p.value AS preferred_length; -CYPHER - -log "Episodic recall: 'What happened last time?' (most recent interaction)" -query <<'CYPHER' -MATCH (i:Interaction)-[:WITH]->(:Client {name:"Acme Corp"}) -RETURN i.when AS date, i.weekday AS weekday, i.duration AS duration, i.summary AS summary -ORDER BY i.when DESC LIMIT 1; -CYPHER - -log "Procedural recall: 'How do we schedule a follow-up?' (steps in order)" -query <<'CYPHER' -MATCH (:Workflow {name:"schedule_follow_up"})-[:STARTS_WITH]->(first:Step) -MATCH p=(first)-[:THEN*0..]->(s:Step) -WITH s, length(p) AS ord ORDER BY ord -RETURN collect(s.name) AS steps; -CYPHER - -log "Interconnected recall: 'Schedule a follow-up with the client like last time.'" -# One traversal joins semantic (client + timezone) + episodic (last meeting) + -# procedural (the workflow steps): semantic fact -> episodic event -> procedural response. -query <<'CYPHER' -MATCH (c:Client {name:"Acme Corp"}) -MATCH (last:Interaction)-[:WITH]->(c) -WITH c, last ORDER BY last.when DESC LIMIT 1 -MATCH (:Workflow {name:"schedule_follow_up"})-[:STARTS_WITH]->(f:Step) -MATCH pth=(f)-[:THEN*0..]->(st:Step) -WITH c, last, st, length(pth) AS o ORDER BY o -RETURN c.contact AS client, c.timezone AS timezone, - last.weekday AS like_last_time_day, last.duration AS duration, - collect(st.name) AS actions; -CYPHER +# ---- 2. Install the Context Graph memory packages ---------------------------- +log "Creating a Python virtualenv ($("$PYBIN" --version)) and installing sessions-graph, actions-graph, skills-graph" +rm -rf "$VENV" +"$PYBIN" -m venv "$VENV" +"$VENV/bin/pip" install --quiet --upgrade pip +"$VENV/bin/pip" install --quiet sessions-graph actions-graph skills-graph memgraph-toolbox + +# ---- 3. Write and recall the three memory types ------------------------------ +log "Writing and recalling semantic, episodic, and procedural memory" +MEMGRAPH_URL="bolt://localhost:${BOLT_PORT}" "$VENV/bin/python" "$SCRIPT_DIR/ai-memory.py" +# ---- 4. Inspect the memory ontology ------------------------------------------ log "Memory ontology via SHOW SCHEMA INFO (returned in constant time)" echo "SHOW SCHEMA INFO;" | query || echo "(enable with --schema-info-enabled, already set)" @@ -169,27 +130,27 @@ echo "SHOW SCHEMA INFO;" | query || echo "(enable with --schema-info-enabled, al cat < episodic (last meeting: 30 min, -Tuesday) -> procedural (book calendar slot, send invite). +by traversing: semantic (Acme Corp, New York) -> episodic (last session: +follow-up meeting) -> procedural (schedule-follow-up skill). Explore the memory graph visually with Memgraph Lab: docker run -d --name aimemory-lab --network ${NET} -p 3000:3000 \\ -e QUICK_CONNECT_MG_HOST=${DB} -e QUICK_CONNECT_MG_PORT=7687 memgraph/lab:3.12.0 open http://localhost:3000 # then run: MATCH p=()-[]-() RETURN p; -Wire the memory into a real harness (so it collects sessions automatically). -Add this to your MCP client config (e.g. Claude Desktop / Cursor / VS Code): +Wire this into a REAL harness so it collects sessions automatically (no seeding +by hand): install the Context Graph plugin for Claude Code or Codex and point it +at this same Memgraph instance -- its defaults (bolt://localhost:7687, no auth, +database memgraph) already match this container. - { - "mcpServers": { - "memgraph-memory": { - "url": "http://localhost:${MCP_PORT}/mcp/" - } - } - } + uv tool install agent-context-graph --with "skills-graph[agent-context-graph]" + agent-context-graph bootstrap --runtime claude-code \\ + --connector skills-graph --connector actions-graph --connector sessions-graph + agent-context-graph config set identity.user_id "your-name" -Your assistant then calls the MCP tools (run_query, get_schema, ...) to write new -semantic/episodic/procedural memory and recall it, exactly as the seeding did. +Every real session then writes Memory/Action/Skill nodes automatically, the +same nodes ai-memory.py just wrote by hand. Full walkthrough: + https://github.com/memgraph/ai-toolkit/tree/main/context-graph Tear everything down when you are done: ./ai-memory.sh clean