ru
Feedback
ToCode

ToCode

Открыть в Telegram

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

Больше
1 418
Подписчики
Нет данных24 часа
-17 дней
-430 день
Архив постов
ToCode
1 417
[{'role': 'user', 'content': 'What shells are installed?'}, {'arguments': '{}', 'call_id': 'call_LLe5SmhcYkLyouuo9lGwkgch', 'name': 'read_shells_file', 'type': 'function_call', 'id': 'fc_688e2ba2fe94819b990373d3ad78a90709782c5a467ec704', 'status': 'completed'}, {'call_id': 'call_LLe5SmhcYkLyouuo9lGwkgch', 'output': '# List of acceptable shells for chpass(1).\n# Ftpd will not allow users to connect who are not using\n# one of these shells.\n\n/bin/bash\n/bin/csh\n/bin/dash\n/bin/ksh\n/bin/sh\n/bin/tcsh\n/bin/zsh\n/usr/local/bin/pwsh\n/usr/local/bin/bash\n/usr/local/bin/zsh\n', 'type': 'function_call_output'}, {'id': 'msg_688e2ba416c4819b94d498d53c029cbd09782c5a467ec704', 'content': [{'annotations': [], 'text': 'The installed shells listed in the \/etc/shells\ file are:\n\n1. \/bin/bash\\n2. \/bin/csh\\n3. \/bin/dash\\n4. \/bin/ksh\\n5. \/bin/sh\\n6. \/bin/tcsh\\n7. \/bin/zsh\\n8. \/usr/local/bin/pwsh\\n9. \/usr/local/bin/bash\\n10. \/usr/local/bin/zsh\', 'type': 'output_text', 'logprobs': []}], 'role': 'assistant', 'status': 'completed', 'type': 'message'}]
The installed shells listed in the \/etc/shells\ file are:

1. \/bin/bash\
2. \/bin/csh\
3. \/bin/dash\
4. \/bin/ksh\
5. \/bin/sh\
6. \/bin/tcsh\
7. \/bin/zsh\
8. \/usr/local/bin/pwsh\
9. \/usr/local/bin/bash\
10. \/usr/local/bin/zsh\

Process finished with exit code 0
תחילה אני רואה את שורת ההדפסה Tool Call שמבהירה לי שהפונקציה שלי נקראה. אחר כך מודפסת רשימת כל ההודעות שמבהירה לי מה קרה. אני יכול לזהות שם: 1. הודעה מהמשתמש עם השאלה שלי. 2. הודעה מהסוכן שמבקש להפעיל את הפונקציה. 3. הודעה אוטומטית מה SDK עם התוצאה של הפונקציה. 4. ההודעה האחרונה של הסוכן שמסכמת את התשובה לשאלה. בהמשך הסידרה נראה עוד דוגמאות להפעלת כלים וגם נכיר את הכלים המובנים ב SDK. בינתיים כמה תרגילים. עכשיו אתם 1. הוסיפו פרמטרים לפונקציה. נסו לתת לסוכן כלי שיוכל לקרוא כל קובץ על הדיסק שלכם. חשבו: האם יש בעיית אבטחה בכלי כזה? איזה כלים לא כדאי לכתוב? 2. העבירו לסוכן שני כלים, אחד שיוצר קובץ ריק ואחד שיוצר תיקייה. בקשו מהסוכן ליצור תיקייה ובדקו שהיא נוצרה. 3. החליפו את התיאור של שני הסוכנים ובקשו מהסוכן ליצור תיקייה. שימו לב שהפעם הופעל הכלי שיוצר קובץ (כי התיאור שלו אמר שהוא יוצר תיקייה) ונוצר קובץ. 4. הפעילו את הקוד הבא:
import asyncio
from agents import Agent, Runner, function_tool

@function_tool()
def create_empty_file(filename: str):
    """
    Creates a new empty file
    :param filename: the filename to check
    """
    print("Tool Call: create_empty_file")

@function_tool()
def check_file_exists(filename: str) -> bool:
    """
    Check if a file exists
    :param filename: the filename to check
    :return: True if exists
    """
    print("Tool Call: check_file_exists")
    return False

async def main():
    agent = Agent(
        name="Assistant",
        tools=[create_empty_file, check_file_exists],
        instructions="You are a mission critical filesystem agent. Some operations may fail. You need to verify every action worked and retry failed operations",
    )

    result = await Runner.run(agent, "Create an empty file named test.txt")
    print(result.to_input_list())
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
מה אתם מצפים שיקרה? מה קרה? נסו את זה עם מודלים שונים ושימו לב אם ההתנהגות שונה ביניהם.

ToCode
1 417
יום 5 - שימוש בכלים זוכרים את השורה-
result = await Runner.run(agent, user_message)
דיברנו על זה שהיא שולחת רשימת הודעות ממשתמש למודל AI, אבל בעצם מדובר בלולאה - לולאת הפעלת הכלים. מה זה הפעלת כלים אנחנו יודעים איך להעביר מידע בדחיפה ל AI במערכת משולבת AI - פשוט מוסיפים את המידע הזה לרשימת ההודעות. זה מה שעשינו כבר עם ההודעה מהמשתמש, וקל לראות איך זה עובד עם מידע נוסף, לדוגמה תוכן של קובץ:
async def main():
    agent = Agent(
        name="Assistant",
        instructions="Answer questions about the file /etc/shells",
    )

    file_content = open('/etc/shells', 'r').read()
    result = await Runner.run(agent, [
        {"role": "user", "content": f"File Contents: {file_content}"},
        {"role": "user", "content": "What shells are installed?"}
    ])
    print(result.final_output)
אבל מה לגבי הכיוון ההפוך? מה אם התשובה לשאלה מוחבאת בתוך קובץ וה AI מצליח לזהות את זה? פה נכנס המנגנון של הפעלת כלים והוא עובד כך: 1. אנחנו שולחים סידרת הודעות לסוכן ומבקשים השלמה. סידרת ההודעות כוללת הודעה שמפרטת רשימה של "פונקציות" שהסוכן יכול לבקש להפעיל. 2. הסוכן יודע לשלוח רק תשובה אחת. בגלל ששלחנו רשימה של פונקציות התשובה עשויה להיות בקשה להפעלת אחת או יותר מפונקציות אלה - זוהי בקשה להפעלת כלים. 3. הקוד שקרא ל API צריך לזהות את הבקשה, להפעיל את הפונקציה ולפנות שוב לסוכן עם התשובה. הפעם רשימת ההודעות תכלול את התשובה הקודמת של הסוכן שמבקשת להפעיל כלי, ואז הודעה עם תוצאת הכלי. 4. עכשיו הסוכן צריך להוסיף הודעה. זו יכולה להיות הודעת סיכום עם תשובה או הודעה עם בקשה להפעלת כלי נוסף. ביצירת הסוכן אנחנו יכולים לבחור את מדיניות הפעלת הכלים, עד מתי להמשיך את הלולאה וגם איזה כלים לאפשר לסוכן להריץ. בואו נראה איך זה עובד באמצעות היפוך הסדר, במקום שאני אעביר מראש את תוכן הקובץ אני אתן לסוכן את האפשרות לקרוא אותו. העברת כלי לסוכן כל סוכן יודע לקבל בבנאי מערך של כלים. כלי הוא בסך הכל פונקציה שמסומנת בתור "כלי" בעזרת דקורטור בשם function_tool מה SDK. התיעוד של הפונקציה עובר לסוכן בתור תיאור הכלי וכך הסוכן יודע באיזה מצבים להפעיל את אותה פונקציה. קוד? בטח:
import asyncio
from agents import Agent, Runner, function_tool

@function_tool()
def read_shells_file():
    """
    Reads the file /etc/shells and returns the full content
    """
    print("Tool Call: read_shells_file")
    with open('/etc/shells', 'r', encoding='utf8') as f:
        return f.read()

async def main():
    agent = Agent(
        name="Assistant",
        tools=[read_shells_file],
        instructions="Answer questions about the file /etc/shells",
    )

    result = await Runner.run(agent, "What shells are installed?")
    print(result.to_input_list())
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
הפונקציה בדוגמה לא מקבלת פרמטרים. התיעוד שלה מבהיר לסוכן שצריך להפעיל אותה כשרוצים לקבל את התוכן של הקובץ /etc/shells. מאחר וביקשנו ממנו לענות על שאלות על הקובץ ושלחנו שאלה על הקובץ הסוכן מבין שכדאי מאוד לקרוא את הקובץ כדי שיוכל לענות על השאלה. כשמפעילים את התוכנית מקבלים את הפלט הבא:
/Users/ynonp/work/projects/ai/leanagentssdk/.venv/bin/python /Users/ynonp/work/projects/ai/leanagentssdk/explain_file.py 
Tool Call: read_shells_file

ToCode
1 417
יום 4 - בואו נכתוב סוכן שיחה אתמול ראינו איך לכתוב תהליך עבודה שמורכב ממספר "סוכנים", כל אחד אחראי לחלק אחר של המשימה. היום נדבר על סוג שני של מערכות משולבות AI שהוא מערכות מבוססות שיחה. במערכות כאלה אין משימה ספציפית שצריך לבצע אלא אנחנו מגיעים לסוכן, שולחים הודעה והסוכן עונה או מבצע ואז מקשיב להמשך השיחה. דוגמאות למערכות כאלה יכולות להיות: 1. ממשק צ'ט עם סוכן חכם כמו באתר ChatGPT. 2. סוכן תמיכה חכם שעונה לפניות של משתמשים במקום נציג אנושי. 3. סוכן מכירות חכם שמתקשר למשתמש ומנהל שיחה כדי למכור מוצר. 4. מורה לאנגלית שמנהל עם משתמש שיעור אנגלית מובנה תוך כדי שיחה. לפני הכל - קוד נתחיל בדוגמת קוד לשיחה עם סוכן חכם משורת הפקודה:
import asyncio

from agents import Agent, Runner, SQLiteSession

async def main():
    agent = Agent(name="Assistant", instructions="Reply very concisely.")

    # Create session instance
    session = SQLiteSession("conversation_123")

    while True:
        user_message = input("> ")
        ai_response = await Runner.run(agent, user_message, session=session)
        print(ai_response.final_output)
        

if __name__ == '__main__':
    asyncio.run(main())
הריצו את הקוד עם משתנה הסביבה OPENAI_API_KEY מכוון למפתח ה API שלכם ותקבלו שיחה עם סוכן חכם. איך זה עובד שורת הקסם בתוכנית היא:
ai_response = await Runner.run(agent, user_message, session=session)
ממשק העבודה עם LLM הוא Stateless, כלומר כל הודעה ל ChatGPT או לכל AI שנבחר מתחילה מאפס. בממשק של סוכן אנחנו רגילים שהסוכן ממשיך את השיחה ובשביל זה הוא צריך להתבסס על ההודעות הישנות. ניהול ההודעות הישנות ושליחתן יחד עם ההודעה החדשה מבוצע בצד הלקוח ובספריית openai-agents זה מנוהל באמצעות מנגנון ה session. כדי להבין את המנגנון בואו נכתוב את הקוד בלי session:
import asyncio

from agents import Agent, Runner, SQLiteSession

async def main():
    agent = Agent(name="Assistant", instructions="Reply very concisely.")
    user_message = input("> ")
    result = await Runner.run(agent, user_message)
    print(result.final_output)

    while True:
        print(f"Debug: messages = {result.to_input_list()}")
        user_message = input("> ")
        result = await Runner.run(agent, result.to_input_list() + [{"role": "user", "content": user_message}])
        print(result.final_output)


if __name__ == '__main__':
    asyncio.run(main())
עכשיו זה ברור: 1. ממשק Runner.run יכול לקבל הודעה בודדת או רשימה של הודעות. 2. הממשק מחזיר אוביקט שמכיל מתודה to_input_list. מתודה זו מחזירה את כל ההודעות הישנות ואת ההודעה האחרונה שה AI השיב. 3. באמצעות שמירת אוביקט התוצאה ושליחת ההודעות הישנות שוב ושוב ל Runner.run אנחנו מקבלים חוויה של שיחה המנוהלת בצד הלקוח. מנגנון Sessions של ספריית הסוכנים מאפשר ניהול אוטומטי של העברת ההודעות הישנות. בדוגמה שראינו ההודעות נשמרות בבסיס נתונים SQLite שנשמר בזיכרון. בהמשך הסידרה נראה איך להשתמש במנגנון גם כדי לשמור את ההודעות בבסיס נתונים אמיתי וגם לערוך הודעות ישנות. עכשיו אתם אחד הדברים שהופכים סוכנים ל"ייחודיים" הוא הודעות המערכת, בדוגמאות הקוד שכתבנו זה מאפיין ה instructions שאנחנו מעבירים לסוכן:
agent = Agent(name="Assistant", instructions="Reply very concisely.")
עדכנו את התוכנית ושנו את מחרוזת ה instructions כדי לייצר סוכנים מסוגים שונים: סוכן שמלמד אנגלית, סוכן מכירות או יועץ לאורח חיים בריא.

ToCode
1 417
title: str = Field(..., title="Title", description="The title of the blog post"),
    content: str = Field(..., title="Content", description="Actual blog post content in markdown format")

async def main(general_topic: str):
    market_research_agent = Agent(
        name="MarketResearcher",
        model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
        output_type=list[BlogPostIdea],
        instructions="""
        You are a market researcher and your job is to suggest cool ideas for blog posts.
        I will send you ideas and you will help me turn them into engaging posts,
        Or I will send you topics and you will help to focus me on the best viral ideas in these niches.        
        """
    )

    result = await Runner.run(market_research_agent, f"Create 5 blog posts subject lines and main concept for: {general_topic}")
    selected_idea = random.sample(result.final_output, 1)[0]

    writer = Agent(
        name="Writer",
        model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
        instructions="""
        You are a copywriter creating engaging and viral blog posts.
        """,
    )
    post = await Runner.run(writer, f"Create a blog post from the following data: {selected_idea.model_dump()}")
    print(post.final_output)

if __name__ == "__main__":
     asyncio.run(main("dogs"))
בשביל להריץ אותו אתם צריכים להגדיר את GITHUB_TOKEN כמו שראינו אתמול או לבחור מודל אחר מ LitellmModel עם מפתח API אחר לבחירתכם. אלה הדברים החשובים בתוכנית: 1. אני יוצר שני סוכנים, כל אחד עם פרומפט הוראות משלו. סוכן חקר השוק אחראי על העלאת רעיונות לפוסטים וסוכן הכתיבה אחראי על כתיבת התוכן. 2. המידע שחוזר מכל סוכן הוא מידע מובנה ולכן אני יכול לבחור פריט אחד באקראי מתוך רשימת פוסטים ולא צריך לפענח את הטקסט שה LLM החזיר. ספריית Agents SDK עושה את הפיענוח עבורי. 3. לסוכן אין זיכרון או מצב פנימי. כל פעם שאני מפעיל Runner.run הוא פונה מחדש למודל השפה הגדול עם הפרומפט שכתבתי ועם פרומפט המערכת ומקבל תשובה. כל בקשה עומדת בפני עצמה. 4. ספריית OpenAI Agents SDK עדיין לא תומכת בהעברת מידע מובנה בתור קלט לפרומפט - זו הסיבה שהשתמשתי ב selected_idea.model_dump() בתוך מחרוזת הפרומפט. פקודה זו הופכת את האוביקט למילון רגיל בפייתון. הפרומפט שמודל השפה הגדול יקבל יכול להיות לכן:
Create a blog post from the following data: {'title': '10 Surprising Ways Dogs Make Our Lives Better', 'main_concepts': ['Emotional and physical benefits of owning a dog', 'How dogs improve mental health', "Lesser-known facts about dogs' positive influence"]}
עכשיו אתם רוצים לשחק עם הדוגמה? מצוין! זו אחלה דרך ללמוד. הוסיפו סוכן "עורך" שישפר את הטקסט ויהפוך אותו ליותר מעניין וגם סוכן "מדרג" שמקבל את הרעיונות ומדרג אותם כדי לבחור את הפוסט המעניין ביותר.

ToCode
1 417
יום 3 - דוגמה לתהליך עבודה של שני סוכנים בפוסט היום נכתוב תהליך עבודה ליצירת פוסט לבלוג. התהליך ישלב שני סוכנים וגם קוד פייתון רגיל ודרך הדוגמה נלמד איך נראה תהליך אוטומטי מבוסס AI, מה סוג החיבור שאנחנו צריכים בין קוד ה AI לקוד הפייתון ואיפה משתלבת ספריית הסוכנים. מה אנחנו בונים בשביל להתחיל לדבר על תהליכי עבודה משולבי AI אני רוצה לאפיין תהליך פשוט של יצירת פוסט לבלוג המורכב משני סוכנים וקוד פייתון באמצע. שלבי התהליך: 1. מודל שפה גדול מקבל משימה למצוא רעיונות לפוסטים. הוא מחזיר רשימה של 5 רעיונות. 2. פונקציית פייתון בוחרת את אחד הרעיונות, במקרה שלנו זו פשוט בחירה באקראי אבל בעולם האמיתי אולי היא תסתכל ב DB להבין איזה פוסטים עדיין לא כתבתי או תחפש ברשת או כל מימוש אחר. 3. אחרי שהחלטנו על נושא מודל שפה גדול (יכול להיות אותו מודל או מודל אחר) מקבל את המשימה לכתוב פוסט מפורט לבלוג על נושא זה. 4. קוד פייתון מקבל את הפוסט ומפרסם אותו. בדוגמה שלנו ה"פרסום" זה בסך הכל הדפסה למסך, אבל נוכל לדמיין שאנחנו מעלים את הפוסט לבלוג. נשים לב לסעיפים 1 ו-3 ברשימה שלי: 1. שני הסעיפים מבצעים משימה באמצעות מודל שפה גדול, אבל הם לא חייבים להשתמש באותו מודל. למעשה הרבה פעמים בכתיבת תהליך עבודה נרצה לבחור לכל חלק בתהליך את המודל הטוב ביותר עבורו, וזה לא תמיד יהיה אותו מודל. אין מודל אחד הכי טוב שעושה הכל ובבניית מערכת אג'נטית אנחנו משחקים, מנסים ובוחרים ולפעמים גם מתחרטים. 2. אחרי שמודל השפה הגדול החליט על רעיונות לפוסטים אני צריך לקבל את המידע שחזר ולעבוד איתו מתוך פייתון. יהיה לי נוח אם המידע הזה יגיע בתור מבנה נתונים פייתונאי רגיל. וכבר אנחנו רואים את התפקיד של ספריית OpenAI Agents SDK במערכת שלנו. הספריה תאפשר לי להגדיר שני "סוכנים", לבחור לכל סוכן מודל שפה גדול שמתאים לו, לבחור לכל סוכן פרומפט מערכת שונה שמתאים לו והכי חשוב לקבל מהמודל מידע מובנה בפורמט של מחלקה בפייתון. הגדרת טיפוס חזרה ספריית OpenAI Agents SDK אבל גם ספריות רבות אחרות לעבודה עם מודלי שפה גדולים בפייתון משתמשת ב Pydantic כדי להגדיר מידע מובנה שאנחנו מצפים לקבל חזרה מהמודל. דוגמה ראשונה לקלאס פידנטיק עבור תהליך כתיבת הפוסט תהיה:
from pydantic import BaseModel, Field

class BlogPostIdea(BaseModel):
    title: str = Field(..., title="Title", description="The title of the blog post"),
    main_concepts: list[str] = Field(..., title="Main Concepts", description="Main concepts for the post")
פידנטיק היא ספריית פייתון לתיאור ואימות מבני נתונים. היא מאפשרת לי להגדיר מחלקה ולקבוע שיהיו בה שני שדות, title ו main_concepts, לתאר מה הסוג של כל שדה וגם מה תפקידו. כל התיאורים האלה עוזרים למודל השפה הגדול להבין איך "למלא" את מבני הנתונים. מודלי שפה גדולים מודרניים מאומנים ליצור מידע מובנה וספריות העבודה איתם עוטפות את ההתנהגות הזו כך שאנחנו צריכים רק "לבקש" לקבל חזרה מידע בתור מחלקת pydantic וזה מה שהמודל יחזיר. בשביל להשתמש בטיפוס החזרה בתוך סוכן הרעיונות לפוסטים שלי אני כותב את הסוכן באופן הבא:
    market_research_agent = Agent(
        name="MarketResearcher",
        model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
        output_type=list[BlogPostIdea],
        instructions="""
        You are a market researcher and your job is to suggest cool ideas for blog posts.
        I will send you ideas and you will help me turn them into engaging posts,
        Or I will send you topics and you will help to focus me on the best viral ideas in these niches.
        """
    )
בתוך אוביקט האיתחול אני מעביר בפרמטר output_type את טיפוס הנתונים שאני רוצה לקבל חזרה. זה מספיק. עכשיו כשאני אקרא ל Runner.run אני אקבל בחזרה משהו שמתאים לטיפוס שהגדרתי. קוד התוכנית אנחנו מוכנים עכשיו לראות את קוד התוכנית המלא:
import asyncio
from agents import Agent, Runner
from agents.extensions.models.litellm_model import LitellmModel
import os
from pydantic import BaseModel, Field
import random
class BlogPostIdea(BaseModel):
    title: str = Field(..., title="Title", description="The title of the blog post"),
    main_concepts: list[str] = Field(..., title="Main Concepts", description="Main concepts for the post")

class BlogPost(BaseModel):

ToCode
1 417
בשביל להריץ את הקוד עלינו להתקין את החבילות וגם לקבל מפתח API ל OpenAI. את החבילות אני מתקין עם:
$ pip install "openai-agents" "openai-agents[litellm]"
ובדרך כלל נרצה לבצע את ההתקנה בתוך סביבה וירטואלית. בדוגמת הקוד שלי השתמשתי במודל של גיטהאב, או יותר נכון במודל של OpenAI וניגשתי אליו דרך מערכת המודלים של גיטהאב. עשיתי זאת כדי לקבל גישה בחינם למודל לצורך בדיקות. בעצם OpenAI דורשים תשלום על שימוש במודל כבר מהטוקן הראשון ולכן שימוש ישיר במודל דרך ה API שלהם מצריך יצירת API Key והזנת פרטי אשראי באתר של OpenAI. אנחנו פה עדיין בשלב הלימוד ולכן עדיף להתנסות בלי לשלם. גיטהאב מוכנים לשלם על הטוקנים שלכם בתקופת הלימוד עם הגבלה של כמה עשרות בקשות ביום. בשביל להריץ את הקוד דרכם נצטרך להוציא מפתח גישה דרך הקישור הזה: https://github.com/settings/personal-access-tokens לוחצים על הכפתור Generate New Token ומקפידים לסמן גישת קריאה ל Github Models. את הטוקן שקיבלתם מגדירים בתור משתנה סביבה GITHUB_TOKEN בעת הרצת תוכנית הפייתון. קריאת ה API הראשונה שלי ב TypeScript אם צד השרת שלכם כתוב ב TypeScript או JavaScript תשמחו לשמוע שיש מימוש מקביל לספריית ה OpenAI Agents SDK בטייפסקריפט. גם מימוש זה מאפשר להשתמש גם במודלים אחרים אבל לצערי נכון לכתיבת הפוסט החיבור למודלים של גיטהאב עדיין לא עובד. כאן אפשר לראות רשימה של כל ה Providers שכן אפשר לשלב: https://ai-sdk.dev/providers/ai-sdk-providers מאחר ולא היה לי מודל חינם לניסויים לקחתי את המודל בתשלום של OpenAI וכתבתי את התוכנית הבאה:
import { Agent, run } from 'npm:@openai/agents';

const agent = new Agent({
  name: 'History Tutor',
  model: 'gpt-4.1-nano',
  instructions:
    'You provide assistance with historical queries. Explain important events and context clearly.',
});

const result = await run(agent, 'When did sharks first appear?');

console.log(result.finalOutput);
בשביל להריץ אותה הגדרתי במשתנה הסביבה OPENAI_API_KEY את מפתח הגישה של OpenAI שהוצאתי (בתשלום). אתם יכולים גם לקבל אחד בקישור הזה: https://platform.openai.com/api-keys אחרי שתגדירו תוכלו להריץ את קוד הטייפסקריפט עם:
$ deno run -A main.ts
עכשיו אתם רוצים להמשיך לחקור את תוכניות הדוגמה? הנה שני כיוונים: 1. התבוננו במידע שפקודת run מחזירה - מה אתם יכולים ללמוד על הבקשה? 2. חברו את הסוכן למודלים אחרים ושימו לב לתשובות השונות שכל מודל מחזיר.

ToCode
1 417
יום 2 - קריאת API ראשונה למודל שפה גדול הבסיס של כל מערכת אג'נטית היא היכולת לפנות למודל שפה גדול ולקבל תשובה או יותר נכון "השלמה". בפוסט זה נראה איך לפנות למודל שפה גדול דרך הספריה openai-agents-sdk. ספריות גישה למודלים לפני שניגש לקוד צריך לשאול - למה דרך ספריה? ואם כבר ספריה למה דווקא ספריה מבית היוצר של OpenAI? כל חברות ה AI מציעות את שירותיהן בתשלום דרך ממשק REST API. אפשר לגשת אליהן מכל לקוח שמדבר HTTP. בנוסף יש אינספור מוצרי קוד פתוח שעוטפים את הגישה לאותם הממשקים. הסיבה שלא נרצה להשתמש בממשק ה REST API הבסיסי כדי לדבר עם ספקי המודלים שלנו היא שכל ספק בנה ממשק קצת אחרת. למשל בשביל לקבל השלמה מ OpenAI צריך לפנות לנתיב:
https://api.openai.com/v1/chat/completions
אבל בשביל לקבל השלמה ממודל של אנטרופיק צריך לפנות לנתיב:
https://api.anthropic.com/v1/messages
כשפונים ל OpenAI אנחנו שולחים מערך של הודעות שלכל הודעה יש role ו content, אבל אם נפנה ל Gemini נצטרך להעביר מערך של parts כשלכל part יש text. הרעיון עצמו אצל כל הספקים זהה אבל המימושים השונים גורמים לזה שאם נעבוד עם ממשק ה REST API נצטרך לבצע שינויים משמעותיים בקוד כל פעם שנרצה להחליף ספק או מודל. ספריות הגישה בקוד פתוח מאפשרות לכתוב פעם אחת את קוד ה"שאלה" והספריה כבר מתאימה את הקוד לכל מודל שנבחר. לדוגמה הספריה LiteLLM מאפשרת לי לכתוב שורת פייתון אחת כדי לקבל השלמת טקסט:
response = completion(
  model="openai/gpt-4o",
  messages=[{ "content": "Hello, how are you?","role": "user"}]
)
וכשרוצים לעבוד עם מודל אחר פשוט משנים את המודל. הספריה כבר דואגת להעביר את הפרמטרים ולבחור את ה URL בצורה שמתאימה לספק ה AI. הרמה הבאה של אבסטרקציה היא ספריות שלא רק מאפשרות גישה למודלים אלא גם מכתיבות את מבנה התוכנית ומספקות מימושים לתבניות נפוצות בכתיבת מערכות משולבות AI. ספריות כאלה כוללות: 1. LangGraph - ספריית פייתון שעוזרת לבנות תוכנית משולבת AI במבנה של גרף. 2. CrewAI - ספריית פייתון שמאפשרת לחשוב על התוכנית שלנו בתור צוות של סוכני AI שמדברים ביניהם. 3. Vercel AI SDK - ספריית עבודה עם סוכני AI שגם מטפלת בחיבור לריאקט ועוזרת בבניית ממשק משתמש לבוטים. 4. Autogen - פריימוורק לפיתוח ארכיטקטורה מרובת סוכנים מבית מייקרוסופט ויש עוד המון וכל הזמן יוצאות ספריות חדשות. האמת היא שתחום פיתוח מערכות אג'נטיות הוא עדיין מאוד צעיר והתעשייה מחפשת רעיונות, כלים וספריות. בסידרת פוסטים זו אני אראה את כל הדוגמאות עם הספריה OpenAI Agents SDK. בחרתי בספריה זו מהסיבות הבאות: 1. היא נכתבה על ידי OpenAI ומשלבת הרבה תבניות טובות בצורה פשוטה לשימוש. 2. היא כתובה גם בטייפסקריפט וגם ב Python ומאפשרת פיתוח מערכות בצד שרת וגם בצד לקוח. 3. היא לא עמוסה באבסטרקציות אלא כוללת יחסית מעט מבנים בסיסיים. אל דאגה, למרות שהספריה מבית OpenAI אפשר להשתמש בה כדי לדבר עם מודלים של כל החברות. קריאת ה AI הראשונה שלי בפייתון נכתוב את הקוד הבא בתוך תוכנית פייתון:
import asyncio
from agents import Agent, Runner
from agents.extensions.models.litellm_model import LitellmModel
import os

async def main():
    agent = Agent(
        name="Assistant",
        model=LitellmModel(model="github/gpt-4.1", api_key=os.environ["GITHUB_TOKEN"]),
        instructions="You only respond in haikus.",
    )

    result = await Runner.run(agent, "Tell me about recursion in programming.")
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
הקוד די מסביר את עצמו ובואו נקרא אותו שורה אחר שורה: 1. מתחילים ביבוא הספריות. 2. ממשיכים להגדיר את הפונקציה הראשית main בתור פונקציית async. 3. הפונקציה מתחילה בהגדרה של הסוכן - סוכן הוא אבסטרקציה שמייצגת קריאה אחת או מספר קריאות ל AI כדי לבצע משימה. מאפיין instructions של הסוכן כולל טקסט הוראות הפעלה לאותו סוכן, המודל קובע מה המודל בו משתמשים ו name הוא שם הסוכן. 4. אחרי שיש לנו סוכן נוכל להריץ אותו עם פקודת Runner.run. הפקודה מקבלת את הסוכן ו prompt ויוצאת לדרך לבצע את המשימה. היא מחזירה את התוצאה יחד עם metadata לגבי מה שקרה. 5. בסיום ה main אני מדפיס את הפלט. הרצת התוכנית

ToCode
1 417
יום 1 - פיתוח מערכות אג'נטיות הי חברים, היום אנחנו מתחילים בסדרת 21 ימים על פיתוח מערכות אינטרנט מבוססות סוכנים. כבר היום מערכות אינטרנט רבות מוסיפות ממשק שיחה מבוסס AI בתור ממשק מלווה ל GUI הקלאסי. הדוגמאות בסידרה זו מבוססות על ממשקים כאלה שבניתי ללקוחות ולמערכות שלי ואני מקווה שהן יעזרו גם לכם. קוראים וותיקים זוכרים שאני מפרסם סדרות כאלה כשאני יוצא לחופשות וגם הפעם זה המצב - הבלוג יחזור למתכונתו הרגילה בסוף הסדרה בעוד 21 ימים. מה זה מערכת אג'נטית חברות ה AI הבינו מהר מאוד שהן יושבות על מכרה זהב. שתוך מעט מאוד זמן כל מוצר באינטרנט יצטרך לשלב יכולת בינה מלאכותית יוצרת ושיש פה הזדמנות פז להמציא תשתית חדשה ולהרוויח ממנה. ואכן כש ChatGPT יצא אחד הפיצ'רים הראשונים שקיבלנו היה API, וכמוהו גם כל אחד מהמתחרים. במקביל יש לנו מודלים בקוד פתוח כמו Llama או Deepseek שמאפשרים לכל אחד להתקין אותם על מחשב ולספק "שירותי AI" ללקוחות. דיגיטל אושן למשל עושים את זה ומאפשרים לכם בלחיצת כפתור להתחבר למודל. עבור סידרת הפוסטים שלנו אני אגדיר מערכת אג'ינטית בתור מערכת שמשתמשת באיזשהו API של ספק בינה מלאכותית יוצרת. זה יכול להיות מודל קוד פתוח שאתם מריצים על מחשב בסלון, מודל קוד פתוח שאתם מריצים בענן או שירות API של ספקי ה AI הגדולים. עוד דבר שעובד לטובתנו הוא שממשקי העבודה מול כל ספקי ה AI מאוד דומים אחד לשני: ברמה הבסיסית כולם מקבלים רצף של הודעות ויודעים לייצר הודעה חדשה לשיחה, וכולם יודעים לענות ב Streaming כך שאנחנו יכולים לחבר אותם ישירות לדף אינטרנט. וכן לספקים השונים יש יכולות שונות לדוגמה רבים מהם יודעים לייצר תמונות, חלקם יודעים לחפש באינטרנט, חלקם יודעים להריץ קוד או לייצר תמונות ועוד יכולות שונות ומגוונות. כשאנחנו בונים מערכת משולבת AI (או מערכת אג'נטית) צריך לזכור שמדובר בעולם תוכן מאוד חדש. כל הזמן יש ספריות חדשות וכלים חדשים לעבוד עם אותם ספקי AI וככל שאנשים בונים יותר מערכות משולבות AI מתגלות תבניות חדשות. בסידרה זו אני אעבוד עם ספריית Open AI Agents SDK שזמינה גם לפייתון וגם ל TypeScript. אלה שני הדברים המרכזיים שאנחנו הולכים לבנות: תהליך עבודה משולב AI סוג אחד של מערכות משולבות AI הוא תהליכי עבודה (workflow). תהליך עבודה הוא רצף של פעולות שיש לו סדר קבוע, כמו סקריפט, אבל שחלק מהשלבים בו דורשים השלמת מידע מספק AI. להלן מספר דוגמאות לתהליכי עבודה: 1. סיכום דף אינטרנט - הסקריפט עולה, מושך מהאינטרנט את הדף לקובץ HTML, אולי מוחק את תגיות ה HTML או מתרגם ל Markdown כדי של AI יהיה יותר קל לעבוד עם התוכן ואז שולח את הטקסט ל AI כדי שיסכם אותו. 2. פרסום פוסט לבלוג מרובה שפות - בן אדם כותב פוסט ואז מתחיל סקריפט ששולח את הפוסט ל AI כדי לתרגם אותו. אפשר לרוץ בלולאה או במקביל ולשלוח את הפוסט מספר פעמים לספק AI כל פעם כדי לתרגם לשפה אחרת. בסוף הסקריפט מעלה את כל התרגומים לאתר. 3. זיהוי בעיות אבטחה בפרויקט - סקריפט מושך את קוד הפרויקט ושולח את כל הקוד או קובץ אחרי קובץ ל AI כדי לחפש בעיות אבטחה. בסוף הסקריפט אוסף את כל התוצאות ונעזר ב AI כדי לנסח מייל סיכום שיישלח למפתחים. סוכן עצמאי סוג שני של מערכת אג'נטית הוא סוכן עצמאי. לסוכן כזה יש מטרה והוא מנהל אינטרקציה עם משתמשים כדי לבצע את המטרה. הנה מספר דוגמאות לסוכנים: 1. סוכן שממלא סקר - במקום לשלוח Google Form אני יכול להקים סוכן שיפנה למשתמש, ישאל שאלות ויכתוב בעצמו את התשובות לטופס. 2. סוכן שכותב פוסטים - הסוכן יכול לחפש באינטרנט נושאים מעניינים ולהציג למשתמש את הבחירה, אחרי זה הסוכן ינסח טיוטה ובעזרת דיאלוג עם המשתמש ישפר את הטיוטה עד שהיא תהפוך לפוסט שאפשר לפרסם. 3. סוכן מורה לפייתון - הסוכן ישלח למשתמש הסבר ואז מספר תרגילים, המשתמש יענה על התרגילים ואז הסוכן ייתן משוב ותרגילי המשך בהתבסס על התשובות של המשתמש. בכל סוג מערכת יהיו לנו אתגרים אחרים ולכל סוג מערכת יש את השימושים שלה. רוב הזמן שילוב AI במוצר דורש שילוב AI גם בתהליכי העבודה וגם בצורה אינטרקטיבית באמצעות יצירת סוכנים ולכן בעולם האמיתי נבנה מערכות משני הסוגים. מחר נתקין את הספריה ונבצע קריאות API ראשונות. בינתיים אתם מוזמנים לקפוץ ל AI החביב עליכם ובעזרתו לחשוב על עוד דוגמאות לסוכנים ותהליכי עבודה שתרצו לבנות.

ToCode
1 417
כשהבדיקות נכשלות כשבדיקה אחת נכשלת אפשר להסתכל ולתקן. כש 5 בדיקות נכשלות אפשר להסתכל ולתקן אבל זה יכול לקחת זמן, אז דוחים את זה לאחרי הגירסה. כש 15 בדיקות נכשלות כבר צריך להקצות לתיקון זמן בגאנט ולפתוח כרטיס בג'ירה. לפעמים מישהו מגיע לזה אבל כמעט תמיד יהיה משהו חשוב יותר לעשות. כש 50 בדיקות נכשלות הדרך היחידה להתקדם היא למחוק את כל הבדיקות ולהתחיל מחדש. למעשה המצב הבעייתי הוא כל מה שקורה בין 5 ל 50 בדיקות. זה הכשלונות שמצטברים בלי שאף אחד מטפל בהם ועם הבנה מלאה שזה עדיין לא מספיק גרוע בשביל להתחיל מחדש אבל מתישהו זה כן יהיה. כמו אוכל שנשאר במקרר יותר מדי ועדיין לא העלה עובש אז לא נעים לזרוק אבל גם אי אפשר לאכול. אבל הדבר המקצועי לעשות - גם באוכל, גם בקוד וגם בבדיקות הוא לזרוק מוקדם. אוכל שעוד לא העלה עובש לא יהיה טעים שבוע הבא, ובינתיים הוא רק תופס מקום במקרר. סט בדיקות עם 20 כשלונות לא יתחיל לעבור מעצמו ובינתיים הכשלונות הקבועים רק מסתירים את הכשלונות האמיתיים.

ToCode
1 417
# Advance each iterator to its starting position
    for i, it in enumerate(iterators):
        # The \deque\ trick is a fast way to advance an iterator
        # by 'i' steps without storing the results.
        deque(itertools.islice(it, i), maxlen=0)

    # Zip the advanced iterators together to create the sliding window
    # of size \size\ and stride \1\.
    windowed_iterator = zip(*iterators)

    # If stride is 1, we're done. Otherwise, apply the stride
    # by taking every Nth item from the windowed iterator.
    if stride == 1:
        return windowed_iterator
    else:
        return itertools.islice(windowed_iterator, None, None, stride)
הקוד של ג'מיני מטפל טוב יותר במקרי הקצה ואולי גם יותר יעיל. החיסרון שלו הוא שהוא מושך פריטים מה iterable גם לפני שהקוד החיצוני ביקש אותם. אגב כששאלתי אותו על זה ג'מיני מיהר להתגונן ולהסביר שב 99% מהמקרים זו לא בעיה אני לא בטוח שאני מסכים איתו. יש לכם רעיון נוסף איך לממש חלון גולש בפייתון ורוצים לשתף? אפשר ורצוי להדביק בתגובות או בטלגרם.

ToCode
1 417
תרגיל פייתון: הרחבת pairwise עם גודל וגודל צעד הפונקציה itertools.pairwise בפייתון לוקחת רשימה ומחזירה רשימה של זוגות חופפים מתוך הקלט המקורי, לדוגמה:
>>> import itertools
>>> list(itertools.pairwise(range(100)))
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14), (14, 15), (15, 16), (16, 17), (17, 18), (18, 19), (19, 20), (20, 21), (21, 22), (22, 23), (23, 24), (24, 25), (25, 26), (26, 27), (27, 28), (28, 29), (29, 30), (30, 31), (31, 32), (32, 33), (33, 34), (34, 35), (35, 36), (36, 37), (37, 38), (38, 39), (39, 40), (40, 41), (41, 42), (42, 43), (43, 44), (44, 45), (45, 46), (46, 47), (47, 48), (48, 49), (49, 50), (50, 51), (51, 52), (52, 53), (53, 54), (54, 55), (55, 56), (56, 57), (57, 58), (58, 59), (59, 60), (60, 61), (61, 62), (62, 63), (63, 64), (64, 65), (65, 66), (66, 67), (67, 68), (68, 69), (69, 70), (70, 71), (71, 72), (72, 73), (73, 74), (74, 75), (75, 76), (76, 77), (77, 78), (78, 79), (79, 80), (80, 81), (81, 82), (82, 83), (83, 84), (84, 85), (85, 86), (86, 87), (87, 88), (88, 89), (89, 90), (90, 91), (91, 92), (92, 93), (93, 94), (94, 95), (95, 96), (96, 97), (97, 98), (98, 99)]
בואו נרחיב אותה באמצעות שני פרמטרים: 1. פרמטר size קובע את גודל ה״זוג״ (וכן כל מספר גדול מ-2 כבר לא יהיה זוג). 2. פרמטר stride קובע את גודל הצעד כלומר החפיפה בין הזוגות. לדוגמה ארצה להיות מסוגל להפעיל את הפונקציה כדי לקבל את כל השלשות החופפות בטווח המספרים עד 100, או לקבל קבוצות של 5 מספרים בהפרש של 3 אחת מהשנייה. פיתרון כדי לפתור את התרגיל אני מתחיל עם הקוד של pairwise עצמה מתוך דף התיעוד:
def pairwise(iterable):
    # pairwise('ABCDEFG') → AB BC CD DE EF FG

    iterator = iter(iterable)
    a = next(iterator, None)

    for b in iterator:
        yield a, b
        a = b
בעצם הפונקציה מבצעת איטרציה כפולה - לוקחים את האיבר הראשון למשתנה a, ואז מתחילים לרוץ על האיטרטור עם המשתנה b שעכשיו מקבל את האיבר השני (כי הראשון כבר נלקח ל a). מחזירים את הזוג וממשיכים לסיבוב נוסף, הפעם a מקבל את הערך של b כלומר את האיבר השני ו b מקבל את הדבר הבא מהאיטרטור שזה כבר האיבר השלישי. ומכאן ההרחבה למקרה הכללי ברורה - במקום לקחת רק איבר אחד קדימה עם next נרצה לקחת בלוק שלם קדימה עם itertools.islice. זה הקוד:
import itertools

def each_cons(iterable, size=2, stride=1):
    iterator = iter(iterable)
    chunk = tuple(itertools.islice(iterator, size))
    for b in iterator:
        yield chunk
        chunk = chunk[stride:] + (b,) + tuple(itertools.islice(iterator, stride - 1))
    yield chunk
נשים לב: 1. בגלל העבודה עם איטרטורים אין איטרציה כפולה, כלומר הקוד לא צריך לטעון את כל המידע לזיכרון לפני שמייצר את הזוגות (או שלשות, או רביעיות). 2. הכתיב (b,) זאת הדרך של פייתון לייצר tuple של איבר אחד. הקוד הזה עוזר להמחיש את הפיתרון אבל הוא לא מושלם. קל לראות שכש stride גדול מ size מקבלים chunk-ים גדולים מדי (בגלל אופן היצירה של chunk), אין וולידציה של פרמטרים וכל חיבורי ה tuples האלה יכולים לפגוע בביצועים. נתתי לג'מיני לשפר את המימוש וקיבלתי את הקוד הזה:
import itertools
from collections import deque

def each_cons(iterable, size=2, stride=1):
    """
    Yields overlapping tuples of a given size and stride.

    Like a sliding window, but the window can jump.

    Args:
        iterable: The iterable to process.
        size: The size of each chunk (window).
        stride: The number of items to advance between chunks.

    Yields:
        A tuple of length \size\ for each step.

    Examples:
        >>> list(each_cons([1, 2, 3, 4, 5], size=3, stride=1))
        [(1, 2, 3), (2, 3, 4), (3, 4, 5)]

        >>> list(each_cons([1, 2, 3, 4, 5, 6], size=3, stride=2))
        [(1, 2, 3), (3, 4, 5)]

        >>> list(each_cons([1, 2], size=3))
        []
    """
    if size < 1 or stride < 1:
        raise ValueError("size and stride must be positive integers")

    # Create 'size' independent iterators
    iterators = itertools.tee(iterable, size)

ToCode
1 417
בגישה הפונקציונאלית הרכבה (קומפוזיציה) היא הרכבה של פונקציות, והיא מתבססת על זה שיש לנו הרבה פונקציות קטנות ועצמאיות ואנחנו יכולים לפרק ולהרכיב אותן כדי ליצור מבנים חדשים בקלות. הבעיה עם ספריות פרונטאנד פונקציונאליות היא שהפונקציות גדולות מדי. אין לי איך לקחת רק "חלקים" מקומפוננטה ולבנות מהם קומפוננטות חדשות, או לפחות לרוב זה מסורבל מדי. פונקציות Custom Hooks מציעות פיתרון חלקי בלבד ובאופן כללי קומפוננטות שאנחנו מקבלים מאנשים אחרים צריכות להיכתב מראש בצורה שתאפשר הרחבה שלהן מבחוץ (למשל על ידי שימוש נרחב ב Custom Hooks).

ToCode
1 417
שינוי קומפוננטות מבחוץ רוב הספריות הפופולריות לפיתוח צד לקוח לא תומכות בהרחבה של קומפוננטות מבחוץ וזה חבל. נדמיין שקיבלתי קומפוננטת ריאקט של מונה לחיצות:
export default function Counter() {
    const [count, setCount] = useState(0);
    
    return (
        <div>
            <p>Count = {count}</p>
            <button onClick={() => setCount(c => c + 1)}>+1</button>
        </div>
    );
}
אני רוצה להוסיף לו כפתור איפוס, אבל בלי לשנות את הקובץ המקורי של Counter. אפילו אם אני מוכן לשנות את הקריאות ל Counter מבחוץ כלומר לעטוף אותו באיזה CounterWithReset אין לי איך לעטוף את הקומפוננטה ולשנות את ה Virtual DOM שהיא מחזירה. זה פשוט לא בכללים. בואו נראה שתי דוגמאות לספריות שכן מאפשרות מנגנון כזה ומה הופך אותו לאפשרי. הרחבת Counter ב lit דוגמה ראשונה היא lit שבונה קומפוננטות באמצעות Web Components. ב lit קומפוננטת Counter נראית כך:
import { LitElement, html } from 'lit';

export class MyCounter extends LitElement {
  static properties = {
    count: { type: Number }
  };

  constructor() {
    super();
    this.count = 0;
  }

  render() {
    return html\
      <button @click=${() => this.count--}>-</button>
      <span>${this.count}</span>
      <button @click=${() => this.count++}>+</button>
    \;
  }
}

customElements.define('my-counter', MyCounter);
ההבדל בין זה לריאקט הוא שליט השתמשה בקלאס במקום בפונקציה (קצת כמו קלאסים בריאקט שעשינו פעם). היתרון בקלאס הוא שאפשר להרחיב אותו מבחוץ ולכן אני יכול מקובץ אחר לטעון את הקומפוננטה, לשנות בה חלקים ולייצא מחדש:
import { html } from 'lit';
import { MyCounter } from "./my-counter";

// Inject a reset method
MyCounter.prototype.reset = function () {
  this.count = 0;
  this.requestUpdate(); // tell Lit to re-render
};

// Patch render to add a Reset button
const origRender = MyCounter.prototype.render;
MyCounter.prototype.render = function () {
  return html\
    ${origRender.call(this)}
    <button @click=${() => this.reset()}>Reset</button>
  \;
};

export default MyCounter;
הרינדור של הקומפוננטה הפנימית והרחבה עם כפתור חדש קצת מזכיר שימוש ב Higher Order Component בריאקט, אבל יש פה הבדל משמעותי של גישה לסטייט. עם HoC הסטייט של הקומפוננטה הפנימית היה מוגן ואפשר היה להגיע אליו רק מתוך קוד שרץ באותה קומפוננטה. בעבודה עם קלאסים ושינוי פרוטוטייפים אפשר להגיע גם מבחוץ לאותו משתנה count ששמור בסטייט. הרחבת Counter ב Stimulus גישה שנייה להרחבה היא של ריילס וספריית ה JavaScript שלה, סטימולוס. ה Counter בסטימולוס מורכב מקובץ JavaScript ומקובץ html.erb. זו התבנית:
<div 
  data-controller="counter" 
  data-counter-count-value="0"
>
  <button data-action="counter#decrement">-</button>
  <span data-counter-target="output">0</span>
  <button data-action="counter#increment">+</button>
</div>
וזה קובץ ה JavaScript:
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["output"]
  static values = { count: Number }

  connect() {
    this.update()
  }

  increment() {
    this.countValue++
    this.update()
  }

  decrement() {
    this.countValue--
    this.update()
  }

  update() {
    this.outputTarget.textContent = this.countValue
  }
}
רוצים להוסיף איפוס? אין בעיה אפשר לעדכן את ה Controller מבחוץ ולהוסיף פונקציה:
import CounterController from "./counter_controller"

CounterController.prototype.reset = function () {
  this.countValue = 0;
  this.update()
}
את התבנית אפשר לשנות או לרנדר אותה מקובץ טמפלייט אחר.