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: implement server with background jobs and resume when all answers are provided #49

Merged
merged 23 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
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
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"dependencies": {
"@dead-simple-ai-agent/framework": "0.0.1",
"@langchain/community": "^0.3.17"
"@langchain/community": "^0.3.17",
"fastify": "^5.1.0"
}
}
57 changes: 28 additions & 29 deletions example/src/medical_survey/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,47 +25,46 @@ const askPatient = tool({
})

const nurse = agent({
role: 'Nurse,doctor assistant',
role: 'Nurse',
description: `
You are skille nurse / doctor assistant.
You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit.
Ask user questions about the patient's health and symptoms.
Ask one question at time up to 5 questions.
`,
You are skille nurse / doctor assistant.
You role is to cooperate with reporter to create a pre-visit note for a patient that is about to come for a visit.
Ask user questions about the patient's health and symptoms.
Ask one question at time up to 5 questions.
`,
tools: {
ask_question: askPatient,
askPatient,
},
})

const reporter = agent({
role: 'Reporter',
description: `
You are skilled at preparing great looking markdown reports.
Prepare a report for a patient that is about to come for a visit.
Add info about the patient's health and symptoms.
`,
tools: {},
You are skilled at preparing great looking reports.
You can prepare a report for a patient that is about to come for a visit.
Add info about the patient's health and symptoms.
`,
})

export const preVisitNoteWorkflow = workflow({
members: [nurse, reporter],
description: `
Create a pre-visit note for a patient that is about to come for a visit.
The note should include the patient's health and symptoms.
Include:
- symptoms,
- health issues,
- medications,
- allergies,
- surgeries
Never ask fo:
- personal data,
- sensitive data,
- any data that can be used to identify the patient.
`,
Create a pre-visit note for a patient that is about to come for a visit.
The note should include the patient's health and symptoms.

Include:
- symptoms,
- health issues,
- medications,
- allergies,
- surgeries

Never ask fo:
- personal data,
- sensitive data,
- any data that can be used to identify the patient.
`,
output: `
A markdown report for the patient's pre-visit note.
`,
A markdown report for the patient's pre-visit note.
`,
})
181 changes: 181 additions & 0 deletions example/src/medical_survey_server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* This example demonstrates using framework in server-side environments.
*/
import { isToolCallRequest } from '@dead-simple-ai-agent/framework/supervisor/runTools'
import { iterate } from '@dead-simple-ai-agent/framework/teamwork'
import { workflow, WorkflowState, workflowState } from '@dead-simple-ai-agent/framework/workflow'
import chalk from 'chalk'
import s from 'dedent'
import fastify, { FastifyRequest } from 'fastify'

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

const server = fastify({ logger: false })

const visits: Record<string, WorkflowState> = {}

/**
* This will create a new workflow and return the initial state
*/
server.post('/visits', async () => {
const state = workflowState(preVisitNoteWorkflow)

// Add the state to the visits map
visits[state.id] = state

// Start the visit in the background
runVisit(state.id)

return {
id: state.id,
status: state.status,
}
})

/**
* Call this endpoint to get status of the workflow, or the final result.
*/
server.get('/visits/:id', async (req: FastifyRequest<{ Params: { id: string } }>) => {
const state = visits[req.params.id]
if (!state) {
throw new Error('Workflow not found')
}

if (state.status === 'finished') {
return {
status: state.status,
result: state.messages.at(-1)!.content,
}
}

if (state.status === 'assigned') {
if (state.agentStatus === 'tool') {
return state.agentRequest.findLast(isToolCallRequest)!.tool_calls
}
return {
status: state.status,
agentStatus: state.agentStatus,
}
}

return {
status: state.status,
}
})

/**
* Adds a message to the workflow.
*/
server.post(
'/visits/:id/messages',
async (req: FastifyRequest<{ Params: { id: string }; Body: ToolCallMessage }>) => {
const state = visits[req.params.id]
if (!state) {
throw new Error('Workflow not found')
}

if (state.status !== 'assigned' || state.agentStatus !== 'tool') {
throw new Error('Workflow is not waiting for a message right now')
}

const toolRequestMessage = state.agentRequest.findLast(isToolCallRequest)
if (!toolRequestMessage) {
throw new Error('No tool request message found')
}

const toolCall = toolRequestMessage.tool_calls.find(
(toolCall) => toolCall.id === req.body.tool_call_id
)
if (!toolCall) {
throw new Error('Tool call not found')
}

const agentRequest = state.agentRequest.concat({
role: 'tool',
tool_call_id: toolCall.id,
content: req.body.content,
})

const allToolRequests = toolRequestMessage.tool_calls.map((toolCall) => toolCall.id)
const hasAllToolCalls = allToolRequests.every((toolCallId) =>
agentRequest.some(
(request) => 'tool_call_id' in request && request.tool_call_id === toolCallId
)
)

// Add tool response to the workflow
// Change agent status to `step` if all tool calls have been added, so
// runVisit will continue
if (hasAllToolCalls) {
visits[req.params.id] = {
...state,
agentStatus: 'step',
agentRequest,
}
runVisit(req.params.id)
} else {
visits[req.params.id] = {
...state,
agentRequest,
}
}

return {
hasAllToolCalls,
}
}
)

/**
* Start the server
*/
const port = parseInt(process.env['PORT'] || '3000')
server.listen({ port })

console.log(s`
🚀 Server running at http://localhost:${port}

Things to do:

${chalk.bold('🩺 Create a new visit:')}
${chalk.gray(`curl -X POST http://localhost:${port}/visits`)}

${chalk.bold('💻 Check the status of the visit:')}
${chalk.gray(`curl -X GET http://localhost:${port}/visits/:id`)}

${chalk.bold('🔧 If the workflow is waiting for a tool call, you will get a response like this:')}
${chalk.gray(`[{"id":"<tool_call_id>","type":"function"}]`)}

${chalk.bold('📝 Add a message to the visit:')}
${chalk.gray(`curl -X POST http://localhost:${port}/visits/:id/messages H "Content-Type: application/json" -d '{"tool_call_id":"...","content":"..."}'`)}

Note:
- You can only add messages when the workflow is waiting for a tool call
`)

type ToolCallMessage = {
tool_call_id: string
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)
}
28 changes: 0 additions & 28 deletions example/src/medical_survey_stateless.ts

This file was deleted.

4 changes: 2 additions & 2 deletions example/src/surprise_trip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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/console'
import { workflow } from '@dead-simple-ai-agent/framework/workflow'

import { lookupWikipedia } from '../tools.js'
Expand Down Expand Up @@ -72,7 +71,8 @@ const researchTripWorkflow = workflow({
Comprehensive day-by-day itinerary for the trip to Wrocław, Poland.
Ensure the itinerary integrates flights, hotel information, and all planned activities and dining experiences.
`,
telemetry: logger,
// Uncomment to see the workflow state in the console
// snapshot: logger,
})

const result = await teamwork(researchTripWorkflow)
Expand Down
3 changes: 0 additions & 3 deletions packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
},
"./models/*": {
"bun": "./src/models/*.ts"
},
"./telemetry/*": {
"bun": "./src/telemetry/*.ts"
}
},
"type": "module",
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/src/supervisor/nextTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ 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> {
export async function nextTask(provider: Provider, history: Message[]): Promise<string | null> {
const response = await provider.completions({
messages: [
{
role: 'system',
// tbd: handle subsequent failures
// tbd: include max iterations in system prompt
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.
Expand Down
Loading