Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve abstraction for external providers #123

Merged
merged 29 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cbeb938
init
grabbou Dec 14, 2024
5fdb43e
chore: add temp
grabbou Dec 14, 2024
d830b83
feat
grabbou Dec 14, 2024
901e820
Tweaks
grabbou Dec 14, 2024
2b39f6a
update
grabbou Dec 14, 2024
f988a28
clean-up
grabbou Dec 14, 2024
4e1a215
draft
grabbou Dec 14, 2024
fdb08e9
up
grabbou Dec 15, 2024
3597956
save
grabbou Dec 15, 2024
9f0519b
tweaks
grabbou Dec 15, 2024
82ccbc2
up
grabbou Dec 15, 2024
0105ae8
fix: traveler preferences added as randomly required by grok
pkarw Dec 15, 2024
1fcd0a6
fix: defaults removed from parameters schema for tools calls
pkarw Dec 15, 2024
38d80a2
move things around again
grabbou Dec 15, 2024
71c6146
fix: wikipedia_vector `wikipediaResearch` instructions preciesed
pkarw Dec 15, 2024
e4ac6b8
Merge branch 'feat/completions-2' of github.com:grabbou/ai-agent-fram…
grabbou Dec 15, 2024
da2c799
Merge branch 'feat/completions-2' of github.com:grabbou/ai-agent-fram…
grabbou Dec 15, 2024
f3f1840
fix: firecrawl optional parameter removed
pkarw Dec 15, 2024
0bb26e2
Merge branch 'feat/completions-2' of https://github.com/callstackincu…
pkarw Dec 15, 2024
3ff8423
fix: instruction added to use the visionTool only once
pkarw Dec 15, 2024
e68e34d
tweak
grabbou Dec 15, 2024
3b425c1
tweaks
grabbou Dec 15, 2024
48ddef3
fix: FS operations optimized
pkarw Dec 15, 2024
832eebd
Merge branch 'feat/completions-2' of https://github.com/callstackincu…
pkarw Dec 15, 2024
5074f89
save
grabbou Dec 15, 2024
a39dc03
Merge branch 'feat/completions-2' of github.com:grabbou/ai-agent-fram…
grabbou Dec 15, 2024
94b615e
chore: docs
grabbou Dec 15, 2024
74e024a
chore
grabbou Dec 15, 2024
ae299d5
save
grabbou Dec 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,36 @@ This approach offers several benefits:

This functional approach makes the framework particularly well-suited for building long-running workflows that are distributed across multiple servers in the cloud.

## Guides

### Custom providers

By default, Fabrice uses OpenAI `gpt-4o` model. You can change the default model either for the entire system, or for specific agent.

To do it for the entire workflow:

```ts
import { ollama } from 'fabrice-ai/providers/ollama'

workflow({
/** other options go here */
provider: ollama({ model: 'llama3.2' })
})
```

To change it for specific agent:

```ts
import { ollama } from 'fabrice-ai/providers/ollama'

agent({
/** other options go here */
provider: ollama({ model: 'llama3.2' })
})
```

Note that agent provider always takes precedence over workflow provider. Tools always receive provider from the agent that triggered their execution.

## Contributors

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
Expand Down
Binary file modified bun.lockb
Binary file not shown.
6 changes: 5 additions & 1 deletion example/src/medical_survey.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { grok } from 'fabrice-ai/providers/grok'
import { solution } from 'fabrice-ai/solution'
import { teamwork } from 'fabrice-ai/teamwork'

import { preVisitNoteWorkflow } from './medical_survey/workflow.js'

const result = await teamwork(preVisitNoteWorkflow)
const result = await teamwork({
...preVisitNoteWorkflow,
provider: grok(),
})

