Skip to main contentSkip to footer
PatternscriptintermediateRunnableguided-flow

Episodic and semantic memory: remember turns, and remember facts

Two stores answering different questions. Episodic keeps what was said and when; semantic keeps extracted, deduplicated facts about the user. Retrieval defaults to facts, and reaches for transcripts only when the question is genuinely about what happened.

Key Facts

Level
intermediate
Runtime
Python • OpenAI API
Pattern
Inspectable flow with visible system boundaries
Interaction
Live sandbox • Script
Updated
27 July 2026

Navigate this example

High-level flow

How this example moves from input to execution and reviewable output

Episodic and semantic memory: remember… -> Store reusable memory -> Load environment keys -> Initialize OpenAI client -> Send chat request -> Constrain output schema

Trigger

Episodic and semantic memory: remember…

Runtime

Store reusable memory

Outcome

Load environment keys

Why this page exists

This example is shown as both real source code and a product-facing interaction pattern so learners can connect implementation, UX, and doctrine without leaving the library.

Visual flowReal sourceSandbox or walkthroughMCP access

Doctrine relations

These are reasoning shapes: how an agent decides, when it acts, what it remembers. For how to compose the interface around it, see the AI Pattern Blueprint. For triggers, scheduling, context loading and recovery, see Agent runtime architecture.

Structural

This pattern expresses these principles directly in code.

  • P7 · Establish trust through inspectability

    Semantic memory is a legible key-value store rather than an opaque embedding blob, so what the system believes about a user can be read, checked, and corrected item by item. A memory layer whose contents can only be inspected by asking the model what it remembers is not inspectable in any useful sense.

    episodic_semantic.py:FACTS

Default gap

The textbook form of this pattern violates these by default. Each one carries the fix.

  • P4 · Apply progressive disclosure to system agency

    Retrieved memory is injected silently into the prompt, so an answer shaped by something said weeks ago looks identical to one shaped by the current turn. Fix: state which remembered facts informed an answer, at a level the reader can act on, so a wrong or stale fact is correctable at the point it does damage rather than after.

    episodic_semantic.py:answer

  • P5 · Replace implied magic with clear mental models

    The user is never told a fact was extracted and stored about them. From their side the assistant simply starts knowing things, which is the archetypal implied-magic moment and, once it involves personal detail, a trust problem rather than a delight. Fix: surface what was learned at the moment it is written, and make the fact store viewable and editable rather than only inferable from behaviour.

    episodic_semantic.py:remember

Depends on the wrapper

Your implementation decides these, not the pattern. No verdict is asserted.

  • P8 · Make hand-offs, approvals, and blockers explicit

    Whether the user can refuse, edit, or delete what was remembered is entirely a wrapper decision. The pattern writes to a store and reads from it; it takes no position on consent, retention, or erasure. In most jurisdictions that position is not optional, but it is not the pattern's to hold.

    episodic_semantic.py:remember

Doctrine relations are first-party analysis, not the output of a validation run. Run the validator on your own code to get a scored verdict.

How should this example be used in the platform?

Use the sandbox to understand the experience pattern first, then inspect the source to see how the product boundary, model boundary, and doctrine boundary are actually implemented.

UX pattern: Inspectable flow with visible system boundaries
Apply progressive disclosure to system agency
Replace implied magic with clear mental models
Establish trust through inspectability

Source references

Library entry
pattern-episodic-semantic
Source path
content/patterns/memory/episodic-semantic/episodic_semantic.py
Libraries
openai, pydantic, python-dotenv
Runtime requirements
OPENAI_API_KEY
Related principles
Apply progressive disclosure to system agency, Replace implied magic with clear mental models, Establish trust through inspectability, Make hand-offs, approvals, and blockers explicit

episodic_semantic.py

python
"""
Episodic and semantic memory: remember turns, and remember facts

Two stores, because they answer different questions.

  Episodic  what was said, when. Searchable transcript of past turns.
  Semantic  what is true. Extracted, deduplicated facts about the user.

    Turn -> store episode -> extract facts -> upsert semantic
    Query -> retrieve facts (+ episodes only if needed) -> answer

The failure this prevents is subtle. Loading whole past conversations into
context to "remember" the user is expensive and actively harmful: old
transcripts pull the model toward whatever was being discussed then, not what
is being asked now. Facts are small, current, and steer nothing.

So the default retrieval here is facts only. Episodes are fetched when the
question is genuinely about what happened rather than about what is true.

Run: python episodic_semantic.py
"""

from typing import Literal

from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field

load_dotenv()

client = OpenAI()

MODEL = "gpt-5.4-mini"

# --------------------------------------------------------------
# Two stores. In production these are a vector index and a table;
# the shape of the pattern is the same.
# --------------------------------------------------------------

EPISODES: list[dict[str, str]] = []
FACTS: dict[str, str] = {}


class ExtractedFact(BaseModel):
    key: str = Field(
        description="Stable snake_case identifier, e.g. 'preferred_language'."
    )
    value: str = Field(description="The fact itself, as short as possible.")


