סוכן AI עמיד עם Gemini ו-Temporal

במדריך הזה נסביר איך ליצור לולאה של סוכן בסגנון ReAct שמשתמשת ב-Gemini API לניתוח ול-Temporal לעמידות. קוד המקור המלא של המדריך הזה זמין ב-GitHub.

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

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

ארכיטקטורה

הארכיטקטורה מורכבת משלושה חלקים:

  • תהליך עבודה: הלולאה שמבוססת על סוכנים ומתזמרת את לוגיקת הביצוע.
  • פעילויות: יחידות עבודה נפרדות (קריאות ל-LLM, קריאות לכלים) ש-Temporal הופך לניתנות להמשכה.
  • Worker: התהליך שמבצע את תהליכי העבודה והפעילויות.

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

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

כדי להשלים את ההדרכה הזו, תצטרכו:

  • מפתח Gemini API. אפשר ליצור אותו בחינם ב-Google AI Studio.
  • Python בגרסה 3.10 ואילך.
  • Temporal CLI להפעלת שרת פיתוח מקומי.

הגדרה

לפני שמתחילים, מוודאים שיש לכם שרת פיתוח זמני שפועל באופן מקומי:

temporal server start-dev

לאחר מכן, מתקינים את יחסי התלות הנדרשים:

pip install temporalio google-genai httpx pydantic python-dotenv

יוצרים קובץ .env בספריית הפרויקט עם מפתח Gemini API. אפשר לקבל מפתח API מ-Google AI Studio.

echo "GOOGLE_API_KEY=your-api-key-here" > .env

הטמעה

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

ייבוא והגדרת ארגז חול

מתחילים עם הייבוא שצריך להגדיר מראש. הבלוק workflow.unsafe.imports_passed_through() אומר לארגז החול של תהליך העבודה של Temporal לאפשר למודולים מסוימים לעבור ללא הגבלה. הדבר נחוץ כי כמה ספריות (בעיקר httpx, שהיא מחלקת משנה של urllib.request.Request) משתמשות בתבניות שארגז החול יחסום אחרת.

from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    import pydantic_core  # noqa: F401
    import annotated_types  # noqa: F401

    import httpx
    from pydantic import BaseModel, Field
    from google import genai
    from google.genai import types

הוראות מערכת

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

SYSTEM_INSTRUCTIONS = """
You are a helpful agent that can use tools to help the user.
You will be given an input from the user and a list of tools to use.
You may or may not need to use the tools to satisfy the user ask.
If no tools are needed, respond in haikus.
"""

הגדרות של כלים

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

import json

NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"

class GetWeatherAlertsRequest(BaseModel):
    """Request model for getting weather alerts."""

    state: str = Field(description="Two-letter US state code (e.g. CA, NY)")

async def get_weather_alerts(request: GetWeatherAlertsRequest) -> str:
    """Get weather alerts for a US state.

    Args:
        request: The request object containing:
            - state: Two-letter US state code (e.g. CA, NY)
    """
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    url = f"{NWS_API_BASE}/alerts/active/area/{request.state}"

    async with httpx.AsyncClient() as client:
        response = await client.get(url, headers=headers, timeout=5.0)
        response.raise_for_status()
        return json.dumps(response.json())

לאחר מכן, מגדירים כלים למיקום גיאוגרפי של כתובות IP:

class GetLocationRequest(BaseModel):
    """Request model for getting location info from an IP address."""

    ipaddress: str = Field(description="An IP address")

async def get_ip_address() -> str:
    """Get the public IP address of the current machine."""
    async with httpx.AsyncClient() as client:
        response = await client.get("https://icanhazip.com")
        response.raise_for_status()
        return response.text.strip()

async def get_location_info(request: GetLocationRequest) -> str:
    """Get the location information for an IP address including city, state, and country.

    Args:
        request: The request object containing:
            - ipaddress: An IP address to look up
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(f"http://ip-api.com/json/{request.ipaddress}")
        response.raise_for_status()
        result = response.json()
        return f"{result['city']}, {result['regionName']}, {result['country']}"

מאגר כלים

לאחר מכן, יוצרים מאגר שמתאים בין שמות של כלים לבין פונקציות של מטפלים. הפונקציה get_tools() יוצרת אובייקטים של FunctionDeclaration שתואמים ל-Gemini מתוך הפונקציות שניתנות להפעלה באמצעות FunctionDeclaration.from_callable_with_api_option().

from typing import Any, Awaitable, Callable

ToolHandler = Callable[..., Awaitable[Any