console.log(solution(result))
5 changes: 5 additions & 0 deletions example/src/medical_survey/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const nurse = agent({
You can ask patient questions about their health and symptoms by running "askPatient" tool.
You can only ask one question at a time.

Do not ask the same question twice.
If patient skips a question, ask another question.

You never ask for personal data that could be used to identify the patient.
`,
tools: {
Expand All @@ -33,6 +36,8 @@ export const preVisitNoteWorkflow = workflow({

You can only ask up to 5 questions in total.
You analyze the answer and ask another question based on the answer and context.

Start with a question about the patient's current symptoms.
`,
output: `
Comprehensive markdown pre-visit report that covers:
Expand Down
14 changes: 14 additions & 0 deletions example/src/medical_survey_ollama.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ollama } from 'fabrice-ai/providers/ollama'
import { solution } from 'fabrice-ai/solution'
import { teamwork } from 'fabrice-ai/teamwork'

import { preVisitNoteWorkflow } from './medical_survey/workflow.js'

const result = await teamwork({
...preVisitNoteWorkflow,
provider: ollama({
model: 'llama3.1',
}),
})

console.log(solution(result))
12 changes: 8 additions & 4 deletions example/src/surprise_trip.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { agent } from 'fabrice-ai/agent'
import { grok } from 'fabrice-ai/providers/grok'
import { ollama } from 'fabrice-ai/providers/ollama'
import { solution } from 'fabrice-ai/solution'
import { teamwork } from 'fabrice-ai/teamwork'
import { workflow } from 'fabrice-ai/workflow'
Expand Down Expand Up @@ -50,7 +52,7 @@ const researchTripWorkflow = workflow({
Research and find cool things to do in Wrocław, Poland.

Focus:
- activities and events that match the traveler's interests and age group.
- activities and events that match the traveler's age group.
- highly-rated restaurants and dining experiences.
- landmarks with historic context.
- picturesque and entertaining locations.
Expand All @@ -60,16 +62,18 @@ const researchTripWorkflow = workflow({
- Origin: New York, USA
- Destination: Wrocław, Poland
- Age of the traveler: 30
- Hotel location: Main Square, Wrocław
- Hotel location: Hilton, Main Square, Wrocław
- Flight information: Flight AA123, arriving on 2023-12-15
- How long is the trip: 7 days

Consider flights confirmed.
Flights and hotels are already confirmed.
`,
output: `
Comprehensive day-by-day plan for the trip to Wrocław, Poland.
Ensure the plan includes flights, hotel information, and all planned activities and dining experiences.
`,
// provider: grok(),
// provider: ollama(),
})

