fa
Feedback
ToCode

ToCode

رفتن به کانال در Telegram

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

نمایش بیشتر
1 419
مشترکین
اطلاعاتی وجود ندارد24 ساعت
اطلاعاتی وجود ندارد7 روز
-430 روز
آرشیو پست ها
ToCode
1 419
אחרי התקנת הספריות והגדרת משתנה הסביבה OPENAI_API_KEY נוכל להתקדם לקובץ route.ts ולעדכן אותו לתוכן הבא:
import { Agent, run } from '@openai/agents';

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

export async function POST() {
  const result = await run(agent, 'Tell me a story about a cat.', {
    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',
    },
  });
}
קוד יצירת הסוכן צריך להיראות לכם מוכר כי הוא בדיוק ייבוא של קוד הפייתון שהכרנו בפוסטים קודמים בסידרה. קוד ההזרמה כולל כבר כמה רעיונות חדשים. תחילה אנחנו שמים לב למפתח stream שמופיע באפשרויות שאני מעביר ל run:
  const result = await run(agent, 'Tell me a story about a cat.', {
    stream: true,
  });
בפייתון השתמשתי בפונקציה אחרת כדי לרוץ במצב הזרמה וזה הבדל ראשון בין הספריות. ההזרמה עצמה דורשת לולאת for async שנראית כך:
  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();
      }
    },
  });
קריאת אירועים מסוכן אסינכרוני מבוצעת עם לולאת for async וכל פעם שיש אירוע נוסף נכנסים לגוף הלולאה. עבור כל אירוע אני בודק את הסוג שלו, ואלה אותם סוגים שהכרנו אתמול כשראינו איך להזרים מידע מתוכנית פייתון, רק שהפעם הכתיבה לתשובת ה HTTP לא נעשית עם yield כמו בפייתון אלא עם controller.enqueue. שיחה עם סוכן - ניהול היסטוריה האתגר האחרון שלנו בממשק הוא ליצור שיחה - לקחת הודעה מהמשתמש, לשלוח אותה לסוכן, לשמור את התשובה של הסוכן ובהודעה הבאה לשלוח את כל ההיסטוריה בצירוף ההודעה. בפייתון יכולנו לשמור את היסטוריית השיחות בצד שרת באמצעות session. ב TypeScript ספריית הסוכנים לא תומכת ב Session ולכן נצטרך לבנות את המנגנון בעצמנו. אנחנו יכולים לבחור אם לשמור הודעות ישנות בבסיס נתונים, בזיכרון בצד שרת או בצד לקוח. בדוגמה היום אני בוחר לשמור את כל ההודעות בצד לקוח פשוט כי זה קל למימוש וממילא זה לא משנה לספריית הסוכנים. זה קוד צד הלקוח המעודכן:
'use client';
import { FormEvent, useState } from "react";

type Message = {role: string, content: string };

