@statelyai/agent 0.0.4 → 0.0.6

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,17 +1,17 @@
1
1
  {
2
- // Use IntelliSense to learn about possible attributes.
3
- // Hover to view descriptions of existing attributes.
4
2
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
3
  "version": "0.2.0",
6
4
  "configurations": [
7
5
  {
8
6
  "type": "node",
9
7
  "request": "launch",
10
- "name": "Launch Program",
11
- "skipFiles": ["<node_internals>/**"],
12
- "program": "${file}",
13
- "preLaunchTask": "tsc: build - tsconfig.json",
14
- "outFiles": ["${workspaceFolder}/**/*.js"]
8
+ "name": "Debug Current Test File",
9
+ "autoAttachChildProcesses": true,
10
+ "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
11
+ "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
12
+ "args": ["run", "${relativeFile}"],
13
+ "smartStep": true,
14
+ "console": "integratedTerminal"
15
15
  }
16
16
  ]
17
17
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @statelyai/agent
2
2
 
3
+ ## 0.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#16](https://github.com/statelyai/agent/pull/16) [`3ba5fb2`](https://github.com/statelyai/agent/commit/3ba5fb2392b51dee71f2585ed662b4ee9ecd6c41) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Update to XState 5.8.0
8
+
9
+ ## 0.0.5
10
+
11
+ ### Patch Changes
12
+
13
+ - [#9](https://github.com/statelyai/agent/pull/9) [`d8e7b67`](https://github.com/statelyai/agent/commit/d8e7b673f6d265f37b2096b25d75310845860271) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Add `adapter.fromTool(…)`, which creates an actor that chooses agent logic based on a input.
14
+
15
+ ```ts
16
+ const actor = adapter.fromTool(() => "Draw me a picture of a donut", {
17
+ // tools
18
+ makeIllustration: {
19
+ description: "Makes an illustration",
20
+ run: async (input) => {
21
+ /* ... */
22
+ },
23
+ inputSchema: {
24
+ /* ... */
25
+ },
26
+ },
27
+ getWeather: {
28
+ description: "Gets the weather",
29
+ run: async (input) => {
30
+ /* ... */
31
+ },
32
+ inputSchema: {
33
+ /* ... */
34
+ },
35
+ },
36
+ });
37
+
38
+ //...
39
+ ```
40
+
3
41
  ## 0.0.4
4
42
 
5
43
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ import { Prop, Values, AnyStateMachine, createActor, PromiseActorLogic, AnyEvent
3
3
  import { JSONSchema7 } from 'json-schema-to-ts/lib/types/definitions';
4
4
  import { FromSchema } from 'json-schema-to-ts';
5
5
  import OpenAI from 'openai';
6
- import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources';
7
- import { ChatCompletionCreateParamsBase, ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions';
6
+ import { ChatCompletionCreateParamsBase } from 'openai/resources/chat/completions';
7
+ import { ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming } from 'openai/resources';
8
8
 
9
9
  type EventSchemas = {
10
10
  [key: string]: {
@@ -14,67 +14,80 @@ type EventSchemas = {
14
14
  };
15
15
  };
16
16
  };
17
- interface ContextSchema {
18
- [key: string]: JSONSchema7;
19
- }
17
+ type ContextSchema = JSONSchema7 & {
18
+ type: 'object';
19
+ };
20
20
  type ConvertToJSONSchemas<T> = {
21
21
  [K in keyof T]: {
22
22
  properties: {
23
23
  type: {
24
24
  const: K;
25
25
  };
26
- };
26
+ } & Prop<T[K], 'properties'>;
27
27
  type: 'object';
28
- required: Array<keyof Prop<T[K], 'properties'> | 'type'>;
28
+ required: Array<(keyof Prop<T[K], 'properties'> & string) | 'type'>;
29
29
  additionalProperties: false;
30
- } & T[K];
30
+ };
31
31
  } & {};
32
- type ConvertContextToJSONSchema<T extends ContextSchema> = {
33
- type: 'object';
34
- properties: T;
35
- readonly required: Array<keyof T & string>;
36
- additionalProperties: false;
37
- };
38
32
 
39
- declare function createSchemas<TContextSchema extends ContextSchema, TEventSchemas extends EventSchemas>({ context, events, }: {
33
+ declare function createSchemas<const TContextSchema extends ContextSchema, const TEventSchemas extends EventSchemas>({ context, events, }: {
34
+ /**
35
+ * The JSON schema for the context object.
36
+ *
37
+ * Must be of `{ type: 'object' }`.
38
+ */
40
39
  context: TContextSchema;
40
+ /**
41
+ * An object mapping event types to each event object's JSON Schema.
42
+ */
41
43
  events: TEventSchemas;
42
44
  }): {
43
- context: ConvertContextToJSONSchema<TContextSchema>;
45
+ context: TContextSchema;
44
46
  events: ConvertToJSONSchemas<TEventSchemas>;
45
47
  types: {
46
- context: FromSchema<ConvertContextToJSONSchema<TContextSchema>>;
48
+ context: FromSchema<TContextSchema>;
47
49
  events: FromSchema<Values<ConvertToJSONSchemas<TEventSchemas>>>;
48
50
  };
49
51
  };
50
52
 
51
53
  declare function createAgent<T extends AnyStateMachine>(...args: Parameters<typeof createActor<T>>): xstate.Actor<T>;
52
54
 
53
- interface OpenAIAdapterOutput<T extends {
54
- model: ChatCompletionCreateParamsBase['model'];
55
- }> {
56
- model: T['model'];
55
+ interface StatelyAgentAdapter {
56
+ model: string;
57
57
  /**
58
- * Determines which event to send to the parent state machine actor based on the prompt.
58
+ * Creates actor logic that chooses an event from all of the
59
+ * possible next events of the parent state machine
60
+ * and sends it to the parent actor.
59
61
  */
60
- fromEventChoice: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming, options?: {
61
- /**
62
- * Immediately execute sending the event to the parent actor.
63
- * @default true
64
- */
65
- execute?: boolean;
66
- }) => PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
62
+ fromEvent: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
67
63
  /**
68
- * Creates promise actor logic that resolves with a chat completion.
64
+ * Creates actor logic that resolves with a chat completion.
69
65
  */
70
66
  fromChat: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
71
67
  /**
72
- * Creates observable actor logic that emits a chat completion stream.
68
+ * Creates actor logic that emits a chat completion stream.
73
69
  */
74
70
  fromChatStream: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming) => ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
71
+ /**
72
+ * Creates actor logic that chooses a tool from the provided
73
+ * tools and runs that tool.
74
+ */
75
+ fromTool: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming, tools: {
76
+ [key: string]: Tool<any, any>;
77
+ }) => PromiseActorLogic<{
78
+ result: any;
79
+ tool: string;
80
+ toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall;
81
+ } | undefined, TInput>;
75
82
  }
83
+ interface Tool<TInput, TOutput> {
84
+ description: string;
85
+ inputSchema: any;
86
+ run: (input: TInput) => TOutput;
87
+ }
88
+
76
89
  declare function createOpenAIAdapter<T extends {
77
90
  model: ChatCompletionCreateParamsBase['model'];
78
- }>(openai: OpenAI, settings: T): OpenAIAdapterOutput<T>;
91
+ }>(openai: OpenAI, settings: T): StatelyAgentAdapter;
79
92
 
80
93
  export { createAgent, createOpenAIAdapter, createSchemas };
package/dist/index.js CHANGED
@@ -57,12 +57,7 @@ function createSchemas({
57
57
  events
58
58
  }) {
59
59
  return {
60
- context: {
61
- type: "object",
62
- properties: context,
63
- additionalProperties: false,
64
- required: Object.keys(context)
65
- },
60
+ context,
66
61
  events: createEventSchemas(events),
67
62
  types: {}
68
63
  };
@@ -134,7 +129,7 @@ function fromChatStream(openai, agentSettings, inputFn) {
134
129
  }
135
130
  );
136
131
  }
