@statelyai/agent 0.0.1 → 0.0.2

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.
@@ -0,0 +1,8 @@
1
+ # Changesets
2
+
3
+ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
+ with multi-package repos, or single-package repos to help you version and publish your code. You can
5
+ find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
+
7
+ We have a quick list of common questions to get you started engaging with this project in
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "restricted",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "request": "launch",
10
+ "name": "Launch Program",
11
+ "skipFiles": ["<node_internals>/**"],
12
+ "program": "${file}",
13
+ "preLaunchTask": "tsc: build - tsconfig.json",
14
+ "outFiles": ["${workspaceFolder}/**/*.js"]
15
+ }
16
+ ]
17
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @statelyai/agent
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - e125728: Added `createAgent(...)`
package/dist/index.d.ts CHANGED
@@ -1,6 +1,39 @@
1
- import * as xstate from 'xstate';
2
- import { AnyEventObject } from 'xstate';
3
1
  import OpenAI from 'openai';
2
+ import { Prop, PromiseActorLogic, ObservableActorLogic, AnyEventObject, Values } from 'xstate';
3
+ import { JSONSchema7 } from 'json-schema-to-ts/lib/types/definitions';
4
+ import { FromSchema } from 'json-schema-to-ts';
5
+ import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources';
6
+ import { ChatCompletionCreateParamsBase, ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions';
7
+
8
+ type EventSchemas = {
9
+ [key: string]: {
10
+ description?: string;
11
+ properties?: {
12
+ [key: string]: JSONSchema7;
13
+ };
14
+ };
15
+ };
16
+ interface ContextSchema {
17
+ [key: string]: JSONSchema7;
18
+ }
19
+ type ConvertToJSONSchemas<T> = {
20
+ [K in keyof T]: {
21
+ properties: {
22
+ type: {
23
+ const: K;
24
+ };
25
+ };
26
+ type: 'object';
27
+ required: Array<keyof Prop<T[K], 'properties'> | 'type'>;
28
+ additionalProperties: false;
29
+ } & T[K];
30
+ } & {};
31
+ type ConvertContextToJSONSchema<T extends ContextSchema> = {
32
+ type: 'object';
33
+ properties: T;
34
+ readonly required: Array<keyof T & string>;
35
+ additionalProperties: false;
36
+ };
4
37
 
5
38
  /**
6
39
  * Creates [promise actor logic](https://stately.ai/docs/promise-actors) that uses the OpenAI API to generate a completion.
@@ -9,20 +42,45 @@ import OpenAI from 'openai';
9
42
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
10
43
  *
11
44
  */
12
- declare function fromChatCompletion<TInput>(openai: OpenAI, inputFn: (input: TInput) => OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming): xstate.PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
45
+ declare function fromChatCompletion<TInput>(openai: OpenAI, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming): PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
13
46
  /**
14
47
  * Creates [observable actor logic](https://stately.ai/docs/observable-actors) that uses the OpenAI API to generate a completion stream.
15
48
  *
16
49
  * @param openai The OpenAI instance to use.
17
50
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
18
51
  */
19
- declare function fromChatCompletionStream<TInput>(openai: OpenAI, inputFn: (input: TInput) => OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming): xstate.ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
52
+ declare function fromChatCompletionStream<TInput>(openai: OpenAI, inputFn: (input: TInput) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming): ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
20
53
  /**
21
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.
22
55
  *
23
56
  * @param openai The OpenAI instance to use.
24
57
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
25
58
  */
26
- declare function fromEventChoice<TInput>(openai: OpenAI, inputFn: (input: TInput) => OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming): xstate.PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
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>;
65
+ interface CreateAgentOutput<T extends {
66
+ model: ChatCompletionCreateParamsBase['model'];
67
+ context: ContextSchema;
68
+ events: EventSchemas;
69
+ }> {
70
+ model: T['model'];
71
+ schemas: T;
72
+ types: {
73
+ context: FromSchema<ConvertContextToJSONSchema<T['context']>>;
74
+ events: FromSchema<Values<ConvertToJSONSchemas<T['events']>>>;
75
+ };
76
+ fromEventChoice: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined, TInput>;
77
+ fromChatCompletion: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
78
+ fromChatCompletionStream: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming) => ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
79
+ }
80
+ declare function createAgent<T extends {
81
+ model: ChatCompletionCreateParamsBase['model'];
82
+ context: ContextSchema;
83
+ events: EventSchemas;
84
+ }>(openai: OpenAI, settings: T): CreateAgentOutput<T>;
27
85
 
