@statelyai/agent 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,26 +1,38 @@
1
1
  {
2
2
  "name": "@statelyai/agent",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
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/changelog-github": "^0.5.0",
13
+ "@changesets/cli": "^2.27.1",
18
14
  "@types/node": "^20.10.6",
15
+ "dotenv": "^16.3.1",
16
+ "json-schema-to-ts": "^3.0.0",
17
+ "openai": "^4.24.1",
18
+ "ts-node": "^10.9.2",
19
19
  "tsup": "^8.0.1",
20
20
  "typescript": "^5.3.3"
21
21
  },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
22
25
  "dependencies": {
23
- "openai": "^4.24.1",
24
- "xstate": "^5.3.1"
26
+ "xstate": "^5.5.1"
27
+ },
28
+ "packageManager": "pnpm@8.11.0",
29
+ "scripts": {
30
+ "build": "tsup src/index.ts --format cjs,esm --dts",
31
+ "lint": "tsc",
32
+ "test": "vitest run",
33
+ "example": "ts-node examples/helpers/runner.ts",
34
+ "changeset": "changeset",
35
+ "release": "changeset publish",
36
+ "version": "changeset version"
25
37
  }
26
- }
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/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
- import OpenAI from 'openai';
1
+ import type 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.
@@ -17,14 +34,27 @@ import { getAllTransitions } from './utils';
17
34
  */
