|
| 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