class Extraction(BaseModel):
    facts: list[ExtractedFact] = Field(
        description=(
            "Durable facts about the user or their situation. Empty when the "
            "turn contains nothing worth remembering. Most turns do not."
        )
    )


class Retrieval(BaseModel):
    needs_episodes: bool = Field(
        description=(
            "True only when the question is about what was previously said or "
            "done, rather than about what is currently true."
        )
    )
    why: str


def remember(user_text: str, assistant_text: str) -> list[ExtractedFact]:
    """Append the episode verbatim, then distil anything durable into facts."""
    EPISODES.append({"user": user_text, "assistant": assistant_text})

    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Extract durable facts about the user from this exchange. "
                    "A durable fact is still true next month. Preferences, "
                    "constraints, and stable context qualify. Questions asked, "
                    "one-off requests, and anything about the current task do "
                    "not. Return an empty list when nothing qualifies."
                ),
            },
            {"role": "user", "content": f"User: {user_text}\nAssistant: {assistant_text}"},
        ],
        response_format=Extraction,
    )
    extracted = completion.choices[0].message.parsed.facts
    for fact in extracted:
        # Upsert, so a changed preference replaces rather than accumulates.
        # Append-only fact stores end up holding both the old answer and the
        # new one, with nothing to say which is current.
        FACTS[fact.key] = fact.value
    return extracted


def decide_retrieval(question: str) -> Retrieval:
    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Decide whether answering needs the past transcript, or "
                    "whether the known facts are enough. Transcripts are "
                    "expensive and drag the answer toward old topics, so "
                    "prefer facts unless the question is genuinely about what "
                    "was said or done."
                ),
            },
            {"role": "user", "content": question},
        ],
        response_format=Retrieval,
    )
    return completion.choices[0].message.parsed


def answer(question: str) -> str:
    plan = decide_retrieval(question)
    print(f"  retrieval: facts{' + episodes' if plan.needs_episodes else ' only'}")
    print(f"      why  : {plan.why}")

    known = "\n".join(f"- {k}: {v}" for k, v in FACTS.items()) or "(nothing known)"
    context = f"Known facts:\n{known}"
    if plan.needs_episodes:
        transcript = "\n".join(
            f"User: {e['user']}\nAssistant: {e['assistant']}" for e in EPISODES
        )
        context += f"\n\nPast turns:\n{transcript}"

    completion = client.chat.completions.create(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Answer using the context provided. If it does not contain "
                    "the answer, say you do not know rather than guessing."
                ),
            },
            {"role": "user", "content": f"{context}\n\nQuestion: {question}"},
        ],
    )
    return completion.choices[0].message.content or ""


if __name__ == "__main__":
    print("\n> seeding two turns")
    for user_text, assistant_text in [
        ("I run a small ceramics shop on Shopify.", "Noted, thanks."),
        ("Keep any tooling under 200 a month, that is my ceiling.", "Understood."),
    ]:
        new_facts = remember(user_text, assistant_text)
        print(f"  learned: {[(f.key, f.value) for f in new_facts] or 'nothing durable'}")

    for question in [
        "What platform am I on, and what is my budget?",
        "What exactly did I say when I mentioned the budget?",
    ]:
        print(f"\n> {question}")
        print(f"  answer: {answer(question)}")

What should the learner inspect in the code?

Look for the exact place where system scope is bounded: schema definitions, prompt framing, runtime configuration, and the call site that turns user intent into a concrete model or workflow action.

Look for output contracts and validation
Look for the exact execution call
Look for what the product could expose to the user

How does the sandbox relate to the source?

The sandbox should make the UX legible: what the user sees, what the system is deciding, and how the result becomes reviewable. The source then shows how that behavior is actually implemented.

Read the implementation summary.
Step through the user and system states.
Inspect the source code with the highlighted doctrine decisions in mind.
SandboxInspectable flow with visible system boundaries

Interaction walkthrough

Use the sandbox to step through the user-visible experience, the system work behind it, and the doctrine choice the example is making.

UX explanation

The sandbox explains what the user should see, what the system is doing, and where control or inspectability must remain explicit.

AI design explanation

The page turns raw source into a product-facing pattern: what the model is allowed to decide, what the product should expose, and where deterministic code or review should take over.

Interaction walkthrough

  1. 1Read the implementation summary.
  2. 2Step through the user and system states.
  3. 3Inspect the source code with the highlighted doctrine decisions in mind.

Visible to the user

Two stores answering different questions. Episodic keeps what was said and when; semantic keeps extracted, deduplicated facts about the user. Retrieval defaults to facts, and reaches for transcripts only when the question is genuinely about what happened.

System work

The product prepares a bounded model or workflow task.

Why it matters

The interface should make the delegated task legible before automation happens.

Used in courses and paths

This example currently stands on its own in the library, but it still connects to the principle system and the broader example family.

Related principles

Runtime architecture

Use this example in your agents

This example is also available through the blueprint’s agent-ready layer. Use the For agents page for the public MCP, deterministic exports, and Claude/Cursor setup.

Define triggers, context, and boundaries before increasing autonomy
Make control, observability, and recovery explicit in the runtime
Choose the right operational patterns before delegating to workflows