ADK with Multimodal Tool Interaction : Part 1 ( Custom Tool with Model Callbacks )

1. 📖 מבוא

בשיעור הזה תלמדו איך לתכנן אינטראקציה עם כלי רב-מודאלי בערכה לפיתוח סוכנים (ADK). זהו תהליך ספציפי שבו רוצים שהסוכן יתייחס לקובץ שהועלה כקלט לכלי, וגם יבין את תוכן הקובץ שנוצר על ידי התגובה של הכלי. לכן אפשר לבצע אינטראקציה כמו שרואים בצילום המסך שלמטה. במדריך הזה נפתח סוכן שיכול לעזור למשתמש לערוך תמונה טובה יותר של המוצר שלו

במהלך ה-codelab, תשתמשו בגישה שלב אחר שלב באופן הבא:

  1. הכנת פרויקט Google Cloud
  2. הגדרת ספרייה בעבודה לסביבת קידוד
  3. אתחול הסוכן באמצעות ADK
  4. תכנון כלי לעריכת תמונות שמבוסס על Gemini 2.5 Flash Image
  5. עיצוב פונקציית קריאה חוזרת לטיפול בהעלאת תמונות על ידי משתמשים, שמירת התמונה כארטיפקט והוספתה כהקשר לסוכן
  6. עיצוב פונקציית קריאה חוזרת לטיפול בתמונה שנוצרה על ידי תגובה של כלי, שמירתה כארטיפקט והוספתה כהקשר לסוכן

סקירה כללית של הארכיטקטורה

האינטראקציה הכוללת ב-codelab הזה מוצגת בתרשים הבא

e07eaa83c1615ae7.jpeg

דרישות מוקדמות

  • ניסיון בעבודה עם Python
  • (אופציונלי) מעבדות קוד בסיסיות בנושא ערכה לפיתוח סוכנים (ADK)
  1. goo.gle/adk-foundation
  2. goo.gle/adk-using-tools

מה תלמדו

  • איך משתמשים בהקשר של בקשה להחזרת שיחה כדי לגשת לשירות ארטיפקטים
  • איך לעצב כלי עם הפצה נכונה של נתונים מרובי-אופנים
  • איך משנים את הבקשה ל-LLM של הסוכן כדי להוסיף הקשר של ארטיפקט באמצעות before_model_callback
  • איך עורכים תמונה באמצעות Gemini 2.5 Flash Image

מה תצטרכו

  • דפדפן האינטרנט Chrome
  • חשבון Gmail
  • פרויקט ב-Cloud שמופעל בו חשבון לחיוב

ב-codelab הזה, שמיועד למפתחים בכל הרמות (כולל מתחילים), נעשה שימוש ב-Python באפליקציה לדוגמה. עם זאת, לא נדרש ידע ב-Python כדי להבין את המושגים שמוצגים.

2. 🚀 הכנה להגדרת סדנת פיתוח

שלב 1: בוחרים פרויקט פעיל ב-Cloud Console

במסוף Google Cloud, בדף לבחירת הפרויקט, בוחרים או יוצרים פרויקט ב-Google Cloud (ראו את הקטע הימני העליון במסוף).

6069be756af6452b.png

לוחצים עליו ורואים רשימה של כל הפרויקטים, כמו בדוגמה הזו:

dd8fcf0428ab868f.png

הערך שמסומן בתיבה האדומה הוא מזהה הפרויקט, והוא ישמש לאורך כל המדריך.

הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud. כדי לבדוק את זה, לוחצים על סמל ההמבורגר ☰ בסרגל הימני העליון כדי להציג את תפריט הניווט, ומחפשים את תפריט החיוב.

db07810b26fc61d6.png

אם הכותרת Billing / Overview ( בפינה הימנית העליונה של מסוף Cloud ) מופיעה עם הכיתוב Google Cloud Platform Trial Billing Account, הפרויקט שלכם מוכן לשימוש במדריך הזה. אם לא, חוזרים לתחילת המדריך הזה ומממשים את תקופת הניסיון של חשבון החיוב

45539d4ac57dd995.png

שלב 2: הכרת Cloud Shell

ברוב חלקי המדריכים תשתמשו ב-Cloud Shell. לוחצים על 'הפעלת Cloud Shell' בחלק העליון של מסוף Google Cloud. אם מוצגת בקשה לאישור, לוחצים על אישור.

