@statelyai/agent
Version:
Stateful agents that make decisions based on finite-state machine models
179 lines (171 loc) • 4.29 kB
text/typescript
import { createAgent, fromDecision } from '../src';
import { assign, createActor, fromPromise, log, setup } from 'xstate';
import { getFromTerminal } from './helpers/helpers';
import { z } from 'zod';
import { openai } from '@ai-sdk/openai';
async function searchTavily(
input: string,
options: {
maxResults?: number;
apiKey: string;
}
) {
const body: Record<string, unknown> = {
query: input,
max_results: options.maxResults,
api_key: options.apiKey,
};
const response = await fetch('https://api.tavily.com/search', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
const json = await response.json();
if (!response.ok) {
throw new Error(
`Request failed with status code ${response.status}: ${json.error}`
);
}
if (!Array.isArray(json.results)) {
throw new Error(`Could not parse Tavily results. Please try again.`);
}
return JSON.stringify(json.results);
}
const getWeather = fromPromise(async ({ input }: { input: string }) => {
const results = await searchTavily(
`Get the weather for this location: ${input}`,
{
maxResults: 5,
apiKey: process.env.TAVILY_API_KEY!,
}
);
return results;
});
const agent = createAgent({
name: 'weather',
model: openai('gpt-4-1106-preview'),
events: {
'agent.getWeather': z.object({
location: z.string().describe('The location to get the weather for'),
}),
'agent.reportWeather': z.object({
location: z
.string()
.describe('The location the weather is being reported for'),
highF: z.number().describe('The high temperature today in Fahrenheit'),
lowF: z.number().describe('The low temperature today in Fahrenheit'),
summary: z.string().describe('A summary of the weather conditions'),
}),
'agent.doSomethingElse': z
.object({})
.describe(
'Do something else, because the user did not provide a location'
),
},
});
const machine = setup({
types: {
context: {} as {
location: string;
history: string[];
count: number;
},
events: agent.types.events,
},
actors: {
agent: fromDecision(agent),
getWeather,
getFromTerminal,
},
}).createMachine({
initial: 'getLocation',
context: {
location: '',
count: 0,
history: [],
},
states: {
getLocation: {
invoke: {
src: 'getFromTerminal',
input: 'Location?',
onDone: {
actions: assign({
location: ({ event }) => event.output,
}),
target: 'decide',
},
},
always: {
guard: ({ context }) => context.count >= 3,
target: 'stopped',
},
},
decide: {
entry: log('Deciding...'),
invoke: {
src: 'agent',
input: ({ context }) => ({
context: {
location: context.location,
},
goal: `Decide what to do based on the given location, which may or may not be a location`,
}),
},
on: {
'agent.getWeather': {
actions: log(({ event }) => event),
target: 'gettingWeather',
},
'agent.doSomethingElse': 'getLocation',
},
},
gettingWeather: {
entry: log('Getting weather...'),
invoke: {
src: 'getWeather',
input: ({ context }) => context.location,
onDone: {
actions: [
log(({ event }) => event.output),
assign({
count: ({ context }) => context.count + 1,
}),
],
target: 'reportWeather',
},
},
},
reportWeather: {
invoke: {
src: 'agent',
input: ({ context }) => ({
goal: 'Report the weather', // TODO
}),
},
on: {
'agent.reportWeather': {
actions: log(({ event }) => event),
target: 'getLocation',
},
},
},
stopped: {
entry: log('You have used up your search quota. Goodbye!'),
},
},
exit: () => {
process.exit();
},
});
const actor = createActor(machine, {
input: {
location: 'New York',
},
});
actor.subscribe((s) => {
console.log(s.value);
});
actor.start();