@statelyai/agent
Version:
Stateful agents that make decisions based on finite-state machine models
225 lines (210 loc) • 6.05 kB
text/typescript
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();