From 289a960543f719e94365faed640b63e44f31de39 Mon Sep 17 00:00:00 2001 From: Piotr Karwatka Date: Sat, 7 Dec 2024 23:11:22 +0100 Subject: [PATCH 01/19] [feat] server example --- bun.lockb | Bin 449124 -> 465196 bytes example/package.json | 3 +- example/src/medical_survey/workflow_server.ts | 46 +++++++++++++ example/src/medical_survey_server.ts | 65 ++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 example/src/medical_survey/workflow_server.ts create mode 100644 example/src/medical_survey_server.ts diff --git a/bun.lockb b/bun.lockb index be0aa661fa5a8cae6cf93ea2bcfa64b31123b854..40524cdd4a8176be54c9e67e1011396bda0745fd 100755 GIT binary patch delta 20345 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 Ljvjq3vHSl4_UsMH delta 11093 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 B6OaG^ diff --git a/example/package.json b/example/package.json index 3ac7cef..b116c47 100644 --- a/example/package.json +++ b/example/package.json @@ -5,6 +5,7 @@ "type": "module", "dependencies": { "@dead-simple-ai-agent/framework": "0.0.1", - "@langchain/community": "^0.3.17" + "@langchain/community": "^0.3.17", + "fastify": "^5.1.0" } } diff --git a/example/src/medical_survey/workflow_server.ts b/example/src/medical_survey/workflow_server.ts new file mode 100644 index 0000000..5e80176 --- /dev/null +++ b/example/src/medical_survey/workflow_server.ts @@ -0,0 +1,46 @@ +import { agent } from '@dead-simple-ai-agent/framework/agent' +import { workflow } from '@dead-simple-ai-agent/framework/workflow' + +const nurse = agent({ + role: 'Nurse,doctor assistant', + description: ` + You are skille nurse / doctor assistant. + You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit. + Ask user questions about the patient's health and symptoms. + Ask one question at time up to 5 questions. + Asynchronously Wait for the response - which will be next message addedto the workflow. + `, +}) + +const reporter = agent({ + role: 'Reporter', + description: ` + You are skilled at preparing great looking markdown reports. + Prepare a report for a patient that is about to come for a visit. + Add info about the patient's health and symptoms. + `, + tools: {}, +}) + +export const preVisitNoteWorkflow = workflow({ + members: [nurse, reporter], + description: ` + Create a pre-visit note for a patient that is about to come for a visit. + The note should include the patient's health and symptoms. + + Include: + - symptoms, + - health issues, + - medications, + - allergies, + - surgeries + + Never ask fo: + - personal data, + - sensitive data, + - any data that can be used to identify the patient. + `, + output: ` + A markdown report for the patient's pre-visit note. + `, +}) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts new file mode 100644 index 0000000..5942eca --- /dev/null +++ b/example/src/medical_survey_server.ts @@ -0,0 +1,65 @@ +/** + * Example borrowed from CrewAI. + */ + +import { iterate } from '@dead-simple-ai-agent/framework/teamwork' +import { workflowState } from '@dead-simple-ai-agent/framework/workflow' +import fastify, { FastifyReply, FastifyRequest } from 'fastify' +import { promises as fs } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' + +const server = fastify({ logger: false }) + +import { preVisitNoteWorkflow } from './medical_survey/workflow_server.js' + +const dbPath = (id: string) => join(tmpdir(), id + '_workflow_db.json') + +let state = workflowState(preVisitNoteWorkflow) + +server.get('/start', async () => { + const nextState = await iterate(preVisitNoteWorkflow, state) + + await fs.writeFile(dbPath(nextState.id), JSON.stringify(nextState, null, 2), 'utf-8') + + return { + status: 'running', + state: nextState, + } +}) + +server.post( + '/iterate/:id', + async (req: FastifyRequest<{ Params: { id: string }; Body: { message: string } }>) => { + const { id } = req.params + const { message } = req.body + + const path = dbPath(id) + + if (await fs.exists(path)) { + try { + state = JSON.parse(await fs.readFile(path, 'utf-8')) + console.log('πŸ›Ÿ Loaded workflow from', path) + } catch (error) { + console.log(`🚨Error while loading workflow from ${path}. Starting new workflow.`) + } + } + + if (message) { + // message provided within the call - for example a return call from API/Slack/Whatever + state.messages.push({ role: 'user', content: message }) + } + + const nextState = await iterate(preVisitNoteWorkflow, state) + await fs.writeFile(path, JSON.stringify(nextState, null, 2), 'utf-8') + + return nextState + } +) + +const port = parseInt(process.env['PORT'] || '3000', 10) +server.listen({ + port, +}) +console.log(`πŸš€ Server running at http://localhost:${port}`) +console.log(` Server running at http://localhost:${port}`) From f857a82fa1f59b44363ffa44ca2a92c6f8e5d274 Mon Sep 17 00:00:00 2001 From: Piotr Karwatka Date: Sat, 7 Dec 2024 23:13:26 +0100 Subject: [PATCH 02/19] [fix] docs --- example/src/medical_survey_server.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index 5942eca..a61a266 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -62,4 +62,7 @@ server.listen({ port, }) console.log(`πŸš€ Server running at http://localhost:${port}`) -console.log(` Server running at http://localhost:${port}`) +console.log(`Run 'curl -X POST http://localhost:${port}/start' to start the workflow`) +console.log( + `Run 'curl -X POST http://localhost:${port}/iterate/ID -d '{"message":"Hello"}' to iterate the workflow with the message provided optionally as an answer added to the state` +) From 58fb181d94d66872332ac80979b13071c7c056b2 Mon Sep 17 00:00:00 2001 From: Piotr Karwatka Date: Sat, 7 Dec 2024 23:17:46 +0100 Subject: [PATCH 03/19] fixes --- example/src/medical_survey_server.ts | 2 +- packages/framework/src/executor.ts | 19 ++++++++++++++++++- packages/framework/src/teamwork.ts | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index a61a266..66f385e 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -17,7 +17,7 @@ const dbPath = (id: string) => join(tmpdir(), id + '_workflow_db.json') let state = workflowState(preVisitNoteWorkflow) -server.get('/start', async () => { +server.post('/start', async () => { const nextState = await iterate(preVisitNoteWorkflow, state) await fs.writeFile(dbPath(nextState.id), JSON.stringify(nextState, null, 2), 'utf-8') diff --git a/packages/framework/src/executor.ts b/packages/framework/src/executor.ts index cb7a86e..93d3c73 100644 --- a/packages/framework/src/executor.ts +++ b/packages/framework/src/executor.ts @@ -69,7 +69,24 @@ export async function executeTaskWithAgent( ...messages, ], tools: tools.length > 0 ? tools : undefined, - response_format: taskResponseFormat, + response_format: zodResponseFormat( + z.object({ + response: z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('step'), + name: z.string().describe('The name of the step'), + result: z.string().describe('The result of the step'), + reasoning: z.string().describe('The reasoning for this step'), + }), + z.object({ + kind: z.literal('complete'), + result: z.string().describe('The final result of the task'), + reasoning: z.string().describe('The reasoning for completing the task'), + }), + ]), + }), + 'task_result' + ), }) if (response.choices[0].message.tool_calls.length > 0) { const toolResults = await Promise.all( diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index ffc8a4d..fadb11b 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -86,8 +86,13 @@ export async function teamwork( } if (status === 'interrupted') { +<<<<<<< HEAD + return await finalizeQuery(workflow, messages) + // return '🚨 Workflow interrupted due to max iterations limit.' +======= console.log('🚨 Max iterations exceeded ', workflow.maxIterations) return finalizeQuery(workflow, messages) +>>>>>>> 6abee0bdd5c5dafed0c3f5896fa408e9192e8908 } // tbd: recover from errors From 00c306381c4c85432b9feef3f1b1b8bfaa77180f Mon Sep 17 00:00:00 2001 From: Piotr Karwatka Date: Sat, 7 Dec 2024 23:41:33 +0100 Subject: [PATCH 04/19] fixes --- example/src/medical_survey/workflow_server.ts | 3 +- src/index.ts | 379 ++++++++++++++++++ 2 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 src/index.ts diff --git a/example/src/medical_survey/workflow_server.ts b/example/src/medical_survey/workflow_server.ts index 5e80176..f37f241 100644 --- a/example/src/medical_survey/workflow_server.ts +++ b/example/src/medical_survey/workflow_server.ts @@ -6,9 +6,8 @@ const nurse = agent({ description: ` You are skille nurse / doctor assistant. You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit. - Ask user questions about the patient's health and symptoms. Ask one question at time up to 5 questions. - Asynchronously Wait for the response - which will be next message addedto the workflow. + Wait until you get the answer. `, }) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b02052f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,379 @@ +import s from 'dedent' +import OpenAI from 'openai' +import { zodFunction, zodResponseFormat } from 'openai/helpers/zod' +import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions' +import { z } from 'zod' + +// tbd: abstract this away or not? most APIs are OpenAI compatible +const openai = new OpenAI() + +interface Protocol { + requestUserInput(prompt: string): Promise +} + +// tbd: we should replace this with a "HumanInTheLoop" agent of CLI type +// to do so, we need to implement delegation across different agents +// so they can work collaboratively on smaller tasks too +class CLIProtocol implements Protocol { + async requestUserInput(prompt: string): Promise { + return new Promise((resolve) => { + console.log(prompt) + process.stdin.once('data', (data) => { + resolve(data.toString().trim()) + }) + }) + } +} + +type ToolDefinition> = { + name: string + description: string + parameters: T + execute: (parameters: z.infer) => Promise +} + +interface AgentConfig { + role: string + description: string + tools?: ToolDefinition[] + model?: string + protocol?: Protocol +} + +// tbd: implement short-term and long-term memory with different storage models +export class Agent { + role: string + + private description: string + private tools: ToolDefinition[] + private model: string + private protocol: Protocol + + constructor({ + role, + description, + tools = [], + model = 'gpt-4o', + protocol = new CLIProtocol(), + }: AgentConfig) { + this.role = role + this.description = description + this.tools = tools + this.model = model + this.protocol = protocol + } + + async executeTask(messages: Message[], agents: Agent[]): Promise { + // tbd: after we implememt this, it keeps delegating + // const tools = [ + // { + // name: 'ask_a_question', + // description: s` + // Ask a specific question to one of the following agents: + // + // ${agents.map((agent, index) => `${agent.role}`)} + // + // `, + // parameters: z.object({ + // agent: z.number().describe('The index of the agent to ask the question'), + // question: z.string().describe('The question you want the agent to answer'), + // context: z.string().describe('All context necessary to execute the task'), + // }), + // function: async ({ agent, question, context }) => { + // console.log('ask_a_question', agent, question, context) + // const selectedAgent = agents[agent] + // if (!selectedAgent) { + // throw new Error('Invalid agent') + // } + // return selectedAgent.executeTask( + // [ + // { + // role: 'user', + // content: context, + // }, + // { + // role: 'user', + // content: question, + // }, + // ], + // agents + // ) + // }, + // }, + // ] + const response = await openai.beta.chat.completions.parse({ + model: this.model, + // tbd: verify the prompt + messages: [ + { + role: 'system', + content: s` + You are ${this.role}. ${this.description} + + Your job is to complete the assigned task. + 1. You can break down the task into steps + 2. You can use available tools when needed + + First try to complete the task on your own. + Only ask question to the user if you cannot complete the task without their input. + `, + }, + ...messages, + ], + // tbd: add other tools + // tbd: should we include agent description in the prompt too? we need to list responsibilities + // but keep context window in mind + // tools: tools.map(zodFunction), + response_format: zodResponseFormat( + z.object({ + response: z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('step'), + name: z.string().describe('The name of the step'), + result: z.string().describe('The result of the step'), + reasoning: z.string().describe('The reasoning for this step'), + }), + z.object({ + kind: z.literal('complete'), + result: z.string().describe('The final result of the task'), + reasoning: z.string().describe('The reasoning for completing the task'), + }), + ]), + }), + 'task_result' + ), + }) + // if (response.choices[0].message.tool_calls.length > 0) { + // const toolResults = await Promise.all( + // response.choices[0].message.tool_calls.map(async (toolCall) => { + // if (toolCall.type !== 'function') { + // throw new Error('Tool call is not a function') + // } + + // const tool = tools.find((t) => t.name === toolCall.function.name) + // if (!tool) { + // throw new Error(`Unknown tool: ${toolCall.function.name}`) + // } + // console.log('tool call', toolCall) + // const parameters = tool.parameters.parse(toolCall.function.arguments) + + // const content = await tool.function(parameters) + + // return { + // role: 'tool' as const, + // tool_call_id: toolCall.id, + // content: JSON.stringify(content), + // } + // }) + // ) + + // return this.executeTask([...messages, response.choices[0].message, ...toolResults], agents) + // } + + // tbd: verify shape of response + const result = response.choices[0].message.parsed + if (!result) { + throw new Error('No parsed response received') + } + + if (result.response.kind === 'step') { + console.log('πŸš€ Step:', result.response.name) + return this.executeTask( + [ + ...messages, + { + role: 'assistant', + content: result.response.result, + }, + ], + agents + ) + } + + if (result.response.kind === 'complete') { + return result.response.result + } + + // tbd: check if this is reachable + throw new Error('Illegal state') + } + + async requestUserInput(prompt: string): Promise { + return this.protocol.requestUserInput(prompt) + } + + toString(): string { + return s` + Agent role: "${this.role}" + Expertise: "${this.description}" + ` + } +} +export class Team { + async execute(workflow: Workflow): Promise { + const messages: Message[] = [ + { + role: 'assistant', + content: s` + Here is description of the workflow and expected output by the user: + ${workflow.description} + ${workflow.output} + `, + }, + ] + + // tbd: set reasonable max iterations + // eslint-disable-next-line no-constant-condition + while (true) { + const task = await getNextTask(messages) + if (!task) { + return messages.at(-1)!.content as string + } + + console.log('πŸš€ Next task:', task) + + messages.push({ + role: 'user', + content: task, + }) + + // tbd: this throws, handle it + const selectedAgent = await selectAgent(task, workflow.members) + console.log('πŸš€ Selected agent:', selectedAgent.role) + + // tbd: this should just be a try/catch + // tbd: do not return string, but more information or keep memory in agent + try { + const result = await selectedAgent.executeTask(messages, workflow.members) + messages.push({ + role: 'assistant', + content: result, + }) + } catch (error) { + console.log('πŸš€ Task error:', error) + messages.push({ + role: 'assistant', + content: error instanceof Error ? error.message : 'Unknown error', + }) + } + } + } +} + +type Workflow = { + description: string + output: string + members: Agent[] +} + +async function selectAgent(task: string, agents: Agent[]): Promise { + const response = await openai.beta.chat.completions.parse({ + model: 'gpt-4o', + messages: [ + { + role: 'system', + content: s` + You are an agent selector that matches tasks to the most capable agent. + Analyze the task requirements and each agent's capabilities to select the best match. + + Consider: + 1. Required tools and skills + 2. Agent's specialization + 3. Model capabilities + 4. Previous task context if available + `, + }, + { + role: 'user', + content: s` + Here is the task: + ${task} + + Here are the available agents: + + ${agents.map((agent, index) => `${agent}`)} + + + Select the most suitable agent for this task. + `, + }, + ], + temperature: 0.1, + response_format: zodResponseFormat( + z.object({ + agentIndex: z.number(), + reasoning: z.string(), + }), + 'agent_selection' + ), + }) + + const content = response.choices[0].message.parsed + if (!content) { + throw new Error('No content in response') + } + + const agent = agents[content.agentIndex] + if (!agent) { + throw new Error('Invalid agent') + } + + return agent +} + +async function getNextTask(history: Message[]): Promise { + const response = await openai.beta.chat.completions.parse({ + model: 'gpt-4o', + messages: [ + { + role: 'system', + // tbd: handle subsequent failures + content: s` + You are a planner that breaks down complex workflows into smaller, actionable steps. + Your job is to determine the next task that needs to be done based on the original workflow and what has been completed so far. + If all required tasks are completed, return null. + + Rules: + 1. Each task should be self-contained and achievable + 2. Tasks should be specific and actionable + 3. Return null when the workflow is complete + 4. Consider dependencies and order of operations + 5. Use context from completed tasks to inform next steps + `, + }, + ...history, + { + role: 'user', + content: 'What is the next task that needs to be done?', + }, + ], + temperature: 0.2, + response_format: zodResponseFormat( + z.object({ + task: z + .string() + .describe('The next task to be completed or null if the workflow is complete') + .nullable(), + reasoning: z + .string() + .describe('The reasoning for selecting the next task or why the workflow is complete'), + }), + 'next_task' + ), + }) + + try { + const content = response.choices[0].message.parsed + if (!content) { + throw new Error('No content in response') + } + + if (!content.task) { + return null + } + + return content.task + } catch (error) { + throw new Error('Failed to determine next task') + } +} From 0af91d5812deb51bcb9b122f52fc6707b41402f2 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 16:06:52 +0100 Subject: [PATCH 05/19] save work --- bun.lockb | Bin 449124 -> 453036 bytes packages/framework/src/supervisor/nextTask.ts | 3 +- .../{executor.ts => supervisor/runAgent.ts} | 48 ++--- .../framework/src/supervisor/selectAgent.ts | 5 +- packages/framework/src/teamwork.ts | 185 +++++++++++------- packages/framework/src/workflow.ts | 4 +- website/package.json | 1 + 7 files changed, 133 insertions(+), 113 deletions(-) rename packages/framework/src/{executor.ts => supervisor/runAgent.ts} (76%) diff --git a/bun.lockb b/bun.lockb index be0aa661fa5a8cae6cf93ea2bcfa64b31123b854..c2d8aedefbffde8ab89d73e6542cd49cba3b3685 100755 GIT binary patch delta 35712 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 diff --git a/packages/framework/src/supervisor/nextTask.ts b/packages/framework/src/supervisor/nextTask.ts index 3359cf6..02ceee7 100644 --- a/packages/framework/src/supervisor/nextTask.ts +++ b/packages/framework/src/supervisor/nextTask.ts @@ -5,12 +5,13 @@ import { z } from 'zod' import { Provider } from '../models/openai.js' import { Message } from '../types.js' -export async function getNextTask(provider: Provider, history: Message[]): Promise { +export async function nextTask(provider: Provider, history: Message[]): Promise { const response = await provider.completions({ messages: [ { role: 'system', // tbd: handle subsequent failures + // tbd: include max iterations in system prompt content: s` You are a planner that breaks down complex workflows into smaller, actionable steps. Your job is to determine the next task that needs to be done based on the original workflow and what has been completed so far. diff --git a/packages/framework/src/executor.ts b/packages/framework/src/supervisor/runAgent.ts similarity index 76% rename from packages/framework/src/executor.ts rename to packages/framework/src/supervisor/runAgent.ts index ee51e50..bd47f71 100644 --- a/packages/framework/src/executor.ts +++ b/packages/framework/src/supervisor/runAgent.ts @@ -2,14 +2,13 @@ import s from 'dedent' import { zodFunction, zodResponseFormat } from 'openai/helpers/zod' import { z } from 'zod' -import { Agent } from './agent.js' -import { Message } from './types.js' +import { Agent } from '../agent.js' +import { Message } from '../types.js' -export async function executeTaskWithAgent( +export async function runAgent( agent: Agent, - messages: Message[], - team: Agent[] -): Promise { + messages: Message[] +): Promise<[Message[], 'step' | 'complete']> { const tools = agent.tools ? Object.entries(agent.tools).map(([name, tool]) => zodFunction({ @@ -21,7 +20,6 @@ export async function executeTaskWithAgent( : [] const response = await agent.provider.completions({ - // tbd: verify the prompt messages: [ { role: 'system', @@ -82,11 +80,7 @@ export async function executeTaskWithAgent( }) ) - return executeTaskWithAgent( - agent, - [...messages, response.choices[0].message, ...toolResults], - team - ) + return [[response.choices[0].message, ...toolResults], 'step'] } // tbd: verify shape of response @@ -95,25 +89,13 @@ export async function executeTaskWithAgent( throw new Error('No parsed response received') } - if (result.response.kind === 'step') { - console.log('πŸš€ Step:', result.response.name) - return executeTaskWithAgent( - agent, - [ - ...messages, - { - role: 'assistant', - content: result.response.result, - }, - ], - team - ) - } - - if (result.response.kind === 'complete') { - return result.response.result - } - - // tbd: check if this is reachable - throw new Error('Illegal state') + return [ + [ + { + role: 'assistant', + content: result.response.result, + }, + ], + result.response.kind, + ] } diff --git a/packages/framework/src/supervisor/selectAgent.ts b/packages/framework/src/supervisor/selectAgent.ts index b7f5cb1..ad466a1 100644 --- a/packages/framework/src/supervisor/selectAgent.ts +++ b/packages/framework/src/supervisor/selectAgent.ts @@ -4,10 +4,11 @@ import { z } from 'zod' import { Agent } from '../agent.js' import { Provider } from '../models/openai.js' +import { Message } from '../types.js' export async function selectAgent( provider: Provider, - task: string, + agentRequest: Message[], agents: Agent[] ): Promise { const response = await provider.completions({ @@ -29,7 +30,7 @@ export async function selectAgent( role: 'user', content: s` Here is the task: - ${task} + ${agentRequest.map((request) => request.content).join(',')} Here are the available agents: diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index 31de1c4..a480f87 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -1,113 +1,146 @@ -import { executeTaskWithAgent } from './executor.js' import { finalizeWorkflow } from './supervisor/finalizeWorkflow.js' -import { getNextTask } from './supervisor/nextTask.js' +import { nextTask } from './supervisor/nextTask.js' +import { runAgent } from './supervisor/runAgent.js' import { selectAgent } from './supervisor/selectAgent.js' -import { Message, MessageContent } from './types.js' +import { MessageContent } from './types.js' import { Workflow, WorkflowState, workflowState } from './workflow.js' +/** + * Performs single iteration over Workflow and produces its next state. + */ export async function iterate(workflow: Workflow, state: WorkflowState): Promise { - const { provider, members, telemetry } = workflow - const { messages } = state - - telemetry.record({ - type: 'workflow.iteration.start', - data: { - workflow, - state, - }, - }) + const { status, messages } = state - const task = await getNextTask(provider, messages) - if (!task) { + /** + * When number of messages exceedes number of maximum iterations, we must force finish the workflow + * and produce best final answer + */ + if (messages.length > workflow.maxIterations) { + const content = await finalizeWorkflow(workflow.provider, messages) return { ...state, - messages, status: 'finished', + messages: state.messages.concat({ + role: 'user', + content, + }), } } - telemetry.record({ - type: 'workflow.iteration.nextTask', - data: { - workflow, - task, - }, - }) + /** + * When workflow is idle, we must get next task to work on, or finish the workflow otherwise. + */ + if (status === 'idle') { + const task = await nextTask(workflow.provider, messages) + if (task) { + return { + ...state, + status: 'pending', + agentRequest: [ + { + role: 'user', + content: task, + }, + ], + } + } else { + return { + ...state, + status: 'finished', + } + } + } - if (messages.length > workflow.maxIterations) { + /** + * When workflow is pending, we must find best agent to work on it. + */ + if (status === 'pending') { + const selectedAgent = await selectAgent( + workflow.provider, + state.agentRequest!, + workflow.members + ) return { ...state, - messages, - status: 'interrupted', + status: 'running', + agent: selectedAgent.role, } } - // tbd: get rid of console.logs, use telemetry instead - console.log('πŸš€ Next task:', task) - - const selectedAgent = await selectAgent(provider, task, members) - console.log('πŸš€ Selected agent:', selectedAgent.role) - - const agentRequest: Message[] = [ - ...messages, - { - role: 'user', - content: task, - }, - ] - - try { - const result = await executeTaskWithAgent(selectedAgent, agentRequest, members) - return { - ...state, - messages: [ - ...agentRequest, - { + /** + * When workflow is running, we must call assigned agent to continue working on it. + */ + if (status === 'running') { + const agent = workflow.members.find((member) => member.role === state.agent) + if (!agent) { + return { + ...state, + status: 'failed', + messages: state.messages.concat({ role: 'assistant', - content: result, - }, - ], - status: 'running', + content: 'No agent found.', + }), + } } - } catch (error) { - return { - ...state, - messages: [ - ...agentRequest, - { + /** + * When agent finishes running, it will return status to indicate whether it finished processing. + * + * If it finished processing, we will append its final answer to the context. Otherwise, we will + * further extend agentRequest to carry context over to the next iteration. + */ + try { + const [agentResponse, status] = await runAgent(agent, state.agentRequest!) + if (status === 'complete') { + const agentFinalAnswer = agentResponse.at(-1)! + return { + ...state, + status: 'idle', + messages: state.messages.concat(agentFinalAnswer), + } + } + return { + ...state, + status: 'running', + agentRequest: state.agentRequest?.concat(agentResponse), + } + } catch (error) { + return { + ...state, + status: 'failed', + messages: state.messages.concat({ role: 'assistant', content: error instanceof Error ? error.message : 'Unknown error', - }, - ], - status: 'failed', + }), + } } } + + /** + * When workflow fails due to unexpected error, we must attempt recovering or finish the workflow + * otherwise. + */ + if (status === 'failed') { + return { + ...state, + status: 'finished', + } + } + + return state } +/** + * Teamwork runs given workflow and continues iterating over the workflow until it finishes. + */ export async function teamwork( workflow: Workflow, state: WorkflowState = workflowState(workflow) ): Promise { const { status, messages } = state - if (status === 'pending' || status === 'running') { - return teamwork(workflow, await iterate(workflow, state)) - } - if (status === 'finished') { return messages.at(-1)!.content } - if (status === 'failed') { - return ('🚨' + messages.at(-1)!.content) as string - } - - if (status === 'interrupted') { - console.log('🚨 Max iterations exceeded ', workflow.maxIterations) - return finalizeWorkflow(workflow.provider, messages) - } - - // tbd: recover from errors - // tbd: request final answer if took too long - throw new Error('Workflow failed. This is not implemented yet.') + return teamwork(workflow, await iterate(workflow, state)) } diff --git a/packages/framework/src/workflow.ts b/packages/framework/src/workflow.ts index 806154f..8a7f523 100644 --- a/packages/framework/src/workflow.ts +++ b/packages/framework/src/workflow.ts @@ -33,8 +33,10 @@ export type Workflow = Required export type WorkflowState = { id: string - status: 'running' | 'finished' | 'interrupted' | 'failed' | 'pending' + status: 'idle' | 'running' | 'finished' | 'failed' | 'pending' messages: Message[] + agent?: string + agentRequest?: Message[] } /** diff --git a/website/package.json b/website/package.json index 2d24d77..bf1f041 100644 --- a/website/package.json +++ b/website/package.json @@ -11,6 +11,7 @@ "rspress": "^1.37.3" }, "devDependencies": { + "@rspress/plugin-typedoc": "^1.37.4", "@types/node": "^18.11.17" } } From 8a5a084b4126aa7cfcf5c490649585edc1f87fa0 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 16:36:26 +0100 Subject: [PATCH 06/19] feat: refactor tool calling --- packages/framework/src/supervisor/runAgent.ts | 29 ++--------- packages/framework/src/supervisor/runTools.ts | 50 ++++++++++++++++++ packages/framework/src/teamwork.ts | 51 ++++++++++--------- packages/framework/src/workflow.ts | 6 ++- 4 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 packages/framework/src/supervisor/runTools.ts diff --git a/packages/framework/src/supervisor/runAgent.ts b/packages/framework/src/supervisor/runAgent.ts index bd47f71..69be606 100644 --- a/packages/framework/src/supervisor/runAgent.ts +++ b/packages/framework/src/supervisor/runAgent.ts @@ -8,7 +8,7 @@ import { Message } from '../types.js' export async function runAgent( agent: Agent, messages: Message[] -): Promise<[Message[], 'step' | 'complete']> { +): Promise<[Message[], 'step' | 'complete' | 'tool']> { const tools = agent.tools ? Object.entries(agent.tools).map(([name, tool]) => zodFunction({ @@ -56,34 +56,11 @@ export async function runAgent( 'task_result' ), }) - if (response.choices[0].message.tool_calls.length > 0) { - const toolResults = await Promise.all( - response.choices[0].message.tool_calls.map(async (toolCall) => { - if (toolCall.type !== 'function') { - throw new Error('Tool call is not a function') - } - - const tool = agent.tools ? agent.tools[toolCall.function.name] : null - if (!tool) { - throw new Error(`Unknown tool: ${toolCall.function.name}`) - } - const content = await tool.execute(toolCall.function.parsed_arguments, { - provider: agent.provider, - messages, - }) - return { - role: 'tool' as const, - tool_call_id: toolCall.id, - content: JSON.stringify(content), - } - }) - ) - - return [[response.choices[0].message, ...toolResults], 'step'] + if (response.choices[0].message.tool_calls.length > 0) { + return [[response.choices[0].message], 'tool'] } - // tbd: verify shape of response const result = response.choices[0].message.parsed if (!result) { throw new Error('No parsed response received') diff --git a/packages/framework/src/supervisor/runTools.ts b/packages/framework/src/supervisor/runTools.ts new file mode 100644 index 0000000..bad2e1d --- /dev/null +++ b/packages/framework/src/supervisor/runTools.ts @@ -0,0 +1,50 @@ +import type { ParsedChatCompletionMessage } from 'openai/resources/beta/chat/completions.mjs' +import { ChatCompletionToolMessageParam } from 'openai/resources/index.mjs' + +import { Agent } from '../agent.js' +import { Message } from '../types.js' + +/** + * Asserts that given message requests tool calls + */ +function isToolCallRequest(message?: Message): message is ParsedChatCompletionMessage { + return message ? 'tool_calls' in message : false +} + +export async function runTools( + agent: Agent, + agentRequest: Message[] +): Promise { + // tbd: find cleaner way to do this + const messages = Array.from(agentRequest) + const toolCallRequest = messages.pop() + + if (!isToolCallRequest(toolCallRequest)) { + throw new Error('Invalid tool request') + } + + const toolResults = await Promise.all( + toolCallRequest.tool_calls.map(async (toolCall) => { + if (toolCall.type !== 'function') { + throw new Error('Tool call is not a function') + } + + const tool = agent.tools ? agent.tools[toolCall.function.name] : null + if (!tool) { + throw new Error(`Unknown tool: ${toolCall.function.name}`) + } + + const content = await tool.execute(toolCall.function.parsed_arguments, { + provider: agent.provider, + messages, + }) + return { + role: 'tool' as const, + tool_call_id: toolCall.id, + content: JSON.stringify(content), + } + }) + ) + + return toolResults +} diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index a480f87..253e56b 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -1,6 +1,7 @@ import { finalizeWorkflow } from './supervisor/finalizeWorkflow.js' import { nextTask } from './supervisor/nextTask.js' import { runAgent } from './supervisor/runAgent.js' +import { runTools } from './supervisor/runTools.js' import { selectAgent } from './supervisor/selectAgent.js' import { MessageContent } from './types.js' import { Workflow, WorkflowState, workflowState } from './workflow.js' @@ -62,7 +63,8 @@ export async function iterate(workflow: Workflow, state: WorkflowState): Promise ) return { ...state, - status: 'running', + status: 'assigned', + agentStatus: 'idle', agent: selectedAgent.role, } } @@ -70,7 +72,7 @@ export async function iterate(workflow: Workflow, state: WorkflowState): Promise /** * When workflow is running, we must call assigned agent to continue working on it. */ - if (status === 'running') { + if (status === 'assigned') { const agent = workflow.members.find((member) => member.role === state.agent) if (!agent) { return { @@ -82,37 +84,40 @@ export async function iterate(workflow: Workflow, state: WorkflowState): Promise }), } } + + /** + * When agentStatus is `tool`, an agent is waiting for the tools results. + * We must run all the tools in order to proceed to the next step. + */ + if (state.agentStatus === 'tool') { + const toolsResponse = await runTools(agent, state.agentRequest!) + return { + ...state, + agentStatus: 'step', + agentRequest: state.agentRequest?.concat(toolsResponse), + } + } + /** * When agent finishes running, it will return status to indicate whether it finished processing. * * If it finished processing, we will append its final answer to the context. Otherwise, we will * further extend agentRequest to carry context over to the next iteration. */ - try { - const [agentResponse, status] = await runAgent(agent, state.agentRequest!) - if (status === 'complete') { - const agentFinalAnswer = agentResponse.at(-1)! - return { - ...state, - status: 'idle', - messages: state.messages.concat(agentFinalAnswer), - } - } + const [agentResponse, status] = await runAgent(agent, state.agentRequest!) + if (status === 'complete') { + const agentFinalAnswer = agentResponse.at(-1)! return { ...state, - status: 'running', - agentRequest: state.agentRequest?.concat(agentResponse), - } - } catch (error) { - return { - ...state, - status: 'failed', - messages: state.messages.concat({ - role: 'assistant', - content: error instanceof Error ? error.message : 'Unknown error', - }), + status: 'idle', + messages: state.messages.concat(agentFinalAnswer), } } + return { + ...state, + agentStatus: status, + agentRequest: state.agentRequest?.concat(agentResponse), + } } /** diff --git a/packages/framework/src/workflow.ts b/packages/framework/src/workflow.ts index 8a7f523..0e95bf5 100644 --- a/packages/framework/src/workflow.ts +++ b/packages/framework/src/workflow.ts @@ -31,12 +31,16 @@ export const workflow = (options: WorkflowOptions): Workflow => { export type Workflow = Required +export type AgentStatus = 'idle' | 'step' | 'tool' + export type WorkflowState = { id: string - status: 'idle' | 'running' | 'finished' | 'failed' | 'pending' + status: 'idle' | 'assigned' | 'finished' | 'failed' | 'pending' messages: Message[] + agent?: string agentRequest?: Message[] + agentStatus?: AgentStatus } /** From 12063513b791ed89ae89f13723ccd8db53a68ab3 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 16:54:20 +0100 Subject: [PATCH 07/19] chore: updates --- packages/framework/src/teamwork.ts | 4 +-- packages/framework/src/workflow.ts | 42 +++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index 253e56b..679f813 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -76,7 +76,7 @@ export async function iterate(workflow: Workflow, state: WorkflowState): Promise const agent = workflow.members.find((member) => member.role === state.agent) if (!agent) { return { - ...state, + id: state.id, status: 'failed', messages: state.messages.concat({ role: 'assistant', @@ -116,7 +116,7 @@ export async function iterate(workflow: Workflow, state: WorkflowState): Promise return { ...state, agentStatus: status, - agentRequest: state.agentRequest?.concat(agentResponse), + agentRequest: state.agentRequest.concat(agentResponse), } } diff --git a/packages/framework/src/workflow.ts b/packages/framework/src/workflow.ts index 0e95bf5..78c9cb3 100644 --- a/packages/framework/src/workflow.ts +++ b/packages/framework/src/workflow.ts @@ -31,25 +31,49 @@ export const workflow = (options: WorkflowOptions): Workflow => { export type Workflow = Required -export type AgentStatus = 'idle' | 'step' | 'tool' - -export type WorkflowState = { +/** + * Base workflow + */ +type BaseWorkflowState = { id: string - status: 'idle' | 'assigned' | 'finished' | 'failed' | 'pending' messages: Message[] +} + +/** + * Different states workflow is in, in between execution from agents + */ +type IdleWorkflowState = BaseWorkflowState & { + status: 'idle' | 'finished' | 'failed' +} - agent?: string - agentRequest?: Message[] - agentStatus?: AgentStatus +/** + * Supervisor selected the task, and is now pending assignement of an agent + */ +type PendingWorkflowState = BaseWorkflowState & { + status: 'pending' + agentRequest: Message[] +} + +/** + * State in which an agent is assigned and work is pending + */ +type AssignedWorkflowState = BaseWorkflowState & { + status: 'assigned' + + agent: string + agentRequest: Message[] + agentStatus: 'idle' | 'step' | 'tool' } +export type WorkflowState = IdleWorkflowState | PendingWorkflowState | AssignedWorkflowState + /** * Helper utility to create a workflow state with defaults. */ -export const workflowState = (workflow: Workflow): WorkflowState => { +export const workflowState = (workflow: Workflow): IdleWorkflowState => { return { id: randomUUID(), - status: 'pending', + status: 'idle', messages: [ { role: 'assistant' as const, From fb0658c25514e073c7d5a45e903d4b68f0439e75 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 16:55:29 +0100 Subject: [PATCH 08/19] clean-up folder structure: --- packages/framework/src/supervisor/iterate.ts | 134 +++++++++++++++++++ packages/framework/src/teamwork.ts | 134 +------------------ 2 files changed, 135 insertions(+), 133 deletions(-) create mode 100644 packages/framework/src/supervisor/iterate.ts diff --git a/packages/framework/src/supervisor/iterate.ts b/packages/framework/src/supervisor/iterate.ts new file mode 100644 index 0000000..70f518e --- /dev/null +++ b/packages/framework/src/supervisor/iterate.ts @@ -0,0 +1,134 @@ +import { Workflow, WorkflowState } from '../workflow.js' +import { finalizeWorkflow } from './finalizeWorkflow.js' +import { nextTask } from './nextTask.js' +import { runAgent } from './runAgent.js' +import { runTools } from './runTools.js' +import { selectAgent } from './selectAgent.js' + +/** + * Performs single iteration over Workflow and produces its next state. + */ +export async function iterate(workflow: Workflow, state: WorkflowState): Promise { + const { status, messages } = state + + /** + * When number of messages exceedes number of maximum iterations, we must force finish the workflow + * and produce best final answer + */ + if (messages.length > workflow.maxIterations) { + const content = await finalizeWorkflow(workflow.provider, messages) + return { + ...state, + status: 'finished', + messages: state.messages.concat({ + role: 'user', + content, + }), + } + } + + /** + * When workflow is idle, we must get next task to work on, or finish the workflow otherwise. + */ + if (status === 'idle') { + const task = await nextTask(workflow.provider, messages) + if (task) { + return { + ...state, + status: 'pending', + agentRequest: [ + { + role: 'user', + content: task, + }, + ], + } + } else { + return { + ...state, + status: 'finished', + } + } + } + + /** + * When workflow is pending, we must find best agent to work on it. + */ + if (status === 'pending') { + const selectedAgent = await selectAgent( + workflow.provider, + state.agentRequest!, + workflow.members + ) + return { + ...state, + status: 'assigned', + agentStatus: 'idle', + agent: selectedAgent.role, + } + } + + /** + * When workflow is running, we must call assigned agent to continue working on it. + */ + if (status === 'assigned') { + const agent = workflow.members.find((member) => member.role === state.agent) + if (!agent) { + return { + id: state.id, + status: 'failed', + messages: state.messages.concat({ + role: 'assistant', + content: 'No agent found.', + }), + } + } + + /** + * When agentStatus is `tool`, an agent is waiting for the tools results. + * We must run all the tools in order to proceed to the next step. + */ + if (state.agentStatus === 'tool') { + const toolsResponse = await runTools(agent, state.agentRequest!) + return { + ...state, + agentStatus: 'step', + agentRequest: state.agentRequest?.concat(toolsResponse), + } + } + + /** + * When agent finishes running, it will return status to indicate whether it finished processing. + * + * If it finished processing, we will append its final answer to the context. Otherwise, we will + * further extend agentRequest to carry context over to the next iteration. + */ + const [agentResponse, status] = await runAgent(agent, state.agentRequest!) + if (status === 'complete') { + const agentFinalAnswer = agentResponse.at(-1)! + return { + ...state, + status: 'idle', + messages: state.messages.concat(agentFinalAnswer), + } + } + return { + ...state, + agentStatus: status, + agentRequest: state.agentRequest.concat(agentResponse), + } + } + + /** + * When workflow fails due to unexpected error, we must attempt recovering or finish the workflow + * otherwise. + */ + if (status === 'failed') { + return { + ...state, + status: 'finished', + } + } + + return state +} diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index 679f813..7ad709d 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -1,139 +1,7 @@ -import { finalizeWorkflow } from './supervisor/finalizeWorkflow.js' -import { nextTask } from './supervisor/nextTask.js' -import { runAgent } from './supervisor/runAgent.js' -import { runTools } from './supervisor/runTools.js' -import { selectAgent } from './supervisor/selectAgent.js' +import { iterate } from './supervisor/iterate.js' import { MessageContent } from './types.js' import { Workflow, WorkflowState, workflowState } from './workflow.js' -/** - * Performs single iteration over Workflow and produces its next state. - */ -export async function iterate(workflow: Workflow, state: WorkflowState): Promise { - const { status, messages } = state - - /** - * When number of messages exceedes number of maximum iterations, we must force finish the workflow - * and produce best final answer - */ - if (messages.length > workflow.maxIterations) { - const content = await finalizeWorkflow(workflow.provider, messages) - return { - ...state, - status: 'finished', - messages: state.messages.concat({ - role: 'user', - content, - }), - } - } - - /** - * When workflow is idle, we must get next task to work on, or finish the workflow otherwise. - */ - if (status === 'idle') { - const task = await nextTask(workflow.provider, messages) - if (task) { - return { - ...state, - status: 'pending', - agentRequest: [ - { - role: 'user', - content: task, - }, - ], - } - } else { - return { - ...state, - status: 'finished', - } - } - } - - /** - * When workflow is pending, we must find best agent to work on it. - */ - if (status === 'pending') { - const selectedAgent = await selectAgent( - workflow.provider, - state.agentRequest!, - workflow.members - ) - return { - ...state, - status: 'assigned', - agentStatus: 'idle', - agent: selectedAgent.role, - } - } - - /** - * When workflow is running, we must call assigned agent to continue working on it. - */ - if (status === 'assigned') { - const agent = workflow.members.find((member) => member.role === state.agent) - if (!agent) { - return { - id: state.id, - status: 'failed', - messages: state.messages.concat({ - role: 'assistant', - content: 'No agent found.', - }), - } - } - - /** - * When agentStatus is `tool`, an agent is waiting for the tools results. - * We must run all the tools in order to proceed to the next step. - */ - if (state.agentStatus === 'tool') { - const toolsResponse = await runTools(agent, state.agentRequest!) - return { - ...state, - agentStatus: 'step', - agentRequest: state.agentRequest?.concat(toolsResponse), - } - } - - /** - * When agent finishes running, it will return status to indicate whether it finished processing. - * - * If it finished processing, we will append its final answer to the context. Otherwise, we will - * further extend agentRequest to carry context over to the next iteration. - */ - const [agentResponse, status] = await runAgent(agent, state.agentRequest!) - if (status === 'complete') { - const agentFinalAnswer = agentResponse.at(-1)! - return { - ...state, - status: 'idle', - messages: state.messages.concat(agentFinalAnswer), - } - } - return { - ...state, - agentStatus: status, - agentRequest: state.agentRequest.concat(agentResponse), - } - } - - /** - * When workflow fails due to unexpected error, we must attempt recovering or finish the workflow - * otherwise. - */ - if (status === 'failed') { - return { - ...state, - status: 'finished', - } - } - - return state -} - /** * Teamwork runs given workflow and continues iterating over the workflow until it finishes. */ From 2785b49ec8f1b6fc5771b9f8187453d9f099eb2c Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 16:57:20 +0100 Subject: [PATCH 09/19] chore: update --- packages/framework/src/teamwork.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index 7ad709d..53bca37 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -17,3 +17,9 @@ export async function teamwork( return teamwork(workflow, await iterate(workflow, state)) } + +/** + * Re-export iterate for advanced use cases that need more control over the default `teamwork` behavior. + * This is typically useful for distributed environments + */ +export { iterate } from './supervisor/iterate.js' From e1b79afb06dbc6f4f292473701a70a7ed3abf551 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 17:19:02 +0100 Subject: [PATCH 10/19] save --- example/src/surprise_trip.ts | 4 +- packages/framework/package.json | 3 -- packages/framework/src/supervisor/iterate.ts | 7 ++++ packages/framework/src/telemetry.ts | 9 +++++ packages/framework/src/telemetry/base.ts | 39 -------------------- packages/framework/src/telemetry/console.ts | 7 ---- packages/framework/src/workflow.ts | 12 +++--- website/rspress.config.ts | 13 +++++++ website/tsconfig.json | 5 ++- 9 files changed, 41 insertions(+), 58 deletions(-) create mode 100644 packages/framework/src/telemetry.ts delete mode 100644 packages/framework/src/telemetry/base.ts delete mode 100644 packages/framework/src/telemetry/console.ts diff --git a/example/src/surprise_trip.ts b/example/src/surprise_trip.ts index 10aaff6..25a5dc0 100644 --- a/example/src/surprise_trip.ts +++ b/example/src/surprise_trip.ts @@ -4,7 +4,7 @@ import { agent } from '@dead-simple-ai-agent/framework/agent' import { teamwork } from '@dead-simple-ai-agent/framework/teamwork' -import { logger } from '@dead-simple-ai-agent/framework/telemetry/console' +import { logger } from '@dead-simple-ai-agent/framework/telemetry' import { workflow } from '@dead-simple-ai-agent/framework/workflow' import { lookupWikipedia } from '../tools.js' @@ -72,7 +72,7 @@ const researchTripWorkflow = workflow({ Comprehensive day-by-day itinerary for the trip to WrocΕ‚aw, Poland. Ensure the itinerary integrates flights, hotel information, and all planned activities and dining experiences. `, - telemetry: logger, + snapshot: logger, }) const result = await teamwork(researchTripWorkflow) diff --git a/packages/framework/package.json b/packages/framework/package.json index 4f30f63..bf1200e 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -9,9 +9,6 @@ }, "./models/*": { "bun": "./src/models/*.ts" - }, - "./telemetry/*": { - "bun": "./src/telemetry/*.ts" } }, "type": "module", diff --git a/packages/framework/src/supervisor/iterate.ts b/packages/framework/src/supervisor/iterate.ts index 70f518e..fcacc15 100644 --- a/packages/framework/src/supervisor/iterate.ts +++ b/packages/framework/src/supervisor/iterate.ts @@ -9,6 +9,13 @@ import { selectAgent } from './selectAgent.js' * Performs single iteration over Workflow and produces its next state. */ export async function iterate(workflow: Workflow, state: WorkflowState): Promise { + workflow.snapshot(state) + const nextState = await nextTick(workflow, state) + workflow.snapshot(nextState) + return nextState +} + +async function nextTick(workflow: Workflow, state: WorkflowState): Promise { const { status, messages } = state /** diff --git a/packages/framework/src/telemetry.ts b/packages/framework/src/telemetry.ts new file mode 100644 index 0000000..7a7d126 --- /dev/null +++ b/packages/framework/src/telemetry.ts @@ -0,0 +1,9 @@ +import { WorkflowState } from './workflow.js' + +export type Telemetry = (state: WorkflowState) => void + +export const noop: Telemetry = () => {} + +export const logger: Telemetry = (state) => { + console.log(state) +} diff --git a/packages/framework/src/telemetry/base.ts b/packages/framework/src/telemetry/base.ts deleted file mode 100644 index f898213..0000000 --- a/packages/framework/src/telemetry/base.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Workflow, WorkflowState } from '../workflow.js' - -// Base event structure -export type BaseTelemetryEvent = { - workflowId: string - correlationId?: string -} - -/** - * Emitted when a workflow begins execution. - */ -export type WorkflowStartEvent = { - type: 'workflow.iteration.start' - data: { - workflow: Workflow - state: WorkflowState - } -} - -/** - * Emitted when a workflow gets the next task - */ -export type WorkflowEndEvent = { - type: 'workflow.iteration.nextTask' - data: { - workflow: Workflow - task: string - } -} - -export type TelemetryEvent = WorkflowStartEvent | WorkflowEndEvent - -export type Telemetry = { - record: (event: TelemetryEvent) => void | Promise -} - -export const noopTelemetry: Telemetry = { - record: () => {}, -} diff --git a/packages/framework/src/telemetry/console.ts b/packages/framework/src/telemetry/console.ts deleted file mode 100644 index 889d6f7..0000000 --- a/packages/framework/src/telemetry/console.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Telemetry } from './base.js' - -export const logger: Telemetry = { - record: (event) => { - console.log(event) - }, -} diff --git a/packages/framework/src/workflow.ts b/packages/framework/src/workflow.ts index 78c9cb3..da16d28 100644 --- a/packages/framework/src/workflow.ts +++ b/packages/framework/src/workflow.ts @@ -4,7 +4,7 @@ import s from 'dedent' import { Agent } from './agent.js' import { openai, Provider } from './models/openai.js' -import { noopTelemetry, Telemetry } from './telemetry/base.js' +import { noop, Telemetry } from './telemetry.js' import { Message } from './types.js' type WorkflowOptions = { @@ -14,7 +14,7 @@ type WorkflowOptions = { provider?: Provider maxIterations?: number - telemetry?: Telemetry + snapshot?: Telemetry } /** @@ -24,7 +24,7 @@ export const workflow = (options: WorkflowOptions): Workflow => { return { maxIterations: 50, provider: openai(), - telemetry: noopTelemetry, + snapshot: noop, ...options, } } @@ -42,14 +42,14 @@ type BaseWorkflowState = { /** * Different states workflow is in, in between execution from agents */ -type IdleWorkflowState = BaseWorkflowState & { +export type IdleWorkflowState = BaseWorkflowState & { status: 'idle' | 'finished' | 'failed' } /** * Supervisor selected the task, and is now pending assignement of an agent */ -type PendingWorkflowState = BaseWorkflowState & { +export type PendingWorkflowState = BaseWorkflowState & { status: 'pending' agentRequest: Message[] } @@ -57,7 +57,7 @@ type PendingWorkflowState = BaseWorkflowState & { /** * State in which an agent is assigned and work is pending */ -type AssignedWorkflowState = BaseWorkflowState & { +export type AssignedWorkflowState = BaseWorkflowState & { status: 'assigned' agent: string diff --git a/website/rspress.config.ts b/website/rspress.config.ts index 0f37a9b..97505e2 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -1,5 +1,6 @@ import * as path from 'node:path' +import { pluginTypeDoc } from '@rspress/plugin-typedoc' import { defineConfig } from 'rspress/config' export default defineConfig({ @@ -19,4 +20,16 @@ export default defineConfig({ }, ], }, + plugins: [ + pluginTypeDoc({ + entryPoints: [ + path.join(__dirname, '../packages/framework/src/agent.ts'), + path.join(__dirname, '../packages/framework/src/teamwork.ts'), + path.join(__dirname, '../packages/framework/src/tool.ts'), + path.join(__dirname, '../packages/framework/src/workflow.ts'), + path.join(__dirname, '../packages/framework/src/models/openai.ts'), + path.join(__dirname, '../packages/framework/src/telemetry.ts'), + ], + }), + ], }) diff --git a/website/tsconfig.json b/website/tsconfig.json index 3c43903..86afe4d 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "../tsconfig.json" + "extends": "../tsconfig.json", + "include": [ + "../packages/framework/src" + ] } From 8c3d96ff5e38a467cdb536a3dd5c017dc392babe Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 17:23:36 +0100 Subject: [PATCH 11/19] chore: remove --- packages/framework/src/supervisor/iterate.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/framework/src/supervisor/iterate.ts b/packages/framework/src/supervisor/iterate.ts index fcacc15..1e20a07 100644 --- a/packages/framework/src/supervisor/iterate.ts +++ b/packages/framework/src/supervisor/iterate.ts @@ -9,7 +9,6 @@ import { selectAgent } from './selectAgent.js' * Performs single iteration over Workflow and produces its next state. */ export async function iterate(workflow: Workflow, state: WorkflowState): Promise { - workflow.snapshot(state) const nextState = await nextTick(workflow, state) workflow.snapshot(nextState) return nextState From 44ffad9aaadcef7146805cbf62722787f669c351 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Sun, 8 Dec 2024 18:10:28 +0100 Subject: [PATCH 12/19] chore: tweaks --- .../src/supervisor/{iterate.ts => nextTick.ts} | 8 +------- packages/framework/src/teamwork.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 11 deletions(-) rename packages/framework/src/supervisor/{iterate.ts => nextTick.ts} (92%) diff --git a/packages/framework/src/supervisor/iterate.ts b/packages/framework/src/supervisor/nextTick.ts similarity index 92% rename from packages/framework/src/supervisor/iterate.ts rename to packages/framework/src/supervisor/nextTick.ts index 1e20a07..69c242b 100644 --- a/packages/framework/src/supervisor/iterate.ts +++ b/packages/framework/src/supervisor/nextTick.ts @@ -8,13 +8,7 @@ import { selectAgent } from './selectAgent.js' /** * Performs single iteration over Workflow and produces its next state. */ -export async function iterate(workflow: Workflow, state: WorkflowState): Promise { - const nextState = await nextTick(workflow, state) - workflow.snapshot(nextState) - return nextState -} - -async function nextTick(workflow: Workflow, state: WorkflowState): Promise { +export async function nextTick(workflow: Workflow, state: WorkflowState): Promise { const { status, messages } = state /** diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index 53bca37..f38346b 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -1,4 +1,4 @@ -import { iterate } from './supervisor/iterate.js' +import { nextTick } from './supervisor/nextTick.js' import { MessageContent } from './types.js' import { Workflow, WorkflowState, workflowState } from './workflow.js' @@ -19,7 +19,10 @@ export async function teamwork( } /** - * Re-export iterate for advanced use cases that need more control over the default `teamwork` behavior. - * This is typically useful for distributed environments + * Iterate performs single iteration over workflow and returns its next state */ -export { iterate } from './supervisor/iterate.js' +export async function iterate(workflow: Workflow, state: WorkflowState): Promise { + const nextState = await nextTick(workflow, state) + workflow.snapshot(nextState) + return nextState +} From 975f1cf0066e4d2b01fc5713279012e9f3cd84c1 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 00:13:32 +0400 Subject: [PATCH 13/19] fix --- example/src/surprise_trip.ts | 3 ++- packages/framework/src/supervisor/nextTick.ts | 18 ++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/example/src/surprise_trip.ts b/example/src/surprise_trip.ts index 25a5dc0..2b35100 100644 --- a/example/src/surprise_trip.ts +++ b/example/src/surprise_trip.ts @@ -72,7 +72,8 @@ const researchTripWorkflow = workflow({ Comprehensive day-by-day itinerary for the trip to WrocΕ‚aw, Poland. Ensure the itinerary integrates flights, hotel information, and all planned activities and dining experiences. `, - snapshot: logger, + // Uncomment to see the workflow state in the console + // snapshot: logger, }) const result = await teamwork(researchTripWorkflow) diff --git a/packages/framework/src/supervisor/nextTick.ts b/packages/framework/src/supervisor/nextTick.ts index 69c242b..69c64e6 100644 --- a/packages/framework/src/supervisor/nextTick.ts +++ b/packages/framework/src/supervisor/nextTick.ts @@ -55,11 +55,7 @@ export async function nextTick(workflow: Workflow, state: WorkflowState): Promis * When workflow is pending, we must find best agent to work on it. */ if (status === 'pending') { - const selectedAgent = await selectAgent( - workflow.provider, - state.agentRequest!, - workflow.members - ) + const selectedAgent = await selectAgent(workflow.provider, state.agentRequest, workflow.members) return { ...state, status: 'assigned', @@ -93,23 +89,25 @@ export async function nextTick(workflow: Workflow, state: WorkflowState): Promis return { ...state, agentStatus: 'step', - agentRequest: state.agentRequest?.concat(toolsResponse), + agentRequest: state.agentRequest.concat(toolsResponse), } } /** * When agent finishes running, it will return status to indicate whether it finished processing. * - * If it finished processing, we will append its final answer to the context. Otherwise, we will - * further extend agentRequest to carry context over to the next iteration. + * If it finished processing, we will append its final answer to the context, as well as + * first message from `agentRequest`, which holds the actual task, excluding middle-steps. + * + * If further processing is required, we will carry `agentRequest` over to the next iteration. */ - const [agentResponse, status] = await runAgent(agent, state.agentRequest!) + const [agentResponse, status] = await runAgent(agent, state.agentRequest) if (status === 'complete') { const agentFinalAnswer = agentResponse.at(-1)! return { ...state, status: 'idle', - messages: state.messages.concat(agentFinalAnswer), + messages: state.messages.concat(state.agentRequest[0], agentFinalAnswer), } } return { From 94680cd6a3a1dd89af4eb302ef48883dce1dc11f Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 03:12:04 +0400 Subject: [PATCH 14/19] remove file --- src/index.ts | 379 --------------------------------------------------- 1 file changed, 379 deletions(-) delete mode 100644 src/index.ts diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index b02052f..0000000 --- a/src/index.ts +++ /dev/null @@ -1,379 +0,0 @@ -import s from 'dedent' -import OpenAI from 'openai' -import { zodFunction, zodResponseFormat } from 'openai/helpers/zod' -import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions' -import { z } from 'zod' - -// tbd: abstract this away or not? most APIs are OpenAI compatible -const openai = new OpenAI() - -interface Protocol { - requestUserInput(prompt: string): Promise -} - -// tbd: we should replace this with a "HumanInTheLoop" agent of CLI type -// to do so, we need to implement delegation across different agents -// so they can work collaboratively on smaller tasks too -class CLIProtocol implements Protocol { - async requestUserInput(prompt: string): Promise { - return new Promise((resolve) => { - console.log(prompt) - process.stdin.once('data', (data) => { - resolve(data.toString().trim()) - }) - }) - } -} - -type ToolDefinition> = { - name: string - description: string - parameters: T - execute: (parameters: z.infer) => Promise -} - -interface AgentConfig { - role: string - description: string - tools?: ToolDefinition[] - model?: string - protocol?: Protocol -} - -// tbd: implement short-term and long-term memory with different storage models -export class Agent { - role: string - - private description: string - private tools: ToolDefinition[] - private model: string - private protocol: Protocol - - constructor({ - role, - description, - tools = [], - model = 'gpt-4o', - protocol = new CLIProtocol(), - }: AgentConfig) { - this.role = role - this.description = description - this.tools = tools - this.model = model - this.protocol = protocol - } - - async executeTask(messages: Message[], agents: Agent[]): Promise { - // tbd: after we implememt this, it keeps delegating - // const tools = [ - // { - // name: 'ask_a_question', - // description: s` - // Ask a specific question to one of the following agents: - // - // ${agents.map((agent, index) => `${agent.role}`)} - // - // `, - // parameters: z.object({ - // agent: z.number().describe('The index of the agent to ask the question'), - // question: z.string().describe('The question you want the agent to answer'), - // context: z.string().describe('All context necessary to execute the task'), - // }), - // function: async ({ agent, question, context }) => { - // console.log('ask_a_question', agent, question, context) - // const selectedAgent = agents[agent] - // if (!selectedAgent) { - // throw new Error('Invalid agent') - // } - // return selectedAgent.executeTask( - // [ - // { - // role: 'user', - // content: context, - // }, - // { - // role: 'user', - // content: question, - // }, - // ], - // agents - // ) - // }, - // }, - // ] - const response = await openai.beta.chat.completions.parse({ - model: this.model, - // tbd: verify the prompt - messages: [ - { - role: 'system', - content: s` - You are ${this.role}. ${this.description} - - Your job is to complete the assigned task. - 1. You can break down the task into steps - 2. You can use available tools when needed - - First try to complete the task on your own. - Only ask question to the user if you cannot complete the task without their input. - `, - }, - ...messages, - ], - // tbd: add other tools - // tbd: should we include agent description in the prompt too? we need to list responsibilities - // but keep context window in mind - // tools: tools.map(zodFunction), - response_format: zodResponseFormat( - z.object({ - response: z.discriminatedUnion('kind', [ - z.object({ - kind: z.literal('step'), - name: z.string().describe('The name of the step'), - result: z.string().describe('The result of the step'), - reasoning: z.string().describe('The reasoning for this step'), - }), - z.object({ - kind: z.literal('complete'), - result: z.string().describe('The final result of the task'), - reasoning: z.string().describe('The reasoning for completing the task'), - }), - ]), - }), - 'task_result' - ), - }) - // if (response.choices[0].message.tool_calls.length > 0) { - // const toolResults = await Promise.all( - // response.choices[0].message.tool_calls.map(async (toolCall) => { - // if (toolCall.type !== 'function') { - // throw new Error('Tool call is not a function') - // } - - // const tool = tools.find((t) => t.name === toolCall.function.name) - // if (!tool) { - // throw new Error(`Unknown tool: ${toolCall.function.name}`) - // } - // console.log('tool call', toolCall) - // const parameters = tool.parameters.parse(toolCall.function.arguments) - - // const content = await tool.function(parameters) - - // return { - // role: 'tool' as const, - // tool_call_id: toolCall.id, - // content: JSON.stringify(content), - // } - // }) - // ) - - // return this.executeTask([...messages, response.choices[0].message, ...toolResults], agents) - // } - - // tbd: verify shape of response - const result = response.choices[0].message.parsed - if (!result) { - throw new Error('No parsed response received') - } - - if (result.response.kind === 'step') { - console.log('πŸš€ Step:', result.response.name) - return this.executeTask( - [ - ...messages, - { - role: 'assistant', - content: result.response.result, - }, - ], - agents - ) - } - - if (result.response.kind === 'complete') { - return result.response.result - } - - // tbd: check if this is reachable - throw new Error('Illegal state') - } - - async requestUserInput(prompt: string): Promise { - return this.protocol.requestUserInput(prompt) - } - - toString(): string { - return s` - Agent role: "${this.role}" - Expertise: "${this.description}" - ` - } -} -export class Team { - async execute(workflow: Workflow): Promise { - const messages: Message[] = [ - { - role: 'assistant', - content: s` - Here is description of the workflow and expected output by the user: - ${workflow.description} - ${workflow.output} - `, - }, - ] - - // tbd: set reasonable max iterations - // eslint-disable-next-line no-constant-condition - while (true) { - const task = await getNextTask(messages) - if (!task) { - return messages.at(-1)!.content as string - } - - console.log('πŸš€ Next task:', task) - - messages.push({ - role: 'user', - content: task, - }) - - // tbd: this throws, handle it - const selectedAgent = await selectAgent(task, workflow.members) - console.log('πŸš€ Selected agent:', selectedAgent.role) - - // tbd: this should just be a try/catch - // tbd: do not return string, but more information or keep memory in agent - try { - const result = await selectedAgent.executeTask(messages, workflow.members) - messages.push({ - role: 'assistant', - content: result, - }) - } catch (error) { - console.log('πŸš€ Task error:', error) - messages.push({ - role: 'assistant', - content: error instanceof Error ? error.message : 'Unknown error', - }) - } - } - } -} - -type Workflow = { - description: string - output: string - members: Agent[] -} - -async function selectAgent(task: string, agents: Agent[]): Promise { - const response = await openai.beta.chat.completions.parse({ - model: 'gpt-4o', - messages: [ - { - role: 'system', - content: s` - You are an agent selector that matches tasks to the most capable agent. - Analyze the task requirements and each agent's capabilities to select the best match. - - Consider: - 1. Required tools and skills - 2. Agent's specialization - 3. Model capabilities - 4. Previous task context if available - `, - }, - { - role: 'user', - content: s` - Here is the task: - ${task} - - Here are the available agents: - - ${agents.map((agent, index) => `${agent}`)} - - - Select the most suitable agent for this task. - `, - }, - ], - temperature: 0.1, - response_format: zodResponseFormat( - z.object({ - agentIndex: z.number(), - reasoning: z.string(), - }), - 'agent_selection' - ), - }) - - const content = response.choices[0].message.parsed - if (!content) { - throw new Error('No content in response') - } - - const agent = agents[content.agentIndex] - if (!agent) { - throw new Error('Invalid agent') - } - - return agent -} - -async function getNextTask(history: Message[]): Promise { - const response = await openai.beta.chat.completions.parse({ - model: 'gpt-4o', - messages: [ - { - role: 'system', - // tbd: handle subsequent failures - content: s` - You are a planner that breaks down complex workflows into smaller, actionable steps. - Your job is to determine the next task that needs to be done based on the original workflow and what has been completed so far. - If all required tasks are completed, return null. - - Rules: - 1. Each task should be self-contained and achievable - 2. Tasks should be specific and actionable - 3. Return null when the workflow is complete - 4. Consider dependencies and order of operations - 5. Use context from completed tasks to inform next steps - `, - }, - ...history, - { - role: 'user', - content: 'What is the next task that needs to be done?', - }, - ], - temperature: 0.2, - response_format: zodResponseFormat( - z.object({ - task: z - .string() - .describe('The next task to be completed or null if the workflow is complete') - .nullable(), - reasoning: z - .string() - .describe('The reasoning for selecting the next task or why the workflow is complete'), - }), - 'next_task' - ), - }) - - try { - const content = response.choices[0].message.parsed - if (!content) { - throw new Error('No content in response') - } - - if (!content.task) { - return null - } - - return content.task - } catch (error) { - throw new Error('Failed to determine next task') - } -} From 94524703780ad5712201e1daa9724232594d3d4c Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 03:31:34 +0400 Subject: [PATCH 15/19] save --- bun.lockb | Bin 465196 -> 465196 bytes example/src/medical_survey/workflow.ts | 57 +++++++++--------- example/src/medical_survey/workflow_server.ts | 45 -------------- example/src/medical_survey_server.ts | 55 +++++++++++------ example/src/medical_survey_stateless.ts | 28 --------- website/rspress.config.ts | 1 + 6 files changed, 67 insertions(+), 119 deletions(-) delete mode 100644 example/src/medical_survey/workflow_server.ts delete mode 100644 example/src/medical_survey_stateless.ts diff --git a/bun.lockb b/bun.lockb index 40524cdd4a8176be54c9e67e1011396bda0745fd..a8e395b77b21a992b65f626772e0019f4f0e1268 100755 GIT binary patch delta 37 tcmZ3pNoLI^nT8g|7N!>F7M3ln=Edxcai)5PdIs%w#jM-yirL;R1pxEd42S># delta 37 pcmZ3pNoLI^nT8g|7N!>F7M3ln=Edwx3}Dc1SIoNIu9)rJQUKw#3poG) diff --git a/example/src/medical_survey/workflow.ts b/example/src/medical_survey/workflow.ts index ad33bcb..22a2a3f 100644 --- a/example/src/medical_survey/workflow.ts +++ b/example/src/medical_survey/workflow.ts @@ -25,47 +25,46 @@ const askPatient = tool({ }) const nurse = agent({ - role: 'Nurse,doctor assistant', + role: 'Nurse', description: ` - You are skille nurse / doctor assistant. - You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit. - Ask user questions about the patient's health and symptoms. - Ask one question at time up to 5 questions. - `, + You are skille nurse / doctor assistant. + You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit. + Ask user questions about the patient's health and symptoms. + Ask one question at time up to 5 questions. + `, tools: { - ask_question: askPatient, + askPatient, }, }) const reporter = agent({ role: 'Reporter', description: ` - You are skilled at preparing great looking markdown reports. - Prepare a report for a patient that is about to come for a visit. - Add info about the patient's health and symptoms. - `, - tools: {}, + You are skilled at preparing great looking reports. + You can prepare a report for a patient that is about to come for a visit. + Add info about the patient's health and symptoms. + `, }) export const preVisitNoteWorkflow = workflow({ members: [nurse, reporter], description: ` - Create a pre-visit note for a patient that is about to come for a visit. - The note should include the patient's health and symptoms. - - Include: - - symptoms, - - health issues, - - medications, - - allergies, - - surgeries - - Never ask fo: - - personal data, - - sensitive data, - - any data that can be used to identify the patient. - `, + Create a pre-visit note for a patient that is about to come for a visit. + The note should include the patient's health and symptoms. + + Include: + - symptoms, + - health issues, + - medications, + - allergies, + - surgeries + + Never ask fo: + - personal data, + - sensitive data, + - any data that can be used to identify the patient. + `, output: ` - A markdown report for the patient's pre-visit note. - `, + A markdown report for the patient's pre-visit note. + `, }) diff --git a/example/src/medical_survey/workflow_server.ts b/example/src/medical_survey/workflow_server.ts deleted file mode 100644 index f37f241..0000000 --- a/example/src/medical_survey/workflow_server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { agent } from '@dead-simple-ai-agent/framework/agent' -import { workflow } from '@dead-simple-ai-agent/framework/workflow' - -const nurse = agent({ - role: 'Nurse,doctor assistant', - description: ` - You are skille nurse / doctor assistant. - You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit. - Ask one question at time up to 5 questions. - Wait until you get the answer. - `, -}) - -const reporter = agent({ - role: 'Reporter', - description: ` - You are skilled at preparing great looking markdown reports. - Prepare a report for a patient that is about to come for a visit. - Add info about the patient's health and symptoms. - `, - tools: {}, -}) - -export const preVisitNoteWorkflow = workflow({ - members: [nurse, reporter], - description: ` - Create a pre-visit note for a patient that is about to come for a visit. - The note should include the patient's health and symptoms. - - Include: - - symptoms, - - health issues, - - medications, - - allergies, - - surgeries - - Never ask fo: - - personal data, - - sensitive data, - - any data that can be used to identify the patient. - `, - output: ` - A markdown report for the patient's pre-visit note. - `, -}) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index 66f385e..20f27bb 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -1,37 +1,57 @@ /** - * Example borrowed from CrewAI. + * This example demonstrates using framework in server-side environments. */ import { iterate } from '@dead-simple-ai-agent/framework/teamwork' -import { workflowState } from '@dead-simple-ai-agent/framework/workflow' -import fastify, { FastifyReply, FastifyRequest } from 'fastify' +import { WorkflowState, workflowState } from '@dead-simple-ai-agent/framework/workflow' +import fastify, { FastifyRequest } from 'fastify' import { promises as fs } from 'fs' import { tmpdir } from 'os' import { join } from 'path' +import { preVisitNoteWorkflow } from './medical_survey/workflow.js' + const server = fastify({ logger: false }) -import { preVisitNoteWorkflow } from './medical_survey/workflow_server.js' +const dbPath = (id: string) => join(tmpdir(), `${id}_workflow.json`) -const dbPath = (id: string) => join(tmpdir(), id + '_workflow_db.json') +const visits: Record = {} -let state = workflowState(preVisitNoteWorkflow) +/** + * This will create a new workflow and return the initial state + */ +server.post('/visits', async () => { + const state = workflowState(preVisitNoteWorkflow) + visits[state.id] = state + return state +}) -server.post('/start', async () => { - const nextState = await iterate(preVisitNoteWorkflow, state) +/** + * Call this endpoint to iterate on the workflow + */ - await fs.writeFile(dbPath(nextState.id), JSON.stringify(nextState, null, 2), 'utf-8') +// tbd: - return { - status: 'running', - state: nextState, - } -}) +type ToolCallMessage = { + tool_call_id: string + content: string +} +/** + * Once you get response for tools, you can execute this endpoint to continue the workflow. + */ server.post( - '/iterate/:id', - async (req: FastifyRequest<{ Params: { id: string }; Body: { message: string } }>) => { - const { id } = req.params + '/visits/:id/messages', + async (req: FastifyRequest<{ Params: { id: string }; Body: ToolCallMessage }>) => { + const state = visits[req.params.id] + if (!state) { + throw new Error('Workflow not found') + } + + if (!state.status !== 'assigned') { + throw new Error('Workflow is not waiting for a message right now') + } + const { message } = req.body const path = dbPath(id) @@ -61,6 +81,7 @@ const port = parseInt(process.env['PORT'] || '3000', 10) server.listen({ port, }) + console.log(`πŸš€ Server running at http://localhost:${port}`) console.log(`Run 'curl -X POST http://localhost:${port}/start' to start the workflow`) console.log( diff --git a/example/src/medical_survey_stateless.ts b/example/src/medical_survey_stateless.ts deleted file mode 100644 index edf6c66..0000000 --- a/example/src/medical_survey_stateless.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Example borrowed from CrewAI. - */ - -import { iterate } from '@dead-simple-ai-agent/framework/teamwork' -import { workflowState } from '@dead-simple-ai-agent/framework/workflow' -import { promises as fs } from 'fs' -import { tmpdir } from 'os' -import { join } from 'path' - -import { preVisitNoteWorkflow } from './medical_survey/workflow.js' - -const tmpDir = tmpdir() -const dbPath = join(tmpDir, 'stepping_survey_workflow_db.json') - -let state = workflowState(preVisitNoteWorkflow) -if (await fs.exists(dbPath)) { - try { - state = JSON.parse(await fs.readFile(dbPath, 'utf-8')) - console.log('πŸ›Ÿ Loaded workflow from', dbPath) - } catch (error) { - console.log(`🚨Error while loading workflow from ${dbPath}. Starting new workflow.`) - } -} - -const nextState = await iterate(preVisitNoteWorkflow, state) - -await fs.writeFile(dbPath, JSON.stringify(nextState, null, 2), 'utf-8') diff --git a/website/rspress.config.ts b/website/rspress.config.ts index 0f37a9b..8bce85f 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ light: '/rspress-light-logo.png', dark: '/rspress-dark-logo.png', }, + globalStyles: path.join(__dirname, 'global.css'), themeConfig: { socialLinks: [ { From 5ff9914a1fc1c296f26236a52fec02c45e1897d8 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 03:58:08 +0400 Subject: [PATCH 16/19] init --- example/src/medical_survey_server.ts | 134 +++++++++++++----- example/src/surprise_trip.ts | 1 - packages/framework/src/supervisor/runTools.ts | 2 +- 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index 20f27bb..6b8c2ba 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -2,19 +2,16 @@ * This example demonstrates using framework in server-side environments. */ +import { isToolCallRequest } from '@dead-simple-ai-agent/framework/supervisor/runTools' import { iterate } from '@dead-simple-ai-agent/framework/teamwork' import { WorkflowState, workflowState } from '@dead-simple-ai-agent/framework/workflow' +import s from 'dedent' import fastify, { FastifyRequest } from 'fastify' -import { promises as fs } from 'fs' -import { tmpdir } from 'os' -import { join } from 'path' import { preVisitNoteWorkflow } from './medical_survey/workflow.js' const server = fastify({ logger: false }) -const dbPath = (id: string) => join(tmpdir(), `${id}_workflow.json`) - const visits: Record = {} /** @@ -22,23 +19,49 @@ const visits: Record = {} */ server.post('/visits', async () => { const state = workflowState(preVisitNoteWorkflow) + + // Add the state to the visits map visits[state.id] = state + + // Start the visit in the background + runVisit(state) + return state }) /** - * Call this endpoint to iterate on the workflow + * Call this endpoint to get status of the workflow, or the final result. */ +server.get('/visits/:id', async (req: FastifyRequest<{ Params: { id: string } }>) => { + const state = visits[req.params.id] + if (!state) { + throw new Error('Workflow not found') + } -// tbd: + if (state.status === 'finished') { + return { + status: state.status, + result: state.messages.at(-1)!.content, + } + } -type ToolCallMessage = { - tool_call_id: string - content: string -} + if (state.status === 'assigned') { + if (state.agentStatus === 'tool') { + return state.agentRequest.at(-1)!.content + } + return { + status: state.status, + agentStatus: state.agentStatus, + } + } + + return { + status: state.status, + } +}) /** - * Once you get response for tools, you can execute this endpoint to continue the workflow. + * Adds a message to the workflow. */ server.post( '/visits/:id/messages', @@ -48,42 +71,77 @@ server.post( throw new Error('Workflow not found') } - if (!state.status !== 'assigned') { + if (state.status !== 'assigned' || state.agentStatus !== 'tool') { throw new Error('Workflow is not waiting for a message right now') } - const { message } = req.body - - const path = dbPath(id) + const toolRequestMessage = state.agentRequest.findLast(isToolCallRequest) + if (!toolRequestMessage) { + throw new Error('No tool request message found') + } - if (await fs.exists(path)) { - try { - state = JSON.parse(await fs.readFile(path, 'utf-8')) - console.log('πŸ›Ÿ Loaded workflow from', path) - } catch (error) { - console.log(`🚨Error while loading workflow from ${path}. Starting new workflow.`) - } + const toolCall = toolRequestMessage.tool_calls.find( + (toolCall) => toolCall.id === req.body.tool_call_id + ) + if (!toolCall) { + throw new Error('Tool call not found') } - if (message) { - // message provided within the call - for example a return call from API/Slack/Whatever - state.messages.push({ role: 'user', content: message }) + visits[req.params.id] = { + ...state, + agentRequest: state.agentRequest.concat({ + role: 'tool', + tool_call_id: toolCall.id, + content: req.body.content, + }), } - const nextState = await iterate(preVisitNoteWorkflow, state) - await fs.writeFile(path, JSON.stringify(nextState, null, 2), 'utf-8') + const allToolRequests = toolRequestMessage.tool_calls.map((toolCall) => toolCall.id) + const hasAllToolCalls = allToolRequests.every((toolCallId) => + state.agentRequest.some( + (request) => 'tool_call_id' in request && request.tool_call_id === toolCallId + ) + ) - return nextState + if (hasAllToolCalls) { + runVisit(visits[req.params.id]) + } + + return { + hasAllToolCalls, + } } ) -const port = parseInt(process.env['PORT'] || '3000', 10) -server.listen({ - port, -}) +/** + * Start the server + */ +const port = parseInt(process.env['PORT'] || '3000') +server.listen({ port }) -console.log(`πŸš€ Server running at http://localhost:${port}`) -console.log(`Run 'curl -X POST http://localhost:${port}/start' to start the workflow`) -console.log( - `Run 'curl -X POST http://localhost:${port}/iterate/ID -d '{"message":"Hello"}' to iterate the workflow with the message provided optionally as an answer added to the state` -) +console.log(s` + πŸš€ Server running at http://localhost:${port} + + Run 'curl -X POST http://localhost:${port}/visits' to create a new visit + Run 'curl -X POST http://localhost:${port}/visits/:id/messages -d '{"tool_call_id":"...","content":"..."}' to add a message to the visit +`) + +type ToolCallMessage = { + tool_call_id: string + content: string +} + +/** + * Helper function, inspired by `teamwork`. + * It will continue running the visit in the background and will stop when the workflow is finished. + */ +async function runVisit(state: WorkflowState): Promise { + if ( + state.status === 'finished' || + (state.status === 'assigned' && state.agentStatus === 'tool') + ) { + return state + } + + return runVisit(await iterate(preVisitNoteWorkflow, state)) +} diff --git a/example/src/surprise_trip.ts b/example/src/surprise_trip.ts index 2b35100..75ae3c3 100644 --- a/example/src/surprise_trip.ts +++ b/example/src/surprise_trip.ts @@ -4,7 +4,6 @@ import { agent } from '@dead-simple-ai-agent/framework/agent' import { teamwork } from '@dead-simple-ai-agent/framework/teamwork' -import { logger } from '@dead-simple-ai-agent/framework/telemetry' import { workflow } from '@dead-simple-ai-agent/framework/workflow' import { lookupWikipedia } from '../tools.js' diff --git a/packages/framework/src/supervisor/runTools.ts b/packages/framework/src/supervisor/runTools.ts index bad2e1d..62e9e76 100644 --- a/packages/framework/src/supervisor/runTools.ts +++ b/packages/framework/src/supervisor/runTools.ts @@ -7,7 +7,7 @@ import { Message } from '../types.js' /** * Asserts that given message requests tool calls */ -function isToolCallRequest(message?: Message): message is ParsedChatCompletionMessage { +export function isToolCallRequest(message?: Message): message is ParsedChatCompletionMessage { return message ? 'tool_calls' in message : false } From d72ca9de24eddc7cfaf12bb5fe4cd90f76bafa15 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 04:26:03 +0400 Subject: [PATCH 17/19] save --- example/src/medical_survey_server.ts | 69 ++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index 6b8c2ba..8e6ee58 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -1,10 +1,10 @@ /** * This example demonstrates using framework in server-side environments. */ - import { isToolCallRequest } from '@dead-simple-ai-agent/framework/supervisor/runTools' import { iterate } from '@dead-simple-ai-agent/framework/teamwork' -import { WorkflowState, workflowState } from '@dead-simple-ai-agent/framework/workflow' +import { workflow, WorkflowState, workflowState } from '@dead-simple-ai-agent/framework/workflow' +import chalk from 'chalk' import s from 'dedent' import fastify, { FastifyRequest } from 'fastify' @@ -24,7 +24,7 @@ server.post('/visits', async () => { visits[state.id] = state // Start the visit in the background - runVisit(state) + runVisit(state.id) return state }) @@ -47,7 +47,7 @@ server.get('/visits/:id', async (req: FastifyRequest<{ Params: { id: string } }> if (state.status === 'assigned') { if (state.agentStatus === 'tool') { - return state.agentRequest.at(-1)!.content + return state.agentRequest.findLast(isToolCallRequest)!.tool_calls } return { status: state.status, @@ -87,24 +87,34 @@ server.post( throw new Error('Tool call not found') } - visits[req.params.id] = { - ...state, - agentRequest: state.agentRequest.concat({ - role: 'tool', - tool_call_id: toolCall.id, - content: req.body.content, - }), - } + const agentRequest = state.agentRequest.concat({ + role: 'tool', + tool_call_id: toolCall.id, + content: req.body.content, + }) const allToolRequests = toolRequestMessage.tool_calls.map((toolCall) => toolCall.id) const hasAllToolCalls = allToolRequests.every((toolCallId) => - state.agentRequest.some( + agentRequest.some( (request) => 'tool_call_id' in request && request.tool_call_id === toolCallId ) ) + // Add tool response to the workflow + // Change agent status to `step` if all tool calls have been added, so + // runVisit will continue if (hasAllToolCalls) { - runVisit(visits[req.params.id]) + visits[req.params.id] = { + ...state, + agentStatus: 'step', + agentRequest, + } + runVisit(req.params.id) + } else { + visits[req.params.id] = { + ...state, + agentRequest, + } } return { @@ -122,8 +132,22 @@ server.listen({ port }) console.log(s` πŸš€ Server running at http://localhost:${port} - Run 'curl -X POST http://localhost:${port}/visits' to create a new visit - Run 'curl -X POST http://localhost:${port}/visits/:id/messages -d '{"tool_call_id":"...","content":"..."}' to add a message to the visit + Things to do: + + ${chalk.bold('🩺 Create a new visit:')} + ${chalk.gray(`curl -X POST http://localhost:${port}/visits`)} + + ${chalk.bold('πŸ’» Check the status of the visit:')} + ${chalk.gray(`curl -X GET http://localhost:${port}/visits/:id`)} + + ${chalk.bold('πŸ”§ If the workflow is waiting for a tool call, you will get a response like this:')} + ${chalk.gray(`[{"id":"","type":"function"}]`)} + + ${chalk.bold('πŸ“ Add a message to the visit:')} + ${chalk.gray(`curl -X POST http://localhost:${port}/visits/:id/messages H "Content-Type: application/json" -d '{"tool_call_id":"...","content":"..."}'`)} + + Note: + - You can only add messages when the workflow is waiting for a tool call `) type ToolCallMessage = { @@ -135,13 +159,20 @@ type ToolCallMessage = { * Helper function, inspired by `teamwork`. * It will continue running the visit in the background and will stop when the workflow is finished. */ -async function runVisit(state: WorkflowState): Promise { +async function runVisit(id: string) { + const state = visits[id] + if (!state) { + throw new Error('Workflow not found') + } + if ( state.status === 'finished' || (state.status === 'assigned' && state.agentStatus === 'tool') ) { - return state + return } - return runVisit(await iterate(preVisitNoteWorkflow, state)) + visits[id] = await iterate(preVisitNoteWorkflow, state) + + return runVisit(id) } From ab47be9df70665c63102fc2c512e22ec32bdb70d Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 08:17:39 +0400 Subject: [PATCH 18/19] updtae return --- example/src/medical_survey_server.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index 8e6ee58..bd26ed8 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -26,7 +26,10 @@ server.post('/visits', async () => { // Start the visit in the background runVisit(state.id) - return state + return { + id: state.id, + status: state.status, + } }) /** From e8d8e1117484931b27698cb6f154d9d417edafe3 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Mon, 9 Dec 2024 11:40:31 +0400 Subject: [PATCH 19/19] tweak --- website/rspress.config.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/website/rspress.config.ts b/website/rspress.config.ts index fef994c..0748e55 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -1,6 +1,5 @@ import * as path from 'node:path' -import { pluginTypeDoc } from '@rspress/plugin-typedoc' import { defineConfig } from 'rspress/config' // import { pluginTypeDoc } from '@rspress/plugin-typedoc' @@ -12,7 +11,6 @@ export default defineConfig({ light: '/rspress-light-logo.png', dark: '/rspress-dark-logo.png', }, - globalStyles: path.join(__dirname, 'global.css'), themeConfig: { socialLinks: [ { @@ -23,18 +21,6 @@ export default defineConfig({ ], }, plugins: [ -<<<<<<< HEAD - pluginTypeDoc({ - entryPoints: [ - path.join(__dirname, '../packages/framework/src/agent.ts'), - path.join(__dirname, '../packages/framework/src/teamwork.ts'), - path.join(__dirname, '../packages/framework/src/tool.ts'), - path.join(__dirname, '../packages/framework/src/workflow.ts'), - path.join(__dirname, '../packages/framework/src/models/openai.ts'), - path.join(__dirname, '../packages/framework/src/telemetry.ts'), - ], - }), -======= // pluginTypeDoc({ // entryPoints: [ // path.join(__dirname, '../packages/framework/src/agent.ts'), @@ -45,6 +31,5 @@ export default defineConfig({ // path.join(__dirname, '../packages/framework/src/telemetry.ts'), // ], // }), ->>>>>>> main ], })