diff --git a/example/src/surprise_trip.ts b/example/src/surprise_trip.ts index 8be9548..f92e70c 100644 --- a/example/src/surprise_trip.ts +++ b/example/src/surprise_trip.ts @@ -1,4 +1,5 @@ import { agent } from 'fabrice-ai/agent' +import { parallelSupervisor } from 'fabrice-ai/agents/supervisor' import { solution } from 'fabrice-ai/solution' import { teamwork } from 'fabrice-ai/teamwork' import { logger } from 'fabrice-ai/telemetry' @@ -48,6 +49,7 @@ const researchTripWorkflow = workflow({ restaurantScout, landmarkScout, itineraryCompiler, + supervisor: parallelSupervisor({ parallelism: 2 }), }, description: ` Research and find cool things to do in Wrocław, Poland. diff --git a/packages/framework/src/agent.ts b/packages/framework/src/agent.ts index 078ee65..322e9b9 100644 --- a/packages/framework/src/agent.ts +++ b/packages/framework/src/agent.ts @@ -2,10 +2,11 @@ import s from 'dedent' import { zodFunction, zodResponseFormat } from 'openai/helpers/zod.js' import { z } from 'zod' -import { Message, request, response } from './messages.js' +import { request, response } from './messages.js' import { openai, Provider } from './models.js' import { finish, WorkflowState } from './state.js' import { Tool } from './tool.js' +import { Message } from './types.js' import { Workflow } from './workflow.js' export type AgentOptions = Partial diff --git a/packages/framework/src/agents/supervisor.ts b/packages/framework/src/agents/supervisor.ts index 8db6c88..8fa2532 100644 --- a/packages/framework/src/agents/supervisor.ts +++ b/packages/framework/src/agents/supervisor.ts @@ -7,74 +7,138 @@ import { Message, system } from '../messages.js' import { request, response } from '../messages.js' import { delegate } from '../state.js' -const defaults: AgentOptions = { - run: async (state, context, workflow) => { - const [workflowRequest, ...messages] = state.messages - - const res = await workflow.team[state.agent].provider.completions({ - messages: [ - system(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 and what has been completed so far. +type ParallelSupervisorOptions = AgentOptions & { + parallelism?: number +} - You can run tasks in parallel, if they do not depend on each other results. Otherwise, run them sequentially. +export const parallelSupervisor = (options: ParallelSupervisorOptions) => { + const { parallelism = 3 } = options - 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 - `), - response('What is the request?'), - workflowRequest, - response('What has been completed so far?'), - ...getSteps(messages), - ], - temperature: 0.2, - response_format: zodResponseFormat( - z.object({ - tasks: z - .array( - z.object({ - task: z.string().describe('The next task to be completed'), - reasoning: z.string().describe('The reasoning for selecting the next task'), - }) - ) - .describe('Next tasks, or empty array if the workflow is complete'), - }), - 'next_tasks' - ), - }) + return agent({ + run: async (state, context, workflow) => { + const [workflowRequest, ...messages] = state.messages - try { - const content = res.choices[0].message.parsed - if (!content) { - throw new Error('No content in response') - } + const res = await workflow.team[state.agent].provider.completions({ + messages: [ + system(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 and what has been completed so far. - if (content.tasks.length === 0) { - return { - ...state, - status: 'finished', + You can run tasks in parallel, if they do not depend on each other results. + Otherwise, you must run them sequentially. + + Rules: + 1. Each task should be self-contained and achievable + 2. Tasks should be specific and actionable + 3. Return empty array if all required tasks are completed + 4. Consider dependencies and order of operations + 5. Use context from completed tasks to inform next steps + 6. You can run up to "${parallelism}" tasks in parallel. + `), + response('What is the request?'), + workflowRequest, + response('What has been completed so far?'), + ...getSteps(messages), + ], + temperature: 0.2, + response_format: zodResponseFormat( + z.object({ + tasks: z + .array( + z.object({ + task: z.string().describe('The next task to be completed'), + reasoning: z.string().describe('The reasoning for selecting the next task'), + }) + ) + .describe('Next tasks, or empty array if the workflow is complete'), + }), + 'next_tasks' + ), + }) + try { + const content = res.choices[0].message.parsed + if (!content) { + throw new Error('No content in response') + } + if (content.tasks.length === 0) { + return { + ...state, + status: 'finished', + } } + return delegate( + state, + content.tasks.map((item) => ['resourcePlanner', request(item.task)]) + ) + } catch (error) { + throw new Error('Failed to determine next task') } - - return delegate( - state, - content.tasks.map((item) => ['resourcePlanner', request(item.task)]) - ) - } catch (error) { - throw new Error('Failed to determine next task') - } - }, + }, + ...options, + }) } -export const supervisor = (options?: AgentOptions) => - agent({ - ...defaults, +export const supervisor = (options: AgentOptions) => { + return agent({ + run: async (state, context, workflow) => { + const [workflowRequest, ...messages] = state.messages + + const res = await workflow.team[state.agent].provider.completions({ + messages: [ + { + role: 'system', + 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 and what has been completed so far. + + 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 + `, + }, + response('What is the request?'), + workflowRequest, + response('What has been completed so far?'), + ...getSteps(messages), + ], + 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 = res.choices[0].message.parsed + if (!content) { + throw new Error('No content in response') + } + if (!content.task) { + return { + ...state, + status: 'finished', + } + } + return delegate(state, [['resourcePlanner', request(content.task)]]) + } catch (error) { + throw new Error('Failed to determine next task') + } + }, ...options, }) +} const getSteps = (conversation: Message[]): Message[] => { const messagePairs = conversation.reduce(