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