const result = await teamwork(researchTripWorkflow)
Expand Down
9 changes: 8 additions & 1 deletion packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@
"types": "./dist/types/*.d.ts",
"require": "./dist/*.cjs",
"import": "./dist/*.js"
},
"./providers/*": {
"bun": "./src/providers/*.ts",
"types": "./dist/providers/*.d.ts",
"require": "./dist/providers/*.cjs",
"import": "./dist/providers/*.js"
}
},
"type": "module",
"dependencies": {
"chalk": "^5.3.0",
"dedent": "^1.5.3",
"openai": "^4.76.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"license": "MIT",
"repository": {
Expand Down
85 changes: 34 additions & 51 deletions packages/framework/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import s from 'dedent'
import { zodFunction, zodResponseFormat } from 'openai/helpers/zod.js'
import { z } from 'zod'

import { assistant, getSteps, system, user } from './messages.js'
import { openai, Provider } from './models.js'
import { assistant, getSteps, system, toolCalls, user } from './messages.js'
import { Provider } from './models.js'
import { finish, WorkflowState } from './state.js'
import { Tool } from './tool.js'
import { Message } from './types.js'
Expand All @@ -17,33 +16,28 @@ export type Agent = {
tools: {
[key: AgentName]: Tool
}
provider: Provider
run: (state: WorkflowState, context: Message[], workflow: Workflow) => Promise<WorkflowState>
provider?: Provider
run: (
provider: Provider,
state: WorkflowState,
context: Message[],
workflow: Workflow
) => Promise<WorkflowState>
}

export const agent = (options: AgentOptions = {}): Agent => {
const { description, tools = {}, provider = openai() } = options
const { description, tools = {}, provider } = options

return {
description,
tools,
provider,
run:
options.run ??
(async (state, context, workflow) => {
const mappedTools = tools
? Object.entries(tools).map(([name, tool]) =>
zodFunction({
name,
parameters: tool.parameters,
description: tool.description,
})
)
: []

(async (provider, state, context, workflow) => {
const [, ...messages] = context

const response = await provider.completions({
const response = await provider.chat({
messages: [
system(s`
${description}
Expand All @@ -56,6 +50,7 @@ export const agent = (options: AgentOptions = {}): Agent => {
Do not fabricate or assume information not present in the input.

Try to complete the task on your own.
If you do not have tool to call, use general knowledge to complete the task.
`),
assistant('What have been done so far?'),
user(
Expand All @@ -68,54 +63,42 @@ export const agent = (options: AgentOptions = {}): Agent => {
assistant('What is the task assigned to me?'),
...state.messages,
],
tools: mappedTools.length > 0 ? mappedTools : undefined,
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'),
nextStep: z
.string()
.nullable()
.describe('The next step to complete the task, or null if task is complete'),
}),
z.object({
kind: z.literal('error'),
reasoning: z.string().describe('The reason why you cannot complete the task'),
}),
]),
tools,
response_format: {
step: z.object({
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'),
nextStep: z
.string()
.nullable()
.describe('The next step to complete the task, or null if task is complete'),
}),
error: z.object({
reasoning: z.string().describe('The reason why you cannot complete the task'),
}),
'task_result'
),
},
})

if (response.choices[0].message.tool_calls.length > 0) {
if (response.type === 'tool_call') {
return {
...state,
status: 'paused',
messages: [...state.messages, response.choices[0].message],
messages: [...state.messages, toolCalls(response.value)],
}
}

const message = response.choices[0].message.parsed
if (!message) {
throw new Error('No parsed response received')
}

if (message.response.kind === 'error') {
throw new Error(message.response.reasoning)
if (response.type === 'error') {
throw new Error(response.value.reasoning)
}

const agentResponse = assistant(message.response.result)
const agentResponse = assistant(response.value.result)

if (message.response.nextStep) {
if (response.value.nextStep) {
return {
...state,
status: 'running',
messages: [...state.messages, agentResponse, user(message.response.nextStep)],
messages: [...state.messages, agentResponse, user(response.value.nextStep)],
}
}

Expand Down
20 changes: 7 additions & 13 deletions packages/framework/src/agents/final_boss.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import s from 'dedent'
import { zodResponseFormat } from 'openai/helpers/zod'
import { z } from 'zod'

import { agent, AgentOptions } from '../agent.js'
Expand All @@ -8,8 +7,8 @@ import { user } from '../messages.js'
import { finish } from '../state.js'

const defaults: AgentOptions = {
run: async (state, context, workflow) => {
const response = await workflow.team[state.agent].provider.completions({
run: async (provider, state, context) => {
const response = await provider.chat({
messages: [
{
role: 'system',
Expand All @@ -23,18 +22,13 @@ const defaults: AgentOptions = {
the main goal while responding with the final answer
`),
],
response_format: zodResponseFormat(
z.object({
finalAnswer: z.string().describe('The final result of the task'),
response_format: {
task_result: z.object({
final_answer: z.string().describe('The final result of the task'),
}),
'task_result'
),
},
})
const message = response.choices[0].message.parsed
if (!message) {
throw new Error('No parsed response received')
}
return finish(state, assistant(message.finalAnswer))
return finish(state, assistant(response.value.final_answer))
},
}

Expand Down
20 changes: 6 additions & 14 deletions packages/framework/src/agents/resource_planner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import s from 'dedent'
import { zodResponseFormat } from 'openai/helpers/zod'
import { z } from 'zod'

import { agent, AgentOptions } from '../agent.js'
Expand All @@ -8,8 +7,8 @@ import { user } from '../messages.js'
import { handoff } from '../state.js'

const defaults: AgentOptions = {
run: async (state, context, workflow) => {
const response = await workflow.team[state.agent].provider.completions({
run: async (provider, state, context, workflow) => {
const response = await provider.chat({
messages: [
{
role: 'system',
Expand All @@ -35,21 +34,14 @@ const defaults: AgentOptions = {
...state.messages,
],
temperature: 0.1,
response_format: zodResponseFormat(
z.object({
response_format: {
select_agent: z.object({
agent: z.enum(Object.keys(workflow.team) as [string, ...string[]]),
reasoning: z.string(),
}),
'agent_selection'
),
},
})

const message = response.choices[0].message.parsed
if (!message) {
throw new Error('No content in response')
}

return handoff(state, message.agent)
return handoff(state, response.value.agent)
},
}

Expand Down
Loading