28
- export { fromChatCompletion, fromChatCompletionStream, fromEventChoice };
86
+ export { createAgent, fromChatCompletion, fromChatCompletionStream, fromEventChoice };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ createAgent: () => createAgent,
23
24
  fromChatCompletion: () => fromChatCompletion,
24
25
  fromChatCompletionStream: () => fromChatCompletionStream,
25
26
  fromEventChoice: () => fromEventChoice
@@ -35,13 +36,40 @@ function getAllTransitions(state) {
35
36
  const transitions = nodes.map((node) => [...node.transitions.values()]).flat(2);
36
37
  return transitions;
37
38
  }
39
+ function createEventSchemas(eventSchemaMap) {
40
+ const resolvedEventSchemaMap = {};
41
+ for (const [key, schema] of Object.entries(eventSchemaMap)) {
42
+ resolvedEventSchemaMap[key] = {
43
+ type: "object",
44
+ required: ["type"],
45
+ properties: {
46
+ type: {
47
+ const: key
48
+ },
49
+ ...schema.properties
50
+ },
51
+ additionalProperties: false,
52
+ ...schema
53
+ };
54
+ }
55
+ return resolvedEventSchemaMap;
56
+ }
38
57
 
39
58
  // src/openai.ts
40
59
  function fromChatCompletion(openai, inputFn) {
41
60
  return (0, import_xstate.fromPromise)(
42
61
  async ({ input }) => {
43
62
  const openAiInput = inputFn(input);
44
- const response = await openai.chat.completions.create(openAiInput);
63
+ const params = typeof openAiInput === "string" ? {
64
+ model: "gpt-3.5-turbo-1106",
65
+ messages: [
66
+ {
67
+ role: "user",
68
+ content: openAiInput
69
+ }
70
+ ]
71
+ } : openAiInput;
72
+ const response = await openai.chat.completions.create(params);
45
73
  return response;
46
74
  }
47
75
  );
@@ -52,8 +80,17 @@ function fromChatCompletionStream(openai, inputFn) {
52
80
  const observers = /* @__PURE__ */ new Set();
53
81
  (async () => {
54
82
  const openAiInput = inputFn(input);
83
+ const resolvedParams = typeof openAiInput === "string" ? {
84
+ model: "gpt-3.5-turbo-1106",
85
+ messages: [
86
+ {
87
+ role: "user",
88
+ content: openAiInput
89
+ }
90
+ ]
91
+ } : openAiInput;
55
92
  const stream = await openai.chat.completions.create({
56
- ...openAiInput,
93
+ ...resolvedParams,
57
94
  stream: true
58
95
  });
59
96
  for await (const part of stream) {
@@ -76,7 +113,7 @@ function fromChatCompletionStream(openai, inputFn) {
76
113
  }
77
114
  );
78
115
  }
79
- function fromEventChoice(openai, inputFn) {
116
+ function fromEventChoice(openai, machineTypes, inputFn) {
80
117
  return (0, import_xstate.fromPromise)(
81
118
  async ({ input, self }) => {
82
119
  const transitions = getAllTransitions(self._parent.getSnapshot());
@@ -90,17 +127,26 @@ function fromEventChoice(openai, inputFn) {
90
127
  type: "function",
91
128
  function: {
92
129
  name,
93
- description: t.description,
130
+ description: t.description ?? machineTypes.schemas.events[t.eventType]?.description,
94
131
  parameters: {
95
132
  type: "object",
96
- properties: t.meta?.parameters ?? {}
133
+ properties: machineTypes.schemas.events[t.eventType]?.properties ?? {}
97
134
  }
98
135
  }
99
136
  };
100
137
  });
101
138
  const openAiInput = inputFn(input);
139
+ const completionParams = typeof openAiInput === "string" ? {
140
+ model: "gpt-4-1106-preview",
141
+ messages: [
142
+ {
143
+ role: "user",
144
+ content: openAiInput
145
+ }
146
+ ]
147
+ } : openAiInput;
102
148
  const completion = await openai.chat.completions.create({
103
- ...openAiInput,
149
+ ...completionParams,
104
150
  tools
105
151
  });
106
152
  const toolCalls = completion.choices[0]?.message.tool_calls;
@@ -112,12 +158,31 @@ function fromEventChoice(openai, inputFn) {
112
158
  };
113
159
  });
114
160
  }
115
- return toolCalls ?? void 0;
161
+ return void 0;
116
162
  }
117
163
  );
