ADK とマルチモーダル ツールの操作 : パート 1(モデル コールバックを使用したカスタムツール)

1. 📖 はじめに

この Codelab では、Agent Development Kit(ADK)でマルチモーダル ツール インタラクションを設計する方法について説明します。これは、エージェントがアップロードされたファイルをツールの入力として参照し、ツールレスポンスによって生成されたファイルの内容も理解する必要がある特定のフローです。そのため、次のスクリーンショットのようなインタラクションが可能になります。このチュートリアルでは、ユーザーが商品ショーケース用のより良い写真を編集できるようにするエージェントを開発します。

この Codelab では、次の手順で進めます。

  1. Google Cloud プロジェクトを準備する
  2. コーディング環境の作業ディレクトリを設定する
  3. ADK を使用してエージェントを初期化する
  4. Gemini 2.5 Flash Image を使用して写真を編集できるツールを設計する
  5. ユーザーがアップロードした画像を処理し、アーティファクトとして保存して、エージェントのコンテキストに追加するコールバック関数を設計する
  6. ツールレスポンスによって生成された画像を処理し、アーティファクトとして保存して、エージェントのコンテキストに追加するコールバック関数を設計する

アーキテクチャの概要

この Codelab の全体的なインタラクションを次の図に示します。

e07eaa83c1615ae7.jpeg

前提条件

  • Python の操作に慣れていること
  • (省略可)Agent Development Kit(ADK)に関する基礎的な Codelab
  1. goo.gle/adk-foundation
  2. goo.gle/adk-using-tools

学習内容

  • コールバック コンテキストを利用してアーティファクト サービスにアクセスする方法
  • 適切なマルチモーダル データ伝播でツールを設計する方法
  • before_model_callback を使用してアーティファクト コンテキストを追加するようにエージェント LLM リクエストを変更する方法
  • Gemini 2.5 Flash Image を使用して画像を編集する方法

必要なもの

  • Chrome ウェブブラウザ
  • Gmail アカウント
  • 請求先アカウントが有効になっている Cloud プロジェクト

この Codelab は、初心者を含むあらゆるレベルのデベロッパーを対象としており、サンプル アプリケーションでは Python を使用しています。ただし、ここで説明するコンセプトを理解するために Python の知識は必要ありません。

2. 🚀 ワークショップ開発の設定を準備する

ステップ 1: Cloud Console で有効なプロジェクトを選択する

[Google Cloud コンソール] の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します(コンソールの左上部分を参照)。

6069be756af6452b.png

クリックすると、次の例のようにプロジェクトの一覧が表示されます。

dd8fcf0428ab868f.png

赤いボックスで示されている値はプロジェクト ID であり、この値はチュートリアル全体で使用されます。

Cloud プロジェクトに対して課金が有効になっていることを確認します。これを確認するには、左上のバーにあるハンバーガー アイコン ☰ をクリックしてナビゲーション メニューを表示し、[請求] メニューを見つけます。

db07810b26fc61d6.png

[Billing / Overview] タイトル( Cloud コンソールの左上部分 )の下に「Google Cloud Platform トライアル請求先アカウント」が表示されている場合、このチュートリアルでプロジェクトを使用する準備ができています。表示されていない場合は、このチュートリアルの最初に戻って、トライアル請求先アカウントを利用してください。

45539d4ac57dd995.png

ステップ 2: Cloud Shell に慣れる

チュートリアルのほとんどの部分で Cloud Shell を使用します。Google Cloud コンソールの上部にある「Cloud Shell をアクティブにする」アイコン をクリックします。承認を求めるメッセージが表示されたら、[承認] をクリックします。

26f20e837ff06119.png79b06cc89a99f840.png

Cloud Shell に接続したら、シェル(またはターミナル)がアカウントで認証されているかどうかを確認する必要があります。

gcloud auth list

次の出力例のように個人の Gmail が表示されていれば、問題ありません。

Credentialed Accounts

ACTIVE: *
ACCOUNT: alvinprayuda@gmail.com

To set the active account, run:
    $ gcloud config set account `ACCOUNT`

表示されていない場合は、ブラウザを更新して、プロンプトが表示されたら [承認] をクリックしてください(接続の問題で中断される可能性があります)。

