{
"cells": [
{
"cell_type": "markdown",
"id": "a1c583eb",
"metadata": {
"papermill": {
"duration": 0.005738,
"end_time": "2026-05-27T09:19:50.050075+00:00",
"exception": false,
"start_time": "2026-05-27T09:19:50.044337+00:00",
"status": "completed"
},
"tags": []
},
"source": [
"# 08 · Episodic + Semantic Memory — dual-memory persistent agent\n",
"\n",
"> **TL;DR.** An assistant with **two** distinct memory systems that persist across `run()` calls:\n",
"> - **Episodic memory** = FAISS vector store of past *conversations*. Recalled by similarity search (\"have I seen something like this before?\").\n",
"> - **Semantic memory** = NetworkX graph of structured *(subject, predicate, object)* facts. Recalled by entity match (\"what do I know about X?\").\n",
"> Each interaction: **retrieve → answer → extract facts → save episode**.\n",
">\n",
"> **Reach for it when** you want a long-running personal assistant that genuinely remembers what the user told it.\n",
"> **Avoid when** each call is one-shot with no continuity (stateless API endpoints).\n",
"\n",
"| Property | Value |\n",
"|---|---|\n",
"| Origin | Tulving (1972) episodic vs semantic memory distinction; modern LLM versions: MemGPT (2023), generative agents (Park et al., 2023) |\n",
"| Memory backends | FAISS (vector) + NetworkX (graph) — both **in-process, no Docker** |\n",
"| Persistence | On the architecture **instance** — memory survives across `run()` calls |\n",
"| Provider gating | Needs embeddings (Nebius/OpenAI/HF/Ollama) + structured-output LLM |\n",
"| Cost per turn | 3 LLM calls (answer + fact-extractor) + 1 embedding call (the new episode) |\n",
"| Composability | Reuses `EpisodicMemory` + `SemanticMemory` from `agentic_architectures.memory` |\n",
"\n",
"This is the first notebook in the repo where **state persists across calls on the same architecture instance**. Subsequent calls become richer as memory accumulates."
]
},
{
"cell_type": "markdown",
"id": "ddfc324b",
"metadata": {
"papermill": {
"duration": 0.008144,
"end_time": "2026-05-27T09:19:50.067278+00:00",
"exception": false,
"start_time": "2026-05-27T09:19:50.059134+00:00",
"status": "completed"
},
"tags": []
},
"source": [
"## 2 · Architecture at a glance\n",
"\n",
"```mermaid\n",
"flowchart LR\n",
" Q([user query]) --> R[Retrieve
FAISS recall episodes
+ NetworkX entity-match facts]\n",
" R --> A[Answer
LLM with retrieved context]\n",
" A --> X[Extract facts
structured-output (s,p,o) triples]\n",
" X --> S[Save episode
append Q&A to FAISS]\n",
" S --> Z([answer])\n",
"\n",
" M[(Persistent memory
FAISS + NetworkX)] -.read.-> R\n",
" X -.write.-> M\n",
" S -.write.-> M\n",
"\n",
" style R fill:#fff3e0,stroke:#f57c00\n",
" style X fill:#e8f5e9,stroke:#388e3c\n",
" style S fill:#e8f5e9,stroke:#388e3c\n",
"```\n",
"\n",
"**Linear flow per call** — but the dotted lines matter: the persistent memory store is read at the start and written at the end of every `run()`. The next call sees the accumulated memory."
]
},
{
"cell_type": "markdown",
"id": "056d27f3",
"metadata": {
"papermill": {
"duration": 0.004342,
"end_time": "2026-05-27T09:19:50.077623+00:00",
"exception": false,
"start_time": "2026-05-27T09:19:50.073281+00:00",
"status": "completed"
},
"tags": []
},
"source": [
"## 3 · Theory\n",
"\n",
"### 3.1 · Why two memory systems\n",
"\n",
"Tulving (1972) distinguished:\n",
"- **Episodic memory** — autobiographical, *time- and place-bound*. *\"I had pizza on Tuesday with Sarah.\"*\n",
"- **Semantic memory** — abstract, *general knowledge*. *\"Pizza originated in Italy. Sarah is my sister.\"*\n",
"\n",
"An LLM agent needs **both**:\n",
"\n",
"| Question type | Best served by |\n",
"|---|---|\n",
"| *\"Have we discussed this before?\"* | Episodic — vector recall over past conversations |\n",
"| *\"What did you say last Tuesday?\"* | Episodic — time-bounded retrieval |\n",
"| *\"What do you know about Alex?\"* | Semantic — entity-centric facts |\n",
"| *\"Who works at Anthropic?\"* | Semantic — relation query |\n",
"| *\"Summarize our relationship\"* | Both — episodes give nuance, facts give structure |\n",
"\n",
"Vector-only memory (FAISS alone) fails on \"what do you know about X\" because the embedding of *\"X\"* matches anything with X in it — including the same fact repeated 10 different ways. Graph-only memory fails on continuity (\"did we talk about this last week?\") because there's no notion of *when*. The two systems are complementary.\n",
"\n",
"### 3.2 · The four-step per-call flow\n",
"\n",
"Every call to `arch.run(query)` runs these four steps in order:\n",
"\n",
"1. **Retrieve.** Vector-recall the top-K most similar past episodes (FAISS `similarity_search`). Brute-force-match entities in the query against the graph's nodes; pull their 1-hop facts.\n",
"2. **Answer.** LLM is given the retrieved context block + the new query. Crucially the prompt says *\"answer ONLY from the memory above — do not fabricate\"* so the agent doesn't invent facts.\n",
"3. **Extract.** A second LLM call with `with_structured_output(_ExtractedFacts)` pulls `(subject, predicate, object)` triples from the Q&A. Triples are added to the graph.\n",
"4. **Save episode.** The full `User: ... / Assistant: ...` exchange is embedded and appended to FAISS for future similarity recall.\n",
"\n",
"Cost per call: **2 LLM calls** (answer + fact-extractor) + **1 embedding call** (the new episode). The retrieval step is local (no LLM).\n",
"\n",
"### 3.3 · Fact-extraction discipline\n",
"\n",
"The fact-extractor schema is the load-bearing piece — bad triples poison semantic memory permanently:\n",
"\n",
"```python\n",
"class _Triple(BaseModel):\n",
" subject: str # named entity only\n",
" predicate: str # short relation verb (works_at, lives_in, likes)\n",
" object: str # entity or short literal\n",
"\n",
"class _ExtractedFacts(BaseModel):\n",
" facts: list[_Triple] = Field(\n",
" description=\"Atomic triples about specific named entities. \"\n",
" \"Skip generic claims. Empty list if no concrete facts.\"\n",
" )\n",
"```\n",
"\n",
"The `Field` description is the model's only instruction — it must say *atomic*, *specific*, *skip generic*. Otherwise you get triples like `(\"user\", \"wants\", \"to learn\")` which clutter the graph without being useful.\n",
"\n",
"### 3.4 · Where this sits\n",
"\n",
"| Pattern | Memory across calls? | Episodic? | Semantic? | Use when |\n",
"|---|---|---|---|---|\n",
"| Reflection (nb 01), ReAct (nb 03), … | no | no | no | one-shot tasks |\n",
"| **Episodic + Semantic** *(this nb)* | **yes** | **yes** | **yes** | personal assistant, continuity matters |\n",
"| Reflexion (nb 18) | yes | yes (verbal reflections) | no | self-improvement across episodes |\n",
"| Voyager (nb 29) | yes | no | yes (skill library) | open-ended exploration |\n",
"| MemGPT (nb 31) | yes | yes (paged) | yes (paged) | very long-running with limited context window |\n",
"| GraphRAG (nb 27) | yes (read-only) | no | yes (KG over corpus) | query-time entity reasoning over docs |\n",
"\n",
"### 3.5 · What goes wrong (you'll see in § 9)\n",
"\n",
"1. **Fact-extractor over-extracts.** Produces triples like `(\"user\", \"said\", \"hello\")` — useless noise.\n",
"2. **Fact-extractor under-extracts.** Misses obvious facts (\"I work at X\" → no triple). Tighten the extractor prompt.\n",
"3. **Entity-match is brittle.** \"Mochi\" vs \"mochi\" vs \"my cat\" — naïve string matching misses synonyms. Mitigation: lowercase normalisation + entity-linking (extension).\n",
"4. **Episodic recall returns near-duplicates.** Vector store finds 3 nearly identical past episodes. Mitigation: dedup by content hash before adding.\n"
]
},
{
"cell_type": "markdown",
"id": "e8dfe434",
"metadata": {
"papermill": {
"duration": 0.016737,
"end_time": "2026-05-27T09:19:50.094360+00:00",
"exception": false,
"start_time": "2026-05-27T09:19:50.077623+00:00",
"status": "completed"
},
"tags": []
},
"source": [
"## 4 · Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "fbfbed78",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-27T09:19:50.100381Z",
"iopub.status.busy": "2026-05-27T09:19:50.100381Z",
"iopub.status.idle": "2026-05-27T09:19:50.940626Z",
"shell.execute_reply": "2026-05-27T09:19:50.940626Z"
},
"papermill": {
"duration": 0.840245,
"end_time": "2026-05-27T09:19:50.940626+00:00",
"exception": false,
"start_time": "2026-05-27T09:19:50.100381+00:00",
"status": "completed"
},
"tags": []
},
"outputs": [
{
"data": {
"text/html": [
"
Provider: nebius · Model: meta-llama/Llama-3.3-70B-Instruct ─────────────────────────────────────────────────────\n", "\n" ], "text/plain": [ "\u001b[1;36mProvider: nebius · Model: meta-llama/Llama-\u001b[0m\u001b[1;36m3.3\u001b[0m\u001b[1;36m-70B-Instruct\u001b[0m \u001b[92m─────────────────────────────────────────────────────\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
Vector backend: faiss · Graph backend: networkx \n", "\n" ], "text/plain": [ "Vector backend: \u001b[1mfaiss\u001b[0m · Graph backend: \u001b[1mnetworkx\u001b[0m \n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from agentic_architectures import get_llm, enable_langsmith, settings\n", "from agentic_architectures.architectures import EpisodicSemanticAgent\n", "from agentic_architectures.ui import print_md, print_header, print_step\n", "\n", "enable_langsmith()\n", "print_header(f\"Provider: {settings.llm_provider} · Model: {settings.llm_model}\")\n", "print_md(f\"Vector backend: **{settings.vector_backend}** · Graph backend: **{settings.graph_backend}**\")" ] }, { "cell_type": "markdown", "id": "ba158704", "metadata": { "papermill": { "duration": 0.008328, "end_time": "2026-05-27T09:19:50.956500+00:00", "exception": false, "start_time": "2026-05-27T09:19:50.948172+00:00", "status": "completed" }, "tags": [] }, "source": [ "## 5 · Library walkthrough\n", "\n", "Source: [`src/agentic_architectures/architectures/episodic_semantic.py`](../src/agentic_architectures/architectures/episodic_semantic.py).\n", "\n", "Five exposed methods:\n", "\n", "| Method | Calls | Returns |\n", "|---|---|---|\n", "| `_retrieve(query)` | 0 LLM (1 embedding for query) | `{episodes, facts}` |\n", "| `_format_context(eps, facts)` | 0 | markdown context block |\n", "| `_answer(query, context)` | **1 LLM** | answer string |\n", "| `_extract_and_save_facts(query, answer)` | **1 LLM (structured)** | list of new triples (also written to graph) |\n", "| `_save_episode(query, answer)` | 0 (1 embedding) | None (writes to vector store) |\n", "\n", "`run(task)` calls all five in order. Memory backends (`self.episodic`, `self.semantic`) are instantiated in `__init__` and live for the lifetime of the architecture instance." ] }, { "cell_type": "code", "execution_count": 2, "id": "553b0669", "metadata": { "execution": { "iopub.execute_input": "2026-05-27T09:19:50.975185Z", "iopub.status.busy": "2026-05-27T09:19:50.975185Z", "iopub.status.idle": "2026-05-27T09:19:50.994242Z", "shell.execute_reply": "2026-05-27T09:19:50.994242Z" }, "papermill": { "duration": 0.027037, "end_time": "2026-05-27T09:19:50.994242+00:00", "exception": false, "start_time": "2026-05-27T09:19:50.967205+00:00", "status": "completed" }, "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- Triple schema ---\n", "{\n", " \"properties\": {\n", " \"subject\": {\n", " \"description\": \"A specific named entity (person, place, thing).\",\n", " \"title\": \"Subject\",\n", " \"type\": \"string\"\n", " },\n", " \"predicate\": {\n", " \"description\": \"A short relation verb (e.g. 'works_at', 'lives_in', 'likes').\",\n", " \"title\": \"Predicate\",\n", " ...\n", "\n", "--- ExtractedFacts schema ---\n", "{\n", " \"$defs\": {\n", " \"_Triple\": {\n", " \"properties\": {\n", " \"subject\": {\n", " \"description\": \"A specific named entity (person, place, thing).\",\n", " \"title\": \"Subject\",\n", " \"type\": \"string\"\n", " },\n", " \"predicate\": {\n", " \"description\": \"A short relation verb (e.g. 'wor...\n" ] } ], "source": [ "from agentic_architectures.architectures.episodic_semantic import _Triple, _ExtractedFacts\n", "import json\n", "print('--- Triple schema ---')\n", "print(json.dumps(_Triple.model_json_schema(), indent=2)[:300] + '...')\n", "print()\n", "print('--- ExtractedFacts schema ---')\n", "print(json.dumps(_ExtractedFacts.model_json_schema(), indent=2)[:300] + '...')" ] }, { "cell_type": "markdown", "id": "886302ee", "metadata": { "papermill": { "duration": 0.015955, "end_time": "2026-05-27T09:19:51.010197+00:00", "exception": false, "start_time": "2026-05-27T09:19:50.994242+00:00", "status": "completed" }, "tags": [] }, "source": [ "## 6 · State (or rather: persistent instance attributes)\n", "\n", "Unlike the other architectures so far, the **state lives on the `arch` instance, not in LangGraph state**. The two memory stores (`arch.episodic`, `arch.semantic`) are mutated by each call to `run()`. The next call sees the updated memory.\n", "\n", "| Attribute | Type | What lives there |\n", "|---|---|---|\n", "| `arch.episodic` | `EpisodicMemory` | FAISS vector store + Python list of `Episode` dataclasses |\n", "| `arch.semantic` | `SemanticMemory` | NetworkX `MultiDiGraph` of `(subject)-[predicate]->(object)` |\n", "\n", "To **reset** memory between experiments, just create a new `EpisodicSemanticAgent()` instance.\n", "\n", "You can swap memory backends per-instance:\n", "```python\n", "from agentic_architectures.memory import EpisodicMemory, SemanticMemory\n", "agent = EpisodicSemanticAgent(\n", " episodic=EpisodicMemory(collection_name=\"alex_assistant\"),\n", " semantic=SemanticMemory(backend=\"neo4j\"), # if you have NEO4J_URI in .env\n", ")\n", "```" ] }, { "cell_type": "markdown", "id": "c1ce3b2b", "metadata": { "papermill": { "duration": 0.012242, "end_time": "2026-05-27T09:19:51.025904+00:00", "exception": false, "start_time": "2026-05-27T09:19:51.013662+00:00", "status": "completed" }, "tags": [] }, "source": [ "## 7 · Build the graph\n", "\n", "The graph is trivial — a single `interact` node — because the real flow is sequential Python in `run()`, not LangGraph state-machine logic. We keep the graph for the standard `.diagram()` API." ] }, { "cell_type": "code", "execution_count": 3, "id": "1b5b8721", "metadata": { "execution": { "iopub.execute_input": "2026-05-27T09:19:51.041874Z", "iopub.status.busy": "2026-05-27T09:19:51.041874Z", "iopub.status.idle": "2026-05-27T09:19:54.842929Z", "shell.execute_reply": "2026-05-27T09:19:54.840918Z" }, "papermill": { "duration": 3.817025, "end_time": "2026-05-27T09:19:54.842929+00:00", "exception": false, "start_time": "2026-05-27T09:19:51.025904+00:00", "status": "completed" }, "tags": [] }, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
Onboarding turns — memory is being populated ──────────────────────────────────────────────────────────────────────\n", "\n" ], "text/plain": [ "\u001b[1;36mOnboarding turns — memory is being populated\u001b[0m \u001b[92m──────────────────────────────────────────────────────────────────────\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
› [Turn 1] USER\n", "\n" ], "text/plain": [ "\u001b[1;35m›\u001b[0m \u001b[1m[\u001b[0m\u001b[1mTurn \u001b[0m\u001b[1;36m1\u001b[0m\u001b[1m]\u001b[0m\u001b[1m USER\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
Hi! My name is Alex and I work as a senior engineer at Anthropic.\n",
"[Turn 1] ASSISTANT: Hello Alex, nice to meet you. What can I assist you with today?\n",
" facts extracted=1 | total entities stored=2 | total episodes=1\n",
"\n"
],
"text/plain": [
"Hi! My name is Alex and I work as a senior engineer at Anthropic.\n",
"\u001b[1m[\u001b[0mTurn \u001b[1;36m1\u001b[0m\u001b[1m]\u001b[0m ASSISTANT: Hello Alex, nice to meet you. What can I assist you with today?\n",
" facts \u001b[33mextracted\u001b[0m=\u001b[1;36m1\u001b[0m | total entities \u001b[33mstored\u001b[0m=\u001b[1;36m2\u001b[0m | total \u001b[33mepisodes\u001b[0m=\u001b[1;36m1\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"data": {
"text/html": [
"› [Turn 2] USER\n", "\n" ], "text/plain": [ "\u001b[1;35m›\u001b[0m \u001b[1m[\u001b[0m\u001b[1mTurn \u001b[0m\u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m\u001b[1m USER\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
I really enjoy pickleball on weekends and I have a cat named Mochi.\n",
"[Turn 2] ASSISTANT: Nice to hear that, Alex. It sounds like you have some fun hobbies and a pet to keep you \n",
"company.\n",
" facts extracted=2 | total entities stored=4 | total episodes=2\n",
"\n"
],
"text/plain": [
"I really enjoy pickleball on weekends and I have a cat named Mochi.\n",
"\u001b[1m[\u001b[0mTurn \u001b[1;36m2\u001b[0m\u001b[1m]\u001b[0m ASSISTANT: Nice to hear that, Alex. It sounds like you have some fun hobbies and a pet to keep you \n",
"company.\n",
" facts \u001b[33mextracted\u001b[0m=\u001b[1;36m2\u001b[0m | total entities \u001b[33mstored\u001b[0m=\u001b[1;36m4\u001b[0m | total \u001b[33mepisodes\u001b[0m=\u001b[1;36m2\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n"
]
},
{
"data": {
"text/html": [
"› [Turn 3] USER\n", "\n" ], "text/plain": [ "\u001b[1;35m›\u001b[0m \u001b[1m[\u001b[0m\u001b[1mTurn \u001b[0m\u001b[1;36m3\u001b[0m\u001b[1m]\u001b[0m\u001b[1m USER\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
Mochi is a 4-year-old Tonkinese. I adopted her in 2022 from a shelter in San Francisco.\n", "[Turn 3] ASSISTANT: Nice to hear more about Mochi, Alex. I recall that you enjoy pickleball and work as a senior \n", "engineer at Anthropic.\n", " facts extracted=3 | total entities stored=7 | total episodes=3\n", "\n" ], "text/plain": [ "Mochi is a \u001b[1;36m4\u001b[0m-year-old Tonkinese. I adopted her in \u001b[1;36m2022\u001b[0m from a shelter in San Francisco.\n", "\u001b[1m[\u001b[0mTurn \u001b[1;36m3\u001b[0m\u001b[1m]\u001b[0m ASSISTANT: Nice to hear more about Mochi, Alex. I recall that you enjoy pickleball and work as a senior \n", "engineer at Anthropic.\n", " facts \u001b[33mextracted\u001b[0m=\u001b[1;36m3\u001b[0m | total entities \u001b[33mstored\u001b[0m=\u001b[1;36m7\u001b[0m | total \u001b[33mepisodes\u001b[0m=\u001b[1;36m3\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "data": { "text/html": [ "
Recall turn — agent must answer from memory ONLY ──────────────────────────────────────────────────────────────────\n", "\n" ], "text/plain": [ "\u001b[1;36mRecall turn — agent must answer from memory ONLY\u001b[0m \u001b[92m──────────────────────────────────────────────────────────────────\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
Q: What do you know about me so far? Summarise everything. \n", "\n", "A: I know that you're Alex, you work at Anthropic, you enjoy pickleball, and you own a Tonkinese cat named Mochi, \n", "who you adopted in 2022 from a San Francisco shelter. \n", "\n" ], "text/plain": [ "\u001b[1mQ:\u001b[0m What do you know about me so far? Summarise everything. \n", "\n", "\u001b[1mA:\u001b[0m I know that you're Alex, you work at Anthropic, you enjoy pickleball, and you own a Tonkinese cat named Mochi, \n", "who you adopted in 2022 from a San Francisco shelter. \n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "data": { "text/html": [ "
episodes recalled: 3 · facts recalled: 6 ────────────────────────────────────────────────────────────────────────\n", "\n" ], "text/plain": [ "\u001b[1;36mepisodes recalled: \u001b[0m\u001b[1;36m3\u001b[0m\u001b[1;36m · facts recalled: \u001b[0m\u001b[1;36m6\u001b[0m \u001b[92m────────────────────────────────────────────────────────────────────────\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "ONBOARDING = [\n", " \"Hi! My name is Alex and I work as a senior engineer at Anthropic.\",\n", " \"I really enjoy pickleball on weekends and I have a cat named Mochi.\",\n", " \"Mochi is a 4-year-old Tonkinese. I adopted her in 2022 from a shelter in San Francisco.\",\n", "]\n", "RECALL_QUERY = \"What do you know about me so far? Summarise everything.\"\n", "\n", "print_header(\"Onboarding turns — memory is being populated\")\n", "for i, query in enumerate(ONBOARDING, 1):\n", " r = arch.run(query)\n", " print_step(\n", " f\"[Turn {i}] USER\",\n", " query + f\"\\n[Turn {i}] ASSISTANT: {r.output[:200]}\\n \"\n", " f\"facts extracted={r.metadata['new_facts_extracted']} | \"\n", " f\"total entities stored={r.metadata['total_entities_stored']} | \"\n", " f\"total episodes={r.metadata['total_episodes_stored']}\"\n", " )\n", " print()\n", "\n", "print_header(\"Recall turn — agent must answer from memory ONLY\")\n", "recall = arch.run(RECALL_QUERY)\n", "print_md(f\"**Q:** {RECALL_QUERY}\\n\\n**A:** {recall.output}\")\n", "print()\n", "print_header(\n", " f\"episodes recalled: {recall.metadata['episodes_recalled']} · \"\n", " f\"facts recalled: {recall.metadata['facts_recalled']}\"\n", ")" ] }, { "cell_type": "markdown", "id": "6b5da442", "metadata": { "papermill": { "duration": 0.006992, "end_time": "2026-05-27T09:20:13.545372+00:00", "exception": false, "start_time": "2026-05-27T09:20:13.538380+00:00", "status": "completed" }, "tags": [] }, "source": [ "### 8.0 · What just happened, briefly\n", "\n", "Three signals to check above:\n", "\n", "- **Facts grew with each onboarding turn.** If `total_entities_stored` plateaued at 0 or 1, the fact-extractor failed — check its prompt.\n", "- **The recall turn's `facts_recalled` > 0.** Proof that the entity-match retrieval found relevant graph nodes when answering \"what do you know about me\".\n", "- **The recall answer is grounded.** Inspect the verbatim answer — does it mention specific facts from earlier turns? If yes, memory is doing its job. If the answer is generic (\"I know various things about you\"), memory was bypassed.\n", "\n", "### 8.1 · Inspect the populated memory directly" ] }, { "cell_type": "code", "execution_count": 5, "id": "d2bfa617", "metadata": { "execution": { "iopub.execute_input": "2026-05-27T09:20:13.558561Z", "iopub.status.busy": "2026-05-27T09:20:13.558561Z", "iopub.status.idle": "2026-05-27T09:20:13.577905Z", "shell.execute_reply": "2026-05-27T09:20:13.576894Z" }, "papermill": { "duration": 0.028896, "end_time": "2026-05-27T09:20:13.579915+00:00", "exception": false, "start_time": "2026-05-27T09:20:13.551019+00:00", "status": "completed" }, "tags": [] }, "outputs": [ { "data": { "text/html": [ "
All triples in semantic memory ────────────────────────────────────────────────────────────────────────────────────\n", "\n" ], "text/plain": [ "\u001b[1;36mAll triples in semantic memory\u001b[0m \u001b[92m────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " (Alex) --[works_at]--> (Anthropic)\n", " (Alex) --[works_at]--> (Anthropic)\n", " (Alex) --[enjoys]--> (pickleball)\n", " (Alex) --[enjoys]--> (pickleball)\n", " (Alex) --[owns]--> (Mochi)\n", " (Alex) --[owns]--> (Mochi)\n", " (Mochi) --[is_a]--> (Tonkinese)\n", " (Mochi) --[adopted_in]--> (2022)\n", " (Mochi) --[adopted_in]--> (2022)\n", " (Mochi) --[adopted_from]--> (San Francisco shelter)\n", " (Mochi) --[adopted_from]--> (San Francisco shelter)\n", "\n" ] }, { "data": { "text/html": [ "
Episodic memory (4 episodes) ──────────────────────────────────────────────────────────────────────────────────────\n", "\n" ], "text/plain": [ "\u001b[1;36mEpisodic memory \u001b[0m\u001b[1;36m(\u001b[0m\u001b[1;36m4\u001b[0m\u001b[1;36m episodes\u001b[0m\u001b[1;36m)\u001b[0m \u001b[92m──────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " [1] User: Hi! My name is Alex and I work as a senior engineer at Anthropic. Assistant: Hello Alex, nice to meet you. What ca...\n", " [2] User: I really enjoy pickleball on weekends and I have a cat named Mochi. Assistant: Nice to hear that, Alex. It sounds ...\n", " [3] User: Mochi is a 4-year-old Tonkinese. I adopted her in 2022 from a shelter in San Francisco. Assistant: Nice to hear mo...\n", " [4] User: What do you know about me so far? Summarise everything. Assistant: I know that you're Alex, you work at Anthropic,...\n" ] } ], "source": [ "import networkx as nx\n", "from agentic_architectures.memory.graph import NetworkXGraphMemory\n", "\n", "print_header(\"All triples in semantic memory\")\n", "backend = arch.semantic.backend\n", "if isinstance(backend, NetworkXGraphMemory):\n", " for u, v, d in backend._g.edges(data=True):\n", " print(f\" ({u}) --[{d.get('predicate', '?')}]--> ({v})\")\n", "\n", "print()\n", "print_header(f\"Episodic memory ({len(arch.episodic.episodes)} episodes)\")\n", "for i, ep in enumerate(arch.episodic.episodes, 1):\n", " snippet = ep.content[:120].replace('\\n', ' ')\n", " print(f\" [{i}] {snippet}...\")" ] }, { "cell_type": "markdown", "id": "00bf8ef0", "metadata": { "papermill": { "duration": 0.015744, "end_time": "2026-05-27T09:20:13.597655+00:00", "exception": false, "start_time": "2026-05-27T09:20:13.581911+00:00", "status": "completed" }, "tags": [] }, "source": [ "## 9 · What we just observed\n", "\n", "The cells above ran **4 sequential calls on the same `arch` instance** — 3 onboarding turns that populated memory, then a recall turn that queried it.\n", "\n", "### 9.1 · Memory growth across onboarding turns\n", "\n", "| Turn | User message (truncated) | Facts extracted | Entities stored | Episodes stored |\n", "|---|---|---|---|---|\n", "| 1 | Hi! My name is Alex and I work as a senior engineer at Anthropic. | 1 | 2 | 1 |\n", "| 2 | I really enjoy pickleball on weekends and I have a cat named Mochi. | 2 | 4 | 2 |\n", "| 3 | Mochi is a 4-year-old Tonkinese. I adopted her in 2022 from a shelter in San Fra… | 3 | 7 | 3 |\n", "\n", "### 9.2 · Final state of semantic memory (all triples)\n", "\n", "| # | Subject | Predicate | Object |\n", "|---|---|---|---|\n", "| 1 | Alex | works_at | Anthropic |\n", "| 2 | Alex | works_at | Anthropic |\n", "| 3 | Alex | enjoys | pickleball |\n", "| 4 | Alex | enjoys | pickleball |\n", "| 5 | Alex | owns | Mochi |\n", "| 6 | Alex | owns | Mochi |\n", "| 7 | Mochi | is_a | Tonkinese |\n", "| 8 | Mochi | adopted_in | 2022 |\n", "| 9 | Mochi | adopted_in | 2022 |\n", "| 10 | Mochi | adopted_from | San Francisco shelter |\n", "| 11 | Mochi | adopted_from | San Francisco shelter |\n", "\n", "### 9.3 · Recall test\n", "\n", "**Q:** What do you know about me so far? Summarise everything.\n", "\n", "**A:** I know that you're Alex, you work at Anthropic, you enjoy pickleball, and you own a Tonkinese cat named Mochi, who you adopted in 2022 from a San Francisco shelter.\n", "\n", "- **`episodes_recalled`** = 3 (vector similarity hits over the 4 stored episodes)\n", "- **`facts_recalled`** = 6 (graph entity-match hits)\n", "\n", "### 9.4 · Patterns surfaced in this run\n", "\n", "- **Healthy extraction**: 7 entities stored after 3 onboarding turns (2.3 entities/turn average).\n", "\n", "- **Semantic recall worked**: the recall query retrieved 6 fact(s) from the graph. This is the *single most important signal* that the dual-memory design is doing its job — the answer is grounded in stored facts, not hallucination.\n", "\n", "- **Episodic recall worked**: vector similarity returned 3 past episode(s) most similar to the recall query.\n", "\n", "- **Recall answer is well-grounded**: 6/6 of the stored fact-objects appear in the recall answer. The agent is genuinely answering from memory.\n", "\n", "### 9.5 · The takeaway\n", "\n", "A *healthy* dual-memory run looks like:\n", "\n", "1. **Linear growth** of entities + episodes across onboarding turns.\n", "2. **Recall query retrieves both** episodes (vector) and facts (graph).\n", "3. **Recall answer mentions specific stored objects** — proof the agent is answering from memory, not training data.\n", "4. **No duplicated triples** in the final graph (entity-match wasn't fooled by synonyms).\n", "\n", "Compare those four signals to what you see above to judge memory quality. The single most important production metric is **fact-recall precision**: did the agent recall facts that are *actually* relevant to the query?" ] }, { "cell_type": "markdown", "id": "0ba917fe", "metadata": { "papermill": { "duration": 0.015746, "end_time": "2026-05-27T09:20:13.613401+00:00", "exception": false, "start_time": "2026-05-27T09:20:13.597655+00:00", "status": "completed" }, "tags": [] }, "source": [ "## 10 · Try other providers / swap to Neo4j\n", "\n", "Episodic + Semantic memory needs **embeddings** (for FAISS) + **structured output** (for fact extraction). Both are widely supported. The cell below also demonstrates swapping graph backend to **Neo4j** if you have NEO4J_URI set in `.env` (e.g. AuraDB Free)." ] }, { "cell_type": "code", "execution_count": 6, "id": "a0e48ef2", "metadata": { "execution": { "iopub.execute_input": "2026-05-27T09:20:13.633891Z", "iopub.status.busy": "2026-05-27T09:20:13.633891Z", "iopub.status.idle": "2026-05-27T09:20:13.666165Z", "shell.execute_reply": "2026-05-27T09:20:13.664482Z" }, "papermill": { "duration": 0.0442, "end_time": "2026-05-27T09:20:13.668162+00:00", "exception": false, "start_time": "2026-05-27T09:20:13.623962+00:00", "status": "completed" }, "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[skip] openai: no API key\n", "[skip] NEO4J_PASSWORD not set — staying on NetworkX backend\n" ] } ], "source": [ "from agentic_architectures.llm.factory import provider_supports_structured_output\n", "from agentic_architectures.memory import EpisodicMemory, SemanticMemory\n", "\n", "for p in [\"openai\"]:\n", " key = settings.api_key_for(p)\n", " if key is None or not key.get_secret_value():\n", " print(f\"[skip] {p}: no API key\")\n", " continue\n", " if not provider_supports_structured_output(p):\n", " print(f\"[skip] {p}: no structured output\")\n", " continue\n", " print_header(f\"Re-running on {p}\")\n", " other_arch = EpisodicSemanticAgent(llm=get_llm(provider=p))\n", " other_arch.run(\"My name is Pat and I love coffee from Portland.\")\n", " r = other_arch.run(\"What do you know about me?\")\n", " print(r.output[:300])\n", " print()\n", "\n", "# Try Neo4j swap if configured\n", "if settings.neo4j_password and settings.neo4j_password.get_secret_value():\n", " print_header(\"Swapping graph backend to Neo4j\")\n", " try:\n", " neo4j_arch = EpisodicSemanticAgent(\n", " semantic=SemanticMemory(backend=\"neo4j\"),\n", " )\n", " neo4j_arch.semantic.reset() # start clean\n", " neo4j_arch.run(\"My name is Sam and I live in Tokyo.\")\n", " r = neo4j_arch.run(\"Where do I live?\")\n", " print(r.output[:200])\n", " print(f\"facts stored in Neo4j: {neo4j_arch.metadata if hasattr(neo4j_arch, 'metadata') else r.metadata['total_entities_stored']}\")\n", " except Exception as e:\n", " print(f\"[Neo4j skip] {type(e).__name__}: {e}\")\n", "else:\n", " print(\"[skip] NEO4J_PASSWORD not set — staying on NetworkX backend\")" ] }, { "cell_type": "markdown", "id": "80bcfa43", "metadata": { "papermill": { "duration": 0.002038, "end_time": "2026-05-27T09:20:13.682159+00:00", "exception": false, "start_time": "2026-05-27T09:20:13.680121+00:00", "status": "completed" }, "tags": [] }, "source": [ "## 11 · Failure modes, safety, extensions\n", "\n", "### 11.1 · Where this breaks\n", "\n", "| Failure | Mechanism | Mitigation |\n", "|---|---|---|\n", "| **Fact-extractor over-extracts** | Triples like `(user, said, hello)` flood the graph | Tighten schema description; reject triples with generic verbs |\n", "| **Fact-extractor under-extracts** | Misses obvious \"I am X\" / \"I work at Y\" facts | Stronger prompt; or hand-curate a domain vocabulary of predicates |\n", "| **Entity match brittle** | \"Mochi\" / \"mochi\" / \"my cat\" treated as different entities | Normalise lowercase + entity-linking (extension) |\n", "| **Vector recall returns near-duplicates** | Same fact in 5 different episodes all match the query | Dedup by content hash before adding to FAISS |\n", "| **Memory grows unbounded** | 10K episodes = slow recall + huge embedding cost | MemGPT-style tiered memory (notebook 31) |\n", "| **Privacy leak across users** | If `arch` instance shared across users, memory leaks | Always create a fresh `EpisodicSemanticAgent()` per user, or use `collection_name=user_id` |\n", "\n", "### 11.2 · Production safety\n", "\n", "- **Per-user isolation.** Memory is on the instance — always per-user. Use `collection_name=\"user_