26f20e837ff06119.png

79b06cc89a99f840.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`

אם לא, כדאי לרענן את הדפדפן ולוודא שלוחצים על אישור כשמופיעה הבקשה ( יכול להיות שהתהליך יופסק בגלל בעיה בחיבור).

בנוסף, צריך לבדוק אם ה-Shell כבר מוגדר למזהה הפרויקט הנכון שיש לכם. אם מופיע ערך בתוך ( ) לפני הסמל $ במסוף ( בצילום המסך שלמטה, הערך הוא "adk-multimodal-tool"), הערך הזה מציין את הפרויקט שהוגדר עבור סשן ה-Shell הפעיל.

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 Editor והגדרת ספריית העבודה של האפליקציה

עכשיו אפשר להגדיר את עורך הקוד כדי לבצע פעולות שקשורות לקוד. נשתמש ב-Cloud Shell Editor לצורך הזה

לוחצים על הלחצן Open Editor כדי לפתוח את Cloud Shell Editor 168eacea651b086c.png.

אחרי זה, עוברים לחלק העליון של Cloud Shell Editor ולוחצים על File->Open Folder (קובץ > פתיחת תיקייה), מאתרים את ספריית שם המשתמש ואת הספרייה adk-multimodal-tool, ואז לוחצים על הלחצן OK (אישור). הפעולה הזו תגדיר את הספרייה שנבחרה כספריית העבודה הראשית. בדוגמה הזו, שם המשתמש הוא alvinprayuda, ולכן נתיב הספרייה מוצג למטה

8eb3f593141dbcbf.png

a4860f6be228d864.png

עכשיו, ספריית העבודה שלכם ב-Cloud Shell Editor אמורה להיראות כך ( בתוך adk-multimodal-tool)

aa2edaf29303167f.png

עכשיו פותחים את הטרמינל של כלי העריכה. כדי לעשות את זה, לוחצים על Terminal -> New Terminal בסרגל התפריטים , או משתמשים בקיצור הדרך Ctrl + Shift + C. חלון טרמינל ייפתח בחלק התחתון של הדפדפן.

74d314f6ff34965b.png

הטרמינל הפעיל הנוכחי צריך להיות בתוך ספריית העבודה adk-multimodal-tool. ב-codelab הזה נשתמש ב-Python 3.12 וב-uv python project manager כדי לפשט את הצורך ביצירה ובניהול של גרסת Python וסביבה וירטואלית. חבילת uv כבר מותקנת מראש ב-Cloud Shell.

מריצים את הפקודה הזו כדי להתקין את התלות הנדרשת בסביבה הווירטואלית בספרייה .venv

uv sync --frozen

כדי לראות את התלויות שהוגדרו במדריך הזה, שהן google-adk, and python-dotenv, בודקים את הקובץ pyproject.toml.

עכשיו צריך להפעיל את ממשקי ה-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 + Click או ללחוץ על הלחצן Web Preview בחלק העליון של Cloud Shell Editor ולבחור באפשרות Preview on port 8080.

edc73e971b9fc60c.png

יוצג דף אינטרנט שבו תוכלו לבחור סוכנים זמינים באמצעות הלחצן הנפתח בפינה הימנית העליונה ( במקרה הזה, האפשרות שצריך לבחור היא product_photo_editor) ולקיים אינטראקציה עם הבוט. נסו להעלות את התמונה הבאה לממשק הצ'אט ולשאול את השאלות הבאות

what is your suggestion for this photo?

a5ff3bc6c19a29ec.jpeg

תוכלו לראות אינטראקציה דומה לזו שמוצגת למטה

c1da4f7cf1466be6.png

כבר עכשיו אפשר לבקש הצעות, אבל בשלב הזה הוא לא יכול לערוך בשבילכם. בואו נעבור לשלב הבא, שבו נצייד את הסוכן בכלי העריכה.

4. 🚀 שינוי הקשר של בקשה למודל שפה גדול (LLM) – תמונה שהועלתה על ידי משתמש

אנחנו רוצים שהסוכן שלנו יוכל לבחור באופן גמיש איזו תמונה מבין התמונות שהועלו הוא רוצה לערוך. עם זאת, כלים של מודלים גדולים של שפה (LLM) מתוכננים בדרך כלל לקבל פרמטרים של סוגי נתונים פשוטים כמו str או int. זהו סוג נתונים שונה מאוד מנתונים מולטימודאליים, שבדרך כלל נתפסים כסוג הנתונים bytes. לכן, נצטרך אסטרטגיה שכוללת את המושג Artifacts כדי לטפל בנתונים האלה. לכן, במקום לספק את נתוני הבייטים המלאים בפרמטר tools, נתכנן את הכלי כך שיקבל את שם מזהה הארטיפקט.

האסטרטגיה הזו תכלול 2 שלבים:

  1. לשנות את הבקשה למודל השפה הגדול כך שכל קובץ שהועלה ישויך למזהה של ארטיפקט, ולהוסיף את זה כהקשר למודל השפה הגדול
  2. תכנון הכלי כך שיקבל מזהי ארטיפקטים כפרמטרים של קלט

בשלב הראשון, כדי לשנות את בקשת ה-LLM, נשתמש בתכונה Callback של ADK. ספציפית, נוסיף את before_model_callback כדי להפעיל את הפונקציה ממש לפני שהסוכן שולח את ההקשר ל-LLM. המחשה מופיעה בתמונה שלמטה 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. בודקים אם החלק מכיל inline_data ( קובץ או תמונה שהועלו). אם כן, מעבדים את הנתונים המוצגים בשורה
  3. יוצרים מזהה ל-inline_data. בדוגמה הזו אנחנו משתמשים בשילוב של שם הקובץ + הנתונים כדי ליצור מזהה גיבוב תוכן
  4. בודקים אם מזהה הארטיפקט כבר קיים. אם לא, שומרים את הארטיפקט באמצעות מזהה הארטיפקט.
  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

ולנסות להעלות את הקובץ שוב ולנהל שיחה, נוכל לבדוק אם שינינו בהצלחה את ההקשר של הבקשה למודל שפה גדול

51404c0704f86ffa.png

f82034bcdda068d9.png

זו אחת הדרכים שבהן אנחנו יכולים לספר ל-LLM על הרצף ועל הזיהוי של נתונים מרובי-אופנים. עכשיו ניצור את הכלי שישתמש במידע הזה

5. 🚀 אינטראקציה עם כלים מרובי-אופנים

עכשיו אפשר להכין כלי שמציין גם את מזהה הארטיפקט כפרמטר הקלט שלו. מריצים את הפקודה הבאה כדי ליצור קובץ חדש 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. טעינת כל פריטי המידע שנוצרו בתהליך הפיתוח של התמונות מ-tool_context באמצעות מזהי הפריטים שסופקו
  4. הנחיה ליצירת עריכה: מוסיפים הוראות לשילוב (כמה תמונות) או לעריכה (תמונה אחת) בצורה מקצועית
  5. קריאה למודל התמונות Gemini 2.5 Flash עם פלט של תמונה בלבד וחילוץ התמונה שנוצרה
  6. שמירת התמונה הערוכה כארטיפקט חדש
  7. החזרת תגובה מובנית עם: סטטוס, מזהה של פריט פלט, מזהי קלט, הנחיה מלאה והודעה

לבסוף, אנחנו יכולים להוסיף את הכלי לסוכן. משנים את התוכן של 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

עכשיו, הסוכן יכול לעזור לכם לשפר את התמונה בהדרגה. הכלי יכול גם להשתמש בתמונה הערוכה בהוראת העריכה הבאה, כי אנחנו מספקים את מזהה הארטיפקט בתשובת הכלי.

עם זאת, במצבו הנוכחי, הסוכן לא יכול לראות ולהבין את תוצאת התמונה הערוכה, כפי שאפשר לראות בדוגמה שלמעלה. הסיבה לכך היא שהתשובה של הכלי שאנחנו נותנים לסוכן היא רק מזהה הארטיפקט ולא תוכן הבייטים עצמו, ולצערנו אנחנו לא יכולים להכניס את תוכן הבייטים ישירות לתשובה של הכלי, כי זה יוביל לשגיאה. לכן צריך להוסיף עוד ענף לוגיקה בתוך הקריאה החוזרת כדי להוסיף את תוכן הבייט כנתונים מוטבעים מתוצאת התגובה של הכלי.

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. בודקים אם החלק הוא תגובה של פונקציה, ואם הוא מופיע ברשימת שמות הכלים שלנו כדי לאפשר שינוי של התוכן
  2. אם מזהה הארטיפקט קיים בתגובה של הכלי, טוענים את תוכן הארטיפקט
  3. לשנות את התוכן כך שיכלול את הנתונים של התמונה הערוכה מתוך התגובה של הכלי

עכשיו אפשר לבדוק אם הסוכן הבין את התמונה הערוכה מהתשובה של הכלי

5d4e880da6f2b9cb.png

מעולה, עכשיו יש לנו סוכן שתומך בתהליך אינטראקציה רב-אופני עם כלי מותאם אישית משלנו.

עכשיו אתם יכולים לנסות אינטראקציה עם הסוכן בתהליך מורכב יותר, למשל להוסיף פריט חדש ( קפה לאטה עם קרח) כדי לשפר את התמונה.

b561a4ae5cb40355.jpeg

e03674e0e1599c33.png

7. סיכום ⭐

עכשיו נחזור למה שכבר עשינו במהלך הסדנה הזו, הנה עיקרי הלמידה:

  1. טיפול בנתונים מרובי-אופנים: למדתי את האסטרטגיה לניהול נתונים מרובי-אופנים (כמו תמונות) בתהליך העבודה של מודל LLM באמצעות שירות הארטיפקטים של ADK, במקום להעביר נתוני בייטים גולמיים ישירות דרך ארגומנטים או תשובות של כלי.
  2. before_model_callback Utilization: Utilized the before_model_callback to intercept and modify the LlmRequest before it is sent to the LLM. הקשנו על התהליך הבא:
  • העלאות של משתמשים: הטמענו לוגיקה לזיהוי נתונים מוטבעים שהמשתמשים מעלים, לשמירתם כארטיפקט עם מזהה ייחודי (למשל, usr_upl_img_...), ולהוסיף טקסט להקשר של ההנחיה עם הפניה למזהה של פריט המידע שנוצר בתהליך הפיתוח, כדי לאפשר למודל שפה גדול (LLM) לבחור את הקובץ הנכון לשימוש בכלי.
  • תשובות של כלים: הטמענו לוגיקה לזיהוי תשובות ספציפיות של פונקציות של כלים שמפיקות ארטיפקטים (לדוגמה, תמונות ערוכות), לטעינת הארטיפקט שנשמר לאחרונה (לדוגמה, edited_img_...), ולהחדיר גם את ההפניה למזהה הארטיפקט וגם את תוכן התמונה ישירות לזרם ההקשר.
  1. עיצוב כלי בהתאמה אישית: יצרתי כלי Python בהתאמה אישית (edit_product_asset) שמקבל רשימה של image_artifact_ids (מזהי מחרוזות) ומשתמש ב-ToolContext כדי לאחזר את נתוני התמונה בפועל משירות Artifacts.
  2. שילוב של מודל ליצירת תמונות: שילבנו את מודל התמונות Gemini 2.5 Flash בכלי בהתאמה אישית כדי לבצע עריכת תמונות על סמך תיאור טקסט מפורט.
  3. אינטראקציה רציפה עם כמה אמצעי קלט: דאגנו שהסוכן יוכל לשמור על סשן עריכה רציף על ידי הבנת התוצאות של קריאות הכלים שלו (התמונה הערוכה) ושימוש בפלט הזה כקלט להוראות הבאות.

8. ➡️ האתגר הבא

ברכות על סיום חלק 1 של האינטראקציה עם הכלי הרב-מודאלי של ADK. במדריך הזה אנחנו מתמקדים באינטראקציה עם כלים בהתאמה אישית. עכשיו אפשר לעבור לשלב הבא ולהסביר איך אפשר ליצור אינטראקציה עם חבילת הכלים של MCP. מעבר אל שיעור ה-Lab הבא

9. 🧹 ניקוי

כדי לא לצבור חיובים לחשבון Google Cloud על המשאבים שבהם השתמשתם ב-Code Lab הזה:

  1. במסוף Google Cloud, עוברים לדף Manage resources.
  2. ברשימת הפרויקטים, בוחרים את הפרויקט שרוצים למחוק ולוחצים על Delete.
  3. כדי למחוק את הפרויקט, כותבים את מזהה הפרויקט בתיבת הדו-שיח ולוחצים על Shut down.