en
Feedback
ToCode

ToCode

Open in Telegram

טיפים קצרים למתכנתים מאת ינון פרק

Show more
1 419
Subscribers
No data24 hours
No data7 days
-430 days
Posts Archive
ToCode
1 419
יום 15 - שימוש בכלים משרתי MCP נסיים את החלק על שימוש בכלים בסקירה קצרה של שרתי MCP וחיבור הסוכנים שלנו אליהם. מהם שרתי MCP ראינו שאנחנו מגדירים כלים לסוכן באמצעות העברת מערך כלים, וכלי הוא בעצם פונקציה:
agent = Agent(
    name="Assistant",
    tools=[read_shells_file],
    instructions="Answer questions about the file /etc/shells",
)
מה קורה אם אנחנו רוצים לשתף כלים עם אחרים או לקחת כלים שאחרים משתפים? בתוכנית פייתון רגילה הייתי אומר שבשביל לשתף "כלי", כלומר פונקציה, נצטרך להגדיר חבילת פייתון, להעלות לרשת ומישהו אחר יתקין את החבילה וישתמש ב import כדי להגיע לפונקציה. הבעיה עם סוכנים היא שלא כולם כותבים את הסוכנים שלהם בפייתון - יש ספריות סוכנים בטייפסקריפט, ברובי, ב Java, באליקסיר ובעצם בכל שפה שאתם יכולים לחשוב עליה מישהו כבר כתב ספריית סוכנים. לכן אם ה"כלי" שאני רוצה לשתף יהיה פונקציית פייתון הרבה אנשים לא יוכלו להשתמש בו. פרוטוקול MCP הוא פרוטוקול לשיתוף "כלים" בין ספריות סוכנים שמאפשר התאמה של הכלים לכל השפות. הפרוטוקול מגדיר: 1. איך נראה "שרת", שזה בעצם אוסף של כלים. 2. איך ניגשים לאותו שרת - דרך הפעלה משורת הפקודה או דרך הרשת. 3. איך מבררים מהשרת איזה כלים יש בו. 4. איך מבררים איזה פרמטר כל כלי צריך. ספריית OpenAI Agents SDK יודעת להתחבר לשרתי MCP ולתת לסוכן גישה לכלים שמאוחסנים על שרתים אלה. איך לחבר סוכן לשרת MCP ניקח לדוגמה שרת MCP בשם fetch שמשתף כלי אחד - הכלי נקרא fetch והוא מקבל URL ומושך את תוכנו מהאינטרנט כדי שהסוכן יוכל להשתמש במידע. השרת נמצא בכתובת:
https://remote.mcpservers.org/fetch/mcp
וכל אחד יכול להתחבר אליו. נניח שיש לי סוכן שצריך להביא מידע מהאינטרנט למשל מ ynet, כלומר אולי אני רוצה להריץ את הפרומפט הבא:
result = await Runner.run(agent, "Summarize the top stories from ynet today. URL is: https://www.ynet.co.il/news/category/184")
לסוכן כזה אני יכול להעביר Tool שמביא דף מהאינטרנט שאני כותב, למשל באמצעות requests, או להשתמש בכלי fetch מתוך שרת ה MCP הציבורי שראינו קודם. מאחר ואנחנו כבר יודעים לכתוב Tool בעצמנו בואו נראה את הקוד שמתחבר לשרת ה MCP הציבורי - ומסתבר שהוא לא מסובך:
import asyncio
from agents import Agent, Runner
from agents.mcp import MCPServer, MCPServerStreamableHttp
from agents.model_settings import ModelSettings


async def run(mcp_server: MCPServer):
    agent = Agent(
        name="Assistant",
        instructions="Use the tools to answer the questions.",
        mcp_servers=[mcp_server],
        model_settings=ModelSettings(tool_choice="required"),
    )

    result = await Runner.run(agent, "Summarize the top stories from ynet today. URL is: https://www.ynet.co.il/news/category/184")
    print(result.to_input_list())
    print(result.final_output)


async def main():
    async with MCPServerStreamableHttp(
        name="Streamable HTTP Python Server",
        params={
            "url": "https://remote.mcpservers.org/fetch/mcp",
        },
    ) as server:
        await run(server)


if __name__ == "__main__":
    asyncio.run(main())
פונקציית main יוצרת את החיבור לשרת ה mcp ומעבירה אותו לפונקציה run. הפונקציה run מעבירה את השרת לסוכן ושולחת את השאילתה. התוצאה היא רשימת הכותרות מ ynet שהסוכן מוריד מהאינטרנט. עכשיו אתם בעמוד הזה: https://github.com/modelcontextprotocol/servers תוכלו למצוא המון שרתי MCP מעניינים עבור הסוכנים שלכם. מצאו שרת שמעניין אתכם וכתבו תוכנית פייתון שממחישה איך הסוכן מתחבר לאוסף הכלים בשרת ה MCP שבחרתם.

ToCode
1 419
סך הכל Handoff הוא קונספט נחמד אבל הרבה פעמים יהיה לנו יותר קל לממש אותו בעצמנו עם הגדרת כלים במיוחד לדברים לא טריוויאליים. בתוך לולאת ה Runner של Agents SDK התבנית המרכזית בה נרצה להשתמש ב Handoff היא סוכן ראשי שממיין פניות וסוכנים ספציפיים שמטפלים בסוגי הפניות כאשר הטיפול לא דורש המשך אינטרקציה עם המשתמש.