118
164
  }
165
+ function createAgent(openai, settings) {
166
+ const obj = {
167
+ model: settings.model,
168
+ schemas: {
169
+ context: {
170
+ type: "object",
171
+ properties: settings.context,
172
+ additionalProperties: false
173
+ },
174
+ events: createEventSchemas(settings.events)
175
+ },
176
+ types: {},
177
+ fromEventChoice: (input) => fromEventChoice(openai, obj, input),
178
+ fromChatCompletion: (input) => fromChatCompletion(openai, input),
179
+ fromChatCompletionStream: (input) => fromChatCompletionStream(openai, input)
180
+ };
181
+ return obj;
182
+ }
119
183
  // Annotate the CommonJS export names for ESM import in node:
120
184
  0 && (module.exports = {
185
+ createAgent,
121
186
  fromChatCompletion,
122
187
  fromChatCompletionStream,
123
188
  fromEventChoice
package/examples/joke.ts CHANGED
@@ -1,185 +1,151 @@
1
1
  import OpenAI from 'openai';
2
- import { assign, fromPromise, createActor, waitFor, setup } from 'xstate';
3
- import { fromEventChoice } from '../src/index';
2
+ import { assign, fromPromise, createActor, setup, log, raise } from 'xstate';
3
+ import { createAgent } from '../src';
4
4
 
5
5
  const openai = new OpenAI({
6
6
  apiKey: process.env.OPENAI_API_KEY,
7
7
  });
8
8
 
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
- });
24
-
25
- return res.choices[0]?.message.content;
26
- }
27
- );
9
+ const agent = createAgent(openai, {
10
+ model: 'gpt-3.5-turbo-1106',
11
+ context: {
12
+ topic: { type: 'string' },
13
+ jokes: {
14
+ type: 'array',
15
+ items: {
16
+ type: 'string',
17
+ },
18
+ desire: { type: ['string', 'null'] },
19
+ lastRating: { type: ['string', 'null'] },
20
+ },
21
+ },
22
+ events: {},
23
+ });
28
24
 
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
- });
25
+ const promptTemplate = (topic: string) => `Tell me a joke about ${topic}.`;
41
26
 
42
- return res.choices[0]?.message.content;
43
- }
44
- );
27
+ const getJokeCompletion = agent.fromChatCompletion(promptTemplate);
45
28
 
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
- });
29
+ const rateJoke = agent.fromChatCompletion(
30
+ (joke: string) => `Rate this joke on a scale of 1 to 10: ${joke}`
31
+ );
54
32
 
55
- return topic;
33
+ const getTopic = fromPromise(async () => {
34
+ const topic = await new Promise<string>((res) => {
35
+ console.log('Give me a topic: \n\n');
36
+ const listener = (data: Buffer) => {
37
+ const result = data.toString().trim();
38
+ process.stdin.off('data', listener);
39
+ res(result);
40
+ };
41
+ process.stdin.on('data', listener);
56
42
  });
57
43
 
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:
44
+ return topic;
45
+ });
78
46
 
