@statelyai/agent 0.0.2 → 0.0.4

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,13 +1,13 @@
1
1
  import OpenAI from 'openai';
2
- import { assign, fromPromise, createActor, setup, log, raise } from 'xstate';
3
- import { createAgent } from '../src';
2
+ import { assign, fromCallback, fromPromise, log, setup } from 'xstate';
3
+ import { createAgent, createOpenAIAdapter, createSchemas } from '../src';
4
+ import { loadingAnimation } from './helpers/loader';
4
5
 
5
6
  const openai = new OpenAI({
6
7
  apiKey: process.env.OPENAI_API_KEY,
7
8
  });
8
9
 
9
- const agent = createAgent(openai, {
10
- model: 'gpt-3.5-turbo-1106',
10
+ const schemas = createSchemas({
11
11
  context: {
12
12
  topic: { type: 'string' },
13
13
  jokes: {
@@ -15,24 +15,41 @@ const agent = createAgent(openai, {
15
15
  items: {
16
16
  type: 'string',
17
17
  },
18
- desire: { type: ['string', 'null'] },
19
- lastRating: { type: ['string', 'null'] },
18
+ },
19
+ desire: { type: ['string', 'null'] as const },
20
+ lastRating: { type: ['string', 'null'] as const },
21
+ },
22
+ events: {
23
+ askForTopic: {
24
+ type: 'object',
25
+ properties: {
26
+ topic: {
27
+ type: 'string',
28
+ },
29
+ },
30
+ },
31
+ endJokes: {
32
+ type: 'object',
33
+ properties: {},
20
34
  },
21
35
  },
22
- events: {},
23
36
  });
24
37
 
25
- const promptTemplate = (topic: string) => `Tell me a joke about ${topic}.`;
38
+ const adapter = createOpenAIAdapter(openai, {
39
+ model: 'gpt-3.5-turbo-1106',
40
+ });
26
41
 
27
- const getJokeCompletion = agent.fromChatCompletion(promptTemplate);
42
+ const getJokeCompletion = adapter.fromChat(
43
+ (topic: string) => `Tell me a joke about ${topic}.`
44
+ );
28
45
 
29
- const rateJoke = agent.fromChatCompletion(
46
+ const rateJoke = adapter.fromChat(
30
47
  (joke: string) => `Rate this joke on a scale of 1 to 10: ${joke}`
31
48
  );
32
49
 
33
50
  const getTopic = fromPromise(async () => {
34
51
  const topic = await new Promise<string>((res) => {
35
- console.log('Give me a topic: \n\n');
52
+ console.log('Give me a joke topic:');
36
53
  const listener = (data: Buffer) => {
37
54
  const result = data.toString().trim();
38
55
  process.stdin.off('data', listener);
@@ -44,33 +61,66 @@ const getTopic = fromPromise(async () => {
44
61
  return topic;
45
62
  });
46
63
 
47
- const decide = agent.fromEventChoice(
64
+ const decide = adapter.fromEventChoice(
48
65
  (lastRating: string) =>
49
66
  `Choose what to do next, given the previous rating of the joke: ${lastRating}`
50
67
  );
68
+ export function getRandomFunnyPhrase() {
69
+ const funnyPhrases = [
70
+ 'Concocting chuckles...',
71
+ 'Brewing belly laughs...',
72
+ 'Fabricating funnies...',
73
+ 'Assembling amusement...',
74
+ 'Molding merriment...',
75
+ 'Whipping up wisecracks...',
76
+ 'Generating guffaws...',
77
+ 'Inventing hilarity...',
78
+ 'Cultivating chortles...',
79
+ 'Hatching howlers...',
80
+ ];
81
+ return funnyPhrases[Math.floor(Math.random() * funnyPhrases.length)]!;
82
+ }
83
+
84
+ export function getRandomRatingPhrase() {
85
+ const ratingPhrases = [
86
+ 'Assessing amusement...',
87
+ 'Evaluating hilarity...',
88
+ 'Ranking chuckles...',
89
+ 'Classifying cackles...',
90
+ 'Scoring snickers...',
91
+ 'Rating roars...',
92
+ 'Judging jollity...',
93
+ 'Measuring merriment...',
94
+ 'Rating rib-ticklers...',
95
+ ];
96
+ return ratingPhrases[Math.floor(Math.random() * ratingPhrases.length)]!;
97
+ }
98
+
99
+ const loader = fromCallback(({ input }: { input: string }) => {
100
+ const anim = loadingAnimation(input);
101
+
102
+ return () => {
103
+ anim.stop();
104
+ };
105
+ });
51
106
 
52
107
  const jokeMachine = setup({
53
- types: {
54
- context: {} as {
55
- topic: string;
56
- jokes: string[];
57
- desire: string | null;
58
- lastRating: string | null;
59
- },
60
- input: {} as { topic: string },
61
- },
108
+ schemas,
109
+ types: schemas.types,
62
110
  actors: {
63
111
  getJokeCompletion,
64
112
  getTopic,
65
113
  rateJoke,
66
114
  decide,
115
+ loader,
67
116
  },
68
117
  }).createMachine({
69
- context: ({ input }) => ({
70
- topic: input.topic,
118
+ context: () => ({
119
+ topic: '',
71
120
  jokes: [],
72
121
  desire: null,
73
122
  lastRating: null,
123
+ loader: null,
74
124
  }),
75
125
  initial: 'waitingForTopic',
76
126
  states: {
@@ -86,56 +136,69 @@ const jokeMachine = setup({
86
136
  },
87
137
  },
88
138
  tellingJoke: {
89
- invoke: {
90
- src: 'getJokeCompletion',
91
- input: ({ context }) => context.topic,
92
- onDone: {
93
- actions: [
94
- assign({
95
- jokes: ({ context, event }) =>
96
- context.jokes.concat(event.output.choices[0]!.message.content!),
97
- }),
98
- log((x) => x.context.jokes.at(-1)),
99
- ],
100
- target: 'rateJoke',
139
+ invoke: [
140
+ {
141
+ src: 'getJokeCompletion',
142
+ input: ({ context }) => context.topic,
143
+ onDone: {
144
+ actions: [
145
+ assign({
146
+ jokes: ({ context, event }) =>
147
+ context.jokes.concat(
148
+ event.output.choices[0]!.message.content!
149
+ ),
150
+ }),
151
+ log((x) => `\n` + x.context.jokes.at(-1)),
152
+ ],
153
+ target: 'rateJoke',
154
+ },
101
155
  },
102
- },
156
+ {
157
+ src: 'loader',
158
+ input: getRandomFunnyPhrase,
159
+ },
160
+ ],
103
161
  },
104
162
  rateJoke: {
105
- invoke: {
106
- src: 'rateJoke',
107
- input: ({ context }) => context.jokes[context.jokes.length - 1]!,
108
- onDone: {
109
- actions: [
110
- assign({
111
- lastRating: ({ event }) =>
112
- event.output.choices[0]!.message.content!,
113
- }),
114
- log(({ context }) => context.lastRating),
115
- ],
116
- target: 'decide',
163
+ invoke: [
164
+ {
165
+ src: 'rateJoke',
166
+ input: ({ context }) => context.jokes[context.jokes.length - 1]!,
167
+ onDone: {
168
+ actions: [
169
+ assign({
170
+ lastRating: ({ event }) =>
171
+ event.output.choices[0]!.message.content!,
172
+ }),
173
+ log(({ context }) => '\n' + context.lastRating),
174
+ ],
175
+ target: 'decide',
176
+ },
117
177
  },
118
- },
178
+ {
179
+ src: 'loader',
180
+ input: getRandomRatingPhrase,
181
+ },
182
+ ],
119
183
  },
120
184
  decide: {
121
185
  invoke: {
122
186
  src: 'decide',
123
187
  input: ({ context }) => context.lastRating!,
124
188
  onDone: {
125
- actions: [
126
- log(({ event }) => event),
127
- raise(({ event }) => event.output![0]!),
128
- ],
189
+ actions: log(({ event }) => event),
129
190
  },
130
191
  },
131
192
  on: {
132
193
  askForTopic: {
133
194
  target: 'waitingForTopic',
195
+ actions: log("That joke wasn't good enough. Let's try again."),
134
196
  description:
135
197
  'Ask for a new topic, because the last joke rated 6 or lower',
136
198
  },
137
199
  endJokes: {
138
200
  target: 'end',
201
+ actions: log('That joke was good enough. Goodbye!'),
139
202
  description: 'End the jokes, since the last joke rated 7 or higher',
140
203
  },
141
204
  },
@@ -144,8 +207,10 @@ const jokeMachine = setup({
144
207
  type: 'final',
145
208
  },
146
209
  },
210
+ exit: () => {
211
+ process.exit();
212
+ },
147
213
  });
148
214
 
149
- const actor = createActor(jokeMachine);
150
-
151
- actor.start();
215
+ const agent = createAgent(jokeMachine);
216
+ agent.start();
File without changes
@@ -1,6 +1,6 @@
1
- import { assign, setup, assertEvent, createActor, raise } from 'xstate';
1
+ import { assign, setup, assertEvent } from 'xstate';
2
2
  import OpenAI from 'openai';
3
- import { createAgent } from '../src/openai';
3
+ import { createOpenAIAdapter, createSchemas, createAgent } from '../src';
4
4
 
5
5
  const openai = new OpenAI({
6
6
  apiKey: process.env.OPENAI_API_KEY,
@@ -8,8 +8,7 @@ const openai = new OpenAI({
8
8
 
9
9
  type Player = 'x' | 'o';
10
10
 
11
- const agent = createAgent(openai, {
12
- model: 'gpt-3.5-turbo-1106',
11
+ const schemas = createSchemas({
13
12
  context: {
14
13
  board: {
15
14
  type: 'array',
@@ -30,11 +29,6 @@ const agent = createAgent(openai, {
30
29
  enum: ['x', 'o'],
31
30
  description: 'The player whose turn it is',
32
31
  },
33
- winner: {
34
- type: ['null', 'string'],
35
- enum: [null, 'x', 'o'],
36
- description: 'The player who won the game',
37
- },
38
32
  gameReport: {
39
33
  type: 'string',
40
34
  description: 'The game report',
@@ -74,17 +68,20 @@ const agent = createAgent(openai, {
74
68
  },
75
69
  });
76
70
 
71
+ const adapter = createOpenAIAdapter(openai, {
72
+ model: 'gpt-4-1106-preview',
73
+ });
74
+
77
75
  const initialContext = {
78
76
  board: Array(9).fill(null) as Array<Player | null>,
79
77
  moves: 0,
80
78
  player: 'x' as Player,
81
- winner: null as Player | null,
82
79
  gameReport: '',
83
80
  events: [],
84
- } satisfies typeof agent.types.context;
81
+ } satisfies typeof schemas.types.context;
85
82
 
86
- const bot = agent.fromEventChoice(
87
- ({ context }: { context: typeof agent.types.context }) => `
83
+ const bot = adapter.fromEventChoice(
84
+ ({ context }: { context: typeof schemas.types.context }) => `
88
85
  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.
89
86
 
90
87
  ${JSON.stringify(context, null, 2)}
@@ -92,14 +89,12 @@ ${JSON.stringify(context, null, 2)}
92
89
  Execute the single best next move to try to win the game. Do not play on an existing cell.`
93
90
  );
94
91
 
95
- const gameReporter = agent.fromChatCompletionStream(
92
+ const gameReporter = adapter.fromChatStream(
96
93
  ({
97
94
  context,
98
95
  }: {
99
- context: typeof agent.types.context;
100
- }) => `The tic-tac-toe game is over. The winner is ${
101
- context.winner ?? 'nobody'
102
- }. This was the ending board state, represented as a 9-element array:
96
+ context: typeof schemas.types.context;
97
+ }) => `Here is the game board:
103
98
 
104
99
  ${JSON.stringify(context.board, null, 2)}
105
100
 
@@ -107,11 +102,33 @@ And here are the events that led to this game state:
107
102
 
108
103
  ${context.events.join('\n')}
109
104
 
105
+ The winner is ${getWinner(context.board)}.
106
+
110
107
  Provide a very short game report analyzing the game.`
111
108
  );
112
109
 
110
+ function getWinner(board: typeof initialContext.board): Player | null {
111
+ const lines = [
112
+ [0, 1, 2],
113
+ [3, 4, 5],
114
+ [6, 7, 8],
115
+ [0, 3, 6],
116
+ [1, 4, 7],
117
+ [2, 5, 8],
118
+ [0, 4, 8],
119
+ [2, 4, 6],
120
+ ] as const;
121
+ for (const [a, b, c] of lines) {
122
+ if (board[a] !== null && board[a] === board[b] && board[a] === board[c]) {
123
+ return board[a]!;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+
113
129
  export const ticTacToeMachine = setup({
114
- types: agent.types,
130
+ schemas,
131
+ types: schemas.types,
115
132
  actors: {
116
133
  bot,
117
134
  gameReporter,
@@ -131,9 +148,6 @@ export const ticTacToeMachine = setup({
131
148
  },
132
149
  }),
133
150
  resetGame: assign(initialContext),
134
- setWinner: assign({
135
- winner: ({ context }) => (context.player === 'x' ? 'o' : 'x'),
136
- }),
137
151
  recordEvent: assign({
138
152
  events: ({ context, event }) => {
139
153
  return [...context.events, JSON.stringify(event)];
@@ -142,37 +156,9 @@ export const ticTacToeMachine = setup({
142
156
  },
143
157
  guards: {
144
158
  checkWin: ({ context }) => {
145
- const { board } = context;
146
- const winningLines = [
147
- [0, 1, 2],
148
- [3, 4, 5],
149
- [6, 7, 8],
150
- [0, 3, 6],
151
- [1, 4, 7],
152
- [2, 5, 8],
153
- [0, 4, 8],
154
- [2, 4, 6],
155
- ];
156
-
157
- for (let line of winningLines) {
158
- const xWon = line.every((index) => {
159
- return board[index] === 'x';
160
- });
159
+ const winner = getWinner(context.board);
161
160
 
162
- if (xWon) {
163
- return true;
164
- }
165
-
166
- const oWon = line.every((index) => {
167
- return board[index] === 'o';
168
- });
169
-
170
- if (oWon) {
171
- return true;
172
- }
173
- }
174
-
175
- return false;
161
+ return !!winner;
176
162
  },
177
163
  checkDraw: ({ context }) => {
178
164
  return context.moves === 9;
@@ -202,11 +188,6 @@ export const ticTacToeMachine = setup({
202
188
  invoke: {
203
189
  src: 'bot',
204
190
  input: ({ context }) => ({ context }),
205
- onDone: {
206
- actions: raise(({ event }) => {
207
- return event.output![0] as any;
208
- }),
209
- },
210
191
  },
211
192
  on: {
212
193
  'x.play': [
@@ -223,11 +204,6 @@ export const ticTacToeMachine = setup({
223
204
  invoke: {
224
205
  src: 'bot',
225
206
  input: ({ context }) => ({ context }),
226
- onDone: {
227
- actions: raise(({ event }) => {
228
- return event.output![0]!;
229
- }),
230
- },
231
207
  },
232
208
  on: {
233
209
  'o.play': [
@@ -261,7 +237,6 @@ export const ticTacToeMachine = setup({
261
237
  states: {
262
238
  winner: {
263
239
  tags: 'winner',
264
- entry: 'setWinner',
265
240
  },
266
241
  draw: {
267
242
  tags: 'draw',
@@ -277,8 +252,8 @@ export const ticTacToeMachine = setup({
277
252
  },
278
253
  });
279
254
 
280
- const actor = createActor(ticTacToeMachine);
281
- actor.subscribe((s) => {
255
+ const agent = createAgent(ticTacToeMachine);
256
+ agent.subscribe((s) => {
282
257
  console.log(s.value, s.context);
283
258
  });
284
- actor.start();
259
+ agent.start();
@@ -0,0 +1,160 @@
1
+ import OpenAI from 'openai';
2
+ import { createAgent, createOpenAIAdapter, createSchemas } from '../src';
3
+ import { assign, fromPromise, log, setup } from 'xstate';
4
+ import { getFromTerminal } from './helpers/helpers';
5
+
6
+ async function searchTavily(
7
+ input: string,
8
+ options: {
9
+ maxResults?: number;
10
+ apiKey: string;
11
+ }
12
+ ) {
13
+ const body: Record<string, unknown> = {
14
+ query: input,
15
+ max_results: options.maxResults,
16
+ api_key: options.apiKey,
17
+ };
18
+
19
+ const response = await fetch('https://api.tavily.com/search', {
20
+ method: 'POST',
21
+ headers: {
22
+ 'content-type': 'application/json',
23
+ },
24
+ body: JSON.stringify(body),
25
+ });
26
+
27
+ const json = await response.json();
28
+ if (!response.ok) {
29
+ throw new Error(
30
+ `Request failed with status code ${response.status}: ${json.error}`
31
+ );
32
+ }
33
+ if (!Array.isArray(json.results)) {
34
+ throw new Error(`Could not parse Tavily results. Please try again.`);
35
+ }
36
+ return JSON.stringify(json.results);
37
+ }
38
+
39
+ const openai = new OpenAI({
40
+ apiKey: process.env.OPENAI_API_KEY,
41
+ });
42
+
43
+ const schemas = createSchemas({
44
+ context: {
45
+ location: { type: 'string' },
46
+ history: { type: 'array', items: { type: 'string' } },
47
+ count: { type: 'number' },
48
+ },
49
+ events: {
50
+ getWeather: {
51
+ description: 'Get the weather for a location',
52
+ properties: {
53
+ location: {
54
+ type: 'string',
55
+ description: 'The location to get the weather for',
56
+ },
57
+ },
58
+ },
59
+ doSomethingElse: {
60
+ description:
61
+ 'Do something else, because the user did not provide a location',
62
+ properties: {},
63
+ },
64
+ },
65
+ });
66
+
67
+ const adapter = createOpenAIAdapter(openai, {
68
+ model: 'gpt-4-1106-preview',
69
+ });
70
+
71
+ const getWeather = fromPromise(async ({ input }: { input: string }) => {
72
+ const results = await searchTavily(
73
+ `Get the weather for this location: ${input}`,
74
+ {
75
+ maxResults: 5,
76
+ apiKey: process.env.TAVILY_API_KEY!,
77
+ }
78
+ );
79
+ return results;
80
+ });
81
+
82
+ const machine = setup({
83
+ schemas,
84
+ types: schemas.types,
85
+ actors: {
86
+ getWeather,
87
+ decide: adapter.fromEventChoice(
88
+ (input: string) =>
89
+ `Decide what to do based on the given input, which may or may not be a location: ${input}`
90
+ ),
91
+ getFromTerminal,
92
+ },
93
+ }).createMachine({
94
+ initial: 'getLocation',
95
+ context: {
96
+ location: '',
97
+ count: 0,
98
+ history: [],
99
+ },
100
+ states: {
101
+ getLocation: {
102
+ invoke: {
103
+ src: 'getFromTerminal',
104
+ input: 'Location?',
105
+ onDone: {
106
+ actions: assign({
107
+ location: ({ event }) => event.output,
108
+ }),
109
+ target: 'decide',
110
+ },
111
+ },
112
+ always: {
113
+ guard: ({ context }) => context.count >= 3,
114
+ target: 'stopped',
115
+ },
116
+ },
117
+ decide: {
118
+ entry: log('Deciding...'),
119
+ invoke: {
120
+ src: 'decide',
121
+ input: ({ context }) => context.location,
122
+ },
123
+ on: {
124
+ getWeather: {
125
+ actions: log(({ event }) => event),
126
+ target: 'gettingWeather',
127
+ },
128
+ doSomethingElse: 'getLocation',
129
+ },
130
+ },
131
+ gettingWeather: {
132
+ entry: log('Getting weather...'),
133
+ invoke: {
134
+ src: 'getWeather',
135
+ input: ({ context }) => context.location,
136
+ onDone: {
137
+ actions: [
138
+ log(({ event }) => event.output),
139
+ assign({
140
+ count: ({ context }) => context.count + 1,
141
+ }),
142
+ ],
143
+ target: 'getLocation',
144
+ },
145
+ },
146
+ },
147
+ stopped: {
148
+ entry: log('You have used up your search quota. Goodbye!'),
149
+ },
150
+ },
151
+ exit: () => {
152
+ process.exit();
153
+ },
154
+ });
155
+
156
+ createAgent(machine, {
157
+ input: {
158
+ location: 'New York',
159
+ },
160
+ }).start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/agent",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -9,22 +9,30 @@
9
9
  "author": "",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
+ "@changesets/changelog-github": "^0.5.0",
12
13
  "@changesets/cli": "^2.27.1",
13
14
  "@types/node": "^20.10.6",
15
+ "dotenv": "^16.3.1",
14
16
  "json-schema-to-ts": "^3.0.0",
17
+ "openai": "^4.24.1",
18
+ "ts-node": "^10.9.2",
15
19
  "tsup": "^8.0.1",
16
20
  "typescript": "^5.3.3"
17
21
  },
18
- "dependencies": {
19
- "openai": "^4.24.1",
20
- "xstate": "^5.3.1"
21
- },
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
+ "dependencies": {
26
+ "xstate": "^5.6.0"
27
+ },
28
+ "packageManager": "pnpm@8.11.0",
25
29
  "scripts": {
26
30
  "build": "tsup src/index.ts --format cjs,esm --dts",
27
31
  "lint": "tsc",
28
- "test": "vitest run"
32
+ "test": "vitest run",
33
+ "example": "ts-node examples/helpers/runner.ts",
34
+ "changeset": "changeset",
35
+ "release": "changeset publish",
36
+ "version": "changeset version"
29
37
  }
30
38
  }
package/readme.md ADDED
@@ -0,0 +1,41 @@
1
+ # Stately Agent (alpha)
2
+
3
+ 🚧 Documentation in progress! Please see [the examples directory](https://github.com/statelyai/agent/tree/main/examples) for working examples.
4
+
5
+ ## Installation
6
+
7
+ Install `openai`, and `@statelyai/agent`:
8
+
9
+ ```bash
10
+ npm install openai @statelyai/agent
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Work in progress. For now, see the examples:
16
+
17
+ - [Joke generator](https://github.com/statelyai/agent/tree/main/examples/joke.ts)
18
+ - Demonstrates `agent.fromChatCompletion(...)` to generate a joke and provide a joke rating
19
+ - Demonstrates `agent.fromEvent(...)` to choose whether to keep generating jokes or stop
20
+ - [Tic-tac-toe](https://github.com/statelyai/agent/tree/main/examples/ticTacToe.ts)
21
+ - Demonstrates `agent.fromEvent(...)` to have an agent play itself in a game of tic-tac-toe with precise events
22
+ - Demonstrates `agent.fromChatCompletionStream(...)` to produce a game report at the end of the game
23
+ - [Weather](https://github.com/statelyai/agent/tree/main/examples/weather.ts)
24
+ - Demonstrates using [Tavily](https://tavily.com/) as an external API
25
+ - Demonstrates `agent.fromEvent(...)` to only use Tavily to get the weather if the user provides a valid location
26
+
27
+ ## Examples
28
+
29
+ First, clone this repo locally. To run the examples in this repo, create a `.env` file at the root of the repo with the following contents:
30
+
31
+ ```bash
32
+ OPENAI_API_KEY="your-openai-api-key"
33
+ ```
34
+
35
+ Then, install the dependencies (`npm install`) and run the examples:
36
+
37
+ ```bash
38
+ npm run example joke
39
+ # or:
40
+ # npm run example ticTacToe
41
+ ```