export default function Home() {
  const [messages, setMessages] = useState<Array<Message>>([]);

  async function sendMessage(ev: FormEvent) {
    ev.preventDefault();
    const form = ev.target as HTMLFormElement;
    const fd = new FormData(form);

    const userMessage = { role: 'user', content: fd.get('message') as string };
    setMessages(prev => [...prev,
      userMessage,
      { role: 'assistant', content: '' }
    ]);
    
    form.reset();

    try {
      const response = await fetch('/chat', {
        method: "POST",
        body: JSON.stringify([...messages, userMessage]),
      });
      const reader = response.body!.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        setMessages(prev => prev.with(-1, {

ToCode
1 419
יום 9 - בואו ננסה את זה בטייפסקריפט ספריית OpenAI Agents SDK זמינה גם בגירסת טייפסקריפט ואפשר להשתמש בה כשכותבים יישומי next.js. נכון, גם ל vercel יש ספריית סוכנים וגם אותה אפשר לחבר בקלות ל next.js, אבל תמיד טוב שיש כמה אפשרויות. בפוסט זה נראה איך ליצור יישום next.js לשיחה עם סוכן באמצעות ספריית OpenAI Agents SDK תוך שימוש בדברים שלמדנו והתאמתם לטייפסקריפט. איך עובד Streaming ב Next.JS האתגר הראשון בפיתוח ממשק ווב לסוכנים הוא ההמתנה הארוכה בין הודעה שאני שולח לבין תשובת הסוכן. בזמן שממתינים אנחנו אוהבים להציג למשתמשים את הטקסט שהסוכן מייצר ב Streaming כדי שלפחות לא יהיה להם משעמם. ב Next.JS הזרמת מידע עובדת באופן הבא: 1. אנחנו מגדירים API Route ביישום שמחזיר ReadableStream. 2. אנחנו כותבים בצורה אסינכרונית ל Stream. 3. כשלקוח פונה עם fetch לנתיב זה הוא יקבל תשובה שמכילה את הכותרת:
transfer-encoding: chunked
4. המידע עצמו יגיע בחלקים כשהוא מוכן. בצד לקוח נוכל ליצור Reader ולקרוא לפונקציה reader.read() בלולאה. פונקציה זו מחזירה את ה chunk-ים של ההודעה מהשרת בהזרמה כשהם מוכנים. קוד? בטח. אני יוצר אפליקציית next.js חדשה ופותח קובץ src/app/chat/route.ts עם התוכן הבא:
export async function POST() {
  const encoder = new TextEncoder();
  const customReadable = new ReadableStream({
    async start(controller) {
      controller.enqueue(encoder.encode('Processing...'));
      await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
      controller.enqueue(encoder.encode('Verifying hash...'));
      await new Promise(resolve => setTimeout(resolve, 1500));
      controller.enqueue(encoder.encode('Scanning file...'));
      await new Promise(resolve => setTimeout(resolve, 2000));
      controller.enqueue(encoder.encode('Generating link...'));
      controller.close(); // Signal end of stream
    },
  });

  return new Response(customReadable, {
    headers: {
      'Content-Type': 'text/plain',
      'Transfer-Encoding': 'chunked',
    },
  });
}
כל קריאה ל:
controller.enqueue(encoder.encode('Processing...'));
כותבת הודעה חדשה ל Stream והחזרת ה Stream בסוף הפונקציה מבטיחה שהדפדפן יוכל לקרוא את המידע ממנו כשהמידע נוצר. בצד הלקוח אני מעדכן את הקובץ page.tsx לתוכן הבא:
'use client';
import { FormEvent, useState } from "react";

export default function Home() {
  const [streamedText, setStreamedText] = useState('');

  async function startStreaming(ev: FormEvent) {
    ev.preventDefault();

    try {
      const response = await fetch('/chat', {
        method: "POST"
      });
      const reader = response.body!.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        setStreamedText(prev => prev + decoder.decode(value));
      }
    } catch (error) {
      console.error('Streaming error:', error);
    }
    setStreamedText(prev => prev + 'Done!');
  }

  return (
    <div >
      <main >
        <p>{streamedText}</p>
        <form onSubmit={startStreaming}>
          <input type="text" />
          <input type="submit" value="Send" />
        </form>
      </main>
    </div>
  );
}
כשהחלק שעוסק בהזרמת התשובה הוא:
const response = await fetch('/chat', {
  method: "POST"
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  setStreamedText(prev => prev + decoder.decode(value));
}
הפעלתי fetch, בניתי Reader מהתשובה ואני קורא בלולאה ל reader.read עד שהשרת מסיים את ההזרמה. כל פעם שמוזרם מידע נוסף אני מעדכן משתנה state שיגרום למידע החדש להופיע על המסך. הזרמת מידע מסוכן נוסיף ליישום את ספריית הסוכנים של openai ואז zod עם:
npm install @openai/agents 'zod@<=3.25.67'
שימו לב שנכון לכתיבת הפוסט ספריית openai-agents תומכת רק בגירסה ישנה יותר של zod ולכן יש לציין גירסה מקסימלית בהתקנה. אני מקווה שבקרוב הם יתקנו את זה.

ToCode
1 419
שמרו את כל הקבצים לפרויקט אצלכם בתיקייה והפעילו את התוכנית. וודאו שאתם מצליחים לדבר עם הסוכן. לאחר מכן החליפו מודל ותראו שאתם מצליחים לנהל שיחה גם עם מודלים נוספים. לסיום פתחו את מסך כלי הפיתוח בדפדפן בטאב Network. הסתכלו על המידע שעובר ברשת בין השרת ללקוח בזמן הזרמת הטוקנים ב Chat. חשבו: מה קורה לשיחה כשמרעננים את העמוד? נסו לחשוב על רעיונות איך לשמור על ההזרמה פעילה גם אחרי ריענון.

ToCode
1 419
margin: 0 auto;
    padding: 20px;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

header {
    text-align: center;
    margin-bottom: 30px;
    color: white;
}

header h1 {
    font-size: 2.5em;
    margin-bottom: 10px;
    text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}

header p {
    font-size: 1.1em;
    opacity: 0.9;
}

.agent-config {
    background: white;
    border-radius: 15px;
    padding: 20px;
    margin-bottom: 20px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}

.config-row {
    margin-bottom: 15px;
}

.config-row:last-child {
    margin-bottom: 0;
}

label {
    display: block;
    margin-bottom: 5px;
    font-weight: 600;
    color: #555;
}

input, textarea {
    width: 100%;
    padding: 12px;
    border: 2px solid #e1e5e9;
    border-radius: 8px;
    font-size: 14px;
    transition: border-color 0.3s ease;
    resize: vertical;
}

input:focus, textarea:focus {
    outline: none;
    border-color: #667eea;
}

.chat-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    background: white;
    border-radius: 15px;
    overflow: hidden;
    box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}

.chat-messages {
    flex: 1;
    padding: 20px;
    overflow-y: auto;
    max-height: 400px;
    min-height: 300px;
}

.message {
    margin-bottom: 15px;
    padding: 12px 16px;
    border-radius: 12px;
    max-width: 80%;
    word-wrap: break-word;
}

.user-message {
    background: #667eea;
    color: white;
    margin-left: auto;
    border-bottom-right-radius: 4px;
}

.assistant-message {
    background: #f1f3f5;
    color: #333;
    border-bottom-left-radius: 4px;
}

.system-message {
    background: #e3f2fd;
    color: #1976d2;
    text-align: center;
    border-radius: 8px;
    padding: 10px;
    font-style: italic;
    margin: 10px auto;
    max-width: 90%;
}

.streaming-message {
    background: #f8f9fa;
    color: #333;
    border-bottom-left-radius: 4px;
    border-left: 3px solid #667eea;
}

.chat-input-container {
    padding: 20px;
    border-top: 1px solid #e1e5e9;
    background: #fafafa;
}

.input-group {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
}

* message-input { *
    flex: 1;
    min-height: 60px;
    resize: none;
}

button {
    background: #667eea;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    cursor: pointer;
    font-weight: 600;
    transition: all 0.3s ease;
    min-width: 80px;
}

button:hover:not(:disabled) {
    background: #5a6fd8;
    transform: translateY(-1px);
}

button:disabled {
    background: #ccc;
    cursor: not-allowed;
    transform: none;
}

.status {
    font-size: 12px;
    color: #666;
    text-align: center;
}

.status.connecting {
    color: #ff9800;
}

.status.streaming {
    color: #4caf50;
}

.status.error {
    color: #f44336;
}

/* Typing indicator */
.typing-indicator {
    display: inline-block;
    padding: 8px 12px;
    background: #f1f3f5;
    border-radius: 12px;
    margin-bottom: 15px;
}

.typing-indicator::after {
    content: '●●●';
    animation: typing 1.4s infinite;
    color: #999;
}

@keyframes typing {
    0%, 60% {
        opacity: 1;
    }
    30% {
        opacity: 0.3;
    }
}

/* Scrollbar styling */
.chat-messages::-webkit-scrollbar {
    width: 6px;
}

.chat-messages::-webkit-scrollbar-track {
    background: #f1f1f1;
}

.chat-messages::-webkit-scrollbar-thumb {
    background: #ccc;
    border-radius: 3px;
}

.chat-messages::-webkit-scrollbar-thumb:hover {
    background: #999;
}

/* Responsive design */
@media (max-width: 600px) {
    .container {
        padding: 10px;
    }
    
    header h1 {
        font-size: 2em;
    }
    
    .message {
        max-width: 95%;
    }
    
    .input-group {
        flex-direction: column;
    }
    
    button {
        align-self: flex-end;
        min-width: 100px;
    }
}
עכשיו אתם

ToCode
1 419
console.warn('Failed to parse JSON:', line, e);
                        }
                    }
                }
            }
        } finally {
            reader.releaseLock();
            this.finalizeStreamingMessage();
        }
    }

    handleStreamEvent(data) {
        switch (data.type) {
            case 'start':
                console.log('Stream started:', data.message);
                break;
                
            case 'token':
                if (data.content) {
                    this.appendToStreamingMessage(data.content);
                }
                break;
                
            case 'message':
                if (data.content) {
                    this.appendToStreamingMessage(data.content);
                }
                break;
                
            case 'agent_update':
                console.log('Agent updated:', data.agent_name);
                break;
                
            case 'tool_call':
                this.appendToStreamingMessage('\n[Tool called]');
                break;
                
            case 'tool_output':
                this.appendToStreamingMessage(\\n[Tool output: ${data.content}]\);
                break;
                
            case 'complete':
                console.log('Stream completed');
                this.status.textContent = 'Response complete';
                break;
                
            default:
                console.log('Unknown event type:', data);
        }
    }

    createStreamingMessage() {
        const messageDiv = document.createElement('div');
        messageDiv.className = 'message streaming-message';
        messageDiv.innerHTML = '<span class="typing-indicator"></span>';
        this.chatMessages.appendChild(messageDiv);
        this.scrollToBottom();
        return messageDiv;
    }

    appendToStreamingMessage(content) {
        if (!this.currentStreamingMessage) return;
        
        // Remove typing indicator if it exists
        const typingIndicator = this.currentStreamingMessage.querySelector('.typing-indicator');
        if (typingIndicator) {
            typingIndicator.remove();
        }
        
        // Append the content
        this.currentStreamingMessage.textContent += content;
        this.scrollToBottom();
    }

    finalizeStreamingMessage() {
        if (!this.currentStreamingMessage) return;
        
        // Remove typing indicator
        const typingIndicator = this.currentStreamingMessage.querySelector('.typing-indicator');
        if (typingIndicator) {
            typingIndicator.remove();
        }
        
        // Change class to final assistant message
        this.currentStreamingMessage.className = 'message assistant-message';
        
        // If the message is empty, add a default message
        if (!this.currentStreamingMessage.textContent.trim()) {
            this.currentStreamingMessage.textContent = 'No response received.';
        }
        
        this.currentStreamingMessage = null;
        this.scrollToBottom();
    }

    addMessage(content, type) {
        const messageDiv = document.createElement('div');
        messageDiv.className = \message ${type}-message\;
        messageDiv.textContent = content;
        this.chatMessages.appendChild(messageDiv);
        this.scrollToBottom();
    }

    scrollToBottom() {
        this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
    }
}