79
- ${desire}
80
- `,
81
- },
82
- ],
83
- })),
47
+ const decide = agent.fromEventChoice(
48
+ (lastRating: string) =>
49
+ `Choose what to do next, given the previous rating of the joke: ${lastRating}`
50
+ );
51
+
52
+ const jokeMachine = setup({
53
+ types: {
54
+ context: {} as {
55
+ topic: string;
56
+ jokes: string[];
57
+ desire: string | null;
58
+ lastRating: string | null;
84
59
  },
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
- },
60
+ input: {} as { topic: string },
61
+ },
62
+ actors: {
63
+ getJokeCompletion,
64
+ getTopic,
65
+ rateJoke,
66
+ decide,
67
+ },
68
+ }).createMachine({
69
+ context: ({ input }) => ({
70
+ topic: input.topic,
71
+ jokes: [],
72
+ desire: null,
73
+ lastRating: null,
74
+ }),
75
+ initial: 'waitingForTopic',
76
+ states: {
77
+ waitingForTopic: {
78
+ invoke: {
79
+ src: 'getTopic',
80
+ onDone: {
81
+ actions: assign({
82
+ topic: ({ event }) => event.output,
83
+ }),
84
+ target: 'tellingJoke',
103
85
  },
104
86
  },
105
- tellingJoke: {
106
- invoke: {
107
- src: 'getJokeCompletion',
108
- input: ({ context }) => ({ topic: context.topic }),
109
- onDone: {
110
- actions: assign({
87
+ },
88
+ tellingJoke: {
89
+ invoke: {
90
+ src: 'getJokeCompletion',
91
+ input: ({ context }) => context.topic,
92
+ onDone: {
93
+ actions: [
94
+ assign({
111
95
  jokes: ({ context, event }) =>
112
- context.jokes.concat(event.output as string),
96
+ context.jokes.concat(event.output.choices[0]!.message.content!),
113
97
  }),
114
- target: 'rateJoke',
115
- },
98
+ log((x) => x.context.jokes.at(-1)),
99
+ ],
100
+ target: 'rateJoke',
116
101
  },
117
102
  },
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,
103
+ },
104
+ 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!,
127
113
  }),
128
- target: 'joked',
129
- },
114
+ log(({ context }) => context.lastRating),
115
+ ],
116
+ target: 'decide',
130
117
  },
131
118
  },
132
- joked: {
133
- invoke: {
134
- src: 'getTopic',
135
- onDone: {
136
- actions: assign({
137
- desire: ({ event }) => event.output,
138
- }),
139
- target: 'decide',
140
- },
119
+ },
120
+ decide: {
121
+ invoke: {
122
+ src: 'decide',
123
+ input: ({ context }) => context.lastRating!,
124
+ onDone: {
125
+ actions: [
126
+ log(({ event }) => event),
127
+ raise(({ event }) => event.output![0]!),
128
+ ],
141
129
  },
142
130
  },
143
- decide: {
144
- invoke: {
145
- src: 'decide',
146
- input: ({ context }) => context.desire!,
131
+ on: {
132
+ askForTopic: {
133
+ target: 'waitingForTopic',
134
+ description:
135
+ 'Ask for a new topic, because the last joke rated 6 or lower',
147
136
  },
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
- },
137
+ endJokes: {
138
+ target: 'end',
139
+ description: 'End the jokes, since the last joke rated 7 or higher',
158
140
  },
159
141
  },
160
- end: {},
161
142
  },
162
- });
163
-
164
- const actor = createActor(chain, {
165
- input: {
166
- topic: 'donuts',
143
+ end: {
144
+ type: 'final',
167
145
  },
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();
146
+ },
147
+ });
179
148
 
180
- await waitFor(actor, (snap) => snap.status === 'done', {
181
- timeout: Infinity,
182
- });
183
- }
149
+ const actor = createActor(jokeMachine);
184
150
 
185
- start();
151
+ actor.start();
@@ -1,6 +1,6 @@
1
1
  import { assign, setup, assertEvent, createActor, raise } from 'xstate';
2
- import { fromChatCompletionStream, fromEventChoice } from '../src/openai';
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,113 @@ 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',
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
+ winner: {
34
+ type: ['null', 'string'],
35
+ enum: [null, 'x', 'o'],
36
+ description: 'The player who won the game',
37
+ },
38
+ gameReport: {
39
+ type: 'string',
40
+ description: 'The game report',
41
+ },
42
+ events: {
43
+ type: 'array',
44
+ items: {
45
+ type: 'string',
46
+ },
47
+ },
48
+ } as const,
49
+ events: {
50
+ 'x.play': {
51
+ properties: {
52
+ index: {
53
+ description: 'The index of the cell to play on',
54
+ type: 'number',
55
+
56
+ minimum: 0,
57
+ maximum: 8,
58
+ },
59
+ },
60
+ },
61
+ 'o.play': {
62
+ properties: {
63
+ index: {
64
+ description: 'The index of the cell to play on',
65
+ type: 'number',
66
+ minimum: 0,
67
+ maximum: 8,
68
+ },
69
+ },
70
+ },
71
+ reset: {
72
+ properties: {},
73
+ },
74
+ },
75
+ });
76
+
11
77
  const initialContext = {
12
78
  board: Array(9).fill(null) as Array<Player | null>,
13
79
  moves: 0,
14
80
  player: 'x' as Player,
15
- winner: undefined as Player | undefined,
81
+ winner: null as Player | null,
16
82
  gameReport: '',
17
- };
83
+ events: [],
84
+ } satisfies typeof agent.types.context;
85
+
86
+ const bot = agent.fromEventChoice(
87
+ ({ context }: { context: typeof agent.types.context }) => `
88
+ 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
+
90
+ ${JSON.stringify(context, null, 2)}
91
+
92
+ Execute the single best next move to try to win the game. Do not play on an existing cell.`
93
+ );
94
+
95
+ const gameReporter = agent.fromChatCompletionStream(
96
+ ({
97
+ context,
98
+ }: {
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:
18
103
 
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
104
  ${JSON.stringify(context.board, null, 2)}
62
105
 
63
- Provide a game report analyzing the game.`,
64
- },
65
- ],
66
- stream: true,
67
- })
68
- ),
106
+ And here are the events that led to this game state:
107
+
108
+ ${context.events.join('\n')}
109
+
110
+ Provide a very short game report analyzing the game.`
111
+ );
112
+
113
+ export const ticTacToeMachine = setup({
114
+ types: agent.types,
115
+ actors: {
116
+ bot,
117
+ gameReporter,
69
118
  },
70
119
  actions: {
71
120
  updateBoard: assign({
@@ -77,11 +126,19 @@ Provide a game report analyzing the game.`,
77
126
  },
