Files
Kristopher Overholt 71c66e02d8 Updates
2025-04-09 03:16:53 -07:00

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 as bytes.
  • 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): The Part object containing your binary data and its mime_type. Providing an accurate mime_type is crucial.
  • Result:

    • Returns the integer version number 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_delta dictionary. This delta maps the filename to the new version, signaling that an artifact was updated.
  • Versioning Behavior: Each call to save_artifact for the same filename (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.
  • Result:
    • Returns the types.Part object containing the artifact data if the specified filename and version exist.
    • Returns None if the artifact or the requested version is not found. Always check the return value!
# 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 filename string you pass to save_artifact and load_artifact:
    • Session Scope (Default): Use a plain filename like "temporary_image.png" or "session_report_v1.pdf". This artifact is tied to the specific app_name, user_id, and the current session_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 the app_name and user_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.
# 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 ToolContext objects, which are passed to Tool execution functions. It's typically used within tools designed to help users manage their files.
  • Result: Returns a sorted list of str filenames, 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 binary data and mime_type. Accurate mime_type is crucial.
    • Direct Service Call: Also requires app_name, user_id, session_id.
  • Result: Returns the integer version number assigned. When using the context method, this save is also automatically recorded in the current event's actions.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 or None for latest.
    • Direct Service Call: Also requires app_name, user_id, session_id.
  • Result: Returns the types.Part if found, otherwise None. 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 filename used in save/load calls:
    • Session Scope (Default): Plain filename (e.g., "report.pdf"). Tied to app_name, user_id, and session_id.
    • User Scope: Prefix filename with "user:" (e.g., "user:avatar.png"). Tied only to app_name and user_id. Accessible from any session for that user.

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 list of str filenames (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

  1. Preparation: The PDF content is loaded into a types.Part object (local_pdf_part) in the main script before the agent interaction loop begins.
  2. Message Construction: For each user query, a single types.Content object is created containing both the local_pdf_part and a types.Part for the text query.
  3. Run Execution: runner.run_async is called with this multi-part new_message.
  4. 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.
  5. 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.
  6. Simplicity: This pattern eliminates the need for explicit save_artifact/load_artifact calls 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.