@statelyai/agent 0.0.2 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3
- "changelog": "@changesets/cli/changelog",
3
+ "changelog": ["@changesets/changelog-github", { "repo": "statelyai/agent" }],
4
4
  "commit": false,
5
5
  "fixed": [],
6
6
  "linked": [],
package/.env.template ADDED
@@ -0,0 +1,6 @@
1
+
2
+ # Get your OpenAI API key from: https://platform.openai.com/signup/
3
+ OPENAI_API_KEY="sk-..."
4
+
5
+ # Get your Tavily API key from: https://app.tavily.com/
6
+ TAVILY_API_KEY="tvly-..."
@@ -0,0 +1,24 @@
1
+ name: Setup Workflow
2
+ description: Composite action that sets up pnpm
3
+ runs:
4
+ using: 'composite'
5
+ steps:
6
+ - uses: pnpm/action-setup@v2
7
+ - uses: actions/setup-node@v4
8
+ with:
9
+ node-version: 20.x
10
+
11
+ - name: Get pnpm store directory
12
+ shell: bash
13
+ id: pnpm-cache
14
+ run: |
15
+ echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
16
+ - uses: actions/cache@v4
17
+ name: Setup pnpm cache
18
+ with:
19
+ path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
20
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
21
+ restore-keys: |
22
+ ${{ runner.os }}-pnpm-store-
23
+ - run: pnpm install
24
+ shell: bash
@@ -0,0 +1,35 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ concurrency: ${{ github.workflow }}-${{ github.ref }}
9
+
10
+ permissions: {}
11
+ jobs:
12
+ release:
13
+ permissions:
14
+ contents: write # to create release (changesets/action)
15
+ issues: write # to post issue comments (changesets/action)
16
+ pull-requests: write # to create pull request (changesets/action)
17
+
18
+ if: github.repository == 'statelyai/agent'
19
+
20
+ timeout-minutes: 20
21
+
22
+ runs-on: ubuntu-latest
23
+
24
+ steps:
25
+ - uses: actions/checkout@v3
26
+ - uses: ./.github/actions/ci-setup
27
+
28
+ - name: Create Release Pull Request or Publish to npm
29
+ uses: changesets/action@v1
30
+ with:
31
+ publish: pnpm run release
32
+ version: pnpm run version
33
+ env:
34
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @statelyai/agent
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1](https://github.com/statelyai/agent/pull/1) [`3dc2880`](https://github.com/statelyai/agent/commit/3dc28809a7ffd915a69d9f3374531c31fc1ee357) Thanks [@mellson](https://github.com/mellson)! - Adds a convenient way to run the examples with `pnpm example ${exampleName}`. If no example name is provided, the script will print the available examples. Also, adds a fun little loading animation to the joke example.
8
+
3
9
  ## 0.0.2
4
10
 
5
11
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -42,26 +42,27 @@ type ConvertContextToJSONSchema<T extends ContextSchema> = {
42
42
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
43
43
  *
44
44
  */
45
- declare function fromChatCompletion<TInput>(openai: OpenAI, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming): PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
45
+ declare function fromChatCompletion<TInput>(openai: OpenAI, agentSettings: CreateAgentOutput<any>, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming): PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
46
46
  /**
47
47
  * Creates [observable actor logic](https://stately.ai/docs/observable-actors) that uses the OpenAI API to generate a completion stream.
48
48
  *
49
49
  * @param openai The OpenAI instance to use.
50
50
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
51
51
  */
52
- declare function fromChatCompletionStream<TInput>(openai: OpenAI, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming): ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
52
+ declare function fromChatCompletionStream<TInput>(openai: OpenAI, agentSettings: CreateAgentOutput<any>, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming): ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
53
53
  /**
54
54
  * Creates [promise actor logic](https://stately.ai/docs/promise-actors) that passes the next possible transitions as functions to [OpenAI tool calls](https://platform.openai.com/docs/guides/function-calling) and returns an array of potential next events.
55
55
  *
56
56
  * @param openai The OpenAI instance to use.
57
57
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
58
58
  */
59
- declare function fromEventChoice<TInput>(openai: OpenAI, machineTypes: {
60
- schemas: {
61
- context: ContextSchema;
62
- events: EventSchemas;
63
- };
64
- }, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming): PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
59
+ declare function fromEventChoice<TInput>(openai: OpenAI, agentSettings: CreateAgentOutput<any>, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, options?: {
60
+ /**
61
+ * Immediately execute sending the event to the parent actor.
62
+ * @default false
63
+ */
64
+ execute?: boolean;
65
+ }): PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
65
66
  interface CreateAgentOutput<T extends {
66
67
  model: ChatCompletionCreateParamsBase['model'];
67
68
  context: ContextSchema;
@@ -73,6 +74,7 @@ interface CreateAgentOutput<T extends {
73
74
  context: FromSchema<ConvertContextToJSONSchema<T['context']>>;
74
75
  events: FromSchema<Values<ConvertToJSONSchemas<T['events']>>>;
75
76
  };
77
+ fromEvent: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined, TInput>;
76
78
  fromEventChoice: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined, TInput>;
77
79
  fromChatCompletion: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
78
80
  fromChatCompletionStream: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming) => ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
package/dist/index.js CHANGED
@@ -56,12 +56,12 @@ function createEventSchemas(eventSchemaMap) {
56
56
  }
57
57
 
58
58
  // src/openai.ts
59
- function fromChatCompletion(openai, inputFn) {
59
+ function fromChatCompletion(openai, agentSettings, inputFn) {
60
60
  return (0, import_xstate.fromPromise)(
61
61
  async ({ input }) => {
62
62
  const openAiInput = inputFn(input);
63
63
  const params = typeof openAiInput === "string" ? {
64
- model: "gpt-3.5-turbo-1106",
64
+ model: agentSettings.model,
65
65
  messages: [
66
66
  {
67
67
  role: "user",
@@ -74,14 +74,14 @@ function fromChatCompletion(openai, inputFn) {
74
74
  }
75
75
  );
76
76
  }
77
- function fromChatCompletionStream(openai, inputFn) {
77
+ function fromChatCompletionStream(openai, agentSettings, inputFn) {
78
78
  return (0, import_xstate.fromObservable)(
79
79
  ({ input }) => {
80
80
  const observers = /* @__PURE__ */ new Set();
81
81
  (async () => {
82
82
  const openAiInput = inputFn(input);
83
83
  const resolvedParams = typeof openAiInput === "string" ? {
84
- model: "gpt-3.5-turbo-1106",
84
+ model: agentSettings.model,
85
85
  messages: [
86
86
  {
87
87
  role: "user",
@@ -113,9 +113,9 @@ function fromChatCompletionStream(openai, inputFn) {
113
113
  }
114
114
  );
115
115
  }
116
- function fromEventChoice(openai, machineTypes, inputFn) {
116
+ function fromEventChoice(openai, agentSettings, inputFn, options) {
117
117
  return (0, import_xstate.fromPromise)(
118
- async ({ input, self }) => {
118
+ async ({ input, self, system }) => {
119
119
  const transitions = getAllTransitions(self._parent.getSnapshot());
120
120
  const functionNameMapping = {};
121
121
  const tools = transitions.filter((t) => {
@@ -127,17 +127,17 @@ function fromEventChoice(openai, machineTypes, inputFn) {
127
127
  type: "function",
128
128
  function: {
129
129
  name,
130
- description: t.description ?? machineTypes.schemas.events[t.eventType]?.description,
130
+ description: t.description ?? agentSettings.schemas.events[t.eventType]?.description,
131
131
  parameters: {
132
132
  type: "object",
133
- properties: machineTypes.schemas.events[t.eventType]?.properties ?? {}
133
+ properties: agentSettings.schemas.events[t.eventType]?.properties ?? {}
134
134
  }
135
135
  }
136
136
  };
137
137
  });
138
138
  const openAiInput = inputFn(input);
139
139
  const completionParams = typeof openAiInput === "string" ? {
140
- model: "gpt-4-1106-preview",
140
+ model: agentSettings.model,
141
141
  messages: [
142
142
  {
143
143
  role: "user",
@@ -151,19 +151,24 @@ function fromEventChoice(openai, machineTypes, inputFn) {
151
151
  });
152
152
  const toolCalls = completion.choices[0]?.message.tool_calls;
153
153
  if (toolCalls) {
154
- return toolCalls.map((tc) => {
154
+ const events = toolCalls.map((tc) => {
155
155
  return {
156
156
  type: functionNameMapping[tc.function.name],
157
157
  ...JSON.parse(tc.function.arguments)
158
158
  };
159
159
  });
160
+ if (options?.execute) {
161
+ events.forEach((event) => {
162
+ system._relay(self, self._parent, event);
163
+ });
164
+ }
160
165
  }
161
166
  return void 0;
162
167
  }
163
168
  );
164
169
  }
165
170
  function createAgent(openai, settings) {
166
- const obj = {
171
+ const agentSettings = {
167
172
  model: settings.model,
168
173
  schemas: {
169
174
  context: {
@@ -174,11 +179,16 @@ function createAgent(openai, settings) {
174
179
  events: createEventSchemas(settings.events)
175
180
  },
176
181
  types: {},
177
- fromEventChoice: (input) => fromEventChoice(openai, obj, input),
178
- fromChatCompletion: (input) => fromChatCompletion(openai, input),
179
- fromChatCompletionStream: (input) => fromChatCompletionStream(openai, input)
182
+ fromEvent: (input) => (
183
+ // @ts-ignore
184
+ fromEventChoice(openai, agentSettings, input, { execute: true })
185
+ ),
186
+ // @ts-ignore infinitely deep
187
+ fromEventChoice: (input) => fromEventChoice(openai, agentSettings, input),
188
+ fromChatCompletion: (input) => fromChatCompletion(openai, agentSettings, input),
189
+ fromChatCompletionStream: (input) => fromChatCompletionStream(openai, agentSettings, input)
180
190
  };
181
- return obj;
191
+ return agentSettings;
182
192
  }
183
193
  // Annotate the CommonJS export names for ESM import in node:
184
194
  0 && (module.exports = {
@@ -0,0 +1,17 @@
1
+ import { fromPromise } from 'xstate';
2
+
3
+ export const getFromTerminal = fromPromise<string, string>(
4
+ async ({ input }) => {
5
+ const topic = await new Promise<string>((res) => {
6
+ console.log(input);
7
+ const listener = (data: Buffer) => {
8
+ const result = data.toString().trim();
9
+ process.stdin.off('data', listener);
10
+ res(result);
11
+ };
12
+ process.stdin.on('data', listener);
13
+ });
14
+
15
+ return topic;
16
+ }
17
+ );
@@ -0,0 +1,32 @@
1
+ // Adapted from https://stackoverflow.com/questions/34848505/how-to-make-a-loading-animation-in-console-application-written-in-javascript-or
2
+
3
+ /**
4
+ * Create and display a loader in the console.
5
+ *
6
+ * @param {string} [text=""] Text to display after loader
7
+ * @param {array.<string>} [chars=["⠙", "⠘", "⠰", "⠴", "⠤", "⠦", "⠆", "⠃", "⠋", "⠉"]]
8
+ * Array of characters representing loader steps
9
+ * @param {number} [delay=100] Delay in ms between loader steps
10
+ * @example
11
+ * let loader = loadingAnimation("Loading…");
12
+ *
13
+ * // Stop loader after 1 second
14
+ * setTimeout(() => clearInterval(loader), 1000);
15
+ * @returns {number} An interval that can be cleared to stop the animation
16
+ */
17
+ export function loadingAnimation(
18
+ text: string = '',
19
+ chars: Array<string> = ['⠙', '⠘', '⠰', '⠴', '⠤', '⠦', '⠆', '⠃', '⠋', '⠉'],
20
+ delay: number = 100
21
+ ) {
22
+ let x = 0;
23
+
24
+ const i = setInterval(function () {
25
+ process.stdout.write('\r' + chars[x++] + ' ' + text);
26
+ x = x % chars.length;
27
+ }, delay);
28
+
29
+ return {
30
+ stop: () => clearInterval(i),
31
+ };
32
+ }
@@ -0,0 +1,27 @@
1
+ import dotenv from 'dotenv';
2
+ import { existsSync, readdirSync } from 'fs';
3
+ dotenv.config();
4
+
5
+ function showExamples() {
6
+ const exampleFiles = readdirSync('./examples', { withFileTypes: true });
7
+ exampleFiles.forEach((file) => {
8
+ if (file.isDirectory()) return;
9
+ const exampleName = file.name.split('.')[0];
10
+ console.log(`- ${exampleName}`);
11
+ });
12
+ process.exit();
13
+ }
14
+
15
+ const exampleParams = process.argv.slice(2);
16
+ if (exampleParams.length === 0) {
17
+ console.error('No example specified, you can choose from:');
18
+ showExamples();
19
+ }
20
+ const exampleName = exampleParams[0];
21
+ const filePath = `./examples/${exampleName}.ts`;
22
+ if (existsSync(filePath)) {
23
+ require(`../${exampleName}.ts`);
24
+ } else {
25
+ console.error(`Example ${exampleName} does not exist, you can choose from:`);
26
+ showExamples();
27
+ }
package/examples/joke.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import OpenAI from 'openai';
2
- import { assign, fromPromise, createActor, setup, log, raise } from 'xstate';
2
+ import {
3
+ assign,
4
+ createActor,
5
+ fromCallback,
6
+ fromPromise,
7
+ log,
8
+ setup,
9
+ } from 'xstate';
3
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,
@@ -22,9 +30,9 @@ const agent = createAgent(openai, {
22
30
  events: {},
23
31
  });
24
32
 
25
- const promptTemplate = (topic: string) => `Tell me a joke about ${topic}.`;
26
-
27
- const getJokeCompletion = agent.fromChatCompletion(promptTemplate);
33
+ const getJokeCompletion = agent.fromChatCompletion(
34
+ (topic: string) => `Tell me a joke about ${topic}.`
35
+ );
28
36
 
29
37
  const rateJoke = agent.fromChatCompletion(
30
38
  (joke: string) => `Rate this joke on a scale of 1 to 10: ${joke}`
@@ -32,7 +40,7 @@ const rateJoke = agent.fromChatCompletion(
32
40
 
33
41
  const getTopic = fromPromise(async () => {
34
42
  const topic = await new Promise<string>((res) => {
35
- console.log('Give me a topic: \n\n');
43
+ console.log('Give me a joke topic:');
36
44
  const listener = (data: Buffer) => {
37
45
  const result = data.toString().trim();
38
46
  process.stdin.off('data', listener);
@@ -44,10 +52,48 @@ const getTopic = fromPromise(async () => {
44
52
  return topic;
45
53
  });
46
54
 
47
- const decide = agent.fromEventChoice(
55
+ const decide = agent.fromEvent(
48
56
  (lastRating: string) =>
49
57
  `Choose what to do next, given the previous rating of the joke: ${lastRating}`
50
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
+ }
74
+
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
+ }
89
+
90
+ const loader = fromCallback(({ input }: { input: string }) => {
91
+ const anim = loadingAnimation(input);
92
+
93
+ return () => {
94
+ anim.stop();
95
+ };
96
+ });
51
97
 
52
98
  const jokeMachine = setup({
53
99
  types: {
@@ -64,13 +110,15 @@ const jokeMachine = setup({
64
110
  getTopic,
65
111
  rateJoke,
66
112
  decide,
113
+ loader,
67
114
  },
68
115
  }).createMachine({
69
- context: ({ input }) => ({
70
- topic: input.topic,
116
+ context: () => ({
117
+ topic: '',
71
118
  jokes: [],
72
119
  desire: null,
73
120
  lastRating: null,
121
+ loader: null,
74
122
  }),
75
123
  initial: 'waitingForTopic',
76
124
  states: {
@@ -86,56 +134,69 @@ const jokeMachine = setup({
86
134
  },
87
135
  },
88
136
  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',
137
+ invoke: [
138
+ {
139
+ src: 'getJokeCompletion',
140
+ input: ({ context }) => context.topic,
141
+ onDone: {
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
+ ],
151
+ target: 'rateJoke',
152
+ },
101
153
  },
102
- },
154
+ {
155
+ src: 'loader',
156
+ input: getRandomFunnyPhrase,
157
+ },
158
+ ],
103
159
  },
104
160
  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',
161
+ invoke: [
162
+ {
163
+ src: 'rateJoke',
164
+ input: ({ context }) => context.jokes[context.jokes.length - 1]!,
165
+ onDone: {
166
+ actions: [
167
+ assign({
168
+ lastRating: ({ event }) =>
169
+ event.output.choices[0]!.message.content!,
170
+ }),
171
+ log(({ context }) => context.lastRating),
172
+ ],
173
+ target: 'decide',
174
+ },
117
175
  },
118
- },
176
+ {
177
+ src: 'loader',
178
+ input: getRandomRatingPhrase,
179
+ },
180
+ ],
119
181
  },
120
182
  decide: {
121
183
  invoke: {
122
184
  src: 'decide',
123
185
  input: ({ context }) => context.lastRating!,
124
186
  onDone: {
125
- actions: [
126
- log(({ event }) => event),
127
- raise(({ event }) => event.output![0]!),
128
- ],
187
+ actions: log(({ event }) => event),
129
188
  },
130
189
  },
131
190
  on: {
132
191
  askForTopic: {
133
192
  target: 'waitingForTopic',
193
+ actions: log("That joke wasn't good enough. Let's try again."),
134
194
  description:
135
195
  'Ask for a new topic, because the last joke rated 6 or lower',
136
196
  },
137
197
  endJokes: {
138
198
  target: 'end',
199
+ actions: log('That joke was good enough. Goodbye!'),
139
200
  description: 'End the jokes, since the last joke rated 7 or higher',
140
201
  },
141
202
  },
@@ -144,8 +205,10 @@ const jokeMachine = setup({
144
205
  type: 'final',
145
206
  },
146
207
  },
208
+ exit: () => {
209
+ process.exit();
210
+ },
147
211
  });
148
212
 
149
213
  const actor = createActor(jokeMachine);
150
-
151
214
  actor.start();
@@ -1,4 +1,4 @@
1
- import { assign, setup, assertEvent, createActor, raise } from 'xstate';
1
+ import { assign, setup, assertEvent, createActor } from 'xstate';
2
2
  import OpenAI from 'openai';
3
3
  import { createAgent } from '../src/openai';
4
4
 
@@ -9,7 +9,7 @@ const openai = new OpenAI({
9
9
  type Player = 'x' | 'o';
10
10
 
11
11
  const agent = createAgent(openai, {
12
- model: 'gpt-3.5-turbo-1106',
12
+ model: 'gpt-4-1106-preview',
13
13
  context: {
14
14
  board: {
15
15
  type: 'array',
@@ -30,11 +30,6 @@ const agent = createAgent(openai, {
30
30
  enum: ['x', 'o'],
31
31
  description: 'The player whose turn it is',
32
32
  },
33
- winner: {
34
- type: ['null', 'string'],
35
- enum: [null, 'x', 'o'],
36
- description: 'The player who won the game',
37
- },
38
33
  gameReport: {
39
34
  type: 'string',
40
35
  description: 'The game report',
@@ -78,12 +73,11 @@ const initialContext = {
78
73
  board: Array(9).fill(null) as Array<Player | null>,
79
74
  moves: 0,
80
75
  player: 'x' as Player,
81
- winner: null as Player | null,
82
76
  gameReport: '',
83
77
  events: [],
84
78
  } satisfies typeof agent.types.context;
85
79
 
86
- const bot = agent.fromEventChoice(
80
+ const bot = agent.fromEvent(
87
81
  ({ context }: { context: typeof agent.types.context }) => `
88
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.
89
83
 
@@ -97,9 +91,7 @@ const gameReporter = agent.fromChatCompletionStream(
97
91
  context,
98
92
  }: {
99
93
  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:
94
+ }) => `Here is the game board:
103
95
 
104
96
  ${JSON.stringify(context.board, null, 2)}
105
97
 
@@ -107,9 +99,30 @@ And here are the events that led to this game state:
107
99
 
108
100
  ${context.events.join('\n')}
109
101
 
102
+ The winner is ${getWinner(context.board)}.
103
+
110
104
  Provide a very short game report analyzing the game.`
111
105
  );
112
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
+
113
126
  export const ticTacToeMachine = setup({
114
127
  types: agent.types,
115
128
  actors: {
@@ -131,9 +144,6 @@ export const ticTacToeMachine = setup({
131
144
  },
132
145
  }),
133
146
  resetGame: assign(initialContext),
134
- setWinner: assign({
135
- winner: ({ context }) => (context.player === 'x' ? 'o' : 'x'),
136
- }),
137
147
  recordEvent: assign({
138
148
  events: ({ context, event }) => {
139
149
  return [...context.events, JSON.stringify(event)];
@@ -142,37 +152,9 @@ export const ticTacToeMachine = setup({
142
152
  },
143
153
  guards: {
144
154
  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
- });
161
-
162
- if (xWon) {
163
- return true;
164
- }
165
-
166
- const oWon = line.every((index) => {
167
- return board[index] === 'o';
168
- });
155
+ const winner = getWinner(context.board);
169
156
 
170
- if (oWon) {
171
- return true;
172
- }
173
- }
174
-
175
- return false;
157
+ return !!winner;
176
158
  },
177
159
  checkDraw: ({ context }) => {
178
160
  return context.moves === 9;
@@ -202,11 +184,6 @@ export const ticTacToeMachine = setup({
202
184
  invoke: {
203
185
  src: 'bot',
204
186
  input: ({ context }) => ({ context }),
205
- onDone: {
206
- actions: raise(({ event }) => {
207
- return event.output![0] as any;
208
- }),
209
- },
210
187
  },
211
188
  on: {
212
189
  'x.play': [
@@ -223,11 +200,6 @@ export const ticTacToeMachine = setup({
223
200
  invoke: {
224
201
  src: 'bot',
225
202
  input: ({ context }) => ({ context }),
226
- onDone: {
227
- actions: raise(({ event }) => {
228
- return event.output![0]!;
229
- }),
230
- },
231
203
  },
232
204
  on: {
233
205
  'o.play': [
@@ -261,7 +233,6 @@ export const ticTacToeMachine = setup({
261
233
  states: {
262
234
  winner: {
263
235
  tags: 'winner',
264
- entry: 'setWinner',
265
236
  },
266
237
  draw: {
267
238
  tags: 'draw',
@@ -277,7 +248,13 @@ export const ticTacToeMachine = setup({
277
248
  },
278
249
  });
279
250
 
280
- 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
+ });
281
258
  actor.subscribe((s) => {
282
259
  console.log(s.value, s.context);
283
260
  });
@@ -0,0 +1,147 @@
1
+ import OpenAI from 'openai';
2
+ import { createAgent, fromEventChoice } from '../src';
3
+ import { assign, createActor, 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
+ const json = await response.json();
27
+ if (!response.ok) {
28
+ throw new Error(
29
+ `Request failed with status code ${response.status}: ${json.error}`
30
+ );
31
+ }
32
+ if (!Array.isArray(json.results)) {
33
+ throw new Error(`Could not parse Tavily results. Please try again.`);
34
+ }
35
+ return JSON.stringify(json.results);
36
+ }
37
+
38
+ const openai = new OpenAI({
39
+ apiKey: process.env.OPENAI_API_KEY,
40
+ });
41
+
42
+ const agent = createAgent(openai, {
43
+ model: 'gpt-4-1106-preview',
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 machine = setup({
68
+ types: agent.types,
69
+ actors: {
70
+ searchTavily: fromPromise(async ({ input }: { input: string }) => {
71
+ const results = await searchTavily(input, {
72
+ maxResults: 5,
73
+ apiKey: process.env.TAVILY_API_KEY!,
74
+ });
75
+ return results;
76
+ }),
77
+ decide: agent.fromEvent(
78
+ (input: string) =>
79
+ `Decide what to do based on the given input, which may or may not be a location: ${input}`
80
+ ),
81
+ getFromTerminal,
82
+ },
83
+ }).createMachine({
84
+ initial: 'getLocation',
85
+ context: {
86
+ location: '',
87
+ count: 0,
88
+ history: [],
89
+ },
90
+ states: {
91
+ getLocation: {
92
+ invoke: {
93
+ src: 'getFromTerminal',
94
+ input: 'Location?',
95
+ onDone: {
96
+ actions: assign({
97
+ location: ({ event }) => event.output,
98
+ }),
99
+ target: 'decide',
100
+ },
101
+ },
102
+ always: {
103
+ guard: ({ context }) => context.count >= 3,
104
+ target: 'stopped',
105
+ },
106
+ },
107
+ decide: {
108
+ entry: log('Deciding...'),
109
+ invoke: {
110
+ src: 'decide',
111
+ input: ({ context }) => context.location,
112
+ },
113
+ on: {
114
+ getWeather: {
115
+ actions: log(({ event }) => event),
116
+ target: 'gettingWeather',
117
+ },
118
+ doSomethingElse: 'getLocation',
119
+ },
120
+ },
121
+ gettingWeather: {
122
+ entry: log('Getting weather...'),
123
+ invoke: {
124
+ src: 'searchTavily',
125
+ input: ({ context }) =>
126
+ `Get the weather for this location: ${context.location}`,
127
+ onDone: {
128
+ actions: [
129
+ log(({ event }) => event.output),
130
+ assign({
131
+ count: ({ context }) => context.count + 1,
132
+ }),
133
+ ],
134
+ target: 'getLocation',
135
+ },
136
+ },
137
+ },
138
+ stopped: {
139
+ entry: log('You have used up your search quota. Goodbye!'),
140
+ },
141
+ },
142
+ exit: () => {
143
+ process.exit();
144
+ },
145
+ });
146
+
147
+ createActor(machine).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.3",
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.5.1"
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
+ ```
package/src/openai.ts CHANGED
@@ -1,4 +1,4 @@
1
- import OpenAI from 'openai';
1
+ import type OpenAI from 'openai';
2
2
  import {
3
3
  AnyEventObject,
4
4
  ObservableActorLogic,
@@ -34,6 +34,7 @@ import {
34
34
  */
35
35
  export function fromChatCompletion<TInput>(
36
36
  openai: OpenAI,
37
+ agentSettings: CreateAgentOutput<any>,
37
38
  inputFn: (
38
39
  input: TInput
39
40
  ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
@@ -44,7 +45,7 @@ export function fromChatCompletion<TInput>(
44
45
  const params: ChatCompletionCreateParamsNonStreaming =
45
46
  typeof openAiInput === 'string'
46
47
  ? {
47
- model: 'gpt-3.5-turbo-1106',
48
+ model: agentSettings.model,
48
49
  messages: [
49
50
  {
50
51
  role: 'user',
@@ -68,6 +69,7 @@ export function fromChatCompletion<TInput>(
68
69
  */
69
70
  export function fromChatCompletionStream<TInput>(
70
71
  openai: OpenAI,
72
+ agentSettings: CreateAgentOutput<any>,
71
73
  inputFn: (
72
74
  input: TInput
73
75
  ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
@@ -81,7 +83,7 @@ export function fromChatCompletionStream<TInput>(
81
83
  const resolvedParams: ChatCompletionCreateParamsBase =
82
84
  typeof openAiInput === 'string'
83
85
  ? {
84
- model: 'gpt-3.5-turbo-1106',
86
+ model: agentSettings.model,
85
87
  messages: [
86
88
  {
87
89
  role: 'user',
@@ -126,13 +128,20 @@ export function fromChatCompletionStream<TInput>(
126
128
  */
127
129
  export function fromEventChoice<TInput>(
128
130
  openai: OpenAI,
129
- machineTypes: { schemas: { context: ContextSchema; events: EventSchemas } },
131
+ agentSettings: CreateAgentOutput<any>,
130
132
  inputFn: (
131
133
  input: TInput
132
- ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
134
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,
135
+ options?: {
136
+ /**
137
+ * Immediately execute sending the event to the parent actor.
138
+ * @default false
139
+ */
140
+ execute?: boolean;
141
+ }
133
142
  ) {
134
143
  return fromPromise<AnyEventObject[] | undefined, TInput>(
135
- async ({ input, self }) => {
144
+ async ({ input, self, system }) => {
136
145
  const transitions = getAllTransitions(self._parent!.getSnapshot());
137
146
  const functionNameMapping: Record<string, string> = {};
138
147
  const tools = transitions
@@ -148,11 +157,11 @@ export function fromEventChoice<TInput>(
148
157
  name,
149
158
  description:
150
159
  t.description ??
151
- machineTypes.schemas.events[t.eventType]?.description,
160
+ agentSettings.schemas.events[t.eventType]?.description,
152
161
  parameters: {
153
162
  type: 'object',
154
163
  properties:
155
- machineTypes.schemas.events[t.eventType]?.properties ?? {},
164
+ agentSettings.schemas.events[t.eventType]?.properties ?? {},
156
165
  },
157
166
  },
158
167
  } as const;
@@ -162,7 +171,7 @@ export function fromEventChoice<TInput>(
162
171
  const completionParams: ChatCompletionCreateParamsNonStreaming =
163
172
  typeof openAiInput === 'string'
164
173
  ? {
165
- model: 'gpt-4-1106-preview',
174
+ model: agentSettings.model,
166
175
  messages: [
167
176
  {
168
177
  role: 'user',
@@ -179,12 +188,19 @@ export function fromEventChoice<TInput>(
179
188
  const toolCalls = completion.choices[0]?.message.tool_calls;
180
189
 
181
190
  if (toolCalls) {
182
- return toolCalls.map((tc) => {
191
+ const events = toolCalls.map((tc) => {
183
192
  return {
184
193
  type: functionNameMapping[tc.function.name],
185
194
  ...JSON.parse(tc.function.arguments),
186
195
  };
187
196
  });
197
+
198
+ if (options?.execute) {
199
+ events.forEach((event) => {
200
+ // @ts-ignore
201
+ system._relay(self, self._parent, event);
202
+ });
203
+ }
188
204
  }
189
205
 
190
206
  return undefined;
@@ -205,6 +221,12 @@ interface CreateAgentOutput<
205
221
  context: FromSchema<ConvertContextToJSONSchema<T['context']>>;
206
222
  events: FromSchema<Values<ConvertToJSONSchemas<T['events']>>>;
207
223
  };
224
+ fromEvent: <TInput>(
225
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
226
+ ) => PromiseActorLogic<
227
+ FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined,
228
+ TInput
229
+ >;
208
230
  fromEventChoice: <TInput>(
209
231
  inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
210
232
  ) => PromiseActorLogic<
@@ -229,7 +251,7 @@ export function createAgent<
229
251
  events: EventSchemas;
230
252
  }
231
253
  >(openai: OpenAI, settings: T): CreateAgentOutput<T> {
232
- const obj: CreateAgentOutput<T> = {
254
+ const agentSettings: CreateAgentOutput<T> = {
233
255
  model: settings.model,
234
256
  schemas: {
235
257
  context: {
@@ -240,11 +262,16 @@ export function createAgent<
240
262
  events: createEventSchemas(settings.events),
241
263
  } as any,
242
264
  types: {} as any,
243
- fromEventChoice: (input) => fromEventChoice(openai, obj, input) as any,
244
- fromChatCompletion: (input) => fromChatCompletion(openai, input),
265
+ fromEvent: (input) =>
266
+ // @ts-ignore
267
+ fromEventChoice(openai, agentSettings, input, { execute: true }),
268
+ // @ts-ignore infinitely deep
269
+ fromEventChoice: (input) => fromEventChoice(openai, agentSettings, input),
270
+ fromChatCompletion: (input) =>
271
+ fromChatCompletion(openai, agentSettings, input),
245
272
  fromChatCompletionStream: (input) =>
246
- fromChatCompletionStream(openai, input),
273
+ fromChatCompletionStream(openai, agentSettings, input),
247
274
  };
248
275
 
249
- return obj as any;
276
+ return agentSettings as any;
250
277
  }
package/dist/index.d.mts DELETED
@@ -1,3 +0,0 @@
1
- declare function helloWorld(): string;
2
-
3
- export { helloWorld };
package/dist/index.mjs DELETED
@@ -1,7 +0,0 @@
1
- // src/index.ts
2
- function helloWorld() {
3
- return "Hello World!";
4
- }
5
- export {
6
- helloWorld
7
- };