diff --git a/packages/framework/src/agent.ts b/packages/framework/src/agent.ts index 6c0a202..cd0147b 100644 --- a/packages/framework/src/agent.ts +++ b/packages/framework/src/agent.ts @@ -1,7 +1,7 @@ import { Tool } from './tool.js' import { RequiredOptionals } from './types.js' -export type AgentOptions = { +type AgentOptions = { role: string description: string tools?: { @@ -18,9 +18,11 @@ const defaults: RequiredOptionals = { /** * Helper utility to create an agent with defaults. */ -export const agent = (options: AgentOptions) => { +export const agent = (options: AgentOptions): Agent => { return { ...defaults, ...options, } } + +export type Agent = Required diff --git a/packages/framework/src/executor.ts b/packages/framework/src/executor.ts new file mode 100644 index 0000000..92eb222 --- /dev/null +++ b/packages/framework/src/executor.ts @@ -0,0 +1,34 @@ +import s from 'dedent' + +import { Message } from './types.js' +import { Workflow } from './workflow.js' + +// tbd: add more context like trace, callstack etc. context should be serializable +type ContextOptions = { + workflow: Workflow + // tbd: move messages to something such as memory + messages?: Message[] +} + +/** + * Helper utility to create a context with defaults. + */ +export const context = (options: ContextOptions): Context => { + return { + ...options, + messages: [ + { + role: 'assistant', + content: s` + Here is description of the workflow and expected output by the user: + ${options.workflow.description} + ${options.workflow.output} + `, + }, + ], + } +} + +export type Context = Required + +// tbd: helper utilities to create contexts from workflows with concrete single task etc. diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index beac631..5d5c127 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -4,7 +4,9 @@ import { zodFunction, zodResponseFormat } from 'openai/helpers/zod' import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions' import { z, ZodType, ZodTypeAny } from 'zod' +import { Context, context } from './executor.js' import { Tool } from './tool.js' +import { Workflow, WorkflowContext } from './workflow.js' // tbd: abstract this away or not? most APIs are OpenAI compatible const openai = new OpenAI() @@ -176,30 +178,15 @@ export class Agent { } } -export async function teamwork( - workflow: Workflow, - context: WorkflowContext = { - messages: [ - { - role: 'assistant', - content: s` - Here is description of the workflow and expected output by the user: - ${workflow.description} - ${workflow.output} - `, - }, - ], - } -): Promise { - // tbd: set reasonable max iterations +async function execute(context: Context): Promise { // eslint-disable-next-line no-constant-condition const task = await getNextTask(context.messages) if (!task) { return context.messages.at(-1)!.content as string // end of the recursion } - if (workflow.maxIterations && context.messages.length > workflow.maxIterations) { - console.debug('Max iterations exceeded ', workflow.maxIterations) + if (context.workflow.maxIterations && context.messages.length > context.workflow.maxIterations) { + console.debug('Max iterations exceeded ', context.workflow.maxIterations) return context.messages.at(-1)!.content as string } @@ -211,13 +198,13 @@ export async function teamwork( }) // tbd: this throws, handle it - const selectedAgent = await selectAgent(task, workflow.members) + const selectedAgent = await selectAgent(task, context.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(context.messages, workflow.members) + const result = await selectedAgent.executeTask(context.messages, context.workflow.members) context.messages.push({ role: 'assistant', content: result, @@ -230,23 +217,16 @@ export async function teamwork( }) } - return teamwork(workflow, context) // next iteration + return execute(context) // next iteration } -type Workflow = { - description: string - output: string - members: Agent[] - maxIterations?: number +export async function teamwork(workflow: Workflow): Promise { + const ctx = context({ workflow }) + return execute(ctx) } type Message = ChatCompletionMessageParam -type WorkflowContext = { - messages: Message[] - // tbd: add more context like trace, callstack etc. context should be serializable -} - async function selectAgent(task: string, agents: Agent[]): Promise { const response = await openai.beta.chat.completions.parse({ model: 'gpt-4o', diff --git a/packages/framework/src/types.ts b/packages/framework/src/types.ts index 55d4ce2..f09cf35 100644 --- a/packages/framework/src/types.ts +++ b/packages/framework/src/types.ts @@ -1,3 +1,5 @@ +import { ChatCompletionMessageParam } from 'openai/resources/index.mjs' + /** * Utility type to get optional keys from T. */ @@ -14,3 +16,5 @@ export type OptionalProperties = Pick> * Utility type to make optional properties required (only includes optional props). */ export type RequiredOptionals = Required> + +export type Message = ChatCompletionMessageParam diff --git a/packages/framework/src/workflow.ts b/packages/framework/src/workflow.ts new file mode 100644 index 0000000..73a9151 --- /dev/null +++ b/packages/framework/src/workflow.ts @@ -0,0 +1,26 @@ +import { Agent } from './agent.js' +import { RequiredOptionals } from './types.js' + +type WorkflowOptions = { + description: string + output: string + members: Agent[] + maxIterations?: number +} + +const defaults: RequiredOptionals = { + // tbd: set reasonable max iterations + maxIterations: 50, +} + +/** + * Helper utility to create a workflow with defaults. + */ +export const workflow = (options: WorkflowOptions): Workflow => { + return { + ...defaults, + ...options, + } +} + +export type Workflow = Required