UNPKG

@statelyai/agent

Version:

Stateful agents that make decisions based on finite-state machine models

225 lines (210 loc) 6.05 kB
import { assign, setup, assertEvent, createActor } from 'xstate'; import { z } from 'zod'; import { createAgent, fromDecision, fromTextStream } from '../src'; import { openai } from '@ai-sdk/openai'; const agent = createAgent({ name: 'tic-tac-toe-bot', model: openai('gpt-4-0125-preview'), events: { 'agent.x.play': z.object({ index: z .number() .min(0) .max(8) .describe('The index of the cell to play on'), }), 'agent.o.play': z.object({ index: z .number() .min(0) .max(8) .describe('The index of the cell to play on'), }), reset: z.object({}).describe('Reset the game to the initial state'), }, context: { board: z .array(z.union([z.literal(null), z.literal('x'), z.literal('o')])) .describe('The 3x3 board represented as a 9-element array.'), moves: z .number() .min(0) .max(9) .describe('The number of moves made in the game.'), player: z .union([z.literal('x'), z.literal('o')]) .describe('The current player (x or o)'), gameReport: z.string(), }, }); type Player = 'x' | 'o'; const initialContext = { board: Array(9).fill(null) as Array<Player | null>, moves: 0, player: 'x' as Player, gameReport: '', } satisfies typeof agent.types.context; function getWinner(board: typeof initialContext.board): Player | null { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ] as const; for (const [a, b, c] of lines) { if (board[a] !== null && board[a] === board[b] && board[a] === board[c]) { return board[a]!; } } return null; } export const ticTacToeMachine = setup({ types: { context: agent.types.context, events: agent.types.events, }, actors: { agent: fromDecision(agent), gameReporter: fromTextStream(agent), }, actions: { updateBoard: assign({ board: ({ context, event }) => { assertEvent(event, ['agent.x.play', 'agent.o.play']); const updatedBoard = [...context.board]; updatedBoard[event.index] = context.player; return updatedBoard; }, moves: ({ context }) => context.moves + 1, player: ({ context }) => (context.player === 'x' ? 'o' : 'x'), }), resetGame: assign(initialContext), printBoard: ({ context }) => { // Print the context.board in a 3 x 3 grid format let boardString = ''; for (let i = 0; i < context.board.length; i++) { if ([0, 3, 6].includes(i)) { boardString += context.board[i] ?? ' '; } else { boardString += ' | ' + (context.board[i] ?? ' '); if ([2, 5].includes(i)) { boardString += '\n--+---+--\n'; } } } console.log(boardString); }, }, guards: { checkWin: ({ context }) => { const winner = getWinner(context.board); return !!winner; }, checkDraw: ({ context }) => { return context.moves === 9; }, isValidMove: ({ context, event }) => { try { assertEvent(event, ['agent.o.play', 'agent.x.play']); } catch { return false; } return context.board[event.index] === null; }, }, }).createMachine({ initial: 'playing', context: initialContext, states: { playing: { always: [ { target: 'gameOver.winner', guard: 'checkWin' }, { target: 'gameOver.draw', guard: 'checkDraw' }, ], initial: 'x', states: { x: { entry: 'printBoard', on: { 'agent.x.play': [ { target: 'o', guard: 'isValidMove', actions: 'updateBoard', }, { target: 'x', reenter: true }, ], }, }, o: { entry: 'printBoard', on: { 'agent.o.play': [ { target: 'x', guard: 'isValidMove', actions: 'updateBoard', }, { target: 'o', reenter: true }, ], }, }, }, }, gameOver: { initial: 'winner', invoke: { src: 'gameReporter', input: ({ context }) => ({ context: { events: agent.getObservations().map((o) => o.event), board: context.board, }, prompt: 'Provide a short game report analyzing the game.', }), onSnapshot: { actions: assign({ gameReport: ({ context, event }) => { console.log( context.gameReport + (event.snapshot.context?.textDelta ?? '') ); return ( context.gameReport + (event.snapshot.context?.textDelta ?? '') ); }, }), }, }, states: { winner: { tags: 'winner', }, draw: { tags: 'draw', }, }, on: { reset: { target: 'playing', actions: 'resetGame', }, }, }, }, }); const actor = createActor(ticTacToeMachine); agent.interact(actor, (observed) => { if (observed.state.matches('playing')) { return { goal: `You are playing a game of tic tac toe. This is the current game state. The 3x3 board is represented by a 9-element array. The first element is the top-left cell, the second element is the top-middle cell, the third element is the top-right cell, the fourth element is the middle-left cell, and so on. The value of each cell is either null, x, or o. The value of null means that the cell is empty. The value of x means that the cell is occupied by an x. The value of o means that the cell is occupied by an o. ${JSON.stringify(observed.state.context, null, 2)} Execute the single best next move to try to win the game. Do not play on an existing cell.`, }; } return; }); actor.start();