137
- function fromEventChoice(openai, agentSettings, inputFn, options) {
132
+ function fromEvent(openai, agentSettings, inputFn) {
138
133
  return (0, import_xstate2.fromPromise)(
139
134
  async ({ input, self, system }) => {
140
135
  const parentSnapshot = self._parent?.getSnapshot();
@@ -177,32 +172,70 @@ function fromEventChoice(openai, agentSettings, inputFn, options) {
177
172
  tools
178
173
  });
179
174
  const toolCalls = completion.choices[0]?.message.tool_calls;
180
- if (toolCalls) {
175
+ if (toolCalls?.length) {
181
176
  const events = toolCalls.map((tc) => {
182
177
  return {
183
178
  type: functionNameMapping[tc.function.name],
184
179
  ...JSON.parse(tc.function.arguments)
185
180
  };
186
181
  });
187
- if (options?.execute) {
188
- events.forEach((event) => {
189
- system._relay(self, self._parent, event);
190
- });
191
- }
182
+ const event = events[0];
183
+ system._relay(self, self._parent, event);
192
184
  }
193
185
  return void 0;
194
186
  }
195
187
  );
196
188
  }
189
+ function fromTool(openai, agentSettings, tools, inputFn) {
190
+ return (0, import_xstate2.fromPromise)(async ({ input }) => {
191
+ const resolvedTools = Object.entries(tools).map(([key, value]) => {
192
+ return {
193
+ type: "function",
194
+ function: {
195
+ name: key,
196
+ description: value.description,
197
+ parameters: value.inputSchema
198
+ }
199
+ };
200
+ });
201
+ const openAiInput = inputFn(input);
202
+ const completionParams = typeof openAiInput === "string" ? {
203
+ model: agentSettings.model,
204
+ messages: [
205
+ {
206
+ role: "user",
207
+ content: openAiInput
208
+ }
209
+ ]
210
+ } : openAiInput;
211
+ const completion = await openai.chat.completions.create({
212
+ ...completionParams,
213
+ tools: resolvedTools
214
+ });
215
+ const toolCalls = completion.choices[0]?.message.tool_calls;
216
+ if (toolCalls?.length) {
217
+ const toolCall = toolCalls[0];
218
+ const tool = tools[toolCall.function.name];
219
+ const args = JSON.parse(toolCall.function.arguments);
220
+ if (tool) {
221
+ const result = await tool.run(args);
222
+ return {
223
+ toolCall,
224
+ tool: toolCall.function.name,
225
+ result
226
+ };
227
+ }
228
+ }
229
+ return void 0;
230
+ });
231
+ }
197
232
  function createOpenAIAdapter(openai, settings) {
198
233
  const agentSettings = {
199
234
  model: settings.model,
200
- fromEventChoice: (input) => (
201
- // @ts-ignore infinitely deep
202
- fromEventChoice(openai, agentSettings, input, { execute: true })
203
- ),
235
+ fromEvent: (input) => fromEvent(openai, agentSettings, input),
204
236
  fromChat: (input) => fromChatCompletion(openai, agentSettings, input),
205
- fromChatStream: (input) => fromChatStream(openai, agentSettings, input)
237
+ fromChatStream: (input) => fromChatStream(openai, agentSettings, input),
238
+ fromTool: (input, tools) => fromTool(openai, agentSettings, tools, input)
206
239
  };
207
240
  return agentSettings;
208
241
  }
package/examples/joke.ts CHANGED
@@ -9,15 +9,19 @@ const openai = new OpenAI({
9
9
 
10
10
  const schemas = createSchemas({
11
11
  context: {
12
- topic: { type: 'string' },
13
- jokes: {
14
- type: 'array',
15
- items: {
16
- type: 'string',
12
+ type: 'object',
13
+ properties: {
14
+ topic: { type: 'string' },
15
+ jokes: {
16
+ type: 'array',
17
+ items: {
18
+ type: 'string',
19
+ },
17
20
  },
21
+ desire: { type: ['string', 'null'] },
22
+ lastRating: { type: ['string', 'null'] },
18
23
  },
19
- desire: { type: ['string', 'null'] as const },
20
- lastRating: { type: ['string', 'null'] as const },
24
+ required: ['topic', 'jokes', 'desire', 'lastRating'],
21
25
  },
22
26
  events: {
23
27
  askForTopic: {
@@ -61,7 +65,7 @@ const getTopic = fromPromise(async () => {
61
65
  return topic;
62
66
  });
63
67
 
64
- const decide = adapter.fromEventChoice(
68
+ const decide = adapter.fromEvent(
65
69
  (lastRating: string) =>
66
70
  `Choose what to do next, given the previous rating of the joke: ${lastRating}`
67
71
  );
@@ -0,0 +1,128 @@
1
+ import OpenAI from 'openai';
2
+ import { createAgent, createOpenAIAdapter, createSchemas } from '../src';
3
+ import { assign, setup } from 'xstate';
4
+ const openai = new OpenAI({
5
+ apiKey: process.env.OPENAI_API_KEY,
6
+ });
7
+
8
+ const adapter = createOpenAIAdapter(openai, {
9
+ model: 'gpt-3.5-turbo-1106',
10
+ });
11
+
12
+ const guessLogic = adapter.fromEvent(
13
+ ({
14
+ previousGuesses,
15
+ lastResult,
16
+ }: {
17
+ previousGuesses: number[];
18
+ lastResult: string;
19
+ }) => `
20
+ Guess the number between 1 and 10. The previous guesses were ${
21
+ previousGuesses.length ? previousGuesses.join(', ') : 'not made yet'
22
+ } and the last result was ${lastResult}.
23
+ `
24
+ );
25
+
26
+ const schemas = createSchemas({
27
+ context: {
28
+ type: 'object',
29
+ properties: {
30
+ lastGuess: {
31
+ type: ['number', 'null'],
32
+ description: 'The last guess',
33
+ },
34
+ previousGuesses: {
35
+ type: 'array',
36
+ items: {
37
+ type: 'number',
38
+ },
39
+ description: 'The previous guesses',
40
+ },
41
+ answer: {
42
+ type: 'number',
43
+ description: 'The answer',
44
+ },
45
+ },
46
+ },
47
+ events: {
48
+ guess: {
49
+ properties: {
50
+ number: {
51
+ // integer
52
+ type: 'number',
53
+ minimum: 1,
54
+ maximum: 10,
55
+ },
56
+ },
57
+ required: ['number'],
58
+ },
59
+ },
60
+ });
61
+
62
+ const machine = setup({
63
+ types: {
64
+ context: {} as {
65
+ previousGuesses: number[];
66
+ answer: number;
67
+ },
68
+ input: {} as { answer: number },
69
+ events: schemas.types.events,
70
+ },
71
+ schemas,
72
+ actors: {
73
+ guessLogic,
74
+ },
75
+ }).createMachine({
76
+ context: ({ input }) => ({
77
+ answer: input.answer,
78
+ previousGuesses: [],
79
+ }),
80
+ initial: 'guessing',
81
+ states: {
82
+ guessing: {
83
+ always: {
84
+ guard: ({ context }) =>
85
+ context.answer === context.previousGuesses.at(-1),
86
+ target: 'winner',
87
+ },
88
+ invoke: {
89
+ src: 'guessLogic',
90
+ input: ({ context }) => ({
91
+ previousGuesses: context.previousGuesses,
92
+ lastResult:
93
+ context.previousGuesses.length === 0
94
+ ? 'not given yet'
95
+ : context.previousGuesses.at(-1)! - context.answer > 0
96
+ ? 'too high'
97
+ : 'too low',
98
+ }),
99
+ },
100
+ on: {
101
+ guess: {
102
+ actions: assign({
103
+ previousGuesses: ({ context, event }) => [
104
+ ...context.previousGuesses,
105
+ event.number,
106
+ ],
107
+ }),
108
+ target: 'guessing',
109
+ reenter: true,
110
+ },
111
+ },
112
+ },
113
+ winner: {
114
+ type: 'final',
115
+ },
116
+ },
117
+ });
118
+
119
+ const agent = createAgent(machine, {
120
+ input: { answer: 4 },
121
+ inspect: (ev) => {
122
+ if (ev.type === '@xstate.event') {
123
+ console.log(ev.event);
124
+ }
125
+ },
126
+ });
127
+
128
+ agent.start();
@@ -10,36 +10,40 @@ type Player = 'x' | 'o';
10
10
 
11
11
  const schemas = createSchemas({
12
12
  context: {
13
- board: {
14
- type: 'array',
15
- items: {
16
- type: ['null', 'string'],
17
- enum: [null, 'x', 'o'],
13
+ type: 'object',
14
+ properties: {
15
+ board: {
16
+ type: 'array',
17
+ items: {
18
+ type: ['null', 'string'],
19
+ enum: [null, 'x', 'o'],
20
+ },
21
+ minItems: 9,
22
+ maxItems: 9,
23
+ description: 'The board of the tic-tac-toe game',
18
24
  },
19
- minItems: 9,
20
- maxItems: 9,
21
- description: 'The board of the tic-tac-toe game',
22
- },
23
- moves: {
24
- type: 'number',
25
- description: 'The number of moves that have been played',
26
- },
27
- player: {
28
- type: 'string',
29
- enum: ['x', 'o'],
30
- description: 'The player whose turn it is',
31
- },
32
- gameReport: {
33
- type: 'string',
34
- description: 'The game report',
35
- },
36
- events: {
37
- type: 'array',
38
- items: {
25
+ moves: {
26
+ type: 'number',
27
+ description: 'The number of moves that have been played',
28
+ },
29
+ player: {
39
30
  type: 'string',
31
+ enum: ['x', 'o'],
32
+ description: 'The player whose turn it is',
33
+ },
34
+ gameReport: {
35
+ type: 'string',
36
+ description: 'The game report',
37
+ },
38
+ events: {
39
+ type: 'array',
40
+ items: {
41
+ type: 'string',
42
+ },
40
43
  },
41
44
  },
42
- } as const,
45
+ required: ['board', 'moves', 'player', 'gameReport', 'events'],
46
+ },
43
47
  events: {
44
48
  'x.play': {
45
49
  properties: {
@@ -80,7 +84,7 @@ const initialContext = {
80
84
  events: [],
81
85
  } satisfies typeof schemas.types.context;
82
86
 
83
- const bot = adapter.fromEventChoice(
87
+ const bot = adapter.fromEvent(
84
88
  ({ context }: { context: typeof schemas.types.context }) => `
85
89
  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.
86
90
 
@@ -84,7 +84,7 @@ const machine = setup({
84
84
  types: schemas.types,
85
85
  actors: {
86
86
  getWeather,
87
- decide: adapter.fromEventChoice(
87
+ decide: adapter.fromEvent(
88
88
  (input: string) =>
89
89
  `Decide what to do based on the given input, which may or may not be a location: ${input}`
90
90
  ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/agent",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,19 +17,20 @@
17
17
  "openai": "^4.24.1",
18
18
  "ts-node": "^10.9.2",
19
19
  "tsup": "^8.0.1",
20
- "typescript": "^5.3.3"
20
+ "typescript": "^5.3.3",
21
+ "vitest": "^1.2.2"
21
22
  },
22
23
  "publishConfig": {
23
24
  "access": "public"
24
25
  },
25
26
  "dependencies": {
26
- "xstate": "^5.6.0"
27
+ "xstate": "^5.8.0"
27
28
  },
28
29
  "packageManager": "pnpm@8.11.0",
29
30
  "scripts": {
30
31
  "build": "tsup src/index.ts --format cjs,esm --dts",
31
32
  "lint": "tsc",
32
- "test": "vitest run",
33
+ "test": "vitest",
33
34
  "example": "ts-node examples/helpers/runner.ts",
34
35
  "changeset": "changeset",
35
36
  "release": "changeset publish",
package/readme.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # Stately Agent (alpha)
2
2
 
3
- 🚧 Documentation in progress! Please see [the examples directory](https://github.com/statelyai/agent/tree/main/examples) for working examples.
3
+ - Read [the documentation](https://stately.ai/docs/agents)
4
+ - See [the examples directory](https://github.com/statelyai/agent/tree/main/examples) for working examples.
4
5
 
5
6
  ## Installation
6
7
 
7
8
  Install `openai`, and `@statelyai/agent`:
8
9
 
9
10
  ```bash
10
- npm install openai @statelyai/agent
11
+ pnpm install openai @statelyai/agent
11
12
  ```
12
13
 
13
14
  ## Usage
@@ -35,7 +36,7 @@ OPENAI_API_KEY="your-openai-api-key"
35
36
  Then, install the dependencies (`npm install`) and run the examples:
36
37
 
37
38
  ```bash
38
- npm run example joke
39
+ pnpm run example joke
39
40
  # or:
40
- # npm run example ticTacToe
41
+ # pnpm run example ticTacToe
41
42
  ```
@@ -0,0 +1,217 @@
1
+ import { test, expect } from 'vitest';
2
+ import { createOpenAIAdapter, createTool } from './adapters/openai';
3
+ import OpenAI from 'openai';
4
+ import { createActor, toPromise } from 'xstate';
5
+
6
+ test('fromTool - weather or illustration', async () => {
7
+ const openAi = new OpenAI({
8
+ apiKey: process.env.OPENAI_API_KEY,
9
+ });
10
+
11
+ const adapter = createOpenAIAdapter(openAi, {
12
+ model: 'gpt-3.5-turbo',
13
+ });
14
+
15
+ const toolChoice = adapter.fromTool(() => 'Create an image of a donut', {
16
+ makeIllustration: {
17
+ description: 'Make an illustration',
18
+ run: async () => 'Illustration',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ name: {
23
+ type: 'string',
24
+ description: 'The name of the illustration',
25
+ },
26
+ },
27
+ required: ['name'],
28
+ },
29
+ },
30
+ getWeather: {
31
+ description: 'Get the weather for a location',
32
+ run: async () => 'Weather',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ location: {
37
+ type: 'object',
38
+ properties: {
39
+ city: {
40
+ type: 'string',
41
+ description: 'The name of the city',
42
+ },
43
+ state: {
44
+ type: 'string',
45
+ description: 'The name of the state',
46
+ },
47
+ },
48
+ required: ['city', 'state'],
49
+ },
50
+ },
51
+ required: ['location'],
52
+ },
53
+ },
54
+ });
55
+
56
+ const actor = createActor(toolChoice);
57
+
58
+ actor.start();
59
+
60
+ const res = await toPromise(actor);
61
+
62
+ expect(res?.result).toBe('Illustration');
63
+ });
64
+
65
+ test('fromTool - GitHub PR description inserter', async () => {
66
+ const openAi = new OpenAI({
67
+ apiKey: process.env.OPENAI_API_KEY,
68
+ });
69
+
70
+ const adapter = createOpenAIAdapter(openAi, {
71
+ model: 'gpt-3.5-turbo-16k-0613',
72
+ });
73
+
74
+ const toolChoice = adapter.fromTool(
75
+ (input: string) =>
76
+ `Create a GitHub PR description for the following: ${input}`,
77
+ {
78
+ fetchGitHubPR: {
79
+ description: 'Fetch a GitHub PR',
80
+ run: async (input: string) => {
81
+ return {
82
+ title: 'Title',
83
+ body: input,
84
+ };
85
+ },
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ repo: {
90
+ type: 'string',
91
+ description: 'The name of the repo',
92
+ },
93
+ number: {
94
+ type: 'number',
95
+ description: 'The number of the PR',
96
+ },
97
+ },
98
+ required: ['repo', 'number'],
99
+ },
100
+ },
101
+ createPullRequestDescription: {
102
+ description: 'Create a GitHub PR description',
103
+ run: () => 'Description',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ title: {
108
+ type: 'string',
109
+ description: 'The title of the PR',
110
+ },
111
+ body: {
112
+ type: 'string',
113
+ description: 'The body of the PR',
114
+ },
115
+ },
116
+ required: ['title', 'body'],
117
+ },
118
+ },
119
+ }
120
+ );
121
+
122
+ const actor = createActor(toolChoice, {
123
+ input:
124
+ // 'Get the details from this: https://github.com/microsoft/TypeScript/pull/47198',
125
+ 'Make a summary of this PR: (some code here)',
126
+ });
127
+
128
+ actor.start();
129
+
130
+ const res = await toPromise(actor);
131
+
132
+ expect(res?.tool).toEqual('createPullRequestDescription');
133
+ expect(res?.result).toEqual('Description');
134
+ });
135
+
136
+ test('fromTool - joke creator or rater', async () => {
137
+ const openAi = new OpenAI({
138
+ apiKey: process.env.OPENAI_API_KEY,
139
+ });
140
+
141
+ const adapter = createOpenAIAdapter(openAi, {
142
+ model: 'gpt-4-1106-preview',
143
+ });
144
+
145
+ const rateJoke = createTool({
146
+ description: 'Rate a joke',
147
+ inputSchema: {
148
+ type: 'object',
149
+ properties: {
150
+ joke: {
151
+ type: 'string',
152
+ description: 'The joke to rate',
153
+ },
154
+ },
155
+ },
156
+ run: async ({ topic }: { topic: string }) => {
157
+ return `Here is a joke about ${topic}`;
158
+ },
159
+ });
160
+
161
+ const createJoke = createTool({
162
+ description: 'Create a joke',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ category: {
167
+ type: 'string',
168
+ description: 'The category of the joke',
169
+ },
170
+ },
171
+ required: ['category'],
172
+ },
173
+ run: async () => {
174
+ return 'Some joke';
175
+ },
176
+ });
177
+
178
+ const toolChoice = adapter.fromTool(
179
+ (input: string) => `
180
+ The user provided this input:
181
+
182
+ <input>
183
+ ${input}
184
+ </input>
185
+
186
+ Determine what to do:
187
+ - If the input is asking for a joke, create a joke,
188
+ - But if the input is providing a joke, then rate the joke.
189
+ `,
190
+ {
191
+ rateJoke,
192
+ createJoke,
193
+ }
194
+ );
195
+
196
+ const actor = createActor(toolChoice, {
197
+ // input: 'Why did the chicken cross the road? To get to the other side!',
198
+ input: 'Tell me a joke about chickens',
199
+ });
200
+
201
+ actor.start();
202
+
203
+ const res = await toPromise(actor);
204
+
205
+ expect(res?.tool).toEqual('createJoke');
206
+ expect(res?.result).toEqual('Some joke');
207
+
208
+ const actor2 = createActor(toolChoice, {
209
+ input:
210
+ 'Check this joke out: Why did the chicken cross the road? To get to the other side!',
211
+ });
212
+
213
+ actor2.start();
214
+
215
+ const res2 = await toPromise(actor2);
216
+ expect(res2?.tool).toEqual('rateJoke');
217
+ });
@@ -1,9 +1,7 @@
1
1
  import type OpenAI from 'openai';
2
2
  import {
3
3
  AnyEventObject,
4
- ObservableActorLogic,
5
4
  Observer,
6
- PromiseActorLogic,
7
5
  fromObservable,
8
6
  fromPromise,
9
7
  isMachineSnapshot,
@@ -11,10 +9,8 @@ import {
11
9
  } from 'xstate';
12
10
  import { getAllTransitions } from '../utils';
13
11
  import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources';
14
- import {
15
- ChatCompletionCreateParamsBase,
16
- ChatCompletionCreateParamsStreaming,
17
- } from 'openai/resources/chat/completions';
12
+ import { ChatCompletionCreateParamsBase } from 'openai/resources/chat/completions';
13
+ import { StatelyAgentAdapter, Tool } from '../types';
18
14
 
19
15
  /**
20
16
  * Creates [promise actor logic](https://stately.ai/docs/promise-actors) that uses the OpenAI API to generate a completion.
@@ -25,7 +21,7 @@ import {
25
21
  */
26
22
  export function fromChatCompletion<TInput>(
27
23
  openai: OpenAI,
28
- agentSettings: OpenAIAdapterOutput<any>,
24
+ agentSettings: StatelyAgentAdapter,
29
25
  inputFn: (
30
26
  input: TInput
31
27
  ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
@@ -60,7 +56,7 @@ export function fromChatCompletion<TInput>(
60
56
  */
61
57
  export function fromChatStream<TInput>(
62
58
  openai: OpenAI,
63
- agentSettings: OpenAIAdapterOutput<any>,
59
+ agentSettings: StatelyAgentAdapter,
64
60
  inputFn: (
65
61
  input: TInput
66
62
  ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
@@ -117,19 +113,12 @@ export function fromChatStream<TInput>(
117
113
  * @param openai The OpenAI instance to use.
118
114
  * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
119
115
  */
120
- export function fromEventChoice<TInput>(
116
+ export function fromEvent<TInput>(
121
117
  openai: OpenAI,
122
- agentSettings: OpenAIAdapterOutput<any>,
118
+ agentSettings: StatelyAgentAdapter,
123
119
  inputFn: (
124
120
  input: TInput
125
- ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,
126
- options?: {
127
- /**
128
- * Immediately execute sending the event to the parent actor.
129
- * @default false
130
- */
131
- execute?: boolean;
132
- }
121
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
133
122
  ) {
134
123
  return fromPromise<AnyEventObject[] | undefined, TInput>(
135
124
  async ({ input, self, system }) => {
@@ -185,7 +174,7 @@ export function fromEventChoice<TInput>(
185
174
 
186
175
  const toolCalls = completion.choices[0]?.message.tool_calls;
187
176
 
188
- if (toolCalls) {
177
+ if (toolCalls?.length) {
189
178
  const events = toolCalls.map((tc) => {
190
179
  return {
191
180
  type: functionNameMapping[tc.function.name],
@@ -193,12 +182,10 @@ export function fromEventChoice<TInput>(
193
182
  };
194
183
  });
195
184
 
196
- if (options?.execute) {
197
- events.forEach((event) => {
198
- // @ts-ignore
199
- system._relay(self, self._parent, event);
200
- });
201
- }
185
+ const event = events[0]!;
186
+
187
+ // @ts-ignore
188
+ system._relay(self, self._parent, event);
202
189
  }
203
190
 
204
191
  return undefined;
@@ -206,54 +193,105 @@ export function fromEventChoice<TInput>(
206
193
  );
207
194
  }
208
195
 
209
- interface OpenAIAdapterOutput<
210
- T extends {
211
- model: ChatCompletionCreateParamsBase['model'];
212
- }
213
- > {
214
- model: T['model'];
215
- /**
216
- * Determines which event to send to the parent state machine actor based on the prompt.
217
- */
218
- fromEventChoice: <TInput>(
219
- inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming,
220
- options?: {
221
- /**
222
- * Immediately execute sending the event to the parent actor.
223
- * @default true
224
- */
225
- execute?: boolean;
226
- }
227
- ) => PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
228
- /**
229
- * Creates promise actor logic that resolves with a chat completion.
230
- */
231
- fromChat: <TInput>(
232
- inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
233
- ) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
234
- /**
235
- * Creates observable actor logic that emits a chat completion stream.
236
- */
237
- fromChatStream: <TInput>(
238
- inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming
239
- ) => ObservableActorLogic<
240
- OpenAI.Chat.Completions.ChatCompletionChunk,
196
+ export function createTool<TInput, T>({
197
+ description,
198
+ inputSchema,
199
+ run,
200
+ }: Tool<TInput, T>): Tool<TInput, T> {
201
+ return {
202
+ description,
203
+ inputSchema,
204
+ run,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * 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.
210
+ *
211
+ * @param openai The OpenAI instance to use.
212
+ * @param inputFn A function that maps arbitrary input to OpenAI chat completion input.
213
+ */
214
+ export function fromTool<TInput>(
215
+ openai: OpenAI,
216
+ agentSettings: StatelyAgentAdapter,
217
+ tools: {
218
+ [key: string]: Tool<any, any>;
219
+ },
220
+ inputFn: (
221
+ input: TInput
222
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
223
+ ) {
224
+ return fromPromise<
225
+ | {
226
+ result: any;
227
+ tool: string;
228
+ toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall;
229
+ }
230
+ | undefined,
241
231
  TInput
242
- >;
232
+ >(async ({ input }) => {
233
+ const resolvedTools = Object.entries(tools).map(([key, value]) => {
234
+ return {
235
+ type: 'function',
236
+ function: {
237
+ name: key,
238
+ description: value.description,
239
+ parameters: value.inputSchema,
240
+ },
241
+ } as const;
242
+ });
243
+
244
+ const openAiInput = inputFn(input);
245
+ const completionParams: ChatCompletionCreateParamsNonStreaming =
246
+ typeof openAiInput === 'string'
247
+ ? {
248
+ model: agentSettings.model,
249
+ messages: [
250
+ {
251
+ role: 'user',
252
+ content: openAiInput,
253
+ },
254
+ ],
255
+ }
256
+ : openAiInput;
257
+ const completion = await openai.chat.completions.create({
258
+ ...completionParams,
259
+ tools: resolvedTools,
260
+ });
261
+
262
+ const toolCalls = completion.choices[0]?.message.tool_calls;
263
+
264
+ if (toolCalls?.length) {
265
+ const toolCall = toolCalls[0]!;
266
+ const tool = tools[toolCall.function.name];
267
+ const args = JSON.parse(toolCall.function.arguments);
268
+
269
+ if (tool) {
270
+ const result = await tool.run(args);
271
+
272
+ return {
273
+ toolCall,
274
+ tool: toolCall.function.name,
275
+ result,
276
+ };
277
+ }
278
+ }
279
+
280
+ return undefined;
281
+ });
243
282
  }
244
283
 
245
284
  export function createOpenAIAdapter<
246
285
  T extends {
247
286
  model: ChatCompletionCreateParamsBase['model'];
248
287
  }
249
- >(openai: OpenAI, settings: T): OpenAIAdapterOutput<T> {
250
- const agentSettings: OpenAIAdapterOutput<T> = {
288
+ >(openai: OpenAI, settings: T): StatelyAgentAdapter {
289
+ const agentSettings: StatelyAgentAdapter = {
251
290
  model: settings.model,
252
- fromEventChoice: (input) =>
253
- // @ts-ignore infinitely deep
254
- fromEventChoice(openai, agentSettings, input, { execute: true }) as any,
291
+ fromEvent: (input) => fromEvent(openai, agentSettings, input),
255
292
  fromChat: (input) => fromChatCompletion(openai, agentSettings, input),
256
293
  fromChatStream: (input) => fromChatStream(openai, agentSettings, input),
294
+ fromTool: (input, tools) => fromTool(openai, agentSettings, tools, input),
257
295
  };
258
296
 
259
297
  return agentSettings;
package/src/schemas.ts CHANGED
@@ -2,36 +2,38 @@ import { Values } from 'xstate';
2
2
  import {
3
3
  ContextSchema,
4
4
  EventSchemas,
5
- ConvertContextToJSONSchema,
6
5
  ConvertToJSONSchemas,
7
6
  createEventSchemas,
8
7
  } from './utils';
9
8
  import { FromSchema } from 'json-schema-to-ts';
10
9
 
11
10
  export function createSchemas<
12
- TContextSchema extends ContextSchema,
13
- TEventSchemas extends EventSchemas
11
+ const TContextSchema extends ContextSchema,
12
+ const TEventSchemas extends EventSchemas
14
13
  >({
15
14
  context,
16
15
  events,
17
16
  }: {
17
+ /**
18
+ * The JSON schema for the context object.
19
+ *
20
+ * Must be of `{ type: 'object' }`.
21
+ */
18
22
  context: TContextSchema;
23
+ /**
24
+ * An object mapping event types to each event object's JSON Schema.
25
+ */
19
26
  events: TEventSchemas;
20
27
  }): {
21
- context: ConvertContextToJSONSchema<TContextSchema>;
28
+ context: TContextSchema;
22
29
  events: ConvertToJSONSchemas<TEventSchemas>;
23
30
  types: {
24
- context: FromSchema<ConvertContextToJSONSchema<TContextSchema>>;
31
+ context: FromSchema<TContextSchema>;
25
32
  events: FromSchema<Values<ConvertToJSONSchemas<TEventSchemas>>>;
26
33
  };
27
34
  } {
28
35
  return {
29
- context: {
30
- type: 'object',
31
- properties: context,
32
- additionalProperties: false,
33
- required: Object.keys(context),
34
- },
36
+ context,
35
37
  events: createEventSchemas(events),
36
38
  types: {} as any,
37
39
  };
package/src/types.ts ADDED
@@ -0,0 +1,61 @@
1
+ import OpenAI from 'openai';
2
+ import {
3
+ ChatCompletionCreateParamsNonStreaming,
4
+ ChatCompletionCreateParamsStreaming,
5
+ } from 'openai/resources';
6
+ import {
7
+ AnyEventObject,
8
+ ObservableActorLogic,
9
+ PromiseActorLogic,
10
+ } from 'xstate';
11
+
12
+ export interface StatelyAgentAdapter {
13
+ model: string;
14
+ /**
15
+ * Creates actor logic that chooses an event from all of the
16
+ * possible next events of the parent state machine
17
+ * and sends it to the parent actor.
18
+ */
19
+ fromEvent: <TInput>(
20
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
21
+ ) => PromiseActorLogic<AnyEventObject[] | undefined, TInput>;
22
+ /**
23
+ * Creates actor logic that resolves with a chat completion.
24
+ */
25
+ fromChat: <TInput>(
26
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
27
+ ) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
28
+ /**
29
+ * Creates actor logic that emits a chat completion stream.
30
+ */
31
+ fromChatStream: <TInput>(
32
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming
33
+ ) => ObservableActorLogic<
34
+ OpenAI.Chat.Completions.ChatCompletionChunk,
35
+ TInput
36
+ >;
37
+ /**
38
+ * Creates actor logic that chooses a tool from the provided
39
+ * tools and runs that tool.
40
+ */
41
+ fromTool: <TInput>(
42
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming,
43
+ tools: {
44
+ [key: string]: Tool<any, any>;
45
+ }
46
+ ) => PromiseActorLogic<
47
+ | {
48
+ result: any;
49
+ tool: string;
50
+ toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall;
51
+ }
52
+ | undefined,
53
+ TInput
54
+ >;
55
+ }
56
+
57
+ export interface Tool<TInput, TOutput> {
58
+ description: string;
59
+ inputSchema: any;
60
+ run: (input: TInput) => TOutput;
61
+ }
package/src/utils.ts CHANGED
@@ -20,26 +20,17 @@ export type EventSchemas = {
20
20
  };
21
21
  };
22
22
 
23
- export interface ContextSchema {
24
- [key: string]: JSONSchema7;
25
- }
23
+ export type ContextSchema = JSONSchema7 & { type: 'object' };
26
24
 
27
25
  export type ConvertToJSONSchemas<T> = {
28
26
  [K in keyof T]: {
29
- properties: { type: { const: K } };
27
+ properties: { type: { const: K } } & Prop<T[K], 'properties'>;
30
28
  type: 'object';
31
- required: Array<keyof Prop<T[K], 'properties'> | 'type'>;
29
+ required: Array<(keyof Prop<T[K], 'properties'> & string) | 'type'>;
32
30
  additionalProperties: false;
33
- } & T[K];
31
+ };
34
32
  } & {};
35
33
 
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
34
  export function createEventSchemas<T extends EventSchemas>(
44
35
  eventSchemaMap: T
45
36
  ): ConvertToJSONSchemas<T> {
@@ -0,0 +1,9 @@
1
+ // vitest.config.ts
2
+ import dotenv from 'dotenv';
3
+ dotenv.config();
4
+
5
+ export default {
6
+ test: {
7
+ testTimeout: 10000, // Global timeout of 10000ms for all tests
8
+ },
9
+ };