78
127
  moves: ({ context }) => context.moves + 1,
79
128
  player: ({ context }) => (context.player === 'x' ? 'o' : 'x'),
129
+ events: ({ context, event }) => {
130
+ return [...context.events, JSON.stringify(event)];
131
+ },
80
132
  }),
81
133
  resetGame: assign(initialContext),
82
134
  setWinner: assign({
83
135
  winner: ({ context }) => (context.player === 'x' ? 'o' : 'x'),
84
136
  }),
137
+ recordEvent: assign({
138
+ events: ({ context, event }) => {
139
+ return [...context.events, JSON.stringify(event)];
140
+ },
141
+ }),
85
142
  },
86
143
  guards: {
87
144
  checkWin: ({ context }) => {
@@ -157,18 +214,8 @@ Provide a game report analyzing the game.`,
157
214
  target: 'o',
158
215
  guard: 'isValidMove',
159
216
  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
217
  },
171
- { reenter: true },
218
+ { target: 'x', reenter: true },
172
219
  ],
173
220
  },
174
221
  },
@@ -177,10 +224,8 @@ Provide a game report analyzing the game.`,
177
224
  src: 'bot',
178
225
  input: ({ context }) => ({ context }),
179
226
  onDone: {
180
- // @ts-ignore
181
227
  actions: raise(({ event }) => {
182
- console.log('output', event.output);
183
- return event.output![0];
228
+ return event.output![0]!;
184
229
  }),
185
230
  },
186
231
  },
@@ -190,18 +235,8 @@ Provide a game report analyzing the game.`,
190
235
  target: 'x',
191
236
  guard: 'isValidMove',
192
237
  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
238
  },
204
- { reenter: true },
239
+ { target: 'o', reenter: true },
205
240
  ],
206
241
  },
207
242
  },
@@ -233,7 +268,7 @@ Provide a game report analyzing the game.`,
233
268
  },