18
35
  export function fromChatCompletion<TInput>(
19
36
  openai: OpenAI,
37
+ agentSettings: CreateAgentOutput<any>,
20
38
  inputFn: (
21
39
  input: TInput
22
- ) => OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
40
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
23
41
  ) {
24
42
  return fromPromise<OpenAI.Chat.Completions.ChatCompletion, TInput>(
25
43
  async ({ input }) => {
26
44
  const openAiInput = inputFn(input);
27
- const response = await openai.chat.completions.create(openAiInput);
45
+ const params: ChatCompletionCreateParamsNonStreaming =
46
+ typeof openAiInput === 'string'
47
+ ? {
48
+ model: agentSettings.model,
49
+ messages: [
50
+ {
51
+ role: 'user',
52
+ content: openAiInput,
53
+ },
54
+ ],
55
+ }
56
+ : openAiInput;
57
+ const response = await openai.chat.completions.create(params);
28
58
 
29
59
  return response;
30
60
  }
@@ -39,9 +69,10 @@ export function fromChatCompletion<TInput>(
39
69
  */
40
70
  export function fromChatCompletionStream<TInput>(
41
71
  openai: OpenAI,
72
+ agentSettings: CreateAgentOutput<any>,
42
73
  inputFn: (
43
74
  input: TInput
44
- ) => OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
75
+ ) => string | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
45
76
  ) {
46
77
  return fromObservable<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>(
47
78
  ({ input }) => {
@@ -49,8 +80,20 @@ export function fromChatCompletionStream<TInput>(
49
80
 
50
81
  (async () => {
51
82
  const openAiInput = inputFn(input);
83
+ const resolvedParams: ChatCompletionCreateParamsBase =
84
+ typeof openAiInput === 'string'
85
+ ? {
86
+ model: agentSettings.model,
87
+ messages: [
88
+ {
89
+ role: 'user',
90
+ content: openAiInput,
91
+ },
92
+ ],
93
+ }
94
+ : openAiInput;
52
95
  const stream = await openai.chat.completions.create({
53
- ...openAiInput,
96
+ ...resolvedParams,
54
97
  stream: true,
55
98
  });
56
99
 
@@ -85,12 +128,20 @@ export function fromChatCompletionStream<TInput>(
85
128
  */
86
129
  export function fromEventChoice<TInput>(
87
130
  openai: OpenAI,
131
+ agentSettings: CreateAgentOutput<any>,
88
132
  inputFn: (
89
133
  input: TInput
90
- ) => 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
+ }
91
142
  ) {
92
143
  return fromPromise<AnyEventObject[] | undefined, TInput>(
93
- async ({ input, self }) => {
144
+ async ({ input, self, system }) => {
94
145
  const transitions = getAllTransitions(self._parent!.getSnapshot());
95
146
  const functionNameMapping: Record<string, string> = {};
96
147
  const tools = transitions
@@ -104,32 +155,123 @@ export function fromEventChoice<TInput>(
104
155
  type: 'function',
105
156
  function: {
106
157
  name,
107
- description: t.description,
158
+ description:
159
+ t.description ??
160
+ agentSettings.schemas.events[t.eventType]?.description,
108
161
  parameters: {
109
162
  type: 'object',
110
- properties: t.meta?.parameters ?? {},
163
+ properties:
164
+ agentSettings.schemas.events[t.eventType]?.properties ?? {},
111
165
  },
112
166
  },
113
167
  } as const;
114
168
  });
169
+
115
170
  const openAiInput = inputFn(input);
171
+ const completionParams: ChatCompletionCreateParamsNonStreaming =
172
+ typeof openAiInput === 'string'
173
+ ? {
174
+ model: agentSettings.model,
175
+ messages: [
176
+ {
177
+ role: 'user',
178
+ content: openAiInput,
179
+ },
180
+ ],
181
+ }
182
+ : openAiInput;
116
183
  const completion = await openai.chat.completions.create({
117
- ...openAiInput,
184
+ ...completionParams,
118
185
  tools,
119
186
  });
120
187
 
121
188
  const toolCalls = completion.choices[0]?.message.tool_calls;
122
189
 
123
190
  if (toolCalls) {
124
- return toolCalls.map((tc) => {
191
+ const events = toolCalls.map((tc) => {
125
192
  return {
126
193
  type: functionNameMapping[tc.function.name],
127
194
  ...JSON.parse(tc.function.arguments),
128
195
  };
129
196
  });
197
+
198
+ if (options?.execute) {
199
+ events.forEach((event) => {
200
+ // @ts-ignore
201
+ system._relay(self, self._parent, event);
202
+ });
203
+ }
130
204
  }
131
205
 
132
- return toolCalls ?? undefined;
206
+ return undefined;
133
207
  }
134
208
  );
135
209
  }
210
+
211
+ interface CreateAgentOutput<
212
+ T extends {
213
+ model: ChatCompletionCreateParamsBase['model'];
214
+ context: ContextSchema;
215
+ events: EventSchemas;
216
+ }
217
+ > {
218
+ model: T['model'];
219
+ schemas: T;
220
+ types: {
221
+ context: FromSchema<ConvertContextToJSONSchema<T['context']>>;
222
+ events: FromSchema<Values<ConvertToJSONSchemas<T['events']>>>;
223
+ };
224
+ fromEvent: <TInput>(
225
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
226
+ ) => PromiseActorLogic<
227
+ FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined,
228
+ TInput
229
+ >;
230
+ fromEventChoice: <TInput>(
231
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
232
+ ) => PromiseActorLogic<
233
+ FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined,
234
+ TInput
235
+ >;
236
+ fromChatCompletion: <TInput>(
237
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming
238
+ ) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
239
+ fromChatCompletionStream: <TInput>(
240
+ inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming
241
+ ) => ObservableActorLogic<
242
+ OpenAI.Chat.Completions.ChatCompletionChunk,
243
+ TInput
244
+ >;
245
+ }
246
+
247
+ export function createAgent<
248
+ T extends {
249
+ model: ChatCompletionCreateParamsBase['model'];
250
+ context: ContextSchema;
251
+ events: EventSchemas;
252
+ }
253
+ >(openai: OpenAI, settings: T): CreateAgentOutput<T> {
254
+ const agentSettings: CreateAgentOutput<T> = {
255
+ model: settings.model,
256
+ schemas: {
257
+ context: {
258
+ type: 'object',
259
+ properties: settings.context,
260
+ additionalProperties: false,
261
+ },
262
+ events: createEventSchemas(settings.events),
263
+ } as any,
264
+ types: {} as any,
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),
272
+ fromChatCompletionStream: (input) =>
273
+ fromChatCompletionStream(openai, agentSettings, input),
274
+ };
275
+
276
+ return agentSettings as any;
277
+ }
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>>;
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
- };