次に、シェルが所有している正しいプロジェクト ID に構成されているかどうかを確認する必要があります。ターミナルの $アイコンの前に ( ) 内に値 が表示されている場合(次のスクリーンショットでは、値は "adk-multimodal-tool" )、この値はアクティブなシェル セッションに構成されたプロジェクトを示します。

10a99ff80839b635.png

表示された がすでに正しい 場合は、次のコマンドスキップ できます。正しくない場合や表示されていない場合は、次のコマンドを実行します。

gcloud config set project <YOUR_PROJECT_ID>

次に、この Codelab のテンプレート作業ディレクトリを GitHub からクローンします。次のコマンドを実行します。これにより、adk-multimodal-tool ディレクトリに作業ディレクトリが作成されます。

git clone https://github.com/alphinside/adk-mcp-multimodal.git adk-multimodal-tool

ステップ 3: Cloud Shell エディタに慣れ、アプリケーションの作業ディレクトリを設定する

これで、コードエディタを設定してコーディングを行うことができます。これには Cloud Shell エディタを使用します。

[エディタを開く] ボタンをクリックすると、Cloud Shell エディタが開きます。168eacea651b086c.png

次に、Cloud Shell エディタの上部にある [ファイル -> フォルダを開く] をクリックし、ユーザー名 ディレクトリを見つけて、adk-multimodal-tool ディレクトリを見つけて [OK] ボタンをクリックします。これにより、選択したディレクトリがメインの作業ディレクトリになります。この例では、ユーザー名は alvinprayuda であるため、ディレクトリ パスは次のようになります。

8eb3f593141dbcbf.pnga4860f6be228d864.png

これで、Cloud Shell エディタの作業ディレクトリは次のようになります(adk-multimodal-tool 内)。

aa2edaf29303167f.png

次に、エディタのターミナルを開きます。メニューバーで [ターミナル -> 新しいターミナル] をクリックするか、[Ctrl + Shift + C] を使用すると、ブラウザの下部にターミナル ウィンドウが開きます。

74d314f6ff34965b.png

現在アクティブなターミナルは、adk-multimodal-tool 作業ディレクトリ内にある必要があります。この Codelab では Python 3.12 を使用し、uv Python プロジェクト マネージャーを使用して、Python のバージョンと仮想環境の作成と管理の必要性を簡素化します。この uv パッケージは Cloud Shell にプリインストールされています。

次のコマンドを実行して、.venv ディレクトリの仮想環境に必要な依存関係をインストールします。

uv sync --frozen

pyproject.toml を確認して、このチュートリアルの宣言された依存関係(google-adk, and python-dotenv)を確認します。

次に、次のコマンドを使用して必要な API を有効にする必要があります。これには時間がかかることがあります。

gcloud services enable aiplatform.googleapis.com

コマンドが正常に実行されると、次のようなメッセージが表示されます。

Operation "operations/..." finished successfully.

3. 🚀 ADK エージェントを初期化する

このステップでは、ADK CLI を使用してエージェントを初期化します。次のコマンドを実行します。

uv run adk create product_photo_editor \
   --model gemini-2.5-flash \
   --project your-project-id \
   --region us-central1

このコマンドを使用すると、次のエージェントに必要な構造をすばやく指定できます。

product_photo_editor/
├── __init__.py
├── .env
├── agent.py

次に、商品説明写真エディタ エージェントを準備します。まず、リポジトリにすでに含まれている prompt.py を、先ほど作成したエージェント ディレクトリにコピーします。

cp prompt.py product_photo_editor/prompt.py

次に、product_photo_editor/agent.py を開き、内容を次のコードに変更します。

from google.adk.agents.llm_agent import Agent
from product_photo_editor.prompt import AGENT_INSTRUCTION

root_agent = Agent(
    model="gemini-2.5-flash",
    name="product_photo_editor",
    description="""A friendly product photo editor assistant that helps small business 
owners edit and enhance their product photos. Perfect for improving photos of handmade 
goods, food products, crafts, and small retail items""",
    instruction=AGENT_INSTRUCTION,
)

これで、写真の提案を求めることができる基本的な写真エディタ エージェントができました。次のコマンドを使用して、エージェントとやり取りできます。

uv run adk web --port 8080

次の例のような出力が生成されます。これは、ウェブ インターフェースにアクセスできることを意味します。

INFO:     Started server process [xxxx]
INFO:     Waiting for application startup.

+-----------------------------------------------------------------------------+
| ADK Web Server started                                                      |
|                                                                             |
| For local testing, access at http://127.0.0.1:8080.                         |
+-----------------------------------------------------------------------------+

INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)

確認するには、URL を Ctrl + クリック するか、Cloud Shell エディタの上部にある [ウェブでプレビュー] ボタンをクリックして、[ポート 8080 でプレビュー] を選択します。

edc73e971b9fc60c.png

次のウェブページが表示されます。左上のプルダウン ボタンで利用可能なエージェントを選択し(この場合は product_photo_editor )、ボットとやり取りできます。チャット インターフェースに次の画像をアップロードして、次の質問をしてみてください。

what is your suggestion for this photo?

a5ff3bc6c19a29ec.jpeg

次のようなインタラクションが表示されます。

c1da4f7cf1466be6.png

提案を求めることはできますが、現時点では編集はできません。次のステップでは、エージェントに編集ツールを装備します。

4. 🚀 LLM リクエスト コンテキストの変更 - ユーザーがアップロードした画像

エージェントが、編集するアップロード済み画像を柔軟に選択できるようにします。ただし、通常、LLM ツールは strint などの単純なデータ型のパラメータを受け入れるように設計されています。これは、通常 bytes データ型として認識されるマルチモーダル データとは非常に異なるデータ型です。そのため、これらのデータを処理するには、 アーティファクト のコンセプトを使用する必要があります。したがって、ツール パラメータに完全なバイトデータを提供するのではなく、アーティファクト識別子名を受け入れるようにツールを設計します。

この戦略には 2 つのステップがあります。

  1. アップロードされた各ファイルがアーティファクト識別子に関連付けられ、LLM のコンテキストとして追加されるように LLM リクエストを変更する
  2. アーティファクト識別子を入力パラメータとして受け入れるようにツールを設計する

最初のステップでは、LLM リクエストを変更するために ADK コールバック機能を使用します。具体的には、エージェントがコンテキストを LLM に送信する直前にタップする before_model_callback を追加します。次の図で説明を確認できます。 722b5fac82954419.png

これを行うには、まず次のコマンドを使用して新しいファイル product_photo_editor/model_callbacks.py を作成します。

touch product_photo_editor/model_callbacks.py

次に、次のコードをファイルにコピーします。

# product_photo_editor/model_callbacks.py

from google.adk.agents.callback_context import CallbackContext
from google.adk.models import LlmResponse, LlmRequest
from google.genai.types import Part
import hashlib
from typing import List


async def before_model_modifier(
    callback_context: CallbackContext, llm_request: LlmRequest
) -> LlmResponse | None:
    """Modify LLM request to include artifact references for images."""
    for content in llm_request.contents:
        if not content.parts:
            continue

        modified_parts = []
        for idx, part in enumerate(content.parts):
            # Handle user-uploaded inline images
            if part.inline_data:
                processed_parts = await _process_inline_data_part(
                    part, callback_context
                )
            # Default: keep part as-is
            else:
                processed_parts = [part]

            modified_parts.extend(processed_parts)

        content.parts = modified_parts


async def _process_inline_data_part(
    part: Part, callback_context: CallbackContext
) -> List[Part]:
    """Process inline data parts (user-uploaded images).

    Returns:
        List of parts including artifact marker and the image.
    """
    artifact_id = _generate_artifact_id(part)

    # Save artifact if it doesn't exist
    if artifact_id not in await callback_context.list_artifacts():
        await callback_context.save_artifact(filename=artifact_id, artifact=part)

    return [
        Part(
            text=f"[User Uploaded Artifact] Below is the content of artifact ID : {artifact_id}"
        ),
        part,
    ]


def _generate_artifact_id(part: Part) -> str:
    """Generate a unique artifact ID for user uploaded image.

    Returns:
        Hash-based artifact ID with proper file extension.
    """
    filename = part.inline_data.display_name or "uploaded_image"
    image_data = part.inline_data.data

    # Combine filename and image data for hash
    hash_input = filename.encode("utf-8") + image_data
    content_hash = hashlib.sha256(hash_input).hexdigest()[:16]

    # Extract file extension from mime type
    mime_type = part.inline_data.mime_type
    extension = mime_type.split("/")[-1]

    return f"usr_upl_img_{content_hash}.{extension}"

before_model_modifier 関数は次の処理を行います。

  1. llm_request.contents 変数にアクセスして、コンテンツを反復処理する
  2. partinline_data (アップロードされたファイル / 画像)が含まれているかどうかを確認し、含まれている場合はインライン データを処理する
  3. inline_data の識別子を作成する。この例では、ファイル名とデータの組み合わせを使用してコンテンツ ハッシュ識別子を作成します。
  4. アーティファクト ID がすでに存在するかどうかを確認し、存在しない場合はアーティファクト ID を使用してアーティファクトを保存する
  5. 次のインライン データのアーティファクト識別子に関するコンテキストを提供するテキスト プロンプトを含めるようにパートを変更する

次に、product_photo_editor/agent.py を変更して、エージェントにコールバックを装備します。

from google.adk.agents.llm_agent import Agent
from product_photo_editor.model_callbacks import before_model_modifier
from product_photo_editor.prompt import AGENT_INSTRUCTION

root_agent = Agent(
    model="gemini-2.5-flash",
    name="product_photo_editor",
    description="""A friendly product photo editor assistant that helps small business 
owners edit and enhance their product photos for online stores, social media, and 
marketing. Perfect for improving photos of handmade goods, food products, crafts, and small retail items""",
    instruction=AGENT_INSTRUCTION,
    before_model_callback=before_model_modifier,
)

これで、エージェントと再度やり取りできます。

uv run adk web --port 8080

ファイルを再度アップロードしてチャットすると、LLM リクエスト コンテキストが正常に変更されたかどうかを確認できます。

51404c0704f86ffa.pngf82034bcdda068d9.png

これは、マルチモーダル データのシーケンスと識別について LLM に伝える方法の 1 つです。次に、この情報を使用するツールを作成しましょう。

5. 🚀 マルチモーダル ツール インタラクション

次に、アーティファクト ID を入力パラメータとして指定するツールを準備します。次のコマンドを実行して、新しいファイル product_photo_editor/custom_tools.py を作成します。

touch product_photo_editor/custom_tools.py

次に、次のコードを product_photo_editor/custom_tools.py にコピーします。

# product_photo_editor/custom_tools.py

from google import genai
from dotenv import load_dotenv
import os
from google.adk.tools import ToolContext
import logging


load_dotenv()

client = genai.Client(
    vertexai=True,
    project=os.getenv("GOOGLE_CLOUD_PROJECT"),
    location=os.getenv("GOOGLE_CLOUD_LOCATION"),
)