234
269
  },
235
270
  on: {
236
- RESET: {
271
+ reset: {
237
272
  target: 'playing',
238
273
  actions: 'resetGame',
239
274
  },
package/package.json CHANGED
@@ -1,26 +1,30 @@
1
1
  {
2
2
  "name": "@statelyai/agent",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
8
- "scripts": {
9
- "build": "tsup src/index.ts --format cjs,esm --dts",
10
- "lint": "tsc",
11
- "test": "vitest run",
12
- "prepublishOnly": "tsup src/index.ts --dts"
13
- },
14
8
  "keywords": [],
15
9
  "author": "",
16
10
  "license": "MIT",
17
11
  "devDependencies": {
12
+ "@changesets/cli": "^2.27.1",
18
13
  "@types/node": "^20.10.6",
14
+ "json-schema-to-ts": "^3.0.0",
19
15
  "tsup": "^8.0.1",
20
16
  "typescript": "^5.3.3"
21
17
  },
22
18
  "dependencies": {
23
19
  "openai": "^4.24.1",
24
20
  "xstate": "^5.3.1"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup src/index.ts --format cjs,esm --dts",
27
+ "lint": "tsc",
28
+ "test": "vitest run"
25
29
  }
26
- }
30
+ }
package/src/index.ts CHANGED
@@ -2,4 +2,5 @@ export {
2
2
  fromChatCompletion,
3
3
  fromChatCompletionStream,
4
4
  fromEventChoice,
5
+ createAgent,
5
6
  } from './openai';
package/src/openai.ts CHANGED
@@ -1,12 +1,29 @@
1
1
  import OpenAI from 'openai';
2
2
  import {
3
3
  AnyEventObject,
4
+ ObservableActorLogic,
4
5
  Observer,
6
+ PromiseActorLogic,
7
+ Values,
5
8
  fromObservable,
6
9
  fromPromise,
10
+ setup,
7
11
  toObserver,
8
12
  } from 'xstate';
9
13
  import { getAllTransitions } from './utils';
14
+ import {
15
+ ContextSchema,
16
+ EventSchemas,
17
+ ConvertContextToJSONSchema,
18
+ ConvertToJSONSchemas,
19
+ createEventSchemas,
20
+ } from './utils';
21
+ import { FromSchema } from 'json-schema-to-ts';
22
+ import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources';
23
+ import {
24
+ ChatCompletionCreateParamsBase,
25
+ ChatCompletionCreateParamsStreaming,
26
+ } from 'openai/resources/chat/completions';
10
27
 
11
28
  /**
12
29
  * Creates [promise actor logic](https://stately.ai/docs/promise-actors) that uses the OpenAI API to generate a completion.
@@ -19,12 +36,24 @@ export function fromChatCompletion<TInput>(
19
36
  openai: OpenAI,
20
37
  inputFn: (
21
38
  input: TInput
22
- ) => OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
39
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
23
40
  ) {
24
41
  return fromPromise<OpenAI.Chat.Completions.ChatCompletion, TInput>(
25
42
  async ({ input }) => {
26
43
  const openAiInput = inputFn(input);
27
- const response = await openai.chat.completions.create(openAiInput);
44
+ const params: ChatCompletionCreateParamsNonStreaming =
45
+ typeof openAiInput === 'string'
46
+ ? {
47
+ model: 'gpt-3.5-turbo-1106',
48
+ messages: [
49
+ {
50
+ role: 'user',
51
+ content: openAiInput,
52
+ },
53
+ ],
54
+ }
55
+ : openAiInput;
56
+ const response = await openai.chat.completions.create(params);
28
57
 
29
58
  return response;
30
59
  }
@@ -41,7 +70,7 @@ export function fromChatCompletionStream<TInput>(
41
70
  openai: OpenAI,
42
71
  inputFn: (
43
72
  input: TInput
44
- ) => OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
73
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
45
74
  ) {
46
75
  return fromObservable<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>(
47
76
  ({ input }) => {
@@ -49,8 +78,20 @@ export function fromChatCompletionStream<TInput>(
49
78
 
50
79
  (async () => {
51
80
  const openAiInput = inputFn(input);
81
+ const resolvedParams: ChatCompletionCreateParamsBase =
82
+ typeof openAiInput === 'string'
83
+ ? {
84
+ model: 'gpt-3.5-turbo-1106',
85
+ messages: [
86
+ {
87
+ role: 'user',
88
+ content: openAiInput,
89
+ },
90
+ ],
91
+ }
92
+ : openAiInput;
52
93
  const stream = await openai.chat.completions.create({
53
- ...openAiInput,
94
+ ...resolvedParams,
54
95
  stream: true,
55
96
  });
56
97
 
@@ -85,9 +126,10 @@ export function fromChatCompletionStream<TInput>(
85
126
  */
86
127
  export function fromEventChoice<TInput>(
87
128
  openai: OpenAI,
129
+ machineTypes: { schemas: { context: ContextSchema; events: EventSchemas } },
88
130
  inputFn: (
89
131
  input: TInput
90
- ) => OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
132
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
91
133
  ) {
92
134
  return fromPromise<AnyEventObject[] | undefined, TInput>(
93
135
  async ({ input, self }) => {
@@ -104,17 +146,33 @@ export function fromEventChoice<TInput>(
104
146
  type: 'function',
105
147
  function: {
106
148
  name,
107
- description: t.description,
149
+ description:
150
+ t.description ??
151
+ machineTypes.schemas.events[t.eventType]?.description,
108
152
  parameters: {
109
153
  type: 'object',
110
- properties: t.meta?.parameters ?? {},
154
+ properties:
155
+ machineTypes.schemas.events[t.eventType]?.properties ?? {},
111
156
  },
112
157
  },
113
158
  } as const;
114
159
  });
160
+
115
161
  const openAiInput = inputFn(input);
162
+ const completionParams: ChatCompletionCreateParamsNonStreaming =
163
+ typeof openAiInput === 'string'
164
+ ? {
165
+ model: 'gpt-4-1106-preview',
166
+ messages: [
167
+ {
168
+ role: 'user',
169
+ content: openAiInput,
170
+ },
171
+ ],
172
+ }
173
+ : openAiInput;
116
174
  const completion = await openai.chat.completions.create({
117
- ...openAiInput,
175
+ ...completionParams,
118
176
  tools,
119
177
  });
120
178
 
@@ -129,7 +187,64 @@ export function fromEventChoice<TInput>(
129
187
  });
130
188
  }
