28 KiB
Artifacts
In the Agent Development Kit (ADK), Artifacts provide a robust way to manage named, versioned binary data. Think of them as the ADK's mechanism for handling files and other non-textual information that needs to be associated with a user interaction session or persistently with a user across multiple sessions.
Introduction: What are Artifacts and Why Use Them?
Concept: At its core, an Artifact is a piece of binary data (like the content of a PDF file, an image, or an audio clip) identified by a unique name (filename) within a specific scope (either a single session or a user's entire history with the application). Every time you save data with the same filename, ADK automatically creates and tracks a new version.
Purpose: While the Session State (state) is excellent for storing small, simple configuration values or conversational context (like strings, numbers, booleans, or small dictionaries), Artifacts are designed for different scenarios:
- Handling Files & Binary Data: Easily store, retrieve, and manage images, audio, video, PDFs, spreadsheets, or any other binary format your agent needs to work with.
- Managing Large Data: Session state isn't optimized for large data blobs. Artifacts offer a dedicated, efficient way to persist substantial amounts of data without overwhelming the session state.
- User File Interaction: Enable users to upload files (saved as artifacts) for processing and download files generated by your agents (loaded from artifacts).
- Persistent User Assets: Store user-specific data like profile pictures or configuration files that should be available across different sessions.
Representation: When you interact with artifact data in your code (saving it or after loading it), it's represented using the standard google.genai.types.Part object. The essential parts for artifacts are within the inline_data attribute, which is a google.genai.types.Blob containing:
data: The raw binary content asbytes.mime_type: A string (like'application/pdf','image/png') indicating the data's format, crucial for correct interpretation later.
import google.genai.types as types
# Assume pdf_bytes holds the binary data of a PDF file
pdf_bytes = b'%PDF-1.4...' # Placeholder for actual PDF bytes
# Create a Part object to represent the artifact data
pdf_artifact_part = types.Part.from_data(
data=pdf_bytes,
mime_type="application/pdf"
)
print(f"Artifact Part created with MIME type: {pdf_artifact_part.inline_data.mime_type}")
Key Benefit: You don't need to worry about how or where this data is stored. An Artifact Service, configured when you set up your application, handles all the storage, retrieval, versioning, and namespacing details behind the scenes. You interact with it through simple methods available in your agent's context.
The Artifact Service: Managing Your Data
Concept: The Artifact Service is the central component responsible for the actual storage logic. It's an implementation of the BaseArtifactService interface and determines the backend used for persistence (like memory or cloud storage).
Configuration: You provide an instance of a chosen Artifact Service when initializing the Runner. The Runner then makes this service available to your agents and tools during execution via the Invocation Context.
from google.adk.runners import Runner
from google.adk.agents import LlmAgent
from google.adk.sessions import InMemorySessionService
# Choose an Artifact Service implementation:
from google.adk.artifacts import InMemoryArtifactService # For testing/temporary
# from google.adk.artifacts import GcsArtifactService # For persistent storage
# Define your agent
my_agent = LlmAgent(name="my_artifact_agent", model="gemini-1.5-flash")
# Instantiate the chosen service
# artifact_service = GcsArtifactService(bucket_name="your-gcs-bucket")
artifact_service = InMemoryArtifactService() # Using in-memory for this example
# Provide the service instance to the Runner
runner = Runner(
agent=my_agent,
app_name="my_artifact_app",
session_service=InMemorySessionService(),
artifact_service=artifact_service # <--- Service configured here
)
# Now, code running within this runner's context can use artifact methods.
Implementations Overview: ADK provides ready-to-use implementations:
InMemoryArtifactService: Stores artifacts directly in the application's memory. It's simple and fast, perfect for local development and testing, but all data is lost when the application stops.GcsArtifactService: Uses Google Cloud Storage (GCS) for persistent storage. Ideal for production environments where data needs to survive restarts and be scalable. Requires a GCS bucket and appropriate permissions.
Consistent Interaction: Regardless of which backend service you configure (InMemory or GCS), the way you interact with artifacts within your agent code (using methods like context.save_artifact and context.load_artifact) remains exactly the same. The service abstracts away the storage details.
3. Working with Artifacts: Core Actions
You primarily interact with artifacts within your agent's logic (like in Callbacks or Tools) using methods provided on the CallbackContext and ToolContext objects. These context objects are automatically passed to your callback functions and tool implementations. They offer a simple interface that abstracts the underlying storage details managed by the configured Artifact Service.
!!! important "Prerequisite: Service Configuration"
Before using these context methods, ensure you have configured an artifact_service (like InMemoryArtifactService or GcsArtifactService) when initializing your Runner, as shown in the previous section. If no service is configured, calling these methods will raise a ValueError.
Saving Artifacts (context.save_artifact)
-
Action: Use this method to store binary data as a new version of an artifact.
-
How: Call
context.save_artifact(filename="your_filename", artifact=your_part_object). -
Key Inputs:
filename(str): The name you want to give the artifact. Use the"user:"prefix here for user-scoped data.artifact(types.Part): ThePartobject containing your binarydataand itsmime_type. Providing an accuratemime_typeis crucial.
-
Result:
- Returns the integer
versionnumber assigned to this specific save operation (starting from 0 for the first save of a given filename). - Automatically records this action in the current Event's
actions.artifact_deltadictionary. This delta maps thefilenameto the newversion, signaling that an artifact was updated.
- Returns the integer
-
Versioning Behavior: Each call to
save_artifactfor the samefilename(within the same scope) creates a new, distinct version.
# Example: Within a callback or tool function
import google.genai.types as types
from google.adk.agents.callback_context import CallbackContext # Or ToolContext
async def save_report(context: CallbackContext, report_bytes: bytes, filename: str):
"""Saves report bytes as a PDF artifact."""
report_artifact_part = types.Part.from_data(
data=report_bytes,
mime_type="application/pdf" # Crucial: Specify the correct type!
)
try:
# Save the artifact using the context method
version = context.save_artifact(filename=filename, artifact=report_artifact_part)
print(f"Successfully saved artifact '{filename}' as version {version}.")
# The resulting event will include: actions.artifact_delta = {filename: version}
return version
except ValueError as e:
print(f"Error: {e}. Is ArtifactService configured in the Runner?")
return None
except Exception as e:
# Handle potential storage backend errors (e.g., GCS permissions)
print(f"An unexpected error occurred saving artifact: {e}")
return None
# latest_report_bytes = await process_report(callback_context, "monthly_summary.pdf")
# specific_report_bytes = await process_report(callback_context, "monthly_summary.pdf", version_to_load=0)
Loading Artifacts (context.load_artifact)
- Action: Use this method to retrieve the data of a previously saved artifact.
- How: Call
context.load_artifact(filename="your_filename", version=N)(where N is optional). - Key Inputs:
filename(str): The name of the artifact to load (can include the"user:"prefix).version(Optional[int]):- If omitted or
None(default): Loads the latest available version. - If an integer is provided: Attempts to load that specific version number.
- If omitted or
- Result:
- Returns the
types.Partobject containing the artifact data if the specified filename and version exist. - Returns
Noneif the artifact or the requested version is not found. Always check the return value!
- Returns the
# Example: Within a callback or tool function
import google.genai.types as types
from google.adk.agents.callback_context import CallbackContext # Or ToolContext
async def process_report(context: CallbackContext, filename: str, version_to_load: int = None):
"""Loads a report artifact and processes its data."""
try:
# Load the artifact using the context method
report_artifact_part = context.load_artifact(filename=filename, version=version_to_load)
if report_artifact_part and report_artifact_part.inline_data:
version_str = f"version {version_to_load}" if version_to_load is not None else "latest version"
print(f"Successfully loaded {version_str} of artifact '{filename}'.")
print(f" MIME Type: {report_artifact_part.inline_data.mime_type}")
# Access the binary data
pdf_bytes = report_artifact_part.inline_data.data
print(f" Report size: {len(pdf_bytes)} bytes.")
# ... proceed to use the pdf_bytes ...
return pdf_bytes
else:
version_str = f"version {version_to_load}" if version_to_load is not None else "latest version"
print(f"Artifact '{filename}' ({version_str}) not found.")
return None
except ValueError as e:
print(f"Error: {e}. Is ArtifactService configured in the Runner?")
return None
except Exception as e:
# Handle potential storage backend errors
print(f"An unexpected error occurred loading artifact: {e}")
return None
# latest_report_bytes = await process_report(callback_context, "monthly_summary.pdf")
# specific_report_bytes = await process_report(callback_context, "monthly_summary.pdf", version_to_load=0)
Scoping: Session vs. User ("user:" prefix)
- Concept: Artifacts can be associated either only with the current interaction session or persistently with the user across different sessions.
- How: You control the scope directly via the
filenamestring you pass tosave_artifactandload_artifact:- Session Scope (Default): Use a plain filename like
"temporary_image.png"or"session_report_v1.pdf". This artifact is tied to the specificapp_name,user_id, and the currentsession_id. It's inaccessible from other sessions, even for the same user. Ideal for temporary data or data strictly related to the current conversation. - User Scope: Prefix the filename with
"user:", for example,"user:profile_picture.jpg"or"user:app_preferences.json". This artifact is tied only to theapp_nameanduser_id. It can be accessed and updated from any session belonging to that user within the application. Perfect for user profiles, long-term settings, or data that should persist beyond a single interaction.
- Session Scope (Default): Use a plain filename like
# Conceptual Example within a save operation:
# This report is specific to the current session
context.save_artifact("session_analysis.pdf", analysis_part)
# This avatar is associated with the user and accessible in future sessions
context.save_artifact("user:avatar.png", avatar_part)
Listing Available Artifacts (tool_context.list_artifacts)
- Action: Use this method to discover the names of all artifacts accessible from the current context.
- How: Call
tool_context.list_artifacts(). - Context: This method is only available on
ToolContextobjects, which are passed to Tool execution functions. It's typically used within tools designed to help users manage their files. - Result: Returns a sorted
listofstrfilenames, including both session-scoped and user-scoped (user:...) artifacts visible to the current user/session.
# Example: Within a tool function (requires ToolContext)
from google.adk.tools.tool_context import ToolContext
def list_my_files(tool_context: ToolContext) -> str:
"""A tool to list the user's available artifacts."""
try:
# Get the list of filenames using the context method
artifact_filenames = tool_context.list_artifacts()
if not artifact_filenames:
return "You don't have any saved artifacts currently accessible."
else:
# Format the list nicely for the LLM or user
file_list_str = "\n".join([f"- {fname}" for fname in artifact_filenames])
return f"Here are the artifacts I can currently access for you:\n{file_list_str}"
except ValueError as e:
print(f"Error: {e}. Is ArtifactService configured in the Runner?")
# Provide a user-friendly error message back to the LLM
return "Sorry, I encountered an error trying to list your files. The artifact system might not be set up correctly."
except Exception as e:
print(f"An unexpected error occurred listing artifacts: {e}")
return "Sorry, an unexpected error occurred while trying to list your files."
# from google.adk.tools import FunctionTool
# list_files_tool = FunctionTool(func=list_my_files, description="Lists files the user has saved.")
# agent = LlmAgent(..., tools=[list_files_tool])
These context methods provide the essential building blocks for incorporating file and binary data management into your ADK agents and tools. Remember to choose the appropriate scope ("user:" prefix or not) and always handle potential errors, especially when loading artifacts that might not exist.
3. Working with Artifacts: Core Actions
You typically interact with artifacts during an agent's run using methods on the CallbackContext or ToolContext objects provided to your Callbacks or Tools. However, you can also interact directly with the ArtifactService instance outside of an agent run, for example, during application setup or to pre-load data before a user session begins.
!!! important "Prerequisite: Service Configuration"
Whether using context methods or direct service interaction, you must first instantiate an ArtifactService (like InMemoryArtifactService or GcsArtifactService) and provide it to your Runner during initialization.
Saving Artifacts
- Action: Storing binary data under a specific
filename. - How (Inside Run - via Context): Call
context.save_artifact(filename="...", artifact=your_part) - How (Outside Run - via Service): Call
artifact_service.save_artifact(app_name="...", user_id="...", session_id="...", filename="...", artifact=your_part) - Key Inputs:
filename(str): The name for the artifact (use"user:"prefix for user-scope).artifact(types.Part): Contains the binarydataandmime_type. Accuratemime_typeis crucial.- Direct Service Call: Also requires
app_name,user_id,session_id.
- Result: Returns the integer
versionnumber assigned. When using the context method, this save is also automatically recorded in the current event'sactions.artifact_delta. - Versioning: Each save creates a new version.
Loading Artifacts
- Action: Retrieving previously saved artifact data.
- How (Inside Run - via Context): Call
context.load_artifact(filename="...", version=N)(N is optional, defaults to latest). - How (Outside Run - via Service): Call
artifact_service.load_artifact(app_name="...", user_id="...", session_id="...", filename="...", version=N) - Key Inputs:
filename(str): Name of the artifact to load (can include"user:").version(Optional[int]): Specific version orNonefor latest.- Direct Service Call: Also requires
app_name,user_id,session_id.
- Result: Returns the
types.Partif found, otherwiseNone. Always check the return value.
Scoping: Session vs. User ("user:" prefix)
- Concept: Controls if data is tied only to the current session or persistently to the user across sessions.
- How: Determined by the
filenameused in save/load calls:- Session Scope (Default): Plain filename (e.g.,
"report.pdf"). Tied toapp_name,user_id, andsession_id. - User Scope: Prefix filename with
"user:"(e.g.,"user:avatar.png"). Tied only toapp_nameanduser_id. Accessible from any session for that user.
- Session Scope (Default): Plain filename (e.g.,
Listing Artifact Filenames (tool_context.list_artifacts)
- Action: Discover available artifact names in the current scope.
- Context: Only available on
ToolContext(passed to tools). - Result: Returns a
listofstrfilenames (session and user-scoped). - (Direct Service Interaction: Use
artifact_service.list_artifact_keys(...))
4. Example: Simple Document Q&A
This example demonstrates a simpler pattern for providing document context to an agent. Instead of pre-loading artifacts or using callbacks to load them during the run, we will load the PDF content into a types.Part before calling the agent, and then pass this Part directly along with the user's text question within the new_message argument to runner.run_async.
Scenario: We want an agent to answer questions about a specific PDF. We'll load the PDF into memory first, then for each question, we'll send both the question text and the PDF data together to the agent.
Step 1: Prepare the Artifact Data (In Memory)
This part happens in your main application script before you start interacting with the agent via the runner. You read the file and create the types.Part object.
import os
from google.genai import types
# ... other necessary imports from ADK ...
# --- Constants and Config ---
# <<< --- REPLACE THIS --- >>>
LOCAL_PDF_PATH = "/content/input_document.pdf" # Path to your PDF
# <<< --- END REPLACE --- >>>
# ... other constants like APP_NAME, USER_ID, SESSION_ID, AGENT_NAME, MODEL_NAME ...
# --- Load the Local PDF into a types.Part object ---
local_pdf_part = None
if os.path.exists(LOCAL_PDF_PATH):
try:
with open(LOCAL_PDF_PATH, "rb") as f:
local_pdf_bytes = f.read()
# Create the Part object directly from bytes
local_pdf_part = types.Part.from_bytes(
data=local_pdf_bytes,
mime_type="application/pdf" # Standard PDF MIME type
)
print(f"Loaded '{LOCAL_PDF_PATH}' into memory as a types.Part.")
except Exception as e:
print(f"ERROR loading local PDF '{LOCAL_PDF_PATH}': {e}")
# Handle error, maybe exit if PDF is critical
exit()
else:
print(f"ERROR: Local PDF file '{LOCAL_PDF_PATH}' not found. Cannot run example.")
exit()
# --- Instantiate Services and Create Session ---
# Although we aren't *manually* saving the artifact here,
# the runner might save input artifacts if a service is provided.
artifact_service = InMemoryArtifactService()
session_service = InMemorySessionService()
session = session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
print(f"Session '{SESSION_ID}' created.")
# --- Ensure GenAI client is configured ---
# genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
Step 2: Define the Agent
The agent definition is simple. Its main job is guided by the instruction telling it to expect the document alongside the user's question. No special artifact-handling logic or callbacks are needed within the agent itself for this pattern.
from google.adk.agents import LlmAgent
# --- Define the Agent ---
doc_qa_agent = LlmAgent(
name=AGENT_NAME,
model="gemini-2.0-flash-exp"
description="Answers questions based on a provided PDF document.",
instruction=f"""You are a Q&A assistant.
A PDF document has been provided directly alongside the user's question.
Answer the user's questions based *only* on the content of that provided document.
If the answer cannot be found within the document, state that clearly.
Do not use external knowledge."""
# No specific callbacks or tools needed for this simple pattern
)
Step 3: Configure Runner
The Runner is configured as usual. Importantly, we still provide the artifact_service. While we aren't manually saving the PDF before the run using the service, the Runner itself might use the service to automatically save input artifacts (like the PDF Part we send) if configured to do so (e.g., via RunConfig), allowing potential future retrieval if needed, though that's not the primary focus of this pattern.
from google.adk.runners import Runner
# --- Configure the Runner ---
runner = Runner(
agent=doc_qa_agent,
app_name=APP_NAME,
session_service=session_service,
artifact_service=artifact_service # Provide service for potential internal use by runner
)
Step 4: Run Interaction (Passing Artifact In-Band)
This is the core of the pattern. When calling runner.run_async, construct the new_message (types.Content) to include multiple types.Part objects: one for the document data (local_pdf_part prepared in Step 1) and one for the user's text question. The ADK Runner and the underlying LLM (like Gemini 1.5 Pro) handle this multi-part message.
from google.genai import types
from google.adk.runners import Runner # Assuming Runner is imported
# Assume 'runner', 'local_pdf_part', 'USER_ID', 'SESSION_ID' are defined as in Step 1 & 3
# --- Interaction Logic ---
async def ask_question_about_document(runner: Runner, query_text: str, document_part: types.Part):
"""Sends a question ALONG WITH the document artifact in the same message."""
if not document_part: # Safety check
print("Cannot ask question: Document Part object is missing.")
return
print(f"\n>>> Asking: {query_text}")
print("<<< Agent:")
# --- Construct the multi-part user message ---
user_content = types.Content(
role='user',
parts=[
document_part, # The actual Part containing PDF bytes and mime_type
types.Part.from_text(text=query_text) # The user's text question
]
)
try:
# Call the runner, passing the combined Content object
# The runner sends this directly to the agent/LLM.
async for event in runner.run_async(
user_id=USER_ID,
session_id=SESSION_ID,
new_message=user_content,
# No 'initial_artifacts' needed here as the artifact is *in* the message
):
# Process and print the agent's response event(s)
if event.content and event.content.parts:
for part in event.content.parts:
# Check for text before printing (response might have other types)
if hasattr(part, 'text') and part.text:
print(part.text, end="", flush=True)
if event.is_final_response():
print() # Ensure a newline after the complete response
except Exception as e:
print(f"\nERROR during agent run: {e}")
import traceback
traceback.print_exc() # Print full traceback for debugging
print("-" * 30)
# --- Example of how to run the interactions ---
async def main():
# Check if the local_pdf_part was successfully loaded in Step 1
if local_pdf_part:
# Pass the loaded PDF Part along with each question
await ask_question_about_document(runner, "Summarize the main points of the provided document.", local_pdf_part)
await ask_question_about_document(runner, "What methodology is described in the document?", local_pdf_part)
await ask_question_about_document(runner, "Is the term 'transformer' mentioned?", local_pdf_part)
await ask_question_about_document(runner, "What is the capital of France?", local_pdf_part) # Test OOS question
else:
print("Cannot run main interaction logic because PDF failed to load initially.")
# --- Execute the main function ---
# Ensure GenAI client is configured and PDF exists before running
# In a script: asyncio.run(main())
# In a notebook/async env: await main()
# Example execution (assuming notebook environment):
# try:
# await main()
# except Exception as e:
# print(f"Error running main: {e}")
How it Works
- Preparation: The PDF content is loaded into a
types.Partobject (local_pdf_part) in the main script before the agent interaction loop begins. - Message Construction: For each user query, a single
types.Contentobject is created containing both thelocal_pdf_partand atypes.Partfor the text query. - Run Execution:
runner.run_asyncis called with this multi-partnew_message. - LLM Processing: The Runner sends this message to the
LlmAgent. The agent's flow passes it to the underlying LLM (Gemini 1.5 Pro). The LLM directly receives the text prompt and the PDF data within the same request context. - Response Generation: Guided by its instructions, the LLM processes the text and the document data together to generate an answer based primarily on the provided PDF.
- Simplicity: This pattern eliminates the need for explicit
save_artifact/load_artifactcalls during the run or complex state management via callbacks for passing the primary document context. The artifact data travels "in-band" with the user's query.
This approach is very effective when the document context is directly tied to the immediate user query and you are using an LLM capable of processing the required mime_type directly (like Gemini 1.5 Pro with PDFs, images, audio, etc.).
5. Choosing Your Storage Backend
ADK offers flexibility in how artifacts are stored. The choice depends on your application's needs:
-
InMemoryArtifactService(google.adk.artifacts.InMemoryArtifactService)- Storage: Python dictionary in application memory.
- Persistence: None. Data is lost when the application process stops.
- Use Case: Ideal for local development, automated testing, short-lived demos, or scenarios where artifacts are purely temporary within a single application run.
- Setup: No external dependencies or configuration needed. Just instantiate:
InMemoryArtifactService().
-
GcsArtifactService(google.adk.artifacts.GcsArtifactService)- Storage: Google Cloud Storage (GCS) buckets.
- Persistence: Yes. Data persists across application restarts and deployments.
- Use Case: Production environments, applications requiring long-term storage, scenarios needing data sharing across instances (via the same bucket), persistent user data.
- Setup: Requires a GCS bucket name during instantiation (
GcsArtifactService(bucket_name="your-bucket")) and appropriate Google Cloud credentials/permissions for the application environment to access the bucket.
Recommendation: Start with InMemoryArtifactService for ease of development. Switch to GcsArtifactService when you need persistence for deployment or longer-term data handling. The core agent logic using context.save/load_artifact or initial_artifacts remains the same regardless of the chosen backend.