@statelyai/agent 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/examples/joke.ts CHANGED
@@ -1,185 +1,214 @@
1
1
  import OpenAI from 'openai';
2
- import { assign, fromPromise, createActor, waitFor, setup } from 'xstate';
3
- import { fromEventChoice } from '../src/index';
2
+ import {
3
+ assign,
4
+ createActor,
5
+ fromCallback,
6
+ fromPromise,
7
+ log,
8
+ setup,
9
+ } from 'xstate';
10
+ import { createAgent } from '../src';
11
+ import { loadingAnimation } from './helpers/loader';
4
12
 
5
13
  const openai = new OpenAI({
6
14
  apiKey: process.env.OPENAI_API_KEY,
7
15
  });
8
16
 
9
- async function start() {
10
- const promptTemplate = (topic: string) => `Tell me a joke about ${topic}.`;
11
-
12
- const getJokeCompletion = fromPromise(
13
- async ({ input }: { input: { topic: string } }) => {
14
- const res = await openai.chat.completions.create({
15
- messages: [
16
- {
17
- role: 'user',
18
- content: promptTemplate(input.topic),
19
- },
20
- ],
21
- model: 'gpt-3.5-turbo',
22
- n: 1,
23
- });
17
+ const agent = createAgent(openai, {
18
+ model: 'gpt-3.5-turbo-1106',
19
+ context: {
20
+ topic: { type: 'string' },
21
+ jokes: {
22
+ type: 'array',
23
+ items: {
24
+ type: 'string',
25
+ },
26
+ desire: { type: ['string', 'null'] },
27
+ lastRating: { type: ['string', 'null'] },
28
+ },
29
+ },
30
+ events: {},
31
+ });
24
32
 
25
- return res.choices[0]?.message.content;
26
- }
27
- );
33
+ const getJokeCompletion = agent.fromChatCompletion(
34
+ (topic: string) => `Tell me a joke about ${topic}.`
35
+ );
36
+
37
+ const rateJoke = agent.fromChatCompletion(
38
+ (joke: string) => `Rate this joke on a scale of 1 to 10: ${joke}`
39
+ );
40
+
41
+ const getTopic = fromPromise(async () => {
42
+ const topic = await new Promise<string>((res) => {
43
+ console.log('Give me a joke topic:');
44
+ const listener = (data: Buffer) => {
45
+ const result = data.toString().trim();
46
+ process.stdin.off('data', listener);
47
+ res(result);
48
+ };
49
+ process.stdin.on('data', listener);
50
+ });
28
51
 
29
- const rateJoke = fromPromise(
30
- async ({ input }: { input: { joke: string } }) => {
31
- const res = await openai.chat.completions.create({
32
- messages: [
33
- {
34
- role: 'user',
35
- content: `Rate this joke on a scale of 1 to 10: ${input.joke}`,
36
- },
37
- ],
38
- model: 'gpt-3.5-turbo',
39
- n: 1,
40
- });
52
+ return topic;
53
+ });
41
54
 
42
- return res.choices[0]?.message.content;
43
- }
44
- );
55
+ const decide = agent.fromEvent(
56
+ (lastRating: string) =>
57
+ `Choose what to do next, given the previous rating of the joke: ${lastRating}`
58
+ );
59
+ export function getRandomFunnyPhrase() {
60
+ const funnyPhrases = [
61
+ 'Concocting chuckles...',
62
+ 'Brewing belly laughs...',
63
+ 'Fabricating funnies...',
64
+ 'Assembling amusement...',
65
+ 'Molding merriment...',
66
+ 'Whipping up wisecracks...',
67
+ 'Generating guffaws...',
68
+ 'Inventing hilarity...',
69
+ 'Cultivating chortles...',
70
+ 'Hatching howlers...',
71
+ ];
72
+ return funnyPhrases[Math.floor(Math.random() * funnyPhrases.length)]!;
73
+ }
45
74
 
46
- const getTopic = fromPromise(async () => {
47
- const topic = await new Promise<string>((res) => {
48
- console.log('Give me a topic: \n\n');
49
- process.stdin.on('data', (data) => {
50
- const eventType = data.toString().trim();
51
- res(eventType);
52
- });
53
- });
75
+ export function getRandomRatingPhrase() {
76
+ const ratingPhrases = [
77
+ 'Assessing amusement...',
78
+ 'Evaluating hilarity...',
79
+ 'Ranking chuckles...',
80
+ 'Classifying cackles...',
81
+ 'Scoring snickers...',
82
+ 'Rating roars...',
83
+ 'Judging jollity...',
84
+ 'Measuring merriment...',
85
+ 'Rating rib-ticklers...',
86
+ ];
87
+ return ratingPhrases[Math.floor(Math.random() * ratingPhrases.length)]!;
88
+ }
54
89
 
55
- return topic;
56
- });
90
+ const loader = fromCallback(({ input }: { input: string }) => {
91
+ const anim = loadingAnimation(input);
57
92
 
58
- const chain = setup({
59
- types: {
60
- context: {} as {
61
- topic: string;
62
- jokes: string[];
63
- desire: string | null;
64
- lastRating: string | null;
65
- },
66
- input: {} as { topic: string },
67
- },
68
- actors: {
69
- getJokeCompletion,
70
- getTopic,
71
- rateJoke,
72
- decide: fromEventChoice(openai, (desire: string) => ({
73
- model: 'gpt-4-1106-preview',
74
- messages: [
75
- {
76
- role: 'user',
77
- content: `Execute the function that best satisfies this desire:
93
+ return () => {
94
+ anim.stop();
95
+ };
96
+ });
78
97
 
79
- ${desire}
80
- `,
81
- },
82
- ],
83
- })),
98
+ const jokeMachine = setup({
99
+ types: {
100
+ context: {} as {
101
+ topic: string;
102
+ jokes: string[];
103
+ desire: string | null;
104
+ lastRating: string | null;
84
105
  },
85
- }).createMachine({
86
- context: ({ input }) => ({
87
- topic: input.topic,
88
- jokes: [],
89
- desire: null,
90
- lastRating: null,
91
- }),
92
- initial: 'waitingForTopic',
93
- states: {
94
- waitingForTopic: {
95
- invoke: {
96
- src: 'getTopic',
97
- onDone: {
98
- actions: assign({
99
- topic: ({ event }) => event.output,
100
- }),
101
- target: 'tellingJoke',
102
- },
106
+ input: {} as { topic: string },
107
+ },
108
+ actors: {
109
+ getJokeCompletion,
110
+ getTopic,
111
+ rateJoke,
112
+ decide,
113
+ loader,
114
+ },
115
+ }).createMachine({
116
+ context: () => ({
117
+ topic: '',
118
+ jokes: [],
119
+ desire: null,
120
+ lastRating: null,
121
+ loader: null,
122
+ }),
123
+ initial: 'waitingForTopic',
124
+ states: {
125
+ waitingForTopic: {
126
+ invoke: {
127
+ src: 'getTopic',
128
+ onDone: {
129
+ actions: assign({
130
+ topic: ({ event }) => event.output,
131
+ }),
132
+ target: 'tellingJoke',
103
133
  },
104
134
  },
105
- tellingJoke: {
106
- invoke: {
135
+ },
136
+ tellingJoke: {
137
+ invoke: [
138
+ {
107
139
  src: 'getJokeCompletion',
108
- input: ({ context }) => ({ topic: context.topic }),
140
+ input: ({ context }) => context.topic,
109
141
  onDone: {
110
- actions: assign({
111
- jokes: ({ context, event }) =>
112
- context.jokes.concat(event.output as string),
113
- }),
142
+ actions: [
143
+ assign({
144
+ jokes: ({ context, event }) =>
145
+ context.jokes.concat(
146
+ event.output.choices[0]!.message.content!
147
+ ),
148
+ }),
149
+ log((x) => x.context.jokes.at(-1)),
150
+ ],
114
151
  target: 'rateJoke',
115
152
  },
116
153
  },
117
- },
118
- rateJoke: {
119
- invoke: {
120
- src: 'rateJoke',
121
- input: ({ context }) => ({
122
- joke: context.jokes[context.jokes.length - 1]!,
123
- }),
124
- onDone: {
125
- actions: assign({
126
- lastRating: ({ event }) => event.output as string,
127
- }),
128
- target: 'joked',
129
- },
154
+ {
155
+ src: 'loader',
156
+ input: getRandomFunnyPhrase,
130
157
  },
131
- },
132
- joked: {
133
- invoke: {
134
- src: 'getTopic',
158
+ ],
159
+ },
160
+ rateJoke: {
161
+ invoke: [
162
+ {
163
+ src: 'rateJoke',
164
+ input: ({ context }) => context.jokes[context.jokes.length - 1]!,
135
165
  onDone: {
136
- actions: assign({
137
- desire: ({ event }) => event.output,
138
- }),
166
+ actions: [
167
+ assign({
168
+ lastRating: ({ event }) =>
169
+ event.output.choices[0]!.message.content!,
170
+ }),
171
+ log(({ context }) => context.lastRating),
172
+ ],
139
173
  target: 'decide',
140
174
  },
141
175
  },
176
+ {
177
+ src: 'loader',
178
+ input: getRandomRatingPhrase,
179
+ },
180
+ ],
181
+ },
182
+ decide: {
183
+ invoke: {
184
+ src: 'decide',
185
+ input: ({ context }) => context.lastRating!,
186
+ onDone: {
187
+ actions: log(({ event }) => event),
188
+ },
142
189
  },
143
- decide: {
144
- invoke: {
145
- src: 'decide',
146
- input: ({ context }) => context.desire!,
190
+ on: {
191
+ askForTopic: {
192
+ target: 'waitingForTopic',
193
+ actions: log("That joke wasn't good enough. Let's try again."),
194
+ description:
195
+ 'Ask for a new topic, because the last joke rated 6 or lower',
147
196
  },
148
- on: {
149
- askForTopic: {
150
- target: 'waitingForTopic',
151
- description:
152
- 'Ask for a new topic, because the last joke was almost perfect',
153
- },
154
- endJokes: {
155
- target: 'end',
156
- description: 'End the jokes, since the last joke was not too good',
157
- },
197
+ endJokes: {
198
+ target: 'end',
199
+ actions: log('That joke was good enough. Goodbye!'),
200
+ description: 'End the jokes, since the last joke rated 7 or higher',
158
201
  },
159
202
  },
160
- end: {},
161
203
  },
162
- });
163
-
164
- const actor = createActor(chain, {
165
- input: {
166
- topic: 'donuts',
204
+ end: {
205
+ type: 'final',
167
206
  },
168
- });
169
-
170
- actor.subscribe((st) => {
171
- console.log('State: ', st.value);
172
-
173
- if (st.context.jokes) {
174
- console.log('Joke: ', st.context.jokes[st.context.jokes.length - 1]);
175
- }
176
- });
177
-
178
- actor.start();
179
-
180
- await waitFor(actor, (snap) => snap.status === 'done', {
181
- timeout: Infinity,
182
- });
183
- }
207
+ },
208
+ exit: () => {
209
+ process.exit();
210
+ },
211
+ });
184
212
 
185
- start();
213
+ const actor = createActor(jokeMachine);
214
+ actor.start();
@@ -1,6 +1,6 @@
1
- import { assign, setup, assertEvent, createActor, raise } from 'xstate';
2
- import { fromChatCompletionStream, fromEventChoice } from '../src/openai';
1
+ import { assign, setup, assertEvent, createActor } from 'xstate';
3
2
  import OpenAI from 'openai';
3
+ import { createAgent } from '../src/openai';
4
4
 
5
5
  const openai = new OpenAI({
6
6
  apiKey: process.env.OPENAI_API_KEY,
@@ -8,64 +8,126 @@ const openai = new OpenAI({
8
8
 
9
9
  type Player = 'x' | 'o';
10
10
 
11
+ const agent = createAgent(openai, {
12
+ model: 'gpt-4-1106-preview',
13
+ context: {
14
+ board: {
15
+ type: 'array',
16
+ items: {
17
+ type: ['null', 'string'],
18
+ enum: [null, 'x', 'o'],
19
+ },
20
+ minItems: 9,
21
+ maxItems: 9,
22
+ description: 'The board of the tic-tac-toe game',
23
+ },
24
+ moves: {
25
+ type: 'number',
26
+ description: 'The number of moves that have been played',
27
+ },
28
+ player: {
29
+ type: 'string',
30
+ enum: ['x', 'o'],
31
+ description: 'The player whose turn it is',
32
+ },
33
+ gameReport: {
34
+ type: 'string',
35
+ description: 'The game report',
36
+ },
37
+ events: {
38
+ type: 'array',
39
+ items: {
40
+ type: 'string',
41
+ },
42
+ },
43
+ } as const,
44
+ events: {
45
+ 'x.play': {
46
+ properties: {
47
+ index: {
48
+ description: 'The index of the cell to play on',
49
+ type: 'number',
50
+
51
+ minimum: 0,
52
+ maximum: 8,
53
+ },
54
+ },
55
+ },
56
+ 'o.play': {
57
+ properties: {
58
+ index: {
59
+ description: 'The index of the cell to play on',
60
+ type: 'number',
61
+ minimum: 0,
62
+ maximum: 8,
63
+ },
64
+ },
65
+ },
66
+ reset: {
67
+ properties: {},
68
+ },
69
+ },
70
+ });
71
+
11
72
  const initialContext = {
12
73
  board: Array(9).fill(null) as Array<Player | null>,
13
74
  moves: 0,
14
75
  player: 'x' as Player,
15
- winner: undefined as Player | undefined,
16
76
  gameReport: '',
17
- };
77
+ events: [],
78
+ } satisfies typeof agent.types.context;
79
+
80
+ const bot = agent.fromEvent(
81
+ ({ context }: { context: typeof agent.types.context }) => `
82
+ 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.
83
+
84
+ ${JSON.stringify(context, null, 2)}
85
+
86
+ Execute the single best next move to try to win the game. Do not play on an existing cell.`
87
+ );
88
+
89
+ const gameReporter = agent.fromChatCompletionStream(
90
+ ({
91
+ context,
92
+ }: {
93
+ context: typeof agent.types.context;
94
+ }) => `Here is the game board:
18
95
 
19
- export const ticTacToeMachine = setup({
20
- types: {} as {
21
- context: typeof initialContext;
22
- events:
23
- | { type: 'x.play'; index: number }
24
- | {
25
- type: 'o.play';
26
- index: number;
27
- }
28
- | { type: 'RESET' };
29
- },
30
- actors: {
31
- bot: fromEventChoice(
32
- openai,
33
- ({ context }: { context: typeof initialContext }) => ({
34
- model: 'gpt-4-1106-preview',
35
- messages: [
36
- {
37
- role: 'system',
38
- content: `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.
39
-
40
- ${JSON.stringify(context, null, 2)}`,
41
- },
42
- {
43
- role: 'user',
44
- content:
45
- 'Execute the single best next move to try to win the game. Do not play on an existing cell.',
46
- },
47
- ],
48
- })
49
- ),
50
- gameReporter: fromChatCompletionStream(
51
- openai,
52
- ({ context }: { context: typeof initialContext }) => ({
53
- model: 'gpt-4-1106-preview',
54
- messages: [
55
- {
56
- role: 'user',
57
- content: `The tic-tac-toe game is over. The winner is ${
58
- context.winner ?? 'nobody'
59
- }. This was the ending board state:
60
-
61
96
  ${JSON.stringify(context.board, null, 2)}
62
97
 
63
- Provide a game report analyzing the game.`,
64
- },
65
- ],
66
- stream: true,
67
- })
68
- ),
98
+ And here are the events that led to this game state:
99
+
100
+ ${context.events.join('\n')}
101
+
102
+ The winner is ${getWinner(context.board)}.
103
+
104
+ Provide a very short game report analyzing the game.`
105
+ );
106
+
107
+ function getWinner(board: typeof initialContext.board): Player | null {
108
+ const lines = [
109
+ [0, 1, 2],
110
+ [3, 4, 5],
111
+ [6, 7, 8],
112
+ [0, 3, 6],
113
+ [1, 4, 7],
114
+ [2, 5, 8],
115
+ [0, 4, 8],
116
+ [2, 4, 6],
117
+ ] as const;
118
+ for (const [a, b, c] of lines) {
119
+ if (board[a] !== null && board[a] === board[b] && board[a] === board[c]) {
120
+ return board[a]!;
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ export const ticTacToeMachine = setup({
127
+ types: agent.types,
128
+ actors: {
129
+ bot,
130
+ gameReporter,
69
131
  },
70
132
  actions: {
71
133
  updateBoard: assign({
@@ -77,45 +139,22 @@ Provide a game report analyzing the game.`,
77
139
  },
78
140
  moves: ({ context }) => context.moves + 1,
79
141
  player: ({ context }) => (context.player === 'x' ? 'o' : 'x'),
142
+ events: ({ context, event }) => {
143
+ return [...context.events, JSON.stringify(event)];
144
+ },
80
145
  }),
81
146
  resetGame: assign(initialContext),
82
- setWinner: assign({
83
- winner: ({ context }) => (context.player === 'x' ? 'o' : 'x'),
147
+ recordEvent: assign({
148
+ events: ({ context, event }) => {
149
+ return [...context.events, JSON.stringify(event)];
150
+ },
84
151
  }),
85
152
  },
86
153
  guards: {
87
154
  checkWin: ({ context }) => {
88
- const { board } = context;
89
- const winningLines = [
90
- [0, 1, 2],
91
- [3, 4, 5],
92
- [6, 7, 8],
93
- [0, 3, 6],
94
- [1, 4, 7],
95
- [2, 5, 8],
96
- [0, 4, 8],
97
- [2, 4, 6],
98
- ];
99
-
100
- for (let line of winningLines) {
101
- const xWon = line.every((index) => {
102
- return board[index] === 'x';
103
- });
104
-
105
- if (xWon) {
106
- return true;
107
- }
108
-
109
- const oWon = line.every((index) => {
110
- return board[index] === 'o';
111
- });
112
-
113
- if (oWon) {
114
- return true;
115
- }
116
- }
155
+ const winner = getWinner(context.board);
117
156
 
118
- return false;
157
+ return !!winner;
119
158
  },
120
159
  checkDraw: ({ context }) => {
121
160
  return context.moves === 9;
@@ -145,11 +184,6 @@ Provide a game report analyzing the game.`,
145
184
  invoke: {
146
185
  src: 'bot',
147
186
  input: ({ context }) => ({ context }),
148
- onDone: {
149
- actions: raise(({ event }) => {
150
- return event.output![0] as any;
151
- }),
152
- },
153
187
  },
154
188
  on: {
155
189
  'x.play': [
@@ -157,18 +191,8 @@ Provide a game report analyzing the game.`,
157
191
  target: 'o',
158
192
  guard: 'isValidMove',
159
193
  actions: 'updateBoard',
160
- meta: {
161
- parameters: {
162
- index: {
163
- description: 'The index of the cell to play on',
164
- type: 'number',
165
- min: 0,
166
- max: 8,
167
- },
168
- },
169
- },
170
194
  },
171
- { reenter: true },
195
+ { target: 'x', reenter: true },
172
196
  ],
173
197
  },
174
198
  },
@@ -176,13 +200,6 @@ Provide a game report analyzing the game.`,
176
200
  invoke: {
177
201
  src: 'bot',
178
202
  input: ({ context }) => ({ context }),
179
- onDone: {
180
- // @ts-ignore
181
- actions: raise(({ event }) => {
182
- console.log('output', event.output);
183
- return event.output![0];
184
- }),
185
- },
186
203
  },
187
204
  on: {
188
205
  'o.play': [
@@ -190,18 +207,8 @@ Provide a game report analyzing the game.`,
190
207
  target: 'x',
191
208
  guard: 'isValidMove',
192
209
  actions: 'updateBoard',
193
- meta: {
194
- parameters: {
195
- index: {
196
- description: 'The index of the cell to play on',
197
- type: 'number',
198
- min: 0,
199
- max: 8,
200
- },
201
- },
202
- },
203
210
  },
204
- { reenter: true },
211
+ { target: 'o', reenter: true },
205
212
  ],
206
213
  },
207
214
  },
@@ -226,14 +233,13 @@ Provide a game report analyzing the game.`,
226
233
  states: {
227
234
  winner: {
228
235
  tags: 'winner',
229
- entry: 'setWinner',
230
236
  },
231
237
  draw: {
232
238
  tags: 'draw',
233
239
  },
234
240
  },
235
241
  on: {
236
- RESET: {
242
+ reset: {
237
243
  target: 'playing',
238
244
  actions: 'resetGame',
239
245
  },
@@ -242,7 +248,13 @@ Provide a game report analyzing the game.`,
242
248
  },
243
249
  });
244
250
 
245
- const actor = createActor(ticTacToeMachine);
251
+ const actor = createActor(ticTacToeMachine, {
252
+ inspect: (e) => {
253
+ if (e.type === '@xstate.event') {
254
+ console.log(e.event);
255
+ }
256
+ },
257
+ });
246
258
  actor.subscribe((s) => {
247
259
  console.log(s.value, s.context);
248
260
  });