diff --git a/packages/components/nodes/agents/BabyAGI/BabyAGI.ts b/packages/components/nodes/agents/BabyAGI/BabyAGI.ts index 8b297215..e8f56aa2 100644 --- a/packages/components/nodes/agents/BabyAGI/BabyAGI.ts +++ b/packages/components/nodes/agents/BabyAGI/BabyAGI.ts @@ -1,15 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { Configuration, CreateChatCompletionRequest, CreateCompletionRequest, OpenAIApi } from 'openai' -import { PineconeClient } from '@pinecone-database/pinecone' -import { CreateIndexRequest } from '@pinecone-database/pinecone/dist/pinecone-generated-ts-fetch' -import { VectorOperationsApi } from '@pinecone-database/pinecone/dist/pinecone-generated-ts-fetch' -import { v4 as uuidv4 } from 'uuid' - -interface Task { - id: string - name: string - priority: number // 1 is highest priority -} +import { BabyAGI } from './core' +import { BaseChatModel } from 'langchain/chat_models' +import { VectorStore } from 'langchain/vectorstores' class BabyAGI_Agents implements INode { label: string @@ -26,351 +18,45 @@ class BabyAGI_Agents implements INode { this.name = 'babyAGI' this.type = 'BabyAGI' this.category = 'Agents' - this.icon = 'babyagi.svg' + this.icon = 'babyagi.jpg' this.description = 'Task Driven Autonomous Agent which creates new task and reprioritizes task list based on objective' + this.baseClasses = ['BabyAGI'] this.inputs = [ + { + label: 'Chat Model', + name: 'model', + type: 'BaseChatModel' + }, + { + label: 'Vector Store', + name: 'vectorStore', + type: 'VectorStore' + }, { label: 'Task Loop', name: 'taskLoop', type: 'number', default: 3 - }, - { - label: 'OpenAI Api Key', - name: 'openAIApiKey', - type: 'password' - }, - { - label: 'Pinecone Api Key', - name: 'pineconeApiKey', - type: 'password' - }, - { - label: 'Pinecone Environment', - name: 'pineconeEnv', - type: 'string' - }, - { - label: 'Pinecone Index', - name: 'pineconeIndex', - type: 'string' - }, - { - label: 'Model Name', - name: 'modelName', - type: 'options', - options: [ - { - label: 'gpt-4', - name: 'gpt-4' - }, - { - label: 'gpt-4-0314', - name: 'gpt-4-0314' - }, - { - label: 'gpt-4-32k-0314', - name: 'gpt-4-32k-0314' - }, - { - label: 'gpt-3.5-turbo', - name: 'gpt-3.5-turbo' - }, - { - label: 'gpt-3.5-turbo-0301', - name: 'gpt-3.5-turbo-0301' - } - ], - default: 'gpt-3.5-turbo', - optional: true } ] } - async getBaseClasses(): Promise { - return ['BabyAGI'] - } + async init(nodeData: INodeData): Promise { + const model = nodeData.inputs?.model as BaseChatModel + const vectorStore = nodeData.inputs?.vectorStore as VectorStore + const taskLoop = nodeData.inputs?.taskLoop as string - async init(): Promise { - return null + const babyAgi = BabyAGI.fromLLM(model, vectorStore, parseInt(taskLoop, 10)) + return babyAgi } async run(nodeData: INodeData, input: string): Promise { - const openAIApiKey = nodeData.inputs?.openAIApiKey as string - const pineconeApiKey = nodeData.inputs?.pineconeApiKey as string - const pineconeEnv = nodeData.inputs?.pineconeEnv as string - const index = nodeData.inputs?.pineconeIndex as string - const modelName = nodeData.inputs?.modelName as string - const taskLoop = nodeData.inputs?.taskLoop as string + const executor = nodeData.instance as BabyAGI const objective = input - const configuration = new Configuration({ - apiKey: openAIApiKey - }) - const openai = new OpenAIApi(configuration) - - const pinecone = new PineconeClient() - await pinecone.init({ - apiKey: pineconeApiKey, - environment: pineconeEnv - }) - - const dimension = 1536 - const metric = 'cosine' - const podType = 'p1' - - const indexList = await pinecone.listIndexes() - if (!indexList.includes(index)) { - const createIndexOptions: CreateIndexRequest = { - createRequest: { - name: index, - dimension, - metric, - podType - } - } - await pinecone.createIndex(createIndexOptions) - } - - let vectorIndex: VectorOperationsApi = pinecone.Index(index) - - let taskList: Task[] = [] - let embeddingList = new Map() - - taskList = [ - { - id: uuidv4(), - name: 'Develop a task list', - priority: 1 - } - ] - - return await mainLoop(openai, pinecone, index, embeddingList, vectorIndex, taskList, objective, modelName, taskLoop) + const res = await executor.call({ objective }) + return res } } -export const getADAEmbedding = async (openai: OpenAIApi, text: string, embeddingList: Map): Promise => { - //console.log('\nGetting ADA embedding for: ', text) - - if (embeddingList.has(text)) { - //console.log('Embedding already exists for: ', text) - const numbers = embeddingList.get(text) - return numbers ?? [] - } - - const embedding = ( - await openai.createEmbedding({ - input: [text], - model: 'text-embedding-ada-002' - }) - ).data?.data[0].embedding - - embeddingList.set(text, embedding) - - return embedding -} - -export const openAICall = async (openai: OpenAIApi, prompt: string, gptVersion: string, temperature = 0.5, max_tokens = 100) => { - if (gptVersion === 'gpt-3.5-turbo' || gptVersion === 'gpt-4' || gptVersion === 'gpt-4-32k') { - // Chat completion - const options: CreateChatCompletionRequest = { - model: gptVersion, - messages: [{ role: 'user', content: prompt }], - temperature, - max_tokens, - n: 1 - } - const data = (await openai.createChatCompletion(options)).data - - return data?.choices[0]?.message?.content.trim() ?? '' - } else { - // Prompt completion - const options: CreateCompletionRequest = { - model: gptVersion, - prompt, - temperature, - max_tokens, - top_p: 1, - frequency_penalty: 0, - presence_penalty: 0 - } - const data = (await openai.createCompletion(options)).data - - return data?.choices[0]?.text?.trim() ?? '' - } -} - -export const taskCreationAgent = async ( - openai: OpenAIApi, - taskList: Task[], - objective: string, - result: string, - taskDescription: string, - gptVersion = 'gpt-3.5-turbo' -): Promise => { - const prompt = `You are an task creation AI that uses the result of an execution agent to create new tasks with the following objective: ${objective}, The last completed task has the result: ${result}. This result was based on this task description: ${taskDescription}. These are incomplete tasks: ${taskList.join( - ', ' - )}. Based on the result, create new tasks to be completed by the AI system that do not overlap with incomplete tasks. Return the tasks as an array.` - const response = await openAICall(openai, prompt, gptVersion) - const newTaskNames = response.split('\n') - - return newTaskNames.map((name) => ({ - id: uuidv4(), - name, - priority: taskList.length + 1 - })) -} - -export const prioritizationAgent = async ( - openai: OpenAIApi, - taskList: Task[], - taskPriority: number, - objective: string, - gptVersion = 'gpt-3.5-turbo' -): Promise => { - const taskNames = taskList.map((t) => t.name) - const startPriority = taskPriority + 1 - - const prompt = `You are an task prioritization AI tasked with cleaning the formatting of and reprioritizing the following tasks: ${taskNames}. Consider the ultimate objective of your team: ${objective}. Do not remove any tasks. Return the result as a list, like: - #. First task - #. Second task - Start the task list with number ${startPriority}.` - const response = await openAICall(openai, prompt, gptVersion) - const newTasks = response.split('\n') - - // Parse and add new tasks - return ( - newTasks - .map((taskString) => { - const taskParts = taskString.trim().split('.', 2) - - if (taskParts.length === 2) { - const id = uuidv4() - const name = taskParts[1].trim() - const priority = parseInt(taskParts[0]) - return { - id, - name, - priority - } as Task - } - }) - // Remove lines that don't have a task - .filter((t) => t !== undefined) - // Sort by priority - .sort((a, b) => a!.priority - b!.priority) as Task[] - ) -} - -export const contextAgent = async ( - openai: OpenAIApi, - pinecone: PineconeClient, - indexName: string, - embeddingList: Map, - objective: string, - topK: number -) => { - const index = pinecone.Index(indexName) - const queryEmbedding = await getADAEmbedding(openai, objective, embeddingList) - - const results = await index.query({ - queryRequest: { - vector: queryEmbedding, - includeMetadata: true, - topK - } - }) - const sortedResults = results.matches?.sort((a, b) => (b?.score ?? 0) - (a?.score ?? 0)) ?? [] - - return sortedResults.map((item) => (item.metadata as any)?.task ?? '') -} - -export const executionAgent = async ( - openai: OpenAIApi, - pinecone: PineconeClient, - indexName: string, - embeddingList: Map, - objective: string, - task: Task, - gptVersion = 'gpt-3.5-turbo' -) => { - const context = await contextAgent(openai, pinecone, indexName, embeddingList, objective, 5) - const prompt = `You are an AI who performs one task based on the following objective: ${objective}.\nTake into account these previously completed tasks: ${context}\nYour task: ${task.name}\nResponse:` - - //console.log('\nexecution prompt: ', prompt, '\n') - - return openAICall(openai, prompt, gptVersion, 0.7, 2000) -} - -export const mainLoop = async ( - openai: OpenAIApi, - pinecone: PineconeClient, - indexName: string, - embeddingList: Map, - index: VectorOperationsApi, - taskList: Task[], - objective: string, - modelName: string, - taskLoop: string -): Promise => { - const RUN_LIMIT = parseInt(taskLoop, 10) || 3 - let finalResult = '' - - for (let run = 0; run < RUN_LIMIT; run++) { - let enrichedResult: any - let task: Task | undefined - - if (taskList.length > 0) { - // Step 1: Pull the task - task = taskList.shift() - - if (!task) { - //console.log('No tasks left to complete. Exiting.') - break - } - - console.log(`\x1b[95m\x1b[1m\n*****TASK LIST*****\n\x1b[0m\x1b[0m - ${taskList.map((t) => ` ${t?.priority}. ${t?.name}`).join('\n')} - \x1b[92m\x1b[1m\n*****NEXT TASK*****\n\x1b[0m\x1b[0m - ${task.name}`) - - // Step 2: Execute the task - const result = await executionAgent(openai, pinecone, indexName, embeddingList, objective, task) - console.log('\x1b[93m\x1b[1m\n*****TASK RESULT*****\n\x1b[0m\x1b[0m') - console.log(result) - finalResult = result - - // Step 3: Enrich result and store in Pinecone - enrichedResult = { data: result } - const vector = enrichedResult.data // extract the actual result from the dictionary - const embeddingResult = await getADAEmbedding(openai, vector, embeddingList) - await index.upsert({ - upsertRequest: { - vectors: [ - { - id: task.id, - values: embeddingResult, - metadata: { task: task.name, result } - } - ] - } - }) - } - - // Step 4: Create new tasks and reprioritize task list - if (enrichedResult) { - const newTasks = await taskCreationAgent(openai, taskList, objective, enrichedResult.data, task!.name) - //console.log('newTasks', newTasks) - taskList = [...taskList, ...newTasks] - - taskList = await prioritizationAgent(openai, taskList, task!.priority, objective, modelName) - //console.log(`Reprioritized task list: ${taskList.map((t) => `[${t?.priority}] ${t?.id}: ${t?.name}`).join(', ')}`) - } else { - break - } - } - - return finalResult -} - module.exports = { nodeClass: BabyAGI_Agents } diff --git a/packages/components/nodes/agents/BabyAGI/babyagi.jpg b/packages/components/nodes/agents/BabyAGI/babyagi.jpg new file mode 100644 index 00000000..cd585139 Binary files /dev/null and b/packages/components/nodes/agents/BabyAGI/babyagi.jpg differ diff --git a/packages/components/nodes/agents/BabyAGI/babyagi.svg b/packages/components/nodes/agents/BabyAGI/babyagi.svg deleted file mode 100644 index c87861e5..00000000 --- a/packages/components/nodes/agents/BabyAGI/babyagi.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/packages/components/nodes/agents/BabyAGI/core.ts b/packages/components/nodes/agents/BabyAGI/core.ts new file mode 100644 index 00000000..95423fff --- /dev/null +++ b/packages/components/nodes/agents/BabyAGI/core.ts @@ -0,0 +1,262 @@ +import { LLMChain } from 'langchain/chains' +import { BaseChatModel } from 'langchain/chat_models' +import { VectorStore } from 'langchain/dist/vectorstores/base' +import { Document } from 'langchain/document' +import { PromptTemplate } from 'langchain/prompts' + +class TaskCreationChain extends LLMChain { + constructor(prompt: PromptTemplate, llm: BaseChatModel) { + super({ prompt, llm }) + } + + static from_llm(llm: BaseChatModel): LLMChain { + const taskCreationTemplate: string = + 'You are a task creation AI that uses the result of an execution agent' + + ' to create new tasks with the following objective: {objective},' + + ' The last completed task has the result: {result}.' + + ' This result was based on this task description: {task_description}.' + + ' These are incomplete tasks list: {incomplete_tasks}.' + + ' Based on the result, create new tasks to be completed' + + ' by the AI system that do not overlap with incomplete tasks.' + + ' Return the tasks as an array.' + + const prompt = new PromptTemplate({ + template: taskCreationTemplate, + inputVariables: ['result', 'task_description', 'incomplete_tasks', 'objective'] + }) + + return new TaskCreationChain(prompt, llm) + } +} + +class TaskPrioritizationChain extends LLMChain { + constructor(prompt: PromptTemplate, llm: BaseChatModel) { + super({ prompt, llm }) + } + + static from_llm(llm: BaseChatModel): TaskPrioritizationChain { + const taskPrioritizationTemplate: string = + 'You are a task prioritization AI tasked with cleaning the formatting of and reprioritizing' + + ' the following task list: {task_names}.' + + ' Consider the ultimate objective of your team: {objective}.' + + ' Do not remove any tasks. Return the result as a numbered list, like:' + + ' #. First task' + + ' #. Second task' + + ' Start the task list with number {next_task_id}.' + const prompt = new PromptTemplate({ + template: taskPrioritizationTemplate, + inputVariables: ['task_names', 'next_task_id', 'objective'] + }) + return new TaskPrioritizationChain(prompt, llm) + } +} + +class ExecutionChain extends LLMChain { + constructor(prompt: PromptTemplate, llm: BaseChatModel) { + super({ prompt, llm }) + } + + static from_llm(llm: BaseChatModel): LLMChain { + const executionTemplate: string = + 'You are an AI who performs one task based on the following objective: {objective}.' + + ' Take into account these previously completed tasks: {context}.' + + ' Your task: {task}.' + + ' Response:' + + const prompt = new PromptTemplate({ + template: executionTemplate, + inputVariables: ['objective', 'context', 'task'] + }) + + return new ExecutionChain(prompt, llm) + } +} + +async function getNextTask( + taskCreationChain: LLMChain, + result: string, + taskDescription: string, + taskList: string[], + objective: string +): Promise { + const incompleteTasks: string = taskList.join(', ') + const response: string = await taskCreationChain.predict({ + result, + task_description: taskDescription, + incomplete_tasks: incompleteTasks, + objective + }) + + const newTasks: string[] = response.split('\n') + + return newTasks.filter((taskName) => taskName.trim()).map((taskName) => ({ task_name: taskName })) +} + +interface Task { + task_id: number + task_name: string +} + +async function prioritizeTasks( + taskPrioritizationChain: LLMChain, + thisTaskId: number, + taskList: Task[], + objective: string +): Promise { + const next_task_id = thisTaskId + 1 + const task_names = taskList.map((t) => t.task_name).join(', ') + const response = await taskPrioritizationChain.predict({ task_names, next_task_id, objective }) + const newTasks = response.split('\n') + const prioritizedTaskList: Task[] = [] + + for (const taskString of newTasks) { + if (!taskString.trim()) { + // eslint-disable-next-line no-continue + continue + } + const taskParts = taskString.trim().split('. ', 2) + if (taskParts.length === 2) { + const task_id = parseInt(taskParts[0].trim(), 10) + const task_name = taskParts[1].trim() + prioritizedTaskList.push({ task_id, task_name }) + } + } + + return prioritizedTaskList +} + +export async function get_top_tasks(vectorStore: VectorStore, query: string, k: number): Promise { + const docs = await vectorStore.similaritySearch(query, k) + let returnDocs: string[] = [] + for (const doc of docs) { + returnDocs.push(doc.metadata.task) + } + return returnDocs +} + +async function executeTask(vectorStore: VectorStore, executionChain: LLMChain, objective: string, task: string, k = 5): Promise { + const context = await get_top_tasks(vectorStore, objective, k) + //const docContent = await retrieve_embeddings(table, task, 0.5); + //console.log(docContent); + return executionChain.predict({ objective, context, task }) +} + +export class BabyAGI { + taskList: Array = [] + + taskCreationChain: TaskCreationChain + + taskPrioritizationChain: TaskPrioritizationChain + + executionChain: ExecutionChain + + taskIdCounter = 1 + + vectorStore: VectorStore + + maxIterations = 3 + + constructor( + taskCreationChain: TaskCreationChain, + taskPrioritizationChain: TaskPrioritizationChain, + executionChain: ExecutionChain, + vectorStore: VectorStore, + maxIterations: number + ) { + this.taskCreationChain = taskCreationChain + this.taskPrioritizationChain = taskPrioritizationChain + this.executionChain = executionChain + this.vectorStore = vectorStore + this.maxIterations = maxIterations + } + + addTask(task: Task) { + this.taskList.push(task) + } + + printTaskList() { + console.log('\x1b[95m\x1b[1m\n*****TASK LIST*****\n\x1b[0m\x1b[0m') + this.taskList.forEach((t) => console.log(`${t.task_id}: ${t.task_name}`)) + } + + printNextTask(task: Task) { + console.log('\x1b[92m\x1b[1m\n*****NEXT TASK*****\n\x1b[0m\x1b[0m') + console.log(`${task.task_id}: ${task.task_name}`) + } + + printTaskResult(result: string) { + console.log('\x1b[93m\x1b[1m\n*****TASK RESULT*****\n\x1b[0m\x1b[0m') + console.log(result) + } + + getInputKeys(): string[] { + return ['objective'] + } + + getOutputKeys(): string[] { + return [] + } + + async call(inputs: Record): Promise { + const { objective } = inputs + const firstTask = inputs.first_task || 'Make a todo list' + this.addTask({ task_id: 1, task_name: firstTask }) + let numIters = 0 + let loop = true + let finalResult = '' + + while (loop) { + if (this.taskList.length) { + this.printTaskList() + + // Step 1: Pull the first task + const task = this.taskList.shift() + if (!task) break + this.printNextTask(task) + + // Step 2: Execute the task + const result = await executeTask(this.vectorStore, this.executionChain, objective, task.task_name) + const thisTaskId = task.task_id + finalResult = result + this.printTaskResult(result) + + // Step 3: Store the result in Pinecone + const docs = new Document({ pageContent: result, metadata: { task: task.task_name } }) + this.vectorStore.addDocuments([docs]) + + // Step 4: Create new tasks and reprioritize task list + const newTasks = await getNextTask( + this.taskCreationChain, + result, + task.task_name, + this.taskList.map((t) => t.task_name), + objective + ) + newTasks.forEach((newTask) => { + this.taskIdCounter += 1 + // eslint-disable-next-line no-param-reassign + newTask.task_id = this.taskIdCounter + this.addTask(newTask) + }) + this.taskList = await prioritizeTasks(this.taskPrioritizationChain, thisTaskId, this.taskList, objective) + } + + numIters += 1 + if (this.maxIterations !== null && numIters === this.maxIterations) { + console.log('\x1b[91m\x1b[1m\n*****TASK ENDING*****\n\x1b[0m\x1b[0m') + console.log(this.maxIterations) + loop = false + this.taskList = [] + } + } + + return finalResult + } + + static fromLLM(llm: BaseChatModel, vectorstore: VectorStore, maxIterations = 3): BabyAGI { + const taskCreationChain = TaskCreationChain.from_llm(llm) + const taskPrioritizationChain = TaskPrioritizationChain.from_llm(llm) + const executionChain = ExecutionChain.from_llm(llm) + return new BabyAGI(taskCreationChain, taskPrioritizationChain, executionChain, vectorstore, maxIterations) + } +}