async def edit_product_asset(
    tool_context: ToolContext,
    change_description: str,
    image_artifact_ids: list = [],
) -> dict[str, str]:
    """Modify an existing product photo or combine multiple product photos.

    This tool lets you make changes to product photos. You can:
    - Edit a single photo (change background, lighting, colors, etc.)
    - Combine multiple products into one photo (arrange them side by side, create bundles, etc.)

    **IMPORTANT**:
    - Make ONE type of change per tool call (background OR lighting OR props OR arrangement)
    - For complex edits, chain multiple tool calls together
    - BE AS DETAILED AS POSSIBLE in the change_description for best results!

    Args:
        change_description: What do you want to do? BE VERY DETAILED AND SPECIFIC!

                          **The more details you provide, the better the result.**
                          Focus on ONE type of change, but describe it thoroughly.

                          For BACKGROUND changes:
                          - "change background to soft pure white with subtle gradient from top to bottom, clean and minimal aesthetic"
                          - "replace background with rustic dark wood table surface with natural grain texture visible, warm brown tones"

                          For ADDING PROPS:
                          - "add fresh pink roses and eucalyptus leaves arranged naturally around the product on the left and right sides,
                            with some petals scattered in front"
                          - "add fresh basil leaves and cherry tomatoes scattered around the product naturally"

                          For LIGHTING changes:
                          - "add soft natural window light coming from the left side at 45 degree angle, creating gentle shadows on the
                            right side, warm morning atmosphere"
                          - "increase brightness with soft diffused studio lighting from above, eliminating harsh shadows"

                          For ARRANGEMENT/POSITIONING:
                          - "reposition product to be perfectly centered in frame with equal space on all sides"
                          - "arrange these three products in a horizontal line, evenly spaced with 2 inches between each"

                          Note: When combining multiple products, you can include background/lighting in the initial arrangement since it's
                                one cohesive setup
        image_artifact_ids: List of image IDs to edit or combine.
                          - For single image: provide a list with one item (e.g., ["product.png"])
                          - For multiple images: provide a list with multiple items (e.g., ["product1.png", "product2.png"])
                          Use multiple images to combine products into one photo.

    Returns:
        dict with keys:
            - 'tool_response_artifact_id': Artifact ID for the edited image
            - 'tool_input_artifact_ids': Comma-separated list of input artifact IDs
            - 'edit_prompt': The full edit prompt used
            - 'status': Success or error status
            - 'message': Additional information or error details
    """
    try:
        # Validate input
        if not image_artifact_ids:
            return {
                "status": "error",
                "tool_response_artifact_id": "",
                "tool_input_artifact_ids": "",
                "edit_prompt": change_description,
                "message": "No images provided. Please provide image_artifact_ids as a list.",
            }

        # Load all images
        image_artifacts = []
        for img_id in image_artifact_ids:
            artifact = await tool_context.load_artifact(filename=img_id)
            if artifact is None:
                logging.error(f"Artifact {img_id} not found")
                return {
                    "status": "error",
                    "tool_response_artifact_id": "",
                    "tool_input_artifact_ids": "",
                    "edit_prompt": change_description,
                    "message": f"Artifact {img_id} not found",
                }

            image_artifacts.append(artifact)

        # Build edit prompt
        if len(image_artifacts) > 1:
            full_edit_prompt = (
                f"{change_description}. "
                f"Combine these {len(image_artifacts)} product images together. "
                "IMPORTANT: Preserve each product's original appearance, shape, color, and design as faithfully as possible. "
                "Only modify for aesthetic enhancements (lighting, background, composition) or viewing angle adjustments. "
                "Do not alter the core product features, branding, or characteristics."
            )
        else:
            full_edit_prompt = (
                f"{change_description}. "
                "IMPORTANT: Preserve the product's original appearance, shape, color, and design as faithfully as possible. "
                "Only modify for aesthetic enhancements (lighting, background, composition) or viewing angle adjustments. "
                "Do not alter the core product features, branding, or characteristics."
            )

        # Build contents list: all images followed by the prompt
        contents = image_artifacts + [full_edit_prompt]

        response = await client.aio.models.generate_content(
            model="gemini-2.5-flash-image",
            contents=contents,
            config=genai.types.GenerateContentConfig(
                response_modalities=["Image"]
            ),
        )

        artifact_id = ""
        logging.info("Gemini Flash Image: response.candidates: ", response.candidates)
        for part in response.candidates[0].content.parts:
            if part.inline_data is not None:
                artifact_id = f"edited_img_{tool_context.function_call_id}.png"
                await tool_context.save_artifact(filename=artifact_id, artifact=part)

        input_ids_str = ", ".join(image_artifact_ids)
        return {
            "status": "success",
            "tool_response_artifact_id": artifact_id,
            "tool_input_artifact_ids": input_ids_str,
            "edit_prompt": full_edit_prompt,
            "message": f"Image edited successfully using {len(image_artifacts)} input image(s)",
        }
    except Exception as e:
        logging.error(e)
        input_ids_str = ", ".join(image_artifact_ids) if image_artifact_ids else ""
        return {
            "status": "error",
            "tool_response_artifact_id": "",
            "tool_input_artifact_ids": input_ids_str,
            "edit_prompt": change_description,
            "message": f"Error editing image: {str(e)}",
        }

ツールコードは次の処理を行います。

  1. ツールの呼び出しに関するベスト プラクティスについて、ツールのドキュメントで詳しく説明する
  2. image_artifact_ids リストが空でないことを検証する
  3. 指定されたアーティファクト ID を使用して、tool_context からすべての画像アーティファクトを読み込む
  4. 編集プロンプトを作成する: プロフェッショナルな組み合わせ(複数画像)または編集(単一画像)を行う手順を追加する
  5. 画像のみの出力で Gemini 2.5 Flash Image モデルを呼び出し、生成された画像を抽出する
  6. 編集した画像を新しいアーティファクトとして保存する
  7. ステータス、出力アーティファクト ID、入力 ID、完全なプロンプト、メッセージを含む構造化されたレスポンスを返す