ToCode
1 419
יום 14 - העברת שליטה לסוכן אחר handoff העברת שליטה היא סוג מיוחד של הפעלת כלי בה סוכן אחד מזהה שיש סוכן אחר שיהיה יותר מיומן ממנו לפתור בעיה ספציפית ולכן מעביר את השליטה לסוכן השני. בואו נראה איך זה מוגדר ב Agents SDK ולמה חשוב לשים לב. למה צריך מספר סוכנים לפני שנגיע לקוד של handoff כדאי לעצור ולשאול - למה צריך מספר סוכנים? האם הסוכן הראשי לא יכול לבצע את כל המשימות? הנה כמה רעיונות לתרחישים בהם מספר סוכנים יכולים לעזור: 1. אם אנחנו יכולים לזהות סוג מסוים של בקשות שיקבלו טיפול טוב יותר ממודל אחר - לדוגמה רוב הבקשות שלי מטופלות בצורה טובה על ידי gpt-4.1-mini אבל יש בקשות מסובכות במיוחד שכדאי לי להעביר למודל יותר גדול. או אולי יש לי סט של בקשות שאני יכול לפתור בעזרת מודל זול יותר וכך להוריד עלויות. 2. אם אנחנו מזהים סוג מסוים של בקשות עבורן סט הוראות (instructions) שונה ייתן תוצאות טובות יותר. 3. אם אנחנו רוצים לשלוט טוב יותר בקונטקסט, כלומר להעביר בקשה מסוימת בלי ההיסטוריה שלה או עם היסטוריה חלקית למודל אחר לטיפול. במקרים כאלה נוכל להגדיר handoff, כלומר הפעלת של כלי מיוחד שפונה לסוכן אחר ומבקש ממנו השלמה. זו מעין השלמה בתוך השלמה כדי להגיע לתשובה סופית. דוגמה - סוכני תרגום בדוגמה הראשונה אני מגדיר שלושה סוכנים: סוכן אחד יודע לתרגם דברים לצרפתית, סוכן שני מתרגם לספרדית וסוכן שלישי אחראי על רוב השיחה. אם הסוכן הראשי מזהה בקשה לתרגום הוא יעביר את הבקשה לסוכן המתאים:
french_agent = Agent(
    name="French Translator",
    instructions="Translate everything to french"
)
spanish_agent = Agent(
    name="Spanish Translator",
    instructions="Translate everything to Spanish")

triage_agent = Agent(name="Triage agent", handoffs=[french_agent, spanish_agent])

async def main():
    session = SQLiteSession("handoffs", "handoffs.db")
    result = await Runner.run(triage_agent, "translate to French: 'hello world'", session=session)
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
הפעלת הקוד מדפיסה bonjour le monde שזה התרגום לצרפתית של hello world. בחינת רשימת ההודעות אחרי הפעלת התוכנית מציגה את הרשימה הבאה:
1|handoffs|{"content": "translate to French: 'hello world'", "role": "user"}|2025-08-17 11:49:06
2|handoffs|{"arguments": "{}", "call_id": "call_Abq5WDOv6C1RZVKNsCg4WO0F", "name": "transfer_to_french_translator", "type": "function_call", "id": "fc_68a1c1b13b8081988a6174005b308dd20e79e771496ac495", "status": "completed"}|2025-08-17 11:49:06
3|handoffs|{"call_id": "call_Abq5WDOv6C1RZVKNsCg4WO0F", "output": "{\"assistant\": \"French Translator\"}", "type": "function_call_output"}|2025-08-17 11:49:06
4|handoffs|{"id": "msg_68a1c1b27324819892a9f00a9d0b95f50e79e771496ac495", "content": [{"annotations": [], "text": "'hello world' se traduit par 'bonjour le monde'.", "type": "output_text", "logprobs": []}], "role": "assistant", "status": "completed", "type": "message"}|2025-08-17 11:49:06
אנחנו רואים בפלט: 1. הודעה ראשונה מהמשתמש עם בקשה לתרגם לצרפתית. 2. הודעה על Tool Call של העברת שליטה לסוכן שני. 3. סיום Tool Call עם הפלט "{"assistant": "French Translator"}". 4. פניה של ה Runner לסוכן התרגום לצרפתית והודעה ממנו עם הטקסט המתורגם. למה לשים לב כשמבצעים Handoff כמה נקודות שכדאי לזכור לגבי Handoff וטיפים לשימוש יעיל בפיצ'ר: 1. אם אתם מעבירים שליטה לסוכן שצריך להמשיך לתקשר עם המשתמש שימו לב שתצטרכו בעצמכם להעביר את הסוכן החדש ל Runner.run בהודעה הבאה (אחרת הוא ירוץ עם הסוכן שעובר בתור פרמטר ראשון). 2. מבחינת המודל Handoff הוא בסך הכל Tool Calling. שם הכלי כברירת מחדל הוא transfer_to_<agent_name>. שימו לב לא להתנגש עם כלים קיימים שאתם מגדירים לסוכן. 3. אם יש לכם קוד ממשק משתמש שמציג היסטוריית שיחה כדאי לזהות Handoff ולתת לו ייצוג ויזואלי שונה מ Tool Calling רגיל כדי להתאים לציפיות המשתמשים.

ToCode
1 419
if result.tool.name == "open_box":
            return ToolsToFinalOutputResult(is_final_output=True, final_output=result.output)
    # Otherwise, continue
    return ToolsToFinalOutputResult(is_final_output=False)
מתי משתמשים בכל דרך בפוסט ראינו שתי דרכים לשלוט ולעצור את לולאת הפעלת הכלים. במערכות אמיתיות נרצה לשלב את שתי הדרכים: 1. נרצה להשתמש ב max_turns כדי לוודא שהמודל לא יוצא משליטה בהפעלת הכלים. הפעלה של יותר מדי כלים עלולה להיות סימן שהמודל נכנס ללולאה ולא מצליח לחשב את התשובה הבאה. 2. נרצה להשתמש בפונקציית הבקרה כדי לממש סוכנים שיודעים "לוותר על השליטה", לדוגמה סוכן שאחראי על ביצוע שלב מסוים בתהליך וכשהוא מסיים הוא יוכל להפעיל כלי "סיום" שיעביר את התוצאה לסוכן אחר להמשך עבודה. נוכל להשתמש בפונקציה גם לבקרה שהסוכן לא מנסה להפעיל כלים בהתבסס על מידע שגוי ולהוציא אותו מלולאות של חישובים שגויים. בהמשך הסידרה נדבר על עוד שני מנגנונים לשליטה על סוכנים - מנגנון Handoff ומנגנון Guardrails, כל אחד מהם בדרכו יעזור לנו לעצור פעולה של סוכן לפני שהסתיימה. עכשיו אתם כתבו סוכן שמקבל שלושה כלים - הצגת רשימת הקבצים בתיקייה, קריאת טקסט בקובץ וכניסה לתיקיה אחרת. השתמשו בסוכן כדי לחפש קובץ שמכיל טקסט מסוים. תעדו את הפעלת הכלים. לאחר מכן הוסיפו "מוקשים", קבצים שכשפותחים אותם לולאת הפעלת הכלים של הסוכן תיעצר.

