From fd4214da6e7759a7ba848f177f827c70be20a2e9 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 15 Mar 2024 11:09:25 -0400 Subject: [PATCH 1/7] Use events instead of chat --- examples/joke.ts | 79 ++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/examples/joke.ts b/examples/joke.ts index 8826098..c87ef4a 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -19,7 +19,7 @@ const schemas = createSchemas({ }, }, desire: { type: ['string', 'null'] }, - lastRating: { type: ['string', 'null'] }, + lastRating: { type: ['number', 'null'] }, }, required: ['topic', 'jokes', 'desire', 'lastRating'], }, @@ -32,10 +32,32 @@ const schemas = createSchemas({ }, }, }, + tellJoke: { + type: 'object', + properties: { + joke: { + type: 'string', + }, + }, + }, endJokes: { type: 'object', properties: {}, }, + rateJoke: { + type: 'object', + properties: { + rating: { + type: 'number', + minimum: 1, + maximum: 10, + }, + explanation: { + type: 'string', + description: 'An explanation for the rating', + }, + }, + }, }, }); @@ -43,11 +65,11 @@ const adapter = createOpenAIAdapter(openai, { model: 'gpt-3.5-turbo-1106', }); -const getJokeCompletion = adapter.fromChat( +const getJokeCompletion = adapter.fromEvent( (topic: string) => `Tell me a joke about ${topic}.` ); -const rateJoke = adapter.fromChat( +const rateJoke = adapter.fromEvent( (joke: string) => `Rate this joke on a scale of 1 to 10: ${joke}` ); @@ -66,7 +88,7 @@ const getTopic = fromPromise(async () => { }); const decide = adapter.fromEvent( - (lastRating: string) => + (lastRating: number) => `Choose what to do next, given the previous rating of the joke: ${lastRating}` ); export function getRandomFunnyPhrase() { @@ -144,54 +166,45 @@ const jokeMachine = setup({ { src: 'getJokeCompletion', input: ({ context }) => context.topic, - onDone: { - actions: [ - assign({ - jokes: ({ context, event }) => - context.jokes.concat( - event.output.choices[0]!.message.content! - ), - }), - log((x) => `\n` + x.context.jokes.at(-1)), - ], - target: 'rateJoke', - }, }, { src: 'loader', input: getRandomFunnyPhrase, }, ], + on: { + tellJoke: { + actions: assign({ + jokes: ({ context, event }) => [...context.jokes, event.joke], + }), + target: 'rateJoke', + }, + }, }, rateJoke: { invoke: [ { src: 'rateJoke', input: ({ context }) => context.jokes[context.jokes.length - 1]!, - onDone: { - actions: [ - assign({ - lastRating: ({ event }) => - event.output.choices[0]!.message.content!, - }), - log(({ context }) => '\n' + context.lastRating), - ], - target: 'decide', - }, }, { src: 'loader', input: getRandomRatingPhrase, }, ], + on: { + rateJoke: { + actions: assign({ + lastRating: ({ event }) => event.rating, + }), + target: 'decide', + }, + }, }, decide: { invoke: { src: 'decide', input: ({ context }) => context.lastRating!, - onDone: { - actions: log(({ event }) => event), - }, }, on: { askForTopic: { @@ -216,5 +229,11 @@ const jokeMachine = setup({ }, }); -const agent = createAgent(jokeMachine); +const agent = createAgent(jokeMachine, { + inspect: (ev) => { + if (ev.type === '@xstate.event') { + console.log(ev.event); + } + }, +}); agent.start(); From 867c2704b86da83619e27418a637f65b59e8df5e Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 15 Mar 2024 11:24:49 -0400 Subject: [PATCH 2/7] Use only events --- examples/joke.ts | 80 ++++++++++++++------------------ examples/numberGuesser.ts | 42 ++++------------- examples/ticTacToe.ts | 98 ++++++++++++++------------------------- examples/weather.ts | 44 +++++++++--------- examples/wordGuesser.ts | 40 ++++++++-------- package.json | 4 +- pnpm-lock.yaml | 18 +++++++ src/agent.ts | 2 +- src/index.ts | 2 +- src/schemas.ts | 29 ++---------- 10 files changed, 148 insertions(+), 211 deletions(-) diff --git a/examples/joke.ts b/examples/joke.ts index c87ef4a..9c14550 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -1,61 +1,44 @@ import OpenAI from 'openai'; import { assign, fromCallback, fromPromise, log, setup } from 'xstate'; -import { createAgent, createOpenAIAdapter, createSchemas } from '../src'; +import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import { loadingAnimation } from './helpers/loader'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); -const schemas = createSchemas({ - context: { +const eventSchemas = defineEvents({ + askForTopic: { type: 'object', properties: { - topic: { type: 'string' }, - jokes: { - type: 'array', - items: { - type: 'string', - }, + topic: { + type: 'string', }, - desire: { type: ['string', 'null'] }, - lastRating: { type: ['number', 'null'] }, }, - required: ['topic', 'jokes', 'desire', 'lastRating'], }, - events: { - askForTopic: { - type: 'object', - properties: { - topic: { - type: 'string', - }, + tellJoke: { + type: 'object', + properties: { + joke: { + type: 'string', }, }, - tellJoke: { - type: 'object', - properties: { - joke: { - type: 'string', - }, + }, + endJokes: { + type: 'object', + properties: {}, + }, + rateJoke: { + type: 'object', + properties: { + rating: { + type: 'number', + minimum: 1, + maximum: 10, }, - }, - endJokes: { - type: 'object', - properties: {}, - }, - rateJoke: { - type: 'object', - properties: { - rating: { - type: 'number', - minimum: 1, - maximum: 10, - }, - explanation: { - type: 'string', - description: 'An explanation for the rating', - }, + explanation: { + type: 'string', + description: 'An explanation for the rating', }, }, }, @@ -131,8 +114,17 @@ const loader = fromCallback(({ input }: { input: string }) => { }); const jokeMachine = setup({ - schemas, - types: schemas.types, + schemas: eventSchemas, + types: { + context: {} as { + topic: string; + jokes: string[]; + desire: string | null; + lastRating: number | null; + loader: string | null; + }, + events: eventSchemas.types, + }, actors: { getJokeCompletion, getTopic, diff --git a/examples/numberGuesser.ts b/examples/numberGuesser.ts index 25201ec..c83e211 100644 --- a/examples/numberGuesser.ts +++ b/examples/numberGuesser.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import { createAgent, createOpenAIAdapter, createSchemas } from '../src'; +import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import { assign, setup } from 'xstate'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, @@ -23,39 +23,17 @@ const guessLogic = adapter.fromEvent( ` ); -const schemas = createSchemas({ - context: { - type: 'object', +const eventSchemas = defineEvents({ + guess: { properties: { - lastGuess: { - type: ['number', 'null'], - description: 'The last guess', - }, - previousGuesses: { - type: 'array', - items: { - type: 'number', - }, - description: 'The previous guesses', - }, - answer: { + number: { + // integer type: 'number', - description: 'The answer', - }, - }, - }, - events: { - guess: { - properties: { - number: { - // integer - type: 'number', - minimum: 1, - maximum: 10, - }, + minimum: 1, + maximum: 10, }, - required: ['number'], }, + required: ['number'], }, }); @@ -66,9 +44,9 @@ const machine = setup({ answer: number; }, input: {} as { answer: number }, - events: schemas.types.events, + events: eventSchemas.types, }, - schemas, + schemas: eventSchemas, actors: { guessLogic, }, diff --git a/examples/ticTacToe.ts b/examples/ticTacToe.ts index a4f1ee1..b872cbc 100644 --- a/examples/ticTacToe.ts +++ b/examples/ticTacToe.ts @@ -1,6 +1,6 @@ import { assign, setup, assertEvent } from 'xstate'; import OpenAI from 'openai'; -import { createOpenAIAdapter, createSchemas, createAgent } from '../src'; +import { createOpenAIAdapter, defineEvents, createAgent } from '../src'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, @@ -8,70 +8,41 @@ const openai = new OpenAI({ type Player = 'x' | 'o'; -const schemas = createSchemas({ - context: { - type: 'object', +const eventSchemas = defineEvents({ + 'x.play': { properties: { - board: { - type: 'array', - items: { - type: ['null', 'string'], - enum: [null, 'x', 'o'], - }, - minItems: 9, - maxItems: 9, - description: 'The board of the tic-tac-toe game', - }, - moves: { + index: { + description: 'The index of the cell to play on', type: 'number', - description: 'The number of moves that have been played', - }, - player: { - type: 'string', - enum: ['x', 'o'], - description: 'The player whose turn it is', - }, - gameReport: { - type: 'string', - description: 'The game report', - }, - events: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - required: ['board', 'moves', 'player', 'gameReport', 'events'], - }, - events: { - 'x.play': { - properties: { - index: { - description: 'The index of the cell to play on', - type: 'number', - minimum: 0, - maximum: 8, - }, + minimum: 0, + maximum: 8, }, }, - 'o.play': { - properties: { - index: { - description: 'The index of the cell to play on', - type: 'number', - minimum: 0, - maximum: 8, - }, + }, + 'o.play': { + properties: { + index: { + description: 'The index of the cell to play on', + type: 'number', + minimum: 0, + maximum: 8, }, }, - reset: { - properties: {}, - }, + }, + reset: { + properties: {}, }, }); +interface GameContext { + board: (Player | null)[]; + moves: number; + player: Player; + gameReport: string; + events: string[]; +} + const adapter = createOpenAIAdapter(openai, { model: 'gpt-4-1106-preview', }); @@ -82,10 +53,10 @@ const initialContext = { player: 'x' as Player, gameReport: '', events: [], -} satisfies typeof schemas.types.context; +} satisfies GameContext; const bot = adapter.fromEvent( - ({ context }: { context: typeof schemas.types.context }) => ` + ({ context }: { context: GameContext }) => ` 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(context, null, 2)} @@ -94,11 +65,7 @@ Execute the single best next move to try to win the game. Do not play on an exis ); const gameReporter = adapter.fromChatStream( - ({ - context, - }: { - context: typeof schemas.types.context; - }) => `Here is the game board: + ({ context }: { context: GameContext }) => `Here is the game board: ${JSON.stringify(context.board, null, 2)} @@ -131,8 +98,11 @@ function getWinner(board: typeof initialContext.board): Player | null { } export const ticTacToeMachine = setup({ - schemas, - types: schemas.types, + schemas: eventSchemas, + types: { + context: {} as GameContext, + events: eventSchemas.types, + }, actors: { bot, gameReporter, diff --git a/examples/weather.ts b/examples/weather.ts index c3c99c6..e8c588e 100644 --- a/examples/weather.ts +++ b/examples/weather.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import { createAgent, createOpenAIAdapter, createSchemas } from '../src'; +import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import { assign, fromPromise, log, setup } from 'xstate'; import { getFromTerminal } from './helpers/helpers'; @@ -40,27 +40,20 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); -const schemas = createSchemas({ - context: { - location: { type: 'string' }, - history: { type: 'array', items: { type: 'string' } }, - count: { type: 'number' }, - }, - events: { - getWeather: { - description: 'Get the weather for a location', - properties: { - location: { - type: 'string', - description: 'The location to get the weather for', - }, +const eventSchemas = defineEvents({ + getWeather: { + description: 'Get the weather for a location', + properties: { + location: { + type: 'string', + description: 'The location to get the weather for', }, }, - doSomethingElse: { - description: - 'Do something else, because the user did not provide a location', - properties: {}, - }, + }, + doSomethingElse: { + description: + 'Do something else, because the user did not provide a location', + properties: {}, }, }); @@ -80,8 +73,15 @@ const getWeather = fromPromise(async ({ input }: { input: string }) => { }); const machine = setup({ - schemas, - types: schemas.types, + schemas: eventSchemas, + types: { + context: {} as { + location: string; + history: string[]; + count: number; + }, + events: eventSchemas.types, + }, actors: { getWeather, decide: adapter.fromEvent( diff --git a/examples/wordGuesser.ts b/examples/wordGuesser.ts index 7a5960c..c2f1ad7 100644 --- a/examples/wordGuesser.ts +++ b/examples/wordGuesser.ts @@ -1,6 +1,6 @@ import { assign, log, setup } from 'xstate'; import { getFromTerminal } from './helpers/helpers'; -import { createAgent, createOpenAIAdapter, createSchemas } from '../src'; +import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import OpenAI from 'openai'; const openAI = new OpenAI({ @@ -11,26 +11,24 @@ const adapter = createOpenAIAdapter(openAI, { model: 'gpt-4-1106-preview', }); -const schemas = createSchemas({ - events: { - guessLetter: { - description: 'Player guesses a letter', - properties: { - letter: { - type: 'string', - description: 'The letter guessed', - maxLength: 1, - minLength: 1, - }, +const eventSchemas = defineEvents({ + guessLetter: { + description: 'Player guesses a letter', + properties: { + letter: { + type: 'string', + description: 'The letter guessed', + maxLength: 1, + minLength: 1, }, }, - guessWord: { - description: 'Player guesses the full word', - properties: { - word: { - type: 'string', - description: 'The word guessed', - }, + }, + guessWord: { + description: 'Player guesses the full word', + properties: { + word: { + type: 'string', + description: 'The word guessed', }, }, }, @@ -45,7 +43,7 @@ const context = { const wordGuesserMachine = setup({ types: { context: {} as typeof context, - events: schemas.types.events, + events: eventSchemas.types, }, actors: { getFromTerminal, @@ -67,7 +65,7 @@ Please make your next guess - type a letter or the full word. You can only make ` ), }, - schemas, + schemas: eventSchemas, }).createMachine({ initial: 'providingWord', context, diff --git a/package.json b/package.json index 020c4fe..fd28e93 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "ts-node": "^10.9.2", "tsup": "^8.0.1", "typescript": "^5.3.3", - "vitest": "^1.2.2" + "vitest": "^1.2.2", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" }, "publishConfig": { "access": "public" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ff47ae..e6f1c37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,12 @@ devDependencies: vitest: specifier: ^1.2.2 version: 1.2.2(@types/node@20.10.6) + zod: + specifier: ^3.22.4 + version: 3.22.4 + zod-to-json-schema: + specifier: ^3.22.4 + version: 3.22.4(zod@3.22.4) packages: @@ -3479,3 +3485,15 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz} engines: {node: '>=12.20'} dev: true + + /zod-to-json-schema@3.22.4(zod@3.22.4): + resolution: {integrity: sha512-2Ed5dJ+n/O3cU383xSY28cuVi0BCQhF8nYqWU5paEpl7fVdqdAmiLdqLyfblbNdfOFwFfi/mqU4O1pwc60iBhQ==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz} + peerDependencies: + zod: ^3.22.4 + dependencies: + zod: 3.22.4 + dev: true + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==, tarball: https://registry.npmjs.org/zod/-/zod-3.22.4.tgz} + dev: true diff --git a/src/agent.ts b/src/agent.ts index a5ebdaa..be0dc1a 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1,4 +1,4 @@ -import { ActorOptions, AnyStateMachine, createActor } from 'xstate'; +import { AnyStateMachine, createActor } from 'xstate'; export function createAgent( ...args: Parameters> diff --git a/src/index.ts b/src/index.ts index bdfd3ae..62ae9b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export { createSchemas } from './schemas'; +export { defineEventSchemas as defineEvents } from './schemas'; export { createAgent } from './agent'; export { createOpenAIAdapter } from './adapters/openai'; diff --git a/src/schemas.ts b/src/schemas.ts index f3b9fbf..32c523e 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,39 +1,18 @@ import { Values } from 'xstate'; import { - ContextSchema, EventSchemas, ConvertToJSONSchemas, createEventSchemas, } from './utils'; import { FromSchema } from 'json-schema-to-ts'; -export function createSchemas< - const TContextSchema extends ContextSchema, - const TEventSchemas extends EventSchemas ->({ - context, - events, -}: { - /** - * The JSON schema for the context object. - * - * Must be of `{ type: 'object' }`. - */ - context?: TContextSchema; - /** - * An object mapping event types to each event object's JSON Schema. - */ - events: TEventSchemas; -}): { - context: TContextSchema | undefined; +export function defineEventSchemas( + events: TEventSchemas +): { events: ConvertToJSONSchemas; - types: { - context: FromSchema; - events: FromSchema>>; - }; + types: FromSchema>>; } { return { - context, events: createEventSchemas(events), types: {} as any, }; From 483fc469aa3ca6fff6175e2e180e40ac66a2e2b3 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 16 Mar 2024 09:44:21 -0400 Subject: [PATCH 3/7] Use Zod --- examples/joke.ts | 49 +++++++++++---------------------------- examples/numberGuesser.ts | 17 ++++---------- examples/ticTacToe.ts | 45 ++++++++++++++++------------------- examples/weather.ts | 23 +++++++----------- examples/wordGuesser.ts | 36 +++++++++++----------------- package.json | 2 +- src/index.ts | 2 +- src/schemas.ts | 29 ++++++++++++++--------- src/utils.ts | 24 ++++++++++++++++++- 9 files changed, 103 insertions(+), 124 deletions(-) diff --git a/examples/joke.ts b/examples/joke.ts index 9c14550..aae1ac6 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -2,46 +2,25 @@ import OpenAI from 'openai'; import { assign, fromCallback, fromPromise, log, setup } from 'xstate'; import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import { loadingAnimation } from './helpers/loader'; +import { z } from 'zod'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); const eventSchemas = defineEvents({ - askForTopic: { - type: 'object', - properties: { - topic: { - type: 'string', - }, - }, - }, - tellJoke: { - type: 'object', - properties: { - joke: { - type: 'string', - }, - }, - }, - endJokes: { - type: 'object', - properties: {}, - }, - rateJoke: { - type: 'object', - properties: { - rating: { - type: 'number', - minimum: 1, - maximum: 10, - }, - explanation: { - type: 'string', - description: 'An explanation for the rating', - }, - }, - }, + askForTopic: z.object({ + topic: z.string().describe('The topic for the joke'), + }), + tellJoke: z.object({ + joke: z.string().describe('The joke text'), + }), + endJokes: z.object({}).describe('End the jokes'), + + rateJoke: z.object({ + rating: z.number().min(1).max(10), + explanation: z.string(), + }), }); const adapter = createOpenAIAdapter(openai, { @@ -123,7 +102,7 @@ const jokeMachine = setup({ lastRating: number | null; loader: string | null; }, - events: eventSchemas.types, + events: eventSchemas.type, }, actors: { getJokeCompletion, diff --git a/examples/numberGuesser.ts b/examples/numberGuesser.ts index c83e211..fb2a0d3 100644 --- a/examples/numberGuesser.ts +++ b/examples/numberGuesser.ts @@ -1,6 +1,7 @@ import OpenAI from 'openai'; import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import { assign, setup } from 'xstate'; +import { z } from 'zod'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); @@ -24,17 +25,9 @@ const guessLogic = adapter.fromEvent( ); const eventSchemas = defineEvents({ - guess: { - properties: { - number: { - // integer - type: 'number', - minimum: 1, - maximum: 10, - }, - }, - required: ['number'], - }, + guess: z.object({ + number: z.number().min(1).max(10).describe('The number guessed'), + }), }); const machine = setup({ @@ -44,7 +37,7 @@ const machine = setup({ answer: number; }, input: {} as { answer: number }, - events: eventSchemas.types, + events: eventSchemas.type, }, schemas: eventSchemas, actors: { diff --git a/examples/ticTacToe.ts b/examples/ticTacToe.ts index b872cbc..d18211c 100644 --- a/examples/ticTacToe.ts +++ b/examples/ticTacToe.ts @@ -1,5 +1,7 @@ import { assign, setup, assertEvent } from 'xstate'; import OpenAI from 'openai'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; import { createOpenAIAdapter, defineEvents, createAgent } from '../src'; const openai = new OpenAI({ @@ -9,32 +11,25 @@ const openai = new OpenAI({ type Player = 'x' | 'o'; const eventSchemas = defineEvents({ - 'x.play': { - properties: { - index: { - description: 'The index of the cell to play on', - type: 'number', - - minimum: 0, - maximum: 8, - }, - }, - }, - 'o.play': { - properties: { - index: { - description: 'The index of the cell to play on', - type: 'number', - minimum: 0, - maximum: 8, - }, - }, - }, - reset: { - properties: {}, - }, + 'x.play': z.object({ + index: z + .number() + .min(0) + .max(8) + .describe('The index of the cell to play on'), + }), + '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'), }); +eventSchemas.type; + interface GameContext { board: (Player | null)[]; moves: number; @@ -101,7 +96,7 @@ export const ticTacToeMachine = setup({ schemas: eventSchemas, types: { context: {} as GameContext, - events: eventSchemas.types, + events: eventSchemas.type, }, actors: { bot, diff --git a/examples/weather.ts b/examples/weather.ts index e8c588e..18c2478 100644 --- a/examples/weather.ts +++ b/examples/weather.ts @@ -2,6 +2,7 @@ import OpenAI from 'openai'; import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import { assign, fromPromise, log, setup } from 'xstate'; import { getFromTerminal } from './helpers/helpers'; +import { z } from 'zod'; async function searchTavily( input: string, @@ -41,20 +42,12 @@ const openai = new OpenAI({ }); const eventSchemas = defineEvents({ - getWeather: { - description: 'Get the weather for a location', - properties: { - location: { - type: 'string', - description: 'The location to get the weather for', - }, - }, - }, - doSomethingElse: { - description: - 'Do something else, because the user did not provide a location', - properties: {}, - }, + getWeather: z.object({ + location: z.string().describe('The location to get the weather for'), + }), + doSomethingElse: z + .object({}) + .describe('Do something else, because the user did not provide a location'), }); const adapter = createOpenAIAdapter(openai, { @@ -80,7 +73,7 @@ const machine = setup({ history: string[]; count: number; }, - events: eventSchemas.types, + events: eventSchemas.type, }, actors: { getWeather, diff --git a/examples/wordGuesser.ts b/examples/wordGuesser.ts index c2f1ad7..62e2135 100644 --- a/examples/wordGuesser.ts +++ b/examples/wordGuesser.ts @@ -2,6 +2,7 @@ import { assign, log, setup } from 'xstate'; import { getFromTerminal } from './helpers/helpers'; import { createAgent, createOpenAIAdapter, defineEvents } from '../src'; import OpenAI from 'openai'; +import { z } from 'zod'; const openAI = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, @@ -11,27 +12,14 @@ const adapter = createOpenAIAdapter(openAI, { model: 'gpt-4-1106-preview', }); -const eventSchemas = defineEvents({ - guessLetter: { - description: 'Player guesses a letter', - properties: { - letter: { - type: 'string', - description: 'The letter guessed', - maxLength: 1, - minLength: 1, - }, - }, - }, - guessWord: { - description: 'Player guesses the full word', - properties: { - word: { - type: 'string', - description: 'The word guessed', - }, - }, - }, +const events = defineEvents({ + guessLetter: z.object({ + letter: z.string().min(1).max(1).describe('The letter guessed'), + }), + + guessWord: z.object({ + word: z.string().describe('The word guessed'), + }), }); const context = { @@ -43,7 +31,7 @@ const context = { const wordGuesserMachine = setup({ types: { context: {} as typeof context, - events: eventSchemas.types, + events: events.type, }, actors: { getFromTerminal, @@ -65,7 +53,9 @@ Please make your next guess - type a letter or the full word. You can only make ` ), }, - schemas: eventSchemas, + schemas: { + events: events.schemas, + }, }).createMachine({ initial: 'providingWord', context, diff --git a/package.json b/package.json index fd28e93..6fd914c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsup src/index.ts --format cjs,esm --dts", - "lint": "tsc", + "lint": "tsc --noEmit", "test": "vitest", "example": "ts-node examples/helpers/runner.ts", "prepublishOnly": "tsup src/index.ts --dts", diff --git a/src/index.ts b/src/index.ts index 62ae9b8..23e54f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export { defineEventSchemas as defineEvents } from './schemas'; +export { defineEvents as defineEvents } from './schemas'; export { createAgent } from './agent'; export { createOpenAIAdapter } from './adapters/openai'; diff --git a/src/schemas.ts b/src/schemas.ts index 32c523e..fb06b8a 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,19 +1,26 @@ import { Values } from 'xstate'; -import { - EventSchemas, - ConvertToJSONSchemas, - createEventSchemas, -} from './utils'; -import { FromSchema } from 'json-schema-to-ts'; +import { createZodEventSchemas } from './utils'; +import { SomeZodObject, TypeOf } from 'zod'; -export function defineEventSchemas( +export type ZodEventTypes = { + // map event types to Zod types + [eventType: string]: SomeZodObject; +}; + +export function defineEvents( events: TEventSchemas ): { - events: ConvertToJSONSchemas; - types: FromSchema>>; + type: Values<{ + [K in keyof TEventSchemas]: { + type: K; + } & TypeOf; + }>; + schemas: { + [K in keyof TEventSchemas]: unknown; + }; } { return { - events: createEventSchemas(events), - types: {} as any, + type: {} as any, + schemas: createZodEventSchemas(events), }; } diff --git a/src/utils.ts b/src/utils.ts index ea63cab..ab9e3be 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,9 @@ import { AnyMachineSnapshot, AnyStateNode, Prop, Values } from 'xstate'; import { FromSchema } from 'json-schema-to-ts'; import { JSONSchema7 } from 'json-schema-to-ts/lib/types/definitions'; +import zodToJsonSchema, { JsonSchema7Type } from 'zod-to-json-schema'; +import { ZodEventTypes } from './schemas'; +import { z } from 'zod'; export function getAllTransitions(state: AnyMachineSnapshot) { const nodes = state._nodes; @@ -15,7 +18,7 @@ export type EventSchemas = { [key: string]: { description?: string; properties?: { - [key: string]: JSONSchema7; + [key: string]: JsonSchema7Type; }; }; }; @@ -55,5 +58,24 @@ export function createEventSchemas( return resolvedEventSchemaMap as ConvertToJSONSchemas; } +export function createZodEventSchemas( + eventSchemaMap: T +): { + [K in keyof T]: unknown; +} { + const resolvedEventSchemaMap = {}; + + for (const [eventType, zodType] of Object.entries(eventSchemaMap)) { + // @ts-ignore + resolvedEventSchemaMap[eventType] = zodToJsonSchema( + zodType.extend({ + type: z.literal(eventType), + }) + ); + } + + return resolvedEventSchemaMap as ConvertToJSONSchemas; +} + export type InferEventsFromSchemas> = FromSchema>; From 044bfbcadaa7ee0c237199e3262359901f7d8511 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 16 Mar 2024 09:46:14 -0400 Subject: [PATCH 4/7] Rename --- examples/weather.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/weather.ts b/examples/weather.ts index 18c2478..bef61af 100644 --- a/examples/weather.ts +++ b/examples/weather.ts @@ -41,7 +41,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); -const eventSchemas = defineEvents({ +const events = defineEvents({ getWeather: z.object({ location: z.string().describe('The location to get the weather for'), }), @@ -66,14 +66,14 @@ const getWeather = fromPromise(async ({ input }: { input: string }) => { }); const machine = setup({ - schemas: eventSchemas, + schemas: events, types: { context: {} as { location: string; history: string[]; count: number; }, - events: eventSchemas.type, + events: events.type, }, actors: { getWeather, From dedadf1153a8a17e9866609464a44814da0b3789 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 16 Mar 2024 09:48:46 -0400 Subject: [PATCH 5/7] types makes more sense --- examples/joke.ts | 2 +- examples/numberGuesser.ts | 2 +- examples/ticTacToe.ts | 4 ++-- examples/weather.ts | 2 +- examples/wordGuesser.ts | 2 +- src/schemas.ts | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/joke.ts b/examples/joke.ts index aae1ac6..1046490 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -102,7 +102,7 @@ const jokeMachine = setup({ lastRating: number | null; loader: string | null; }, - events: eventSchemas.type, + events: eventSchemas.types, }, actors: { getJokeCompletion, diff --git a/examples/numberGuesser.ts b/examples/numberGuesser.ts index fb2a0d3..0cc3f55 100644 --- a/examples/numberGuesser.ts +++ b/examples/numberGuesser.ts @@ -37,7 +37,7 @@ const machine = setup({ answer: number; }, input: {} as { answer: number }, - events: eventSchemas.type, + events: eventSchemas.types, }, schemas: eventSchemas, actors: { diff --git a/examples/ticTacToe.ts b/examples/ticTacToe.ts index d18211c..7a533f7 100644 --- a/examples/ticTacToe.ts +++ b/examples/ticTacToe.ts @@ -28,7 +28,7 @@ const eventSchemas = defineEvents({ reset: z.object({}).describe('Reset the game to the initial state'), }); -eventSchemas.type; +eventSchemas.types; interface GameContext { board: (Player | null)[]; @@ -96,7 +96,7 @@ export const ticTacToeMachine = setup({ schemas: eventSchemas, types: { context: {} as GameContext, - events: eventSchemas.type, + events: eventSchemas.types, }, actors: { bot, diff --git a/examples/weather.ts b/examples/weather.ts index bef61af..5aa784b 100644 --- a/examples/weather.ts +++ b/examples/weather.ts @@ -73,7 +73,7 @@ const machine = setup({ history: string[]; count: number; }, - events: events.type, + events: events.types, }, actors: { getWeather, diff --git a/examples/wordGuesser.ts b/examples/wordGuesser.ts index 62e2135..1fdbfd2 100644 --- a/examples/wordGuesser.ts +++ b/examples/wordGuesser.ts @@ -31,7 +31,7 @@ const context = { const wordGuesserMachine = setup({ types: { context: {} as typeof context, - events: events.type, + events: events.types, }, actors: { getFromTerminal, diff --git a/src/schemas.ts b/src/schemas.ts index fb06b8a..eed8d15 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -10,7 +10,7 @@ export type ZodEventTypes = { export function defineEvents( events: TEventSchemas ): { - type: Values<{ + types: Values<{ [K in keyof TEventSchemas]: { type: K; } & TypeOf; @@ -20,7 +20,7 @@ export function defineEvents( }; } { return { - type: {} as any, + types: {} as any, schemas: createZodEventSchemas(events), }; } From 72e8cc60320a2bb6da29be22a4f05111178a0d79 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 16 Mar 2024 10:45:42 -0400 Subject: [PATCH 6/7] Update examples --- examples/joke.ts | 11 +++++++---- examples/numberGuesser.ts | 8 +++++--- examples/ticTacToe.ts | 10 +++++----- examples/weather.ts | 34 +++++++++++++++++++++++++++++++--- src/adapters/openai.ts | 11 ++++++++--- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/examples/joke.ts b/examples/joke.ts index 1046490..fe03d97 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -8,7 +8,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); -const eventSchemas = defineEvents({ +const events = defineEvents({ askForTopic: z.object({ topic: z.string().describe('The topic for the joke'), }), @@ -93,7 +93,9 @@ const loader = fromCallback(({ input }: { input: string }) => { }); const jokeMachine = setup({ - schemas: eventSchemas, + schemas: { + events: events.schemas, + }, types: { context: {} as { topic: string; @@ -102,7 +104,7 @@ const jokeMachine = setup({ lastRating: number | null; loader: string | null; }, - events: eventSchemas.types, + events: events.types, }, actors: { getJokeCompletion, @@ -112,6 +114,7 @@ const jokeMachine = setup({ loader, }, }).createMachine({ + id: 'joke', context: () => ({ topic: '', jokes: [], @@ -203,7 +206,7 @@ const jokeMachine = setup({ const agent = createAgent(jokeMachine, { inspect: (ev) => { if (ev.type === '@xstate.event') { - console.log(ev.event); + console.log(`\n${ev.actorRef.id}`, ev.event); } }, }); diff --git a/examples/numberGuesser.ts b/examples/numberGuesser.ts index 0cc3f55..a225e08 100644 --- a/examples/numberGuesser.ts +++ b/examples/numberGuesser.ts @@ -24,7 +24,7 @@ const guessLogic = adapter.fromEvent( ` ); -const eventSchemas = defineEvents({ +const events = defineEvents({ guess: z.object({ number: z.number().min(1).max(10).describe('The number guessed'), }), @@ -37,9 +37,11 @@ const machine = setup({ answer: number; }, input: {} as { answer: number }, - events: eventSchemas.types, + events: events.types, + }, + schemas: { + events: events.schemas, }, - schemas: eventSchemas, actors: { guessLogic, }, diff --git a/examples/ticTacToe.ts b/examples/ticTacToe.ts index 7a533f7..b125f77 100644 --- a/examples/ticTacToe.ts +++ b/examples/ticTacToe.ts @@ -10,7 +10,7 @@ const openai = new OpenAI({ type Player = 'x' | 'o'; -const eventSchemas = defineEvents({ +const events = defineEvents({ 'x.play': z.object({ index: z .number() @@ -28,8 +28,6 @@ const eventSchemas = defineEvents({ reset: z.object({}).describe('Reset the game to the initial state'), }); -eventSchemas.types; - interface GameContext { board: (Player | null)[]; moves: number; @@ -93,10 +91,12 @@ function getWinner(board: typeof initialContext.board): Player | null { } export const ticTacToeMachine = setup({ - schemas: eventSchemas, + schemas: { + events: events.schemas, + }, types: { context: {} as GameContext, - events: eventSchemas.types, + events: events.types, }, actors: { bot, diff --git a/examples/weather.ts b/examples/weather.ts index 5aa784b..4e76ba9 100644 --- a/examples/weather.ts +++ b/examples/weather.ts @@ -45,6 +45,14 @@ const events = defineEvents({ getWeather: z.object({ location: z.string().describe('The location to get the weather for'), }), + reportWeather: z.object({ + location: z + .string() + .describe('The location the weather is being reported for'), + highF: z.number().describe('The high temperature today in Fahrenheit'), + lowF: z.number().describe('The low temperature today in Fahrenheit'), + summary: z.string().describe('A summary of the weather conditions'), + }), doSomethingElse: z .object({}) .describe('Do something else, because the user did not provide a location'), @@ -65,8 +73,12 @@ const getWeather = fromPromise(async ({ input }: { input: string }) => { return results; }); +const reportWeather = adapter.fromEvent(() => 'Report the weather'); + const machine = setup({ - schemas: events, + schemas: { + events: events.schemas, + }, types: { context: {} as { location: string; @@ -77,6 +89,7 @@ const machine = setup({ }, actors: { getWeather, + reportWeather, decide: adapter.fromEvent( (input: string) => `Decide what to do based on the given input, which may or may not be a location: ${input}` @@ -133,6 +146,17 @@ const machine = setup({ count: ({ context }) => context.count + 1, }), ], + target: 'reportWeather', + }, + }, + }, + reportWeather: { + invoke: { + src: 'reportWeather', + }, + on: { + reportWeather: { + actions: log(({ event }) => event), target: 'getLocation', }, }, @@ -146,8 +170,12 @@ const machine = setup({ }, }); -createAgent(machine, { +const actor = createAgent(machine, { input: { location: 'New York', }, -}).start(); +}); +actor.subscribe((s) => { + console.log(s.value); +}); +actor.start(); diff --git a/src/adapters/openai.ts b/src/adapters/openai.ts index b61ae64..7da2481 100644 --- a/src/adapters/openai.ts +++ b/src/adapters/openai.ts @@ -140,15 +140,20 @@ export function fromEvent( .map((t) => { const name = t.eventType.replace(/\./g, '_'); functionNameMapping[name] = t.eventType; + const eventSchema = eventSchemaMap[t.eventType]; + const { + description, + properties: { type, ...properties }, + } = eventSchema ?? {}; + return { type: 'function', function: { name, - description: - t.description ?? eventSchemaMap[t.eventType]?.description, + description: t.description ?? description, parameters: { type: 'object', - properties: eventSchemaMap[t.eventType]?.properties ?? {}, + properties: properties ?? {}, }, }, } as const; From 8a2c34b8a99161bf47c72df8eed3f5d3b6a19f5f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 16 Mar 2024 10:48:15 -0400 Subject: [PATCH 7/7] Add changeset --- .changeset/soft-readers-attend.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .changeset/soft-readers-attend.md diff --git a/.changeset/soft-readers-attend.md b/.changeset/soft-readers-attend.md new file mode 100644 index 0000000..30babd5 --- /dev/null +++ b/.changeset/soft-readers-attend.md @@ -0,0 +1,28 @@ +--- +'@statelyai/agent': patch +--- + +The `createSchemas(…)` function has been removed. The `defineEvents(…)` function should be used instead, as it is a simpler way of defining events and event schemas using Zod: + +```ts +import { defineEvents } from '@statelyai/agent'; +import { z } from 'zod'; +import { setup } from 'xstate'; + +const events = defineEvents({ + inc: z.object({ + by: z.number().describe('Increment amount'), + }), +}); + +const machine = setup({ + types: { + events: events.types, + }, + schema: { + events: events.schemas, + }, +}).createMachine({ + // ... +}); +```