131
189
 
132
- return toolCalls ?? undefined;
190
+ return undefined;
133
191
  }
134
192
  );
135
193
  }
194
+
195
+ interface CreateAgentOutput<
196
+ T extends {
197
+ model: ChatCompletionCreateParamsBase['model'];
198
+ context: ContextSchema;
199
+ events: EventSchemas;
200
+ }
201
+ > {
202
+ model: T['model'];
203
+ schemas: T;
204
+ types: {
205
+ context: FromSchema<ConvertContextToJSONSchema<T['context']>>;
206
+ events: FromSchema<Values<ConvertToJSONSchemas<T['events']>>>;
207
+ };
208
+ fromEventChoice: <TInput>(
209
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
210
+ ) => PromiseActorLogic<
211
+ FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined,
212
+ TInput
213
+ >;
214
+ fromChatCompletion: <TInput>(
215
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
216
+ ) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
217
+ fromChatCompletionStream: <TInput>(
218
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming
219
+ ) => ObservableActorLogic<
220
+ OpenAI.Chat.Completions.ChatCompletionChunk,
221
+ TInput
222
+ >;
223
+ }
224
+
225
+ export function createAgent<
226
+ T extends {
227
+ model: ChatCompletionCreateParamsBase['model'];
228
+ context: ContextSchema;
229
+ events: EventSchemas;
230
+ }
231
+ >(openai: OpenAI, settings: T): CreateAgentOutput<T> {
232
+ const obj: CreateAgentOutput<T> = {
233
+ model: settings.model,
234
+ schemas: {
235
+ context: {
236
+ type: 'object',
237
+ properties: settings.context,
238
+ additionalProperties: false,
239
+ },
240
+ events: createEventSchemas(settings.events),
241
+ } as any,
242
+ types: {} as any,
243
+ fromEventChoice: (input) => fromEventChoice(openai, obj, input) as any,
244
+ fromChatCompletion: (input) => fromChatCompletion(openai, input),
245
+ fromChatCompletionStream: (input) =>
246
+ fromChatCompletionStream(openai, input),
247
+ };
248
+
249
+ return obj as any;
250
+ }
package/src/utils.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { AnyMachineSnapshot, AnyStateNode } from 'xstate';
1
+ import { AnyMachineSnapshot, AnyStateNode, Prop, Values } from 'xstate';
2
+ import { FromSchema } from 'json-schema-to-ts';
3
+ import { JSONSchema7 } from 'json-schema-to-ts/lib/types/definitions';
2
4
 