ToCode
1 419
on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x104dd1bc0>,
        strict_json_schema=True,
        is_enabled=True
    ),
    output='An error occurred while running the tool. Please try again. Error: coroutine raised StopIteration',
    run_item=ToolCallOutputItem(
        agent=Agent(
            name='Assistant',
            handoff_description=None,
            tools=[
                FunctionTool(
                    name='open_box',
                    description='Open a box and search for the treasure inside. Returns True if found',
                    params_json_schema={
                        'properties': {
                            'id': {
                                'title': 'Id',
                                'type': 'string'
                            }
                        },
                        'required': ['id'],
                        'title': 'open_box_args',
                        'type': 'object',
                        'additionalProperties': False
                    },
                    on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x104dd1bc0>,
                    strict_json_schema=True,
                    is_enabled=True
                ),
                FunctionTool(
                    name='get_box_ids',
                    description='',
                    params_json_schema={
                        'properties': {},
                        'title': 'get_box_ids_args',
                        'type': 'object',
                        'additionalProperties': False,
                        'required': []
                    },
                    on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x110d2a200>,
                    strict_json_schema=True,
                    is_enabled=True
                )
            ],
            mcp_servers=[],
            mcp_config={},
            instructions=None,
            prompt=None,
            handoffs=[],
            model=<agents.extensions.models.litellm_model.LitellmModel object at 0x110aa7620>,
            model_settings=ModelSettings(
                temperature=None,
                top_p=None,
                frequency_penalty=None,
                presence_penalty=None,
                tool_choice=None,
                parallel_tool_calls=None,
                truncation=None,
                max_tokens=None,
                reasoning=None,
                metadata=None,
                store=None,
                include_usage=None,
                response_include=None,
                extra_query=None,
                extra_body=None,
                extra_headers=None,
                extra_args=None
            ),
            input_guardrails=[],
            output_guardrails=[],
            output_type=None,
            hooks=None,
            tool_use_behavior=<function custom_tool_use_behavior at 0x110d2a0c0>,
            reset_tool_choice=True
        ),
        raw_item={
            'call_id': 'call_4cPEq87qI3ccfPvCEQKZrelY',
            'output': 'An error occurred while running the tool. Please try again. Error: coroutine raised StopIteration',
            'type': 'function_call_output'
        },
        output='An error occurred while running the tool. Please try again. Error: coroutine raised StopIteration',
        type='tool_call_output_item'
    )
)
---

אנחנו רואים שהפונקציה נקראה כל פעם שהמודל מפעיל כלי עם התוצאה של הכלי שהופעל ופרטי הכלי. אין לנו את הפרמטרים שהמודל העביר לכלי. בשביל לעצור את הלולאה אני יכול להחזיר אוביקט ToolsToFinalOutputResult עם is_final_output מוגדר ל True, לדוגמה הקוד הבא עוצר את הלולאה אחרי פתיחת הקופסה הראשונה:
def custom_tool_use_behavior(context, tool_results: list[FunctionToolResult]) -> ToolsToFinalOutputResult:
    print(context)
    for result in tool_results:

ToCode
1 419
handoff_description=None,
            tools=[
                FunctionTool(
                    name='open_box',
                    description='Open a box and search for the treasure inside. Returns True if found',
                    params_json_schema={
                        'properties': {
                            'id': {
                                'title': 'Id',
                                'type': 'string'
                            }
                        },
                        'required': ['id'],
                        'title': 'open_box_args',
                        'type': 'object',
                        'additionalProperties': False
                    },
                    on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x104dd1bc0>,
                    strict_json_schema=True,
                    is_enabled=True
                ),
                FunctionTool(
                    name='get_box_ids',
                    description='',
                    params_json_schema={
                        'properties': {},
                        'title': 'get_box_ids_args',
                        'type': 'object',
                        'additionalProperties': False,
                        'required': []
                    },
                    on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x110d2a200>,
                    strict_json_schema=True,
                    is_enabled=True
                )
            ],
            mcp_servers=[],
            mcp_config={},
            instructions=None,
            prompt=None,
            handoffs=[],
            model=<agents.extensions.models.litellm_model.LitellmModel object at 0x110aa7620>,
            model_settings=ModelSettings(
                temperature=None,
                top_p=None,
                frequency_penalty=None,
                presence_penalty=None,
                tool_choice=None,
                parallel_tool_calls=None,
                truncation=None,
                max_tokens=None,
                reasoning=None,
                metadata=None,
                store=None,
                include_usage=None,
                response_include=None,
                extra_query=None,
                extra_body=None,
                extra_headers=None,
                extra_args=None
            ),
            input_guardrails=[],
            output_guardrails=[],
            output_type=None,
            hooks=None,
            tool_use_behavior=<function custom_tool_use_behavior at 0x110d2a0c0>,
            reset_tool_choice=True
        ),
        raw_item={
            'call_id': 'call_AakkVgr7fikDFcam0VADoa9z',
            'output': 'An error occurred while running the tool. Please try again. Error: coroutine raised StopIteration',
            'type': 'function_call_output'
        },
        output='An error occurred while running the tool. Please try again. Error: coroutine raised StopIteration',
        type='tool_call_output_item'
    )
)

