Skip to content

Commit aac410a

Browse files
GWealecopybara-github
authored andcommitted
docs: add memory service unit guide
Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 962403306
1 parent a39e71a commit aac410a

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

docs/guides/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ This directory contains specific developer guides for the ADK Python implementat
1313
* [Event and NodeInfo](events/event/index.md) - Understanding Event and NodeInfo in workflows.
1414
* [RequestInput](events/request_input/index.md) - How to use RequestInput for human-in-the-loop interactions.
1515

16+
### Memory
17+
* [BaseMemoryService](memory/memory_service/index.md) - Storing finished sessions and recalling them from later conversations.
18+
1619
### Plugins
1720
* [ReflectAndRetryModelPlugin](plugins/reflect_retry_model_plugin/index.md) - Self-healing, concurrent-safe error recovery for model failures.
1821
* [ReflectAndRetryToolPlugin](plugins/reflect_retry_tool_plugin/index.md) - Self-healing, concurrent-safe error recovery for tool failures.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# BaseMemoryService
2+
3+
`BaseMemoryService` is the interface ADK uses to store finished conversations
4+
and search them later. It gives an agent recall that outlives a single session.
5+
6+
## Introduction
7+
8+
A session holds one conversation. When it ends, its events stay in the session
9+
service, but nothing the user said is available to the *next* session. The
10+
memory service closes that gap: hand it a completed session, and a later session
11+
can search the content by query.
12+
13+
The interface has two required halves. `add_session_to_memory` ingests, and
14+
`search_memory` retrieves. Everything memory-related in ADK sits on top of those
15+
two methods — the `load_memory` and `preload_memory` tools, the memory helpers
16+
on `Context`, and the `--memory_service_uri` flag on the CLI. It is all opt-in:
17+
a `Runner` with no `memory_service` runs fine, and the `Context` memory helpers
18+
then raise `ValueError`.
19+
20+
## Get started
21+
22+
This runs one conversation, saves it to memory, then starts a fresh session that
23+
recalls it. The agent carries the `load_memory` tool, so the model decides when
24+
to search.
25+
26+
```python
27+
import asyncio
28+
29+
from google.adk.agents import LlmAgent
30+
from google.adk.memory import InMemoryMemoryService
31+
from google.adk.runners import Runner
32+
from google.adk.sessions import InMemorySessionService
33+
from google.adk.tools import load_memory
34+
from google.genai import types
35+
36+
APP_NAME = "memory_demo"
37+
USER_ID = "user-1"
38+
39+
agent = LlmAgent(
40+
name="memory_agent",
41+
instruction=(
42+
"Answer the user. Call load_memory when the answer might be in an"
43+
" earlier conversation."
44+
),
45+
tools=[load_memory],
46+
)
47+
48+
session_service = InMemorySessionService()
49+
memory_service = InMemoryMemoryService()
50+
runner = Runner(
51+
app_name=APP_NAME,
52+
agent=agent,
53+
session_service=session_service,
54+
memory_service=memory_service,
55+
)
56+
57+
58+
async def ask(session_id: str, text: str) -> None:
59+
message = types.Content(role="user", parts=[types.Part(text=text)])
60+
async for event in runner.run_async(
61+
user_id=USER_ID, session_id=session_id, new_message=message
62+
):
63+
if event.is_final_response() and event.content and event.content.parts:
64+
print(event.content.parts[0].text)
65+
66+
67+
async def main() -> None:
68+
first = await session_service.create_session(
69+
app_name=APP_NAME, user_id=USER_ID
70+
)
71+
await ask(first.id, "My favorite sport is badminton.")
72+
73+
# Nothing is remembered until the finished session is handed to the memory
74+
# service. Re-read it first so the ingested copy has the final events.
75+
completed = await session_service.get_session(
76+
app_name=APP_NAME, user_id=USER_ID, session_id=first.id
77+
)
78+
await memory_service.add_session_to_memory(completed)
79+
80+
second = await session_service.create_session(
81+
app_name=APP_NAME, user_id=USER_ID
82+
)
83+
await ask(second.id, "What sport do I like?")
84+
85+
86+
if __name__ == "__main__":
87+
asyncio.run(main())
88+
```
89+
90+
`InMemoryRunner` wires an `InMemoryMemoryService` for you, so a quick experiment
91+
can skip the explicit `Runner` above and read `runner.memory_service` instead.
92+
93+
## Memory is not session state
94+
95+
This is the most common source of confusion, because both outlive a turn and
96+
both can outlive a session.
97+
98+
Session state is a dictionary. You write `ctx.state["tier"] = "gold"` and read
99+
back exactly `"gold"`. Keys prefixed `user:` are scoped to the user and `app:`
100+
to the application, so those do survive across sessions; keys prefixed `temp:`
101+
never leave the current invocation.
102+
103+
Memory is a corpus, not a dictionary. You do not choose keys and cannot read an
104+
entry back by name. You hand over whole conversations and later ask a question;
105+
the service decides which past content is relevant and returns it as
106+
`MemoryEntry` objects that get spliced into the model's prompt.
107+
108+
So: put a known fact you will look up by name in state. Put "everything the user
109+
has ever told us" in memory, and let retrieval find the part that matters.
110+
111+
## How it works
112+
113+
### Ingestion
114+
115+
`add_session_to_memory(session)` is the required entry point and takes a whole
116+
`Session`. It may be called with the same session repeatedly over its lifetime.
117+
118+
Two optional methods give finer control, and a service that does not support
119+
them raises `NotImplementedError`:
120+
121+
* `add_events_to_memory(*, app_name, user_id, events, session_id=None,
122+
custom_metadata=None)` writes an explicit list of events as an incremental
123+
delta. Use it to persist only the latest turn.
124+
* `add_memory(*, app_name, user_id, memories, custom_metadata=None)` writes
125+
`MemoryEntry` objects directly, for facts you distilled yourself.
126+
127+
The `custom_metadata` keys each service accepts are implementation-defined.
128+
129+
### Retrieval
130+
131+
`search_memory(*, app_name, user_id, query)` returns a `SearchMemoryResponse`
132+
holding `memories`, a list of `MemoryEntry`. Each entry carries `content` (a
133+
`types.Content`) plus optional `id`, `author`, `timestamp`, and
134+
`custom_metadata`. Memory is scoped by the `(app_name, user_id)` pair, so one
135+
user never sees another's memories.
136+
137+
### From inside an agent
138+
139+
`Context` — what tools and callbacks receive — exposes the same operations
140+
already scoped to the running session, so you never pass the identifiers by
141+
hand:
142+
143+
```python
144+
from google.adk.agents import Context
145+
146+
147+
async def save_to_memory(callback_context: Context) -> None:
148+
await callback_context.add_session_to_memory()
149+
```
150+
151+
Attach that as an `after_agent_callback` and each turn is ingested as it
152+
finishes, rather than at some later point you have to remember to trigger.
153+
`Context` also offers `add_events_to_memory`, `add_memory`, and `search_memory`.
154+
155+
## The memory tools
156+
157+
Both tools live in `google.adk.tools` and are ready-made instances, so you add
158+
them to `tools=[...]` directly rather than constructing them.
159+
160+
`load_memory` is model-driven. It is declared with a single `query` string and
161+
appends an instruction telling the model that memory exists and to call the tool
162+
when a question needs it. Retrieval costs a tool call, but only happens when the
163+
model judges it necessary.
164+
165+
`preload_memory` is automatic and is never called by the model. Before every
166+
request it searches memory using the user's message as the query, and appends
167+
any results to the instructions inside a `<PAST_CONVERSATIONS>` block. There is
168+
no tool-call round trip, but every request pays for a search. A failed search
169+
logs a warning and the turn continues.
170+
171+
They compose: `preload_memory` covers the common case, and `load_memory` lets
172+
the model dig for what the raw user message did not surface.
173+
174+
## Implementations
175+
176+
`InMemoryMemoryService` keeps everything in a process-local dict and is for
177+
prototyping and tests. It is thread-safe, but it matches on **keywords, not
178+
meaning**: an entry comes back only when it shares a word with the query. Ask
179+
"what color is my car?" after storing "I drive a blue hatchback" and you get
180+
nothing, because no word overlaps. Do not read that miss as a bug in your agent.
181+
182+
`VertexAiMemoryBankService(project=..., location=..., agent_engine_id=...)` is
183+
the managed option and does semantic retrieval. It consolidates conversations
184+
into durable memories rather than storing raw turns, and it is the only built-in
185+
service that implements all three write methods. `agent_engine_id` is required
186+
and must be the bare ID, not a full resource path.
187+
188+
`VertexAiRagMemoryService(rag_corpus=..., similarity_top_k=...,
189+
vector_distance_threshold=...)` retrieves over a RAG corpus instead, and
190+
supports `add_session_to_memory` and `search_memory` only.
191+
192+
Both managed services need the `gcp` extra; without it, construction raises an
193+
`ImportError` telling you to install `google-adk[gcp]`.
194+
195+
From the CLI, `--memory_service_uri` selects the service:
196+
`agentengine://<agent_engine>` for Memory Bank, `rag://<rag_corpus_id>` for the
197+
RAG corpus, and `memory://` to force the in-memory one.
198+
199+
To write your own, subclass `BaseMemoryService` and implement
200+
`add_session_to_memory` and `search_memory`. Keep the `(app_name, user_id)`
201+
scoping — the tools, the CLI, and `Context` all assume it.
202+
203+
## Limitations
204+
205+
* **Ingestion is explicit.** Sessions do not reach memory on their own. If no
206+
one calls `add_session_to_memory`, memory stays empty.
207+
* **Text only.** Both memory tools read only the text parts of a
208+
`MemoryEntry`; images and other inline data in a stored turn are dropped
209+
when the entry is rendered into the prompt.
210+
211+
## Related samples
212+
213+
* [Memory: recall across sessions](../../../../contributing/samples/context_management/memory)

0 commit comments

Comments
 (0)