@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,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/changelog-github", { "repo": "statelyai/agent" }],
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "restricted",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
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 }}
@@ -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,13 @@
1
+ # @statelyai/agent
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
+
9
+ ## 0.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 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,47 @@ 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, agentSettings: CreateAgentOutput<any>, 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, agentSettings: CreateAgentOutput<any>, 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, 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>;
66
+ interface CreateAgentOutput<T extends {
67
+ model: ChatCompletionCreateParamsBase['model'];
68
+ context: ContextSchema;
69
+ events: EventSchemas;
70
+ }> {
71
+ model: T['model'];
72
+ schemas: T;
73
+ types: {
74
+ context: FromSchema<ConvertContextToJSONSchema<T['context']>>;
75
+ events: FromSchema<Values<ConvertToJSONSchemas<T['events']>>>;
76
+ };
77
+ fromEvent: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined, TInput>;
78
+ fromEventChoice: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<FromSchema<Values<ConvertToJSONSchemas<T['events']>>>[] | undefined, TInput>;
79
+ fromChatCompletion: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsNonStreaming) => PromiseActorLogic<OpenAI.Chat.Completions.ChatCompletion, TInput>;
80
+ fromChatCompletionStream: <TInput>(inputFn: (input: TInput) => string | ChatCompletionCreateParamsStreaming) => ObservableActorLogic<OpenAI.Chat.Completions.ChatCompletionChunk, TInput>;
81
+ }
82
+ declare function createAgent<T extends {
83
+ model: ChatCompletionCreateParamsBase['model'];
84
+ context: ContextSchema;
85
+ events: EventSchemas;
86
+ }>(openai: OpenAI, settings: T): CreateAgentOutput<T>;
27
87
 
28
- export { fromChatCompletion, fromChatCompletionStream, fromEventChoice };
88
+ 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,25 +36,61 @@ 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
- function fromChatCompletion(openai, inputFn) {
59
+ function fromChatCompletion(openai, agentSettings, 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: agentSettings.model,
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
  );
48
76
  }
49
- function fromChatCompletionStream(openai, inputFn) {
77
+ function fromChatCompletionStream(openai, agentSettings, inputFn) {
50
78
  return (0, import_xstate.fromObservable)(
51
79
  ({ input }) => {
52
80
  const observers = /* @__PURE__ */ new Set();
53
81
  (async () => {
54
82
  const openAiInput = inputFn(input);
83
+ const resolvedParams = typeof openAiInput === "string" ? {
84
+ model: agentSettings.model,
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,9 +113,9 @@ function fromChatCompletionStream(openai, inputFn) {
76
113
  }
77
114
  );
78
115
  }
79
- function fromEventChoice(openai, inputFn) {
116
+ function fromEventChoice(openai, agentSettings, inputFn, options) {
80
117
  return (0, import_xstate.fromPromise)(
81
- async ({ input, self }) => {
118
+ async ({ input, self, system }) => {
82
119
  const transitions = getAllTransitions(self._parent.getSnapshot());
83
120
  const functionNameMapping = {};
84
121
  const tools = transitions.filter((t) => {
@@ -90,34 +127,72 @@ function fromEventChoice(openai, inputFn) {
90
127
  type: "function",
91
128
  function: {
92
129
  name,
93
- description: t.description,
130
+ description: t.description ?? agentSettings.schemas.events[t.eventType]?.description,
94
131
  parameters: {
95
132
  type: "object",
96
- properties: t.meta?.parameters ?? {}
133
+ properties: agentSettings.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: agentSettings.model,
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;
107
153
  if (toolCalls) {
108
- return toolCalls.map((tc) => {
154
+ const events = toolCalls.map((tc) => {
109
155
  return {
110
156
  type: functionNameMapping[tc.function.name],
111
157
  ...JSON.parse(tc.function.arguments)
112
158
  };
113
159
  });
160
+ if (options?.execute) {
161
+ events.forEach((event) => {
162
+ system._relay(self, self._parent, event);
163
+ });
164
+ }
114
165
  }
115
- return toolCalls ?? void 0;
166
+ return void 0;
116
167
  }
117
168
  );
118
169
  }
170
+ function createAgent(openai, settings) {
171
+ const agentSettings = {
172
+ model: settings.model,
173
+ schemas: {
174
+ context: {
175
+ type: "object",
176
+ properties: settings.context,
177
+ additionalProperties: false
178
+ },
179
+ events: createEventSchemas(settings.events)
180
+ },
181
+ types: {},
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)
190
+ };
191
+ return agentSettings;
192
+ }
119
193
  // Annotate the CommonJS export names for ESM import in node:
120
194
  0 && (module.exports = {
195
+ createAgent,
121
196
  fromChatCompletion,
122
197
  fromChatCompletionStream,
123
198
  fromEventChoice
@@ -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
+ }