---
RunContextWrapper(
    context=None,
    usage=Usage(
        requests=2,
        input_tokens=939,
        input_tokens_details=InputTokensDetails(
            cached_tokens=0
        ),
        output_tokens=78,
        output_tokens_details=OutputTokensDetails(
            reasoning_tokens=0
        ),
        total_tokens=1017
    )
)
FunctionToolResult(
    tool=FunctionTool(
        name='open_box',
        description='Open a box and search for the treasure inside. Returns True if found',
        params_json_schema={
            'properties': {
                'id': {
                    'title': 'Id',
                    'type': 'string'
                }
            },
            'required': ['id'],
            'title': 'open_box_args',
            'type': 'object',
            'additionalProperties': False
        },

ToCode
1 419
7|boxes|{"call_id": "call_0ukIo397uSCrgRHDNwJugFuC", "output": "True", "type": "function_call_output"}|2025-08-14 10:13:28
8|boxes|{"id": "__fake_id__", "content": [{"annotations": [], "text": "I opened the first box, but didn't find the treasure. Then I opened the second box and found the treasure inside! \n\nI found the treasure by opening only 2 boxes.", "type": "output_text"}], "role": "assistant", "status": "completed", "type": "message"}|2025-08-14 10:13:28

הפעם אנחנו רואים מבנה שונה - המודל לא יכל להעביר את כל הקריאות במכה אחת בתשובה להודעה הראשונה כי הוא לא ידע מה המזהים של הקופסאות. אפילו אחרי שהוא השתמש בכלי כדי לקבל רשימת מזהים הוא עדיין לא רצה לפתוח את כל שלושת הקופסאות במכה אחת כי ביקשתי לצמצם את מספר הפתיחות, לכן אנחנו רואים את העבודה פעולה אחרי פעולה. מה אם היו מאות או אלפי קופסאות? מה אם כלל לא היה אוצר? שליטה על לולאת הפעלת הכלים נשים לב שספריית OpenAI Agents SDK עובדת בלולאה - כל פעם שהמודל מבקש להפעיל כלי הספריה מפעילה את הפונקציה, מוסיפה את ההודעה המתאימה לרשימת ההודעות ופונה שוב למודל. לולאה כזאת יכולה לקחת הרבה זמן מאחר וכל פניה למודל יכולה להיות איטית וגם לעלות בהרבה טוקנים ולכן הרבה כסף, במיוחד אם המודל מתבלבל ומפעיל כלים בלי להגיע לתוצאה. דרך אחת לשלוט בלולאה היא הפרמטר max_turns שניתן להעביר ל Runner.run. פרמטר זה מגביל את מספר האיטרציות - אם המודל ינסה להפעיל כלים יותר פעמים ממה שמוגדר במשתנה הספריה תפסיק את הלולאה ותסיים עם שגיאה. זה נראה כך:
async def main():
    session = SQLiteSession("boxes", "boxes.db")
    result = await Runner.run(agent, """
    Let's play a game. You are given 3 boxes and you need to find the treasure hidden in one of them. You can open any box you want
    to look inside.
    Use open_box tool to open boxes and find the treasure. 
    Try to find it by opening the miniumum number of boxes.
    """, session=session, max_turns=3)
    print(result.final_output)
והרצה עם שגיאה מסתיימת כך:
agents.exceptions.MaxTurnsExceeded: Max turns (3) exceeded
דרך נוספת לשלוט בלולאה היא המאפיין tool_use_behavior בהגדרת סוכן:
agent = Agent(
    name="Assistant",
    model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
    tools=[open_box, get_box_ids],
    tool_use_behavior=custom_tool_use_behavior
)
מאפיין זה יכול לקבל פונקציה שתוכל להחליט אם להמשיך את הלולאה או להפסיק. אני מריץ את הפונקציה הבאה קודם כל בשביל לחקור את הפרמטרים שלה:
def custom_tool_use_behavior(context, tool_results: list[FunctionToolResult]) -> ToolsToFinalOutputResult:
    print(context)
    for result in tool_results:
        print(result)
        print("---")
    # Otherwise, continue
    return ToolsToFinalOutputResult(is_final_output=False)
והפלט לאחר סידור הוא:
RunContextWrapper(
    context=None,
    usage=Usage(
        requests=1,
        input_tokens=437,
        input_tokens_details=InputTokensDetails(
            cached_tokens=0
        ),
        output_tokens=40,
        output_tokens_details=OutputTokensDetails(
            reasoning_tokens=0
        ),
        total_tokens=477
    )
)
FunctionToolResult(
    tool=FunctionTool(
        name='open_box',
        description='Open a box and search for the treasure inside. Returns True if found',
        params_json_schema={
            'properties': {
                'id': {
                    'title': 'Id',
                    'type': 'string'
                }
            },
            'required': ['id'],
            'title': 'open_box_args',
            'type': 'object',
            'additionalProperties': False
        },
        on_invoke_tool=<function function_tool.<locals>._create_function_tool.<locals>._on_invoke_tool at 0x104dd1bc0>,
        strict_json_schema=True,
        is_enabled=True
    ),
    output='An error occurred while running the tool. Please try again. Error: coroutine raised StopIteration',
    run_item=ToolCallOutputItem(
        agent=Agent(
            name='Assistant',

ToCode
1 419
14|weather|{"id": "__fake_id__", "content": [{"annotations": [], "text": "Here is today's weather in major Israeli cities:\n\n- Tel Aviv: Clear sky, 36.9\u00b0C (feels like 40.23\u00b0C), 38% humidity\n- Jerusalem: Clear sky, 38.34\u00b0C (feels like 42.39\u00b0C), 36% humidity\n- Haifa: Clear sky, 33.39\u00b0C (feels like 34.41\u00b0C), 40% humidity\n- Eilat: Clear sky, 42.91\u00b0C (feels like 45.23\u00b0C), 23% humidity\n\nIt\u2019s a very hot and sunny day across the country\u2014plan accordingly!", "type": "output_text"}], "role": "assistant", "status": "completed", "type": "message"}|2025-08-14 09:59:42
הפעם הודעת המודל להפעלת כלי כללה בקשה להפעלת ארבעה כלים. ספריית הסוכנים רשמה את זה בתור ארבע הודעות בטבלה, הוסיפה 4 תשובות ואז פנתה שוב למודל כדי לייצר את התשובה הסופית. לולאת הפעלת כלים מנגנון הפעלת הכלים שראינו נקרא React שזה קיצור של Reason + Act. סוכני ריאקט עובדים בלולאה ויכולים להפעיל כלים שוב ושוב עד שמגיעים לתשובה סופית. נראה דוגמה שכוללת הפעלת מספר כלים אחד אחרי השני - אני מייצר שלוש קופסאות עם מזהים אקראיים ומבקש מהמודל לפתוח קופסאות עד שהוא מגיע לאוצר:
from typing_extensions import TypedDict
from agents import Agent, function_tool, Runner, SQLiteSession
from agents.extensions.models.litellm_model import LitellmModel
from uuid import uuid4
import requests
import os
import asyncio

boxes = [
    {
        "id": str(uuid4()),
        "has_treasure": False
    },
    {
        "id": str(uuid4()),
        "has_treasure": True
    },
    {
        "id": str(uuid4()),
        "has_treasure": False
    }
]

@function_tool
async def open_box(id: str) -> bool:
    """Open a box and search for the treasure inside. Returns True if found"""
    return next(b['has_treasure'] for b in boxes if b['id'] == id)

@function_tool
async def get_box_ids() -> list[str]:
    return [b['id'] for b in boxes]

agent = Agent(
    name="Assistant",
    model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
    tools=[open_box, get_box_ids],
)

async def main():
    session = SQLiteSession("boxes", "boxes.db")
    result = await Runner.run(agent, """
    Let's play a game. You are given 3 boxes and you need to find the treasure hidden in one of them. You can open any box you want
    to look inside.
    Use open_box tool to open boxes and find the treasure. 
    Try to find it by opening the miniumum number of boxes.
    """, session=session)
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
אלה ההודעות שנוצרו הפעם:
sqlite> select * from agent_messages ;
1|boxes|{"content": "\n    Let's play a game. You are given 3 boxes and you need to find the treasure hidden in one of them. You can open any box you want\n    to look inside.\n    Use open_box tool to open boxes and find the treasure. \n    Try to find it by opening the miniumum number of boxes.\n    ", "role": "user"}|2025-08-14 10:13:28
2|boxes|{"arguments": "{}", "call_id": "call_Rjqjj5dQyxCISwpPMR3hrrai", "name": "get_box_ids", "type": "function_call", "id": "__fake_id__"}|2025-08-14 10:13:28
3|boxes|{"call_id": "call_Rjqjj5dQyxCISwpPMR3hrrai", "output": "['c77fa56d-0375-4e8d-9470-3f494b16706c', '1e499625-705d-40f7-bae4-df71bf97c523', 'd3f99c22-33fd-4cce-b7c2-9f7de13c122e']", "type": "function_call_output"}|2025-08-14 10:13:28
4|boxes|{"arguments": "{\"id\":\"c77fa56d-0375-4e8d-9470-3f494b16706c\"}", "call_id": "call_MU2FdBwcGn9ayc0f04UyS1ea", "name": "open_box", "type": "function_call", "id": "__fake_id__"}|2025-08-14 10:13:28
5|boxes|{"call_id": "call_MU2FdBwcGn9ayc0f04UyS1ea", "output": "False", "type": "function_call_output"}|2025-08-14 10:13:28
6|boxes|{"arguments": "{\"id\":\"1e499625-705d-40f7-bae4-df71bf97c523\"}", "call_id": "call_0ukIo397uSCrgRHDNwJugFuC", "name": "open_box", "type": "function_call", "id": "__fake_id__"}|2025-08-14 10:13:28

ToCode
1 419
2|weather|{"arguments": "{\"location\":{\"lat\":32.0853,\"long\":34.7818}}", "call_id": "call_obLi5Lbz93fzOIDRvC195udg", "name": "fetch_weather", "type": "function_call", "id": "__fake_id__"}|2025-08-14 09:54:56
3|weather|{"call_id": "call_obLi5Lbz93fzOIDRvC195udg", "output": "Weather: Clear Sky, Temperature: 36.9\u00b0C (feels like 40.23\u00b0C), Humidity: 38%", "type": "function_call_output"}|2025-08-14 09:54:56
4|weather|{"id": "__fake_id__", "content": [{"annotations": [], "text": "The weather in Tel Aviv is currently clear sky with a temperature of 36.9\u00b0C (feels like 40.23\u00b0C) and humidity at 38%. It's quite hot today!", "type": "output_text"}], "role": "assistant", "status": "completed", "type": "message"}|2025-08-14 09:54:56
נשים לב ששלחנו הודעה מהמשתמש, בתגובה המודל ענה בהודעה עם בקשה להפעלת כלי ונתן לה מזהה call_obLi5Lbz93fzOIDRvC195udg, לאחר מכן ספריית הסוכנים הפעילה את הפונקציה והוסיפה תגובה חדשה עם JSON שכולל את אותו מזהה ובסוף המודל הוסיף הודעה שעונה למשתמש בעזרת התשובה שהגיעה מהכלי. סך הכל הפקודה:
result = await Runner.run(agent, "What is the weather in Tel Aviv?", session=session)
פנתה למודל פעמיים - בפעם הראשונה קיבלה בקשה להפעלת כלי, ובפעם השניה קיבלה את הטקסט שיחזור ל result.final_output. בואו נבעט בו עוד קצת ונבקש את מזג האוויר במספר מקומות:
async def main():
    session = SQLiteSession("weather", "weather.db")
    result = await Runner.run(agent, "I'm planning a trip to Israel, what is the weather in Tel Aviv, Jerusalem, Haifa and Eilat today?", session=session)
    print(result.final_output)
אלה ההודעות בבסיס הנתונים:
5|weather|{"content": "I'm planning a trip to Israel, what is the weather in Tel Aviv, Jerusalem, Haifa and Eilat today?", "role": "user"}|2025-08-14 09:59:42
6|weather|{"arguments": "{\"location\": {\"lat\": 32.0853, \"long\": 34.7818}}", "call_id": "call_la2QHswRUH0Xa80wZNLQ6dOv", "name": "fetch_weather", "type": "function_call", "id": "__fake_id__"}|2025-08-14 09:59:42
7|weather|{"arguments": "{\"location\": {\"lat\": 31.7683, \"long\": 35.2137}}", "call_id": "call_AEGdktcxYfR2MzGS7BzhA9AY", "name": "fetch_weather", "type": "function_call", "id": "__fake_id__"}|2025-08-14 09:59:42
8|weather|{"arguments": "{\"location\": {\"lat\": 32.794, \"long\": 34.9896}}", "call_id": "call_CFb2TkCz6M6eL19aT7c69Zae", "name": "fetch_weather", "type": "function_call", "id": "__fake_id__"}|2025-08-14 09:59:42
9|weather|{"arguments": "{\"location\": {\"lat\": 29.5577, \"long\": 34.9519}}", "call_id": "call_DWgN1qFAru8ScClVHj1jgqIz", "name": "fetch_weather", "type": "function_call", "id": "__fake_id__"}|2025-08-14 09:59:42
10|weather|{"call_id": "call_la2QHswRUH0Xa80wZNLQ6dOv", "output": "Weather: Clear Sky, Temperature: 36.9\u00b0C (feels like 40.23\u00b0C), Humidity: 38%", "type": "function_call_output"}|2025-08-14 09:59:42
11|weather|{"call_id": "call_AEGdktcxYfR2MzGS7BzhA9AY", "output": "Weather: Clear Sky, Temperature: 38.34\u00b0C (feels like 42.39\u00b0C), Humidity: 36%", "type": "function_call_output"}|2025-08-14 09:59:42
12|weather|{"call_id": "call_CFb2TkCz6M6eL19aT7c69Zae", "output": "Weather: Clear Sky, Temperature: 33.39\u00b0C (feels like 34.41\u00b0C), Humidity: 40%", "type": "function_call_output"}|2025-08-14 09:59:42
13|weather|{"call_id": "call_DWgN1qFAru8ScClVHj1jgqIz", "output": "Weather: Clear Sky, Temperature: 42.91\u00b0C (feels like 45.23\u00b0C), Humidity: 23%", "type": "function_call_output"}|2025-08-14 09:59:42

ToCode
1 419
יום 12 - לולאת הפעלת הכלים הפעלת כלים היא מצד אחד היכולת הכי משמעותית של מודלים, היא מהווה בסיס להרבה תבניות עבודה עם מודלים אבל מצד שני בגלל האופי הלא אמין של מודלים היא גם מבלבלת ומקור בלתי נדלה לבאגים. היום נבין יותר לעומק איך מנגנון הפעלת הכלים עובד, מה נקודות הבקרה שלנו במנגנון ולאיזה מטרות נרצה להשתמש בו. איך זה עובד - לולאת הפעלת כלים בואו נתחיל עם תוכנית ראשונה כמעט העתקה מהדוגמה בתיעוד ששואלת מודל מה מזג האוויר ונותנת לו כלי באמצעותו יוכל לענות:
from typing_extensions import TypedDict
from agents import Agent, function_tool, Runner
from agents.extensions.models.litellm_model import LitellmModel

import requests
import os
import asyncio

weather_api_key = os.getenv("OPENWEATHER_API_KEY")
if not weather_api_key:
    raise "Error: OpenWeather API key not found in environment variables"

class Location(TypedDict):
    lat: float
    long: float

@function_tool
async def fetch_weather(location: Location) -> str:
    base_url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        "lat": location["lat"],
        "lon": location["long"],
        "appid": weather_api_key,
        "units": "metric"
    }

    # Run the synchronous requests call in a thread pool
    loop = asyncio.get_event_loop()
    response = await loop.run_in_executor(None, requests.get, base_url, params)
    response.raise_for_status()

    data = response.json()
    weather_desc = data["weather"][0]["description"]
    temp = data["main"]["temp"]
    feels_like = data["main"]["feels_like"]
    humidity = data["main"]["humidity"]

    return f"Weather: {weather_desc.title()}, Temperature: {temp}°C (feels like {feels_like}°C), Humidity: {humidity}%"

agent = Agent(
    name="Assistant",
    model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
    tools=[fetch_weather],
)

async def main():
    result = await Runner.run(agent, "What is the weather in Tel Aviv?")
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
הפעלה של התוכנית אצלי מדפיסה את הפלט:
The weather in Tel Aviv is currently clear with a temperature of 37.05°C (feels like 40.16°C) and humidity at 37%. It's quite hot outside!
אבל בואו נתמקד בכלי ובהפעלת כלי - מה זה אומר שהמודל ״מפעיל כלי״? איך מודל יכול להפעיל כלי? ואיך המודל יודע איזה כלים צריך להפעיל? התשובה היא שהמודל מתקשר עם הקוד שלנו בממשק טקסטואלי בלבד - אנחנו שולחים הודעת טקסט ומקבלים בחזרה טקסט. הטקסט שהמודל מחזיר הוא ההמשך הסביר ביותר ל"שיחה" שאת תחילתה אנחנו שולחים. כך עובדים מודלי שפה גדולים. כשמדובר בכלים יש פה שיתוף פעולה בין כמה רכיבים במערכת: 1. המודל עצמו מאומן בצורה שהודעות מסוג מסוים יובילו לתשובות מסוג מסוים. בפרט בנושא של כלים אם ההודעה מגיעה במבנה מסוים שמכיל שם של כלי ורשימת פרמטרים המודל מאומן לענות בפורמט מתאים מסוים - שנראה כמו JSON עם פרטים על הכלי אותו רוצים להפעיל ורשימת הפרמטרים שלו. 2. ספריית הסוכנים OpenAI Agents מזהה את פורמט התשובה של מודל שמעוניין להפעיל כלי ומפעילה את הפונקציה שהעברתי, בדוגמה שראינו זו פונקציית חישוב מזג האוויר. את התוצאה היא מעבירה חזרה למודל. בממשק העברת כלים המודל מבקש הפעלת כלי ומצרף לבקשה מזהה אקראי, וספריית הסוכנים מצרפת את אותו מזהה בדיוק לתשובה. 3. המודל יכול להחליף להפעיל את הכלי שוב, כלומר להחזיר שוב פעם טקסט שמייצג הפעלת כלי, או לעצור ולהחזיר הודעת סיום שמסכמת את התוצאה של הכלי. נמשיך לחקור - אני מוסיף SQLite Session לתוכנית כדי שיהיה תיעוד לכל ההודעות ומפעיל את הקוד המעודכן:
async def main():
    session = SQLiteSession("weather", "weather.db")
    result = await Runner.run(agent, "What is the weather in Tel Aviv?", session=session)
    print(result.final_output)
עכשיו נוצר לי קובץ חדש בתיקייה בשם weather.db עם כל ההודעות. זה התוכן:
sqlite> select * from agent_messages ;
1|weather|{"content": "What is the weather in Tel Aviv?", "role": "user"}|2025-08-14 09:54:56

ToCode
1 419
name="Assistant",
    tools=[
        WebSearchTool(),
    ],
)

async def main():
    result = await Runner.run(agent, "Which coffee shop should I go to, taking into account the weather today in SF?")
    print(result.final_output)


if __name__ == "__main__":
    asyncio.run(main())
    
המודל מפעיל את כלי החיפוש המובנה, מחפש בתי קפה בסאן פרנסיסקו ומחזיר רשימה של המלצות. כל זה קורה באופן אוטומטי ומאוד נוח. הבעיה היחידה היא המחיר כי חיפוש ברשת לוקח הרבה זמן והרבה טוקנים. הרבה פעמים מפתחים שצריכים לשלב חיפוש ברשת עם מערכת משולבת סוכן משתמשים בכלי חיפוש מתחרים וייעודיים לסוכנים לדוגמה מנוע החיפוש תביא-לי שמאפשר לחפש ברשת באמצעות קוד כזה:
from tavily import TavilyClient
tavily_client = TavilyClient(api_key="tvly-YOUR_API_KEY")
response = tavily_client.search("Who is Leo Messi?")
print(response)
מערכת כזאת תגדיר כלי חיפושי מקומי בשם "תביא-לי" שישתמש ב API של תביא-לי כדי לבצע את החיפוש. עכשיו אתם חישבו - איזה עוד כלים לדעתכם אנשים צריכים וקשה לכתוב או להריץ בצורה מקומית?

ToCode
1 419
יום 11 - כלים מובנים ב API ספקי הגישה למודלים כמו OpenAI ו Anthropic שמו לב למספר אתגרים של מפתחים שבונים מערכות מבוססות סוכנים הקשורים להפעלת כלים ומהווים הזדמנות עסקית לאותם ספקי מודלים: 1. אם המודל כותב קוד ורוצה להריץ אותו, מישהו צריך להרים סביבה וירטואלית בה המודל יוכל להריץ את הקוד בלי לעשות נזק. 2. אם המודל רוצה לחפש ברשת, מישהו צריך לתת לו גישה למנוע חיפוש, אבל מנועי חיפוש כמו גוגל לא מספקים API ולא אוהבים שבוטים מריצים דרכם חיפושים (כי בוטים לא רואים פרסומות). 3. אם המודל רוצה ליצור תמונה יש להשתמש במנגנון AI אחר שיוצר תמונות. ראינו שאנחנו יכולים להעביר כל פונקציית פייתון למודל בתור Tool והמודל יריץ אותה אם היא תעזור לענות על הפרומפט, אבל עבור שלושת הכלים המתוארים למעלה כתיבת ה"כלי", כלומר כתיבה והרצה של הפונקציה עצמה שמריצה קוד, מחפשת ברשת או יוצרת תמונה זה אתגר. כלי צד שרת או כלים מובנים הם הדרך של אותן ספקיות מודלים לעזור בשלושת האתגרים האלה ובאתגרים נוספים דומים. דוגמה 1 - כלי הרצת קוד בואו נכתוב דוגמה ראשונה עם כלי הרצת קוד, אבל לפני שאשתמש בכלי המובנה של OpenAI אני רוצה לכתוב את הדוגמה עם כלי הרצת קוד עצמאי שאני כותב ולדבר על הבעיות שבו. זאת התוכנית:
import asyncio

from agents import Agent, Runner

import json

from typing_extensions import TypedDict, Any
import os
from agents import Agent, FunctionTool, RunContextWrapper, function_tool
from agents.extensions.models.litellm_model import LitellmModel

@function_tool
async def run_python(code: str) -> str:
    """Run provided python code and return the value of "result" global variable as string.

    Args:
        code: python code to execute

    Example input to return the value of 5 + 7:
        x = 5
        y = 7
        result = x + y
    """
    print(f"Running Code: \n\n{code}")
    ns = {}
    exec(code, ns)
    print(f"Result = {ns['result']}")
    return str(ns['result'])

agent = Agent(
    name="Assistant",
    model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
    tools=[run_python],
)

async def main():
    result = await Runner.run(agent, "What is the current os.name ? use Python to find out")
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
הקוד עובד - המודל כתב את התוכנית:
import os
result = os.name
הקוד שלי הריץ אותה עם exec ובסוף החזיר את ערך המשתנה result. אבל גם קל לראות את הבעיות: 1. אני לא מטפל בשגיאות תחביר או בעיות סביבת ריצה. כלי נכון יותר היה מזהה אם יש שגיאה ומעביר אותה למודל כדי שהמודל יכתוב תוכנית אחרת. 2. יותר גרוע - הכלי רץ אצלי על המכונה או במערכת אמיתית על שרת הפרודקשן. מודלים הם לא יצורים אמינים ותקלה או פרומפט זדוני יכולים לעשות הרבה נזק. עכשיו בואו נראה איך זה עובד עם הכלי המובנה של OpenAI. זה הקוד המעודכן:
import asyncio
from agents import Agent, Runner, CodeInterpreterTool

agent = Agent(
    name="Assistant",
    tools=[CodeInterpreterTool(
        tool_config={
            "type": "code_interpreter",
            "container": { "type": "auto" }
        }
    )],
)

async def main():
    result = await Runner.run(agent, "What is the current os.name ? use Python to find out")
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
הקוד נראה הרבה יותר טוב! זה מה שקורה הפעם: 1. כלי Code Interpreter רץ בענן. אני מעביר קונפיגורציה שאומרת שכל פעם שהוא צריך להריץ קוד הוא יכול ליצור לעצמו קונטיינר. 2. בשביל להשתמש בכלי הרצת הקוד אני כבר לא יכול לעבוד דרך LiteLLM והמודל של גיטהאב וחייב להיכנס למודל בתשלום של OpenAI. גם הבקשה עצמה לוקחת הרבה יותר זמן ויותר טוקנים. 3. הקוד רץ בענן של OpenAI על קונטיינר מנותק מהשרת שלי ואני מקבל רק את תשובת המודל. 4. ל OpenAI יש API שלם לעבודה עם קבצים וקונטיינרים כך שמשתמשים מתקדמים יכולים ליצור בעצמם את הקונטיינר, להעלות אליו קבצים, לתת למודל לכתוב קבצים חדשים ולהריץ קוד ובסוף להוריד קבצי תוצאה. דוגמה 2 - חיפוש ברשת כלי מובנה שני הוא כלי החיפוש ברשת. בואו נראה אותו באמצעות הדוגמה הבאה:
import asyncio

from agents import Agent, Runner, WebSearchTool

agent = Agent(

ToCode
1 419
יום 10 - פיתוח סוכן AI בתור בוט לטלגרם עוד ממשק משתמש מעניין לבוטים הוא אפלקיציות המסרים בטלפון. למרות שכולם אוהבים לדבר בווטסאפ כתיבת בוטים לטלגרם זה הרבה יותר קל, נגיש וחינמי ולכן הדוגמה היום היא על טלגרם. בואו נראה איך לכתוב בוט AI שגם ממשיך שיחות ותמיד זמין עבורכם בטלגרם. איך לכתוב בוט לטלגרם אני חוזר לפייתון ובשביל לכתוב בוט לטלגרם נוכל להשתמש בספריה python-telegram-bot. זו דוגמה לבוט טלגרם ראשון מתוך התיעוד שלהם:
from telegram import ForceReply, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Send a message when the command /start is issued."""
    user = update.effective_user
    await update.message.reply_html(
        rf"Hi {user.mention_html()}!",
        reply_markup=ForceReply(selective=True),
    )


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Echo the user message."""
    await update.message.reply_text(update.message.text)


def main() -> None:
    token = os.environ["TELEGRAM_BOT_TOKEN"]
    application = Application.builder().token(token).build()

    application.add_handler(CommandHandler("start", start))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))

    application.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()
בשביל שהבוט יעבוד עליי להגדיר משתנה סביבה בשם TELEGRAM_BOT_TOKEN ובו לכתוב את הטוקן של הבוט שלי. לכל בוט בטלגרם יש טוקן שמזהה אותו ואתם מקבלים את הטוקן ביצירת בוט חדש. איך יוצרים בוט חדש אתם שואלים? הכי קל בעולם, שולחים הודעה ל @BotFather עם הפקודה /newbot עונים על כמה שאלות ומקבלים טוקן. את הטוקן שימו במשתנה סביבה ואז תוכלו להריץ את הבוט בקוד שלמעלה. תראו שזה עובד ואתם יכולים לכתוב לבוט משהו ולקבל בחזרה את אותו טקסט ואז נמשיך לחבר את הסוכן. שילוב AI בשביל להפוך בוט טלגרם לסוכן AI יש לנו שני אתגרים: 1. סוכן AI צריך להכיר את ההודעות הישנות. למדנו איך לשמור הודעות ישנות עם אוביקט Session ואפילו איך לשמור את ה sessions האלה בבסיס נתונים. נרצה כזה בסיס נתונים להודעות מהבוט שלנו. 2. לסוכן AI יכול לקחת זמן לענות. בעבודה ב web ראינו איך להשתמש בממשק הזרמת טקסט כדי להראות את הטקסט נכתב בזמן שהוא נוצר. בטלגרם אין מנגנון הזרמה אבל כן אפשר להציג את האייקון שאומר שהבוט כותב. נשלב את שני הדברים לפונקציה echo:
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Echo the user message."""
    chat_id = update.effective_chat.id
    session_id = f"chat_{chat_id}"
    session = SQLiteSession(session_id, "bot.sql")

    await update.message.reply_chat_action(ChatAction.TYPING)

    result = await Runner.run(
        agent,
        update.message.text,
        session=session
    )
    await update.message.reply_text(result.final_output)
כל שיחה בטלגרם מגיעה עם chat_id שמזהה את השיחה. זה בדיוק מזהה טוב לשיחה שלי בבסיס הנתונים. אני מגדיר session מתוך אותו chat_id ושומר אותו בבסיס נתונים בקובץ בשם bot.sql. הפעולה:
await update.message.reply_chat_action(ChatAction.TYPING)
מגדירה לטלגרם שצריך להציג שהבוט מקליד תשובה ובאופן אוטומטי פעולה זו תסתיים כשנשלח את התגובה המסודרת אחרי שהסוכן יסיים לחשב אותה. עכשיו אתם בעיה אחת בקוד הזה היא שה Sessions יהיו מאוד ארוכים לאורך זמן. ככל שנדבר יותר עם ה AI כך יהיו יותר הודעות ב Session ואז כל הודעה לסוכן תכיל יותר מידע (יותר הודעות ישנות), מה שיגרום למחיר של כל הודעה לעלות ככל שהשיחה מתארכת. חקרו את בסיס הנתונים בקובץ bot.sql שנוצר. חשבו - איך הייתם מגבילים את מספר ההודעות ב session? כמה הודעות אחרונות לדעתכם כדאי לשמור או לשלוח?

ToCode
1 419
content: prev.at(-1)!.content + decoder.decode(value),
          role: prev.at(-1)!.role,
        }));
      }
    } catch (error) {
      console.error('Streaming error:', error);
    }
  }

  return (
    <div >
      <main >
        <ul>
          {messages.map(msg => (
            <li>{msg.role}: {msg.content}</li>
          ))}
        </ul>
        <form onSubmit={sendMessage}>
          <input type="text" name="message" />
          <input type="submit" value="Send" />
        </form>
      </main>
    </div>
  );
}
יש פה הרבה דברים חדשים אז נעבור עליהם אחד אחד: 1. אני שומר בסטייט מערך של הודעות. כל הודעה מכילה את התוכן (content) ומי כתב אותה (role). 2. כשמשתמש מגיש את הטופס אני מוסיף למערך ההודעות שתי הודעות, הודעה מהמשתמש עם התוכן שלקחתי מהטופס והודעה ריקה של ה AI. עוד מעט השרת יתחיל להזרים לי את תוכן ההודעה ואני אשתמש בו כדי למלא את משתנה הסטייט. 3. כשאני מקבל הזרמה מהשרת אני מעדכן את ההודעה האחרונה (הריקה שאני יצרתי) עם התוכן:
const { done, value } = await reader.read();
if (done) break;
setMessages(prev => prev.with(-1, {
  content: prev.at(-1)!.content + decoder.decode(value),
  role: prev.at(-1)!.role,
}));
יצירת הודעת הסוכן ריקה מראש חוסכת לי טיפול מיוחד בהודעה הראשונה. עכשיו נעבור לצד השרת - השרת מקבל הפעם מערך של הודעות ועליו להפעיל את הסוכן עם כל ההיסטוריה. ב SDK של טייפסקריפט יש שתי פונקציות של הספריה בהן עלינו להשתמש כדי "להפוך" את אובייקטי ההודעות שלנו לאוביקטי הודעות שהסוכן צריך. אנחנו מקבלים מהלקוח אוביקט הודעה שנראה כך:
{ role: 'user', content: 'hello' }
אבל בשביל מערך ההודעות שיישלח לסוכן אנחנו צריכים להעביר הודעה שנראית כך:
{ role: 'user', content: [
    { type: 'input_text', text: 'hello' }
] }
התוכן של הודעה מורכב ממערך של "פריטים" שיכולים להגיע בסוגים שונים - כן יש טקסט אבל יכולות להיות גם תמונות וקבצים. שתי פונקציות ההמרה נקראות user ו assistant ולכן בקוד הנתיב בצד השרת עליי לכתוב:
const messages = await request.json();

const messagesForAgent = messages.map((m: any) => 
  (m.role === 'user' ? user(m.content) : assistant(m.content)))
כל השאר נשאר דומה לדוגמה הקודמת וקוד צד השרת המלא בקובץ route.ts הוא:
import { Agent, run, user, assistant } from '@openai/agents';

const agent = new Agent({
  name: 'Assistant',
  instructions: 'You are a helpful assistant',
});

export async function POST(request: Request) {
  const messages = await request.json();

  const messagesForAgent = messages.map((m: any) => 
    (m.role === 'user' ? user(m.content) : assistant(m.content)))

  console.log(messagesForAgent);

  const result = await run(agent, messagesForAgent, {
    stream: true,
  });

  // Convert the result to a standard ReadableStream
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      
      try {
        for await (const event of result) {
          if (event.type === 'raw_model_stream_event') {
            if (event.data.type === "output_text_delta") {
              controller.enqueue(encoder.encode(event.data.delta));
            }                        
          }          
        }
      } catch (error) {
        controller.error(error);
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Transfer-Encoding': 'chunked',
    },
  });
}
אתם יכולים למצוא את הקוד המלא של הדוגמה במאגר הגיט: https://github.com/ynonp/next-openai-agents-demo עכשיו אתם מנגנון ה Tools קיים גם בגירסת הטייפסקריפט בדומה לגירסת הפייתון. קראו את מדריך ה Tools כאן: https://openai.github.io/openai-agents-js/guides/tools/ והוסיפו לסוכן יכולת לחפש ברשת. שימו לב איך נראית הודעה של שימוש בכלי ונסו לחשוב על אפקט ממשק משתמש יפה שתוכלו ליישם להודעות אלה.