From 80cb5a924c0b866f893b47d5879443fb28479bde Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 9 Dec 2024 13:33:12 +0400 Subject: [PATCH] feat: create server-side teamwork and organize utilities (#55) Create server version of teamwork from server Reuse internals across them Change return type of teamwork to state, to align with server variant Introduce print helper, that will in the future handle edge cases as images too (console.log isn't good enough) --- README.md | 33 ++++++++++++++++--- example/src/ecommerce_product_description.ts | 4 +-- example/src/medical_survey.ts | 3 +- example/src/medical_survey_server.ts | 22 ++----------- example/src/surprise_trip.ts | 4 +-- packages/framework/src/server.ts | 20 +++++++++++ packages/framework/src/supervisor/nextTick.ts | 9 +++++ packages/framework/src/teamwork.ts | 18 +++------- packages/framework/src/workflow.ts | 8 +++++ 9 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 packages/framework/src/server.ts diff --git a/README.md b/README.md index c43050b..de80c23 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Here is a simple example of a workflow that researches and plans a trip to Wroc ```ts // First, import all the necessary functions import { agent } from '@dead-simple-ai-agent/framework/agent' -import { teamwork } from '@dead-simple-ai-agent/framework/teamwork' +import { teamwork, breakout } from '@dead-simple-ai-agent/framework/teamwork' import { logger } from '@dead-simple-ai-agent/framework/telemetry/console' -import { workflow } from '@dead-simple-ai-agent/framework/workflow' +import { workflow, workflowState } from '@dead-simple-ai-agent/framework/workflow' // Then, define your agents: @@ -104,10 +104,10 @@ const researchTripWorkflow = workflow({ // Finally, you can run the workflow. // This will block until the workflow is completed. -const result = await teamwork(researchTripWorkflow) +const state = await teamwork(researchTripWorkflow) // Don't forget to log the result! -console.log(result) +console.log(solution(state)) ``` ### Running the example @@ -175,9 +175,32 @@ The framework provides two main ways to orchestrate agent collaboration: The `teamwork` function handles complete workflow execution from start to finish, managing the entire process automatically. It's perfect for simple use cases where you want to get results in a single call. ```typescript -const result = await teamwork(workflow) +import { teamwork } from '@dead-simple-ai-agent/framework' + +const state = await teamwork(workflow) ``` +#### Server-side Teamwork + +The server-side version of teamwork is perfectly suited for long-running workflows that require external tool execution or manual intervention. It will not wait for the tool to be executed, but will return the state of the workflow. + +You can then handle tool calls on your own, and call `teamwork` again when ready. + +```typescript +import { teamwork } from '@dead-simple-ai-agent/framework/server' + +// If status is `assigned`, you need to handle tool calls on your own. +// Otherwise, status is `finished` and you can read the result. +const nextState = await teamwork(workflow) +``` + +This pattern is especially useful for: +- Running workflows in serverless environments +- Handling long-running tool executions +- Implementing manual review steps +- Building interactive workflows +- Managing rate limits and quotas + #### Iterate The `iterate` function provides a stateless, step-by-step execution model. Each call returns the new state without maintaining any internal state. diff --git a/example/src/ecommerce_product_description.ts b/example/src/ecommerce_product_description.ts index ed33c63..30ad55d 100644 --- a/example/src/ecommerce_product_description.ts +++ b/example/src/ecommerce_product_description.ts @@ -4,7 +4,7 @@ import { agent } from '@dead-simple-ai-agent/framework/agent' import { teamwork } from '@dead-simple-ai-agent/framework/teamwork' -import { workflow } from '@dead-simple-ai-agent/framework/workflow' +import { solution, workflow } from '@dead-simple-ai-agent/framework/workflow' import { visionTool } from '@dead-simple-ai-agent/tools/vision' const techExpert = agent({ @@ -49,4 +49,4 @@ const productDescriptionWorkflow = workflow({ const result = await teamwork(productDescriptionWorkflow) -console.log(result) +console.log(solution(result)) diff --git a/example/src/medical_survey.ts b/example/src/medical_survey.ts index 55e787e..3bc521d 100644 --- a/example/src/medical_survey.ts +++ b/example/src/medical_survey.ts @@ -1,7 +1,8 @@ import { teamwork } from '@dead-simple-ai-agent/framework/teamwork' +import { solution } from '@dead-simple-ai-agent/framework/workflow' import { preVisitNoteWorkflow } from './medical_survey/workflow.js' const result = await teamwork(preVisitNoteWorkflow) -console.log(result) +console.log(solution(result)) diff --git a/example/src/medical_survey_server.ts b/example/src/medical_survey_server.ts index 3c7908e..20e0b17 100644 --- a/example/src/medical_survey_server.ts +++ b/example/src/medical_survey_server.ts @@ -1,8 +1,8 @@ /** * This example demonstrates using framework in server-side environments. */ +import { teamwork } from '@dead-simple-ai-agent/framework/server' 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 chalk from 'chalk' import s from 'dedent' @@ -158,24 +158,6 @@ type ToolCallMessage = { 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(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 - } - - visits[id] = await iterate(preVisitNoteWorkflow, state) - - return runVisit(id) + visits[id] = await teamwork(preVisitNoteWorkflow, visits[id]) } diff --git a/example/src/surprise_trip.ts b/example/src/surprise_trip.ts index 25a5dc0..2c0d80a 100644 --- a/example/src/surprise_trip.ts +++ b/example/src/surprise_trip.ts @@ -5,7 +5,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' -import { workflow } from '@dead-simple-ai-agent/framework/workflow' +import { solution, workflow } from '@dead-simple-ai-agent/framework/workflow' import { lookupWikipedia } from '../tools.js' @@ -77,4 +77,4 @@ const researchTripWorkflow = workflow({ const result = await teamwork(researchTripWorkflow) -console.log(result) +console.log(solution(result)) diff --git a/packages/framework/src/server.ts b/packages/framework/src/server.ts new file mode 100644 index 0000000..ae944f1 --- /dev/null +++ b/packages/framework/src/server.ts @@ -0,0 +1,20 @@ +import { iterate } from './supervisor/nextTick.js' +import { teamwork as originalTeamwork } from './teamwork.js' +import { workflowState } from './workflow.js' + +/** + * Like teamwork(), but pauses when a task is assigned with tool to let the agent work independently. + */ +export const teamwork: typeof originalTeamwork = async ( + workflow, + state = workflowState(workflow) +) => { + if ( + state.status === 'finished' || + (state.status === 'assigned' && state.agentStatus === 'tool') + ) { + return state + } + + return teamwork(workflow, await iterate(workflow, state)) +} diff --git a/packages/framework/src/supervisor/nextTick.ts b/packages/framework/src/supervisor/nextTick.ts index 69c64e6..93f2b89 100644 --- a/packages/framework/src/supervisor/nextTick.ts +++ b/packages/framework/src/supervisor/nextTick.ts @@ -130,3 +130,12 @@ export async function nextTick(workflow: Workflow, state: WorkflowState): Promis return state } + +/** + * Iterates over the workflow and takes a snapshot of the state after each iteration. + */ +export async function iterate(workflow: Workflow, state: WorkflowState) { + const nextState = await nextTick(workflow, state) + workflow.snapshot({ prevState: state, nextState }) + return nextState +} diff --git a/packages/framework/src/teamwork.ts b/packages/framework/src/teamwork.ts index c4b9de0..31e0ce4 100644 --- a/packages/framework/src/teamwork.ts +++ b/packages/framework/src/teamwork.ts @@ -1,5 +1,4 @@ -import { nextTick } from './supervisor/nextTick.js' -import { MessageContent } from './types.js' +import { iterate, nextTick } from './supervisor/nextTick.js' import { Workflow, WorkflowState, workflowState } from './workflow.js' /** @@ -8,21 +7,14 @@ import { Workflow, WorkflowState, workflowState } from './workflow.js' export async function teamwork( workflow: Workflow, state: WorkflowState = workflowState(workflow) -): Promise { - const { status, messages } = state - - if (status === 'finished') { - return messages.at(-1)!.content +): Promise { + if (state.status === 'finished') { + return state } - return teamwork(workflow, await iterate(workflow, state)) } /** * Iterate performs single iteration over workflow and returns its next state */ -export async function iterate(workflow: Workflow, state: WorkflowState): Promise { - const nextState = await nextTick(workflow, state) - workflow.snapshot({ prevState: state, nextState }) - return nextState -} +export { nextTick as iterate } diff --git a/packages/framework/src/workflow.ts b/packages/framework/src/workflow.ts index 77f2a84..d07fb13 100644 --- a/packages/framework/src/workflow.ts +++ b/packages/framework/src/workflow.ts @@ -86,3 +86,11 @@ export const workflowState = (workflow: Workflow): IdleWorkflowState => { ], } } + +/** + * Prints the last message from the workflow state in user-friendly format. + */ +export const solution = (state: WorkflowState) => { + // tbd: handle different message shapes + return state.messages.at(-1)?.content +}