// Initialize the chat when the page loads
document.addEventListener('DOMContentLoaded', () => {
    new ChatAgent();
});
ואגב יש גם קובץ CSS שנקרא client/style.css עם התוכן הבא:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    color: #333;
}

.container {
    max-width: 800px;

ToCode
1 419
// Enter key in message input (Shift+Enter for new line)
        this.messageInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                this.sendMessage();
            }
        });

        // Enable send button when there's content
        this.messageInput.addEventListener('input', () => {
            const hasContent = this.messageInput.value.trim().length > 0;
            this.sendButton.disabled = !hasContent || this.isStreaming;
        });

        // Auto-resize message input
        this.messageInput.addEventListener('input', () => {
            this.messageInput.style.height = 'auto';
            this.messageInput.style.height = this.messageInput.scrollHeight + 'px';
        });
    }

    enableInterface() {
        this.messageInput.disabled = false;
        this.sendButton.disabled = true;
        this.status.textContent = 'Ready to chat';
        this.status.className = 'status';
    }

    disableInterface() {
        this.messageInput.disabled = true;
        this.sendButton.disabled = true;
    }

    async sendMessage() {
        const message = this.messageInput.value.trim();
        if (!message || this.isStreaming) return;

        const agentName = this.agentName.value.trim() || 'Assistant';
        const agentInstructions = this.agentInstructions.value.trim() || 'You are a helpful assistant.';

        // Add user message to chat
        this.addMessage(message, 'user');
        
        // Clear input and disable interface
        this.messageInput.value = '';
        this.messageInput.style.height = 'auto';
        this.disableInterface();
        
        // Set streaming status
        this.isStreaming = true;
        this.status.textContent = 'Connecting...';
        this.status.className = 'status connecting';

        try {
            // Make the streaming request
            const response = await fetch('/chat', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    message: message,
                    agent_name: agentName,
                    agent_instructions: agentInstructions
                })
            });

            if (!response.ok) {
                throw new Error(\HTTP error! status: ${response.status}\);
            }

            // Process the streaming response
            await this.processStreamingResponse(response);

        } catch (error) {
            console.error('Error:', error);
            this.addMessage('Sorry, there was an error processing your request.', 'system');
            this.status.textContent = 'Error occurred';
            this.status.className = 'status error';
        } finally {
            this.isStreaming = false;
            this.enableInterface();
        }
    }

    async processStreamingResponse(response) {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        
        this.status.textContent = 'Streaming response...';
        this.status.className = 'status streaming';

        // Initialize streaming message container
        this.currentStreamingMessage = this.createStreamingMessage();
        
        try {
            while (true) {
                const { done, value } = await reader.read();
                
                if (done) break;
                
                // Decode the chunk
                const chunk = decoder.decode(value, { stream: true });
                const lines = chunk.split('\n');
                
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        try {
                            const data = JSON.parse(line.slice(6));
                            this.handleStreamEvent(data);
                        } catch (e) {

ToCode
1 419
ועיקר הקוד נמצא בקובץ ה JavaScript. קובץ זה צריך לטפל בתגובה המוזרמת ולעדכן את רשימת ההודעות כל פעם שמתקבלים טוקנים חדשים מהשרת. הקוד המרכזי שם הוא:
    async processStreamingResponse(response) {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        
        this.status.textContent = 'Streaming response...';
        this.status.className = 'status streaming';

        // Initialize streaming message container
        this.currentStreamingMessage = this.createStreamingMessage();
        
        try {
            while (true) {
                const { done, value } = await reader.read();
                
                if (done) break;
                
                // Decode the chunk
                const chunk = decoder.decode(value, { stream: true });
                const lines = chunk.split('\n');
                
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        try {
                            const data = JSON.parse(line.slice(6));
                            this.handleStreamEvent(data);
                        } catch (e) {
                            console.warn('Failed to parse JSON:', line, e);
                        }
                    }
                }
            }
        } finally {
            reader.releaseLock();
            this.finalizeStreamingMessage();
        }
    }
    
    handleStreamEvent(data) {
        switch (data.type) {
            case 'start':
                console.log('Stream started:', data.message);
                break;
                
            case 'token':
                if (data.content) {
                    this.appendToStreamingMessage(data.content);
                }
                break;
                
            case 'message':
                if (data.content) {
                    this.appendToStreamingMessage(data.content);
                }
                break;
                
            case 'agent_update':
                console.log('Agent updated:', data.agent_name);
                break;
                
            case 'tool_call':
                this.appendToStreamingMessage('\n[Tool called]');
                break;
                
            case 'tool_output':
                this.appendToStreamingMessage(\\n[Tool output: ${data.content}]\);
                break;
                
            case 'complete':
                console.log('Stream completed');
                this.status.textContent = 'Response complete';
                break;
                
            default:
                console.log('Unknown event type:', data);
        }
    }
נשים לב שהקוד מתאים לקוד ההזרמה שכתבתי בפייתון. בפייתון היו לי פקודות כמו:
yield f"data: {json.dumps({'type': 'tool_call', 'message': 'Tool was called'})}\n\n"
כדי להזרים אירוע של tool call, ואז ב JavaScript אנחנו מסתכלים על ה data.type ולפי זה מחליטים איך לטפל בהודעה:
switch (data.type) {
קובץ ה JavaScript המלא לדוגמה הוא:
// file: client/script.js

class ChatAgent {
    constructor() {
        this.chatMessages = document.getElementById('chat-messages');
        this.messageInput = document.getElementById('message-input');
        this.sendButton = document.getElementById('send-button');
        this.status = document.getElementById('status');
        this.agentName = document.getElementById('agent-name');
        this.agentInstructions = document.getElementById('agent-instructions');
        
        this.isStreaming = false;
        this.currentStreamingMessage = null;
        
        this.initializeEventListeners();
        this.enableInterface();
    }

    initializeEventListeners() {
        // Send button click
        this.sendButton.addEventListener('click', () => this.sendMessage());

ToCode
1 419
@app.post("/chat")
async def chat(request: ChatRequest):
    async def generate():
        agent = Agent(
            name=request.agent_name,
            instructions=request.agent_instructions,
        )

        result = Runner.run_streamed(
            agent,
            input=request.message,
        )

        # Send initial message to indicate start
        yield f"data: {json.dumps({'type': 'start', 'message': 'Starting chat...'})}\n\n"

        async for event in result.stream_events():
            # Handle different event types
            if event.type == "raw_response_event":
                if isinstance(event.data, ResponseTextDeltaEvent):
                    # Stream individual tokens
                    token = event.data.delta
                    if token:
                        yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"

            elif event.type == "agent_updated_stream_event":
                yield f"data: {json.dumps({'type': 'agent_update', 'agent_name': event.new_agent.name})}\n\n"

            elif event.type == "run_item_stream_event":
                if event.item.type == "tool_call_item":
                    yield f"data: {json.dumps({'type': 'tool_call', 'message': 'Tool was called'})}\n\n"
                elif event.item.type == "tool_call_output_item":
                    yield f"data: {json.dumps({'type': 'tool_output', 'content': str(event.item.output)})}\n\n"
                elif event.item.type == "message_output_item":
                    message_text = ItemHelpers.text_message_output(event.item)
                    yield f"data: {json.dumps({'type': 'message', 'content': message_text})}\n\n"

        # Send completion signal
        yield f"data: {json.dumps({'type': 'complete'})}\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/plain",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        }
    )

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8080)
קוד צד לקוח בצד הלקוח אני מגדיר קובץ HTML עם אזור להודעות וטופס לשליחת הודעה חדשה:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Agent</title>
    <link rel="stylesheet" href="client/style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>🤖 Chat Agent</h1>
            <p>Powered by Lean Agents SDK</p>
        </header>

        <div class="agent-config">
            <div class="config-row">
                <label for="agent-name">Agent Name:</label>
                <input type="text" id="agent-name" value="Assistant" placeholder="Enter agent name">
            </div>
            <div class="config-row">
                <label for="agent-instructions">Agent Instructions:</label>
                <textarea id="agent-instructions" placeholder="Enter agent instructions" rows="2">You are a helpful assistant.</textarea>
            </div>
        </div>

        <div class="chat-container">
            <div class="chat-messages" id="chat-messages">
                <div class="system-message">
                    Welcome! Configure your agent above and start chatting.
                </div>
            </div>
            
            <div class="chat-input-container">
                <div class="input-group">
                    <textarea 
                        id="message-input" 
                        placeholder="Type your message here..." 
                        rows="3"
                        disabled
                    ></textarea>
                    <button id="send-button" disabled>Send</button>
                </div>
                <div class="status" id="status">Ready to chat</div>
            </div>
        </div>
    </div>

    <script src="/client/script.js"></script>
</body>
</html>

ToCode
1 419
יום 8 - ממשק ווב לסוכן הממשק שאנחנו הכי רגילים לראות בעבודה עם סוכנים הוא הצ'אט בדפדפן ולכן חשוב להבין איך הוא בנוי. בשביל הדוגמה היום אשתמש ב fastapi בצד השרת ובהזרמת מידע HTTP Streaming. בואו נראה את הקוד. קוד צד שרת את השרת כתבתי ב fastapi. הסיבה לבחירה ב fastapi במקום למשל ב flask היא התמיכה המובנית של fastapi ב async. ספריית OpenAI Agents SDK היא אסינכרונית, כלומר לולאת ההזרמה שלה כתובה במבנה:
async for event in result.stream_events():
זה אומר שבשביל להפעיל שיחה עם סוכן ולקבל הזדמנות להזרים את הטוקנים כשהם נוצרים אני צריך Event Loop בתוכנית שלי, והדרך הכי פשוטה לקבל אחת היא להשתמש בפריימוורק ווב שכבר דואג לזה. הפונקציה הראשונה בקוד צד שרת צריכה להיות מוכרת לכם מפוסטים קודמים בסידרה:
    async def generate():
        agent = Agent(
            name=request.agent_name,
            instructions=request.agent_instructions,
        )

        result = Runner.run_streamed(
            agent,
            input=request.message,
        )

        # Send initial message to indicate start
        yield f"data: {json.dumps({'type': 'start', 'message': 'Starting chat...'})}\n\n"

        async for event in result.stream_events():
            # Handle different event types
            if event.type == "raw_response_event":
                if isinstance(event.data, ResponseTextDeltaEvent):
                    # Stream individual tokens
                    token = event.data.delta
                    if token:
                        yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"

            elif event.type == "agent_updated_stream_event":
                yield f"data: {json.dumps({'type': 'agent_update', 'agent_name': event.new_agent.name})}\n\n"

            elif event.type == "run_item_stream_event":
                if event.item.type == "tool_call_item":
                    yield f"data: {json.dumps({'type': 'tool_call', 'message': 'Tool was called'})}\n\n"
                elif event.item.type == "tool_call_output_item":
                    yield f"data: {json.dumps({'type': 'tool_output', 'content': str(event.item.output)})}\n\n"
                elif event.item.type == "message_output_item":
                    message_text = ItemHelpers.text_message_output(event.item)
                    yield f"data: {json.dumps({'type': 'message', 'content': message_text})}\n\n"

        # Send completion signal
        yield f"data: {json.dumps({'type': 'complete'})}\n\n"
יש פה פנייה לסוכן עם run_streamed ואז הפעלת yield עבור כל טקסט שהסוכן מייצר. מי שמשתמש ב generate היא פקודת השיחה:
@app.post("/chat")
async def chat(request: ChatRequest):
    async def generate():
        ...

    return StreamingResponse(
        generate(),
        media_type="text/plain",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        }
    )
וספריית fastapi מטפלת עבורי בצורה אוטומטית בהזרמת כל מידע שמגיע מ yield כחלק מהתשובה לדפדפן. קוד צד השרת המלא בקובץ main.py הוא:
import json
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from agents import Agent, ItemHelpers, Runner
from openai.types.responses import ResponseTextDeltaEvent

app = FastAPI()

* Add CORS middleware *
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

* Mount static files *
app.mount("/client", StaticFiles(directory="client"), name="client")

class ChatRequest(BaseModel):
    message: str
    agent_name: str = "Assistant"
    agent_instructions: str = "You are a helpful assistant."

@app.get("/", response_class=HTMLResponse)
async def index():
    with open("client/index.html", "r") as f:
        return HTMLResponse(content=f.read())

ToCode
1 419
RawResponsesStreamEvent(data=ResponseCompletedEvent(response=Response(id='resp_6895b0fd4464819abe44eb9e456d2d960754fc8434302a7d', created_at=1754640637.0, error=None, incomplete_details=None, instructions='Tell me a joke', metadata={}, model='gpt-4o-2024-08-06', object='response', output=[ResponseOutputMessage(id='msg_6895b0fda330819a9278cd18b1f7a04b0754fc8434302a7d', content=[ResponseOutputText(annotations=[], text='Hi there! Ready for a joke?', type='output_text', logprobs=[])], role='assistant', status='completed', type='message')], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[], top_p=1.0, background=False, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, reasoning=Reasoning(effort=None, generate_summary=None, summary=None), service_tier='default', status='completed', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=ResponseUsage(input_tokens=16, input_tokens_details=InputTokensDetails(cached_tokens=0), output_tokens=9, output_tokens_details=OutputTokensDetails(reasoning_tokens=0), total_tokens=25), user=None, prompt_cache_key=None, safety_identifier=None, store=True), sequence_number=15, type='response.completed'), type='raw_response_event')
-- Message output:
 Hi there! Ready for a joke?
=== Run complete ===

Process finished with exit code 0
אפשר לראות את כל הטוקנים מודפסים בזמן שהם מיוצרים, ואחריהם הודעת סיום עם ההודעה המלאה. בתוכניות אמיתיות נרצה לטפל בצורה שונה בכל סוג הודעה, לדוגמה: 1. הודעות raw_response_event יישלחו לגולש בזמן שהוא ממתין לתשובת המודל כדי להראות את התשובה בזמן שהיא נוצרת. 2. הודעות tool_call_item יציגו למשתמש סימן מיוחד בממשק שאומר שהמודל עכשיו מפעיל כלי, למשל מחפש באינטרנט. 3. הודעות message_output_item יעדכנו את ההודעה שמופיעה בחלון השיחה עם הטקסט המלא של ההודעה. אפשר גם להשתמש באירוע זה כדי לשמור ב DB את ההודעה. עכשיו אתם הוסיפו לתוכנית שלנו Tool כלשהו, למשל פונקציה שמחזירה בצורה אקראית נושא לבדיחה. וודאו שהמודל מפעיל את ה Tool ושימו לב להודעות שמתקבלות. בפוסטים הבאים נראה איך להזרים את תשובות המודל לממשקי ווב, לבוט בטלגרם וגם ללקוח MCP.

ToCode
1 419
RawResponsesStreamEvent(data=ResponseCreatedEvent(response=Response(id='resp_6895b0fd4464819abe44eb9e456d2d960754fc8434302a7d', created_at=1754640637.0, error=None, incomplete_details=None, instructions='Tell me a joke', metadata={}, model='gpt-4o-2024-08-06', object='response', output=[], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[], top_p=1.0, background=False, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, reasoning=Reasoning(effort=None, generate_summary=None, summary=None), service_tier='auto', status='in_progress', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=None, user=None, prompt_cache_key=None, safety_identifier=None, store=True), sequence_number=0, type='response.created'), type='raw_response_event')
RawResponsesStreamEvent(data=ResponseInProgressEvent(response=Response(id='resp_6895b0fd4464819abe44eb9e456d2d960754fc8434302a7d', created_at=1754640637.0, error=None, incomplete_details=None, instructions='Tell me a joke', metadata={}, model='gpt-4o-2024-08-06', object='response', output=[], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[], top_p=1.0, background=False, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, reasoning=Reasoning(effort=None, generate_summary=None, summary=None), service_tier='auto', status='in_progress', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=None, user=None, prompt_cache_key=None, safety_identifier=None, store=True), sequence_number=1, type='response.in_progress'), type='raw_response_event')
RawResponsesStreamEvent(data=ResponseOutputItemAddedEvent(item=ResponseOutputMessage(id='msg_6895b0fda330819a9278cd18b1f7a04b0754fc8434302a7d', content=[], role='assistant', status='in_progress', type='message'), output_index=0, sequence_number=2, type='response.output_item.added'), type='raw_response_event')
RawResponsesStreamEvent(data=ResponseContentPartAddedEvent(content_index=0, item_id='msg_6895b0fda330819a9278cd18b1f7a04b0754fc8434302a7d', output_index=0, part=ResponseOutputText(annotations=[], text='', type='output_text', logprobs=[]), sequence_number=3, type='response.content_part.added'), type='raw_response_event')
[Token]: Hi
[Token]:  there
[Token]: !
[Token]:  Ready
[Token]:  for
[Token]:  a
[Token]:  joke
[Token]: ?
RawResponsesStreamEvent(data=ResponseTextDoneEvent(content_index=0, item_id='msg_6895b0fda330819a9278cd18b1f7a04b0754fc8434302a7d', logprobs=[], output_index=0, sequence_number=12, text='Hi there! Ready for a joke?', type='response.output_text.done'), type='raw_response_event')
RawResponsesStreamEvent(data=ResponseContentPartDoneEvent(content_index=0, item_id='msg_6895b0fda330819a9278cd18b1f7a04b0754fc8434302a7d', output_index=0, part=ResponseOutputText(annotations=[], text='Hi there! Ready for a joke?', type='output_text', logprobs=[]), sequence_number=13, type='response.content_part.done'), type='raw_response_event')
RawResponsesStreamEvent(data=ResponseOutputItemDoneEvent(item=ResponseOutputMessage(id='msg_6895b0fda330819a9278cd18b1f7a04b0754fc8434302a7d', content=[ResponseOutputText(annotations=[], text='Hi there! Ready for a joke?', type='output_text', logprobs=[])], role='assistant', status='completed', type='message'), output_index=0, sequence_number=14, type='response.output_item.done'), type='raw_response_event')

ToCode
1 419
יום 7 - הזרמת תשובות אחד המאפיינים החשובים והחדשים של מערכות אג'נטיות הוא השימוש ב Streaming. הייחוד של מודלי שפה גדולים הוא שהם מייצרים המון פלט ודי לאט. במערכת אינטרקטיבית זה אומר שמשתמשים צריכים לחכות המון זמן עד שיכולים לכתוב את ההודעה הבאה ולהמשיך את השיחה. אפילו בתהליכים אסינכרוניים זה אומר שלמשתמשים קשה להבין איפה בדיוק אנחנו בתהליך. הפיתרון של ספקי המודלים היה ליצור APIs שתומכים בהזרמת מידע ולשלוח את הטקסט מהמודל מיד איך שהוא נוצר, מילה אחרי מילה. התוצאה היא חווית ה Streaming שאנחנו מכירים מאתרי ה Chat - אנחנו אומנם מחכים שהמודל יסיים לכתוב את כל הפלט, אבל לפחות במהלך ההמתנה אנחנו יכולים לראות את התוצאה נבנית מול העיניים. סוכנים שאנחנו בונים לרוב יצטרכו לתת חווית משתמש דומה ולכן כל ספריית עבודה עם סוכנים חייבת לתת פיתרון גם להזרמת המידע. בפוסט זה נראה את הפיתרון של OpenAI Agents SDK. לולאת הזרמת טקסט פשוטה משורת הפקודה בשביל בדיקות ומשחקים נתנו לנו ב Agents SDK לולאת הזרמת טקסט פשוטה שאפשר להריץ משורת הפקודה. זה הקוד:
import asyncio
from agents import Agent, run_demo_loop

async def main() -> None:
    agent = Agent(name="Assistant", instructions="You are a helpful assistant.")
    await run_demo_loop(agent)

if __name__ == "__main__":
    asyncio.run(main())
לולאה זו משמשת לבדיקת סוכן שעובד בתוך Session ומבצעת: 1. קריאת הודעה מהמשתמש. 2. שמירת ההודעה בזיכרון והעברתה לסוכן. 3. הצגת הפלט מהסוכן ב Streaming, מה שמוכן מוצג. 4. קריאת הודעה נוספת והמשך שיחה, כלומר שליחתה לסוכן בצירוף כל ההודעות הקודמות. נסו להפעיל את הלולאה אצלכם ולדבר עם סוכן של OpenAI או ספק אחר דרך LiteLLM. הזרמת מידע מקוד שלנו כמובן שבקוד שלנו אנחנו נעדיף לשלוט בלולאה, בהודעות וב Sessions וגם לא נרצה שהפלט תמיד יוצג למסך ולכן נשתמש בלולאה הדמו רק לבדיקות ובקוד אמיתי נשתמש בפונקציה Runner.run_streamed כדי להריץ סוכן ולקבל ממנו הודעות בהזרמה. כל הודעה שמתקבלת מהסוכן מגיעה עם type. סוג ההודעה הבסיסי הוא raw_response_event ואלה ההודעות שמעבירות את המילים שהמודל מייצר. סוגי הודעות נוספים כוללים דיווח מתי המודל רוצה להפעיל כלי, מתי המודל סיים הודעה שלמה ומתי קיבלנו תוצאה של כלי. הלולאה הבאה מדפיסה את כל ההודעות:
import asyncio
import random
from agents import Agent, ItemHelpers, Runner, function_tool
from openai.types.responses import ResponseTextDeltaEvent

async def main():
    agent = Agent(
        name="Joker",
        instructions="Tell me a joke",
    )

    result = Runner.run_streamed(
        agent,
        input="Hello",
    )
    print("=== Run starting ===")

    async for event in result.stream_events():
        # We'll ignore the raw responses event deltas
        if event.type == "raw_response_event":
            if isinstance(event.data, ResponseTextDeltaEvent):
                print(f"[Token]: {event.data.delta}")
            else:
                print(event)

        # When the agent updates, print that
        elif event.type == "agent_updated_stream_event":
            print(f"Agent updated: {event.new_agent.name}")
            continue

        # When items are generated, print them
        elif event.type == "run_item_stream_event":
            if event.item.type == "tool_call_item":
                print("-- Tool was called")
            elif event.item.type == "tool_call_output_item":
                print(f"-- Tool output: {event.item.output}")
            elif event.item.type == "message_output_item":
                print(f"-- Message output:\n {ItemHelpers.text_message_output(event.item)}")
            else:
                print(f" -- Other Event: {event}")

    print("=== Run complete ===")


if __name__ == "__main__":
    asyncio.run(main())
הרצת התוכנית מדפיסה את הפלט הבא:
/Users/ynonp/work/projects/ai/leanagentssdk/.venv/bin/python /Users/ynonp/work/projects/ai/leanagentssdk/streaming.py 
=== Run starting ===
Agent updated: Joker

ToCode
1 419
יום 6 - ארגון הקוד ובחירת מודלים הפוסט היום מסיים את החלק הראשון של סידרת הפוסטים על פיתוח משולב AI. בסידרה עד עכשיו למדנו איך להתקין את ספריית OpenAI Agents SDK, למדנו על שני סוגי מערכות משולבות AI (סקריפט שרץ ברקע וסוכן אינטרקטיבי) ואנחנו אפילו כבר יודעים איך לכתוב תוכניות פשוטות משני הסוגים. בואו נסיים את החלק הראשון בסקירה של מבנה הקוד הרצוי למערכת וכמה טיפים על בחירת מודלים. מבנה קוד ניזכר איך כתבנו סוכן בתוכניות הדוגמה עד עכשיו:
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.
    """
)
במערכת אמיתית יש שני דברים שיהיו הרבה יותר מסובכים מאשר בדוגמה: בחירת המודל והגדרת בלוק ה instructions. פרומפט טוב מסביר לעומק מה תפקיד המודל, נותן לו קונטקסט מתוך העולם שלכם והכי חשוב כולל דוגמאות לקלט ופלט. פרומפט טוב גם צריך להתאים למודל, ולכן שינוי במודל מחייב הרבה פעמים גם שינוי בניסוח של הפרומפט, ולפעמים גירסה חדשה של המודל שאפילו לא ביקשתם פתאום גורמת לאותו פרומפט לעבוד יותר טוב או פחות טוב. כדאי להתיחס לפרומפטים בתור חלק עצמאי מהקוד ולכן אני אוהב לשמור פרומפט בקובץ נפרד. הרבה פעמים אני גם אוהב לשתול משתנים לתוך הפרומפט וכך אני יכול לשנות "פרמטרים" בסוכן שלי בהתאם לנסיבות. לכן בפרויקט אמיתי תהיה לנו תיקיה בשם agents, בתוכה תיקייה לכל סוכן ובתוכה קובץ סוכן וקובץ הוראות:
agents/
├── researcher/
│   ├── agent.py
│   └── prompt.md
בשביל לשלב משתנים כדאי להשתמש בספריית jinja2 וכך קוד הסוכן יהיה:
from agents import Agent, Runner
from pydantic import BaseModel, Field
from jinja2 import Environment, FileSystemLoader

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")

env = Environment(loader=FileSystemLoader("agents/researcher"))
template = env.get_template("instructions.md")

market_research_agent = Agent(
    name="MarketResearcher",
    output_type=list[BlogPostIdea],
    instructions=template.render()
)
מודלים יש המון מודלים ולא כדאי להתקבע על מודל אחד ש"תמיד עובד". לפעמים חשוב לנו לקבל תוצאה מהר, יש משימות פשוטות שחבל לשלם עליהן הרבה ויש גם מצבים שהמודלים הפשוטים יתנו תוצאה יותר נכונה ממודל Reasoning מתקדם. כל מערכת היא שונה וחשוב לנסות לפתור משימה עם מספר מודלים ומספר פרומפטים ולעשות אופטימיזציה. זה הסדר שאני בודק היום מודלים אולי יעזור גם לכם: 1. ג'מיני פלאש מאוד זול, מהיר וטוב. אם הוא מצליח לפתור את הבעיה שלכם הרווחתם. 2. מודל Deepseek הסיני גם מאוד זול ומהיר. שווה לנסות גם את גירסת ה Chat וגם את גירסת ה Reasoning שלהם. 3. קלוד יקר אבל קלוד סונט מהיר. במשימות תרגום או משימות שדורשות יצירתיות אפשר לקבל ממנו תוצאה טובה ולפעמים זה שווה את המחיר. 4. מודלי ה Reasoning המפורסמים (ג'מיני פרו, o3, אופוס) מאוד יקרים ואיטיים. כדאי להשתמש בהם ב Workflows שרצים ברקע וגם זה רק בחלק קטן מהתהליך. 5. אפשר לקבל גישה למודלי קוד פתוח דרך https://openrouter.ai/models. כדאי לנסות מספר מודלים ולא לבחור את הראשון שעובד, כדאי לנסות על מספר פרומפטים ומדי פעם כדאי לחזור על הניסוי כי דברים משתנים. יש גם מערכות Tracing שמאפשרות לנטר כל בקשה שנשלחת ל AI וכל תשובה שלו ונראה בהמשך הסידרה איך לשלב אותן. עכשיו אתם הירשמו ל OpenRouter, השקיעו 2$ בקרדיטים ושלחו משימה למספר מודלי קוד פתוח כדי לראות את ההבדל בין התשובות ומי נותן לכם את התשובה הטובה ביותר. לאחר מכן השתמשו בדף התיעוד כאן: https://docs.litellm.ai/docs/providers/openrouter כדי לחבר את OpenRouter לקוד סוכן שתכתבו עם OpenAI Agents SDK ותנו למודל קוד פתוח לבצע את המשימות.

ToCode
1 419
[{'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 419
יום 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