יום 20 - משחק איקס עיגול בשילוב AI
ככל שניתן לצפות קדימה, המערכות של העתיד הולכות לשלב ממשקי שיחה עם ממשקים גרפיים קלאסיים. אנחנו כבר רואים אותן: זאת Lovable שמאפשרת לבנות דפי אינטרנט דרך ממשק שיחה ואז לערוך אותם בממשק גרפי "קלאסי"; זה ג'ימייל שמאפשר ל AI לכתוב טיוטה למייל ואז לבן אדם לערוך את הטיוטה ואז להחזיר ל AI לעוד עריכה. שיתופי הפעולה האלה אפשריים בזכות קוד JavaScript מתקדם שמאפשר לאדם ול AI לעבוד על אותו תוצר.
היום נבנה מודל של מערכת כזאת עבור משחק איקס עיגול אותו ניתן לשחק בעזרת AI. העליתי את קוד המשחק המלא לגיטהאב תוכלו למצוא אותו כאן:
https://github.com/ynonp/tictactoe-with-ai
בואו נמשיך לקרוא את הקוד ולהבין את החלקים המרכזיים בארכיטקטורה זו.
1. בואו נשחק איקס עיגול
את המשחק אנחנו מפעילים במצב פיתוח עם הפקודה:
uvicorn app.main:app --reload
אחרי הפעלה אני מקבל את הפלט:
INFO: Will watch for changes in these directories: ['/Users/ynonp/work/projects/ai/leanagentssdk/day20-tictactoe-ai']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [5711] using StatReload
INFO: Started server process [5727]
INFO: Waiting for application startup.
INFO: Application startup complete.
שאומר שהמשחק רץ בהצלחה על פורט 8000. לאחר ההפעלה וכניסה לעמוד בדפדפן אנחנו מוצאים מסך של משחק איקס עיגול עם הטקסט Connected to game assistant ובתיבת צד אני רואה מסך שיחה עם AI.
בשביל לשחק אני יכול ללחוץ על משבצת או לכתוב ל AI ולבקש ממנו לבחור איפה לשחק וגם לשים את ה X במשבצת.
2. איך זה עובד
אנחנו יודעים איך לכתוב הודעה ל AI מתוך ממשק ווב ולהציג את התשובה בחלון הצ'אט, אבל פה יש משהו מעבר - ה AI כאילו "לוחץ על כפתורים" בשמנו. מה קורה כאן?
הסוד נקרא שימוש בכלים, רק שבמקום להשתמש בכלי שיהיה פשוט פונקציית פייתון שתעשה משהו בצד שרת, הכלי שלי שולח בקשה לדפדפן ומבקש להריץ קוד JavaScript מסוים. בצורה כזאת הסוכן רץ בפייתון אבל הפעולה שלו מקבלת ביטוי ב JavaScript.
זה הכלי ששולח פקודת "שחק במשבצת" לדפדפן מתוך הקובץ routes_tictactoe.py:
@function_tool
async def play(ctx: RunContextWrapper[WebsocketContext], row: int, column: int):
"""Make a move in the tic tac toe game at the specified row and column (0-2)"""
websocket = ctx.context.websocket
await websocket.send_json({
"action": "play",
"payload": {"row": row, "column": column}
})
return f"Played at row {row}, column {column}"
אנחנו רואים שהכלי מחזיר טקסט תוצאה לסוכן. מבחינת פונקציונאליות הוא שולח הודעה דרך websocket מהשרת לדפדפן. בצד הדפדפן כתבתי את הפונקציה הבאה המטפלת בהודעות מהשרת בקובץ app.js:
function handleWebSocketMessage(message) {
console.log('Received WebSocket message:', message);
if (message.action === 'play') {
const { row, column } = message.payload;
makeAssistantMove(row, column);
} else if (message.type === 'echo') {
console.log('Echo from server:', message.data);
}
}
החלק האחרון של החיבור הוא העברת המידע. בשביל שהסוכן ידע איפה לשחק ההודעה צריכה לכלול את ההודעה של המשתמש אבל גם את מצב הלוח. הקוד ששולח את ההודעה לשרת יצא קצת ארוך בואו נקרא אותו שוב בקובץ app.js:
async function sendChatMessage(userMessage, gameBoard = null, gameState = null) {
// Add user message to UI and history
addMessageToChat('user', userMessage);
chatHistory.push({ role: "user", content: userMessage });
// If board state is provided, add it as a system message
if (gameBoard && gameState) {
const boardMessage = getBoardStatusMessage(gameBoard);
const statusMessage = getCurrentGameStatus(gameBoard, gameState);
const systemMessage = `${boardMessage} ${statusMessage}`;
// Add to chat history but not to UI (system message)
chatHistory.push({ role: "user", content: systemMessage });
}
// Create assistant message element for streaming
const assistantMessageEl = addMessageToChat('assistant', '', true);
let assistantResponse = '';
try {
const response = await fetch(`/api/complete?client_id=${CLIENT_ID}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({payload: chatHistory}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
// End of stream
assistantMessageEl.classList.remove('streaming');
chatHistory.push({ role: "assistant", content: assistantResponse });
return;
}
if (data.trim()) {
assistantResponse += data;
updateMessageContent(assistantMessageEl, assistantResponse);
}
}
}
}
// Fallback in case [DONE] wasn't received
assistantMessageEl.classList.remove('streaming');
if (assistantResponse) {
chatHistory.push({ role: "assistant", content: assistantResponse });
}
} catch (error) {
console.error('Chat error:', error);
updateMessageContent(assistantMessageEl, 'Sorry, I encountered an error. Please try again.');
assistantMessageEl.classList.remove('streaming');
assistantMessageEl.classList.add('error');
}
}
הקוד מחולק ל-3 חלקים:
בחלק הראשון אנחנו בונים את המערך chatHistory. מערך זה כולל את כל ההודעות שמופיעות על המסך בשיחה והודעה נוספת שמורכבת מהפלט של הפונקציות
getBoardStatusMessageוgetCurrentGameStatus. פונקציות אלה מקודדות למחרוזת את לוח המשחק ואת מצב המשחק (אם מישהו כבר ניצח או אם יש תיקו). המידע הזה יעזור לסוכן להבין מה לענות ויעזור לסוכן לראות את מה שהמשתמש רואה.בחלק השני אנחנו שולחים לשרת את המערך chatHistory. נשים לב שבדוגמה הזאת השרת לא שומר את היסטוריית ההודעות של השחקנים וכל "שחקן" אחראי לזכור מה היסטוריית ההודעות שלו.
בחלק השלישי אנחנו מפענחים את תשובת השרת וכותבים אותה למסך השיחה.
במהלך תשובת השרת ה AI יוכל להפעיל כלים, מה שיגרום לשליחת הודעות דרך ה Web Sockets. זה בסדר, JavaScript מתמודד עם קוד אסינכרוני.
3. עכשיו אתם
הצעד הבא של המשחק הזה הוא להוסיף עוד סוג אינטרקציה ל AI. אחרי כל מהלך של שחקן עדכנו את הקוד כך שה AI יוכל לראות את המהלך ויעודד או ייתן חוות דעת על כל צעד.