最後に、エージェントにツールを装備できます。product_photo_editor/agent.py の内容を次のコードに変更します。

from google.adk.agents.llm_agent import Agent
from product_photo_editor.custom_tools import edit_product_asset
from product_photo_editor.model_callbacks import before_model_modifier
from product_photo_editor.prompt import AGENT_INSTRUCTION

root_agent = Agent(
    model="gemini-2.5-flash",
    name="product_photo_editor",
    description="""A friendly product photo editor assistant that helps small business 
owners edit and enhance their product photos for online stores, social media, and 
marketing. Perfect for improving photos of handmade goods, food products, crafts, and small retail items""",
    instruction=AGENT_INSTRUCTION,
    tools=[
        edit_product_asset,
    ],
    before_model_callback=before_model_modifier,
)

これで、エージェントは写真の編集を支援する準備が 80% 整いました。エージェントとやり取りしてみましょう。

uv run adk web --port 8080

次の画像を別のプロンプトで試してみましょう。

put these muffins in a white plate aesthetically

a5ff3bc6c19a29ec.jpeg

次のようなインタラクションが表示され、エージェントが写真の編集を行うようになります。

92fb33f9c834330a.png

関数呼び出しの詳細を確認すると、ユーザーがアップロードした画像のアーティファクト識別子が提供されます。

f5f440ccb36a4648.png

これで、エージェントは写真を少しずつ継続的に改善するのに役立ちます。ツールレスポンスでアーティファクト識別子を提供するため、編集した写真を次の編集手順で使用することもできます。

ただし、現在の状態では、上の例からわかるように、エージェントは編集された画像の結果を実際に確認して理解することはできません 。これは、エージェントに提供するツールレスポンスがバイトコンテンツ自体ではなくアーティファクト ID のみであるためです。残念ながら、バイトコンテンツをツールレスポンスに直接配置することはできません。エラーが発生します。そのため、コールバック内に別のロジック ブランチを作成して、ツールレスポンスの結果からバイトコンテンツをインライン データとして追加する必要があります。

6. 🚀 LLM リクエスト コンテキストの変更 - 関数レスポンス画像

エージェントが結果を完全に理解できるように、ツールレスポンスの後に編集した画像のバイトデータを追加するように before_model_modifier コールバックを変更しましょう。

product_photo_editor/model_callbacks.py を開き、内容を次のように変更します。

# product_photo_editor/model_callbacks.py

from google.adk.agents.callback_context import CallbackContext
from google.adk.models import LlmResponse, LlmRequest
from google.genai.types import Part
import hashlib
from typing import List


async def before_model_modifier(
    callback_context: CallbackContext, llm_request: LlmRequest
) -> LlmResponse | None:
    """Modify LLM request to include artifact references for images."""
    for content in llm_request.contents:
        if not content.parts:
            continue

        modified_parts = []
        for idx, part in enumerate(content.parts):
            # Handle user-uploaded inline images
            if part.inline_data:
                processed_parts = await _process_inline_data_part(
                    part, callback_context
                )
            # Handle function response parts for image generation/editing
            elif part.function_response:
                if part.function_response.name in [
                    "edit_product_asset",
                ]:
                    processed_parts = await _process_function_response_part(
                        part, callback_context
                    )
                else:
                    processed_parts = [part]
            # Default: keep part as-is
            else:
                processed_parts = [part]

            modified_parts.extend(processed_parts)

        content.parts = modified_parts


async def _process_inline_data_part(
    part: Part, callback_context: CallbackContext
) -> List[Part]:
    """Process inline data parts (user-uploaded images).

    Returns:
        List of parts including artifact marker and the image.
    """
    artifact_id = _generate_artifact_id(part)

    # Save artifact if it doesn't exist
    if artifact_id not in await callback_context.list_artifacts():
        await callback_context.save_artifact(filename=artifact_id, artifact=part)

    return [
        Part(
            text=f"[User Uploaded Artifact] Below is the content of artifact ID : {artifact_id}"
        ),
        part,
    ]