3
5
  export function getAllTransitions(state: AnyMachineSnapshot) {
4
6
  const nodes = state._nodes;
@@ -8,3 +10,59 @@ export function getAllTransitions(state: AnyMachineSnapshot) {
8
10
 
9
11
  return transitions;
10
12
  }
13
+
14
+ export type EventSchemas = {
15
+ [key: string]: {
16
+ description?: string;
17
+ properties?: {
18
+ [key: string]: JSONSchema7;
19
+ };
20
+ };
21
+ };
22
+
23
+ export interface ContextSchema {
24
+ [key: string]: JSONSchema7;
25
+ }
26
+
27
+ export type ConvertToJSONSchemas<T> = {
28
+ [K in keyof T]: {
29
+ properties: { type: { const: K } };
30
+ type: 'object';
31
+ required: Array<keyof Prop<T[K], 'properties'> | 'type'>;
32
+ additionalProperties: false;
33
+ } & T[K];
34
+ } & {};
35
+
36
+ export type ConvertContextToJSONSchema<T extends ContextSchema> = {
37
+ type: 'object';
38
+ properties: T;
39
+ readonly required: Array<keyof T & string>;
40
+ additionalProperties: false;
41
+ };
42
+
43
+ export function createEventSchemas<T extends EventSchemas>(
44
+ eventSchemaMap: T
45
+ ): ConvertToJSONSchemas<T> {
46
+ const resolvedEventSchemaMap = {};
47
+
48
+ for (const [key, schema] of Object.entries(eventSchemaMap)) {
49
+ // @ts-ignore
50
+ resolvedEventSchemaMap[key] = {
51
+ type: 'object',
52
+ required: ['type'],
53
+ properties: {
54
+ type: {
55
+ const: key,
56
+ },
57
+ ...schema.properties,
58
+ },
59
+ additionalProperties: false,
60
+ ...schema,
61
+ } as JSONSchema7;
62
+ }
63
+
64
+ return resolvedEventSchemaMap as ConvertToJSONSchemas<T>;
65
+ }
66
+
67
+ export type InferEventsFromSchemas<T extends ConvertToJSONSchemas<any>> =
68
+ FromSchema<Values<T>>;