Skip to content

Commit

Permalink
feat: abstract model and remove context (#16)
Browse files Browse the repository at this point in the history
Quite a few changes, but here's breakdown:

- Removed context and made it flat with Workflow
- Removed workflow.message.push in favor of passing messages as second
argument in recursion. This will make workflow immutable and less stable
in case it goes in parallel
- Moved selectAgent and getNextTask to supervisor 
- Moved remaining files from index to teamwork (this is where main
execution happens)
  • Loading branch information
grabbou authored Dec 7, 2024
1 parent ae03aee commit 0c38b45
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 224 deletions.
3 changes: 1 addition & 2 deletions example/surprise_trip.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/**
* Example borrowed from CrewAI.
*/
import { teamwork } from '@dead-simple-ai-agent/framework'

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 { lookupWikipedia } from './tools.js'
Expand Down
3 changes: 3 additions & 0 deletions packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
},
"./agent": {
"bun": "./src/agent.ts"
},
"./teamwork": {
"bun": "./src/teamwork.ts"
}
},
"type": "module",
Expand Down
5 changes: 3 additions & 2 deletions packages/framework/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { openai, Provider } from './models/openai.js'
import { Tool } from './tool.js'
import { RequiredOptionals } from './types.js'

Expand All @@ -7,12 +8,12 @@ type AgentOptions = {
tools?: {
[key: string]: Tool
}
model?: string
provider?: Provider
}

const defaults: RequiredOptionals<AgentOptions> = {
tools: {},
model: 'gpt-4o',
provider: openai(),
}

/**
Expand Down
54 changes: 10 additions & 44 deletions packages/framework/src/executor.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,10 @@
import s from 'dedent'
import OpenAI from 'openai'
import { zodFunction, zodResponseFormat } from 'openai/helpers/zod'
import { z } from 'zod'

import { Agent } from './agent.js'
import { Message } from './types.js'

const openai = new OpenAI()

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:
<workflow>${options.workflow.description}</workflow>
<output>${options.workflow.output}</output>
`,
},
],
}
}

export type Context = Required<ContextOptions>

// tbd: helper utilities to create contexts from workflows with concrete single task etc.

export async function executeTaskWithAgent(
Expand All @@ -57,22 +24,21 @@ export async function executeTaskWithAgent(
)
: []

const response = await openai.beta.chat.completions.parse({
model: agent.model as string,
const response = await agent.provider.completions({
// tbd: verify the prompt
messages: [
{
role: 'system',
content: s`
You are ${agent.role}. ${agent.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.
`,
You are ${agent.role}. ${agent.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,
],
Expand Down
175 changes: 0 additions & 175 deletions packages/framework/src/index.ts

This file was deleted.

40 changes: 40 additions & 0 deletions packages/framework/src/models/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import OpenAI, { ClientOptions } from 'openai'
import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/index.mjs'

import { RequiredOptionals } from '../types.js'

type OpenAIOptions = {
model?: string
options?: ClientOptions
}

const defaults: RequiredOptionals<OpenAIOptions> = {
model: 'gpt-4o',
options: {},
}

type ChatCompletionParseParams = ChatCompletionCreateParamsNonStreaming

/**
* Helper utility to create a model configuration with defaults.
*/
export const openai = (options?: OpenAIOptions) => {
const config = {
...defaults,
...options,
}

const client = new OpenAI(config.options)

return {
model: config.model,
completions: <T extends Omit<ChatCompletionParseParams, 'model'>>(params: T) =>
client.beta.chat.completions.parse.bind(client.beta.chat.completions)({
...params,
model: config.model,
}),
client,
}
}

export type Provider = ReturnType<typeof openai>
62 changes: 62 additions & 0 deletions packages/framework/src/supervisor/nextTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import s from 'dedent'
import { zodResponseFormat } from 'openai/helpers/zod.mjs'
import { z } from 'zod'

import { Provider } from '../models/openai.js'
import { Message } from '../types.js'

export async function getNextTask(provider: Provider, history: Message[]): Promise<string | null> {
const response = await provider.completions({
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')
}
}
Loading

0 comments on commit 0c38b45

Please sign in to comment.