def _generate_artifact_id(part: Part) -> str:
    """Generate a unique artifact ID for user uploaded image.

    Returns:
        Hash-based artifact ID with proper file extension.
    """
    filename = part.inline_data.display_name or "uploaded_image"
    image_data = part.inline_data.data

    # Combine filename and image data for hash
    hash_input = filename.encode("utf-8") + image_data
    content_hash = hashlib.sha256(hash_input).hexdigest()[:16]

    # Extract file extension from mime type
    mime_type = part.inline_data.mime_type
    extension = mime_type.split("/")[-1]

    return f"usr_upl_img_{content_hash}.{extension}"


async def _process_function_response_part(
    part: Part, callback_context: CallbackContext
) -> List[Part]:
    """Process function response parts and append artifacts.

    Returns:
        List of parts including the original function response and artifact.
    """
    artifact_id = part.function_response.response.get("tool_response_artifact_id")

    if not artifact_id:
        return [part]

    artifact = await callback_context.load_artifact(filename=artifact_id)

    return [
        part,  # Original function response
        Part(
            text=f"[Tool Response Artifact] Below is the content of artifact ID : {artifact_id}"
        ),
        artifact,
    ]

上記の変更されたコードでは、次の機能を追加しています。

  1. Part が関数レスポンスであるかどうか、コンテンツの変更を許可するためにツール名リストに含まれているかどうかを確認する
  2. ツールレスポンスのアーティファクト識別子が存在する場合は、アーティファクト コンテンツを読み込む
  3. ツールレスポンスから編集した画像のデータを含めるようにコンテンツを変更する

これで、エージェントがツールレスポンスから編集した画像を完全に理解しているかどうかを確認できます。

5d4e880da6f2b9cb.png

これで、独自のカスタムツールを使用してマルチモーダル インタラクション フローをサポートするエージェントができました。

次に、より複雑なフローでエージェントとやり取りしてみましょう。たとえば、新しいアイテム(アイスラテ)を追加して写真を改善します。

b561a4ae5cb40355.jpege03674e0e1599c33.png

7. ⭐ まとめ

この Codelab で行ったことを振り返ってみましょう。主な学習内容は次のとおりです。

  1. マルチモーダル データの処理: ツールの引数やレスポンスを介して生のバイトデータを直接渡すのではなく、ADK のアーティファクト サービスを使用して、LLM コンテキスト フロー内でマルチモーダル データ(画像など)を管理する戦略を学習しました。
  2. before_model_callback の利用: before_model_callback を使用して、LLM に送信される前に LlmRequest をインターセプトして変更しました。次のフローをタップしました。
  • ユーザーのアップロード: ユーザーがアップロードしたインライン データを検出し、一意に識別されたアーティファクト(usr_upl_img_... など)として保存し、アーティファクト ID を参照するテキストをプロンプト コンテキストに挿入して、LLM がツールで使用する正しいファイルを選択できるようにするロジックを実装しました。
  • ツールレスポンス: アーティファクト(編集した画像など)を生成する特定のツール関数レスポンスを検出し、新しく保存されたアーティファクト(edited_img_... など)を読み込み、アーティファクト ID 参照と画像コンテンツの両方をコンテキスト ストリームに直接挿入するロジックを実装しました。
  1. カスタムツールの設計: image_artifact_ids リスト(文字列識別子)を受け取り、ToolContext を使用してアーティファクト サービスから実際の画像データを取得するカスタム Python ツール(edit_product_asset)を作成しました。
  2. 画像生成モデルの統合: カスタムツール内に Gemini 2.5 Flash Image モデルを統合し、詳細なテキスト記述に基づいて画像編集を実行しました。
  3. 継続的なマルチモーダル インタラクション: エージェントが独自のツール呼び出しの結果(編集した画像)を理解し、その出力を後続の手順の入力として使用することで、継続的な編集セッションを維持できるようにしました。

8. ➡️ 次の課題

ADK マルチモーダル ツール インタラクションのパート 1 を完了しました。このチュートリアルでは、カスタムツールのインタラクションに焦点を当てています。これで、マルチモーダル MCP ツールセットとやり取りする方法の次のステップに進む準備ができました。次のラボに進みます

9. 🧹 クリーンアップ

この Codelab で使用したリソースについて、Google Cloud アカウントに課金されないようにするには、次の手順を行います。

  1. Google Cloud コンソールで、[リソースの管理] ページに移動します。
  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。