diff --git a/packages/components/nodes/agents/XMLAgent/XMLAgent.ts b/packages/components/nodes/agents/XMLAgent/XMLAgent.ts new file mode 100644 index 00000000..49109947 --- /dev/null +++ b/packages/components/nodes/agents/XMLAgent/XMLAgent.ts @@ -0,0 +1,203 @@ +import { flatten } from 'lodash' +import { ChainValues } from '@langchain/core/utils/types' +import { AgentStep } from '@langchain/core/agents' +import { RunnableSequence } from '@langchain/core/runnables' +import { ChatOpenAI } from '@langchain/openai' +import { Tool } from '@langchain/core/tools' +import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts' +import { XMLAgentOutputParser } from 'langchain/agents/xml/output_parser' +import { formatLogToMessage } from 'langchain/agents/format_scratchpad/log_to_message' +import { getBaseClasses } from '../../../src/utils' +import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface' +import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler' +import { AgentExecutor } from '../../../src/agents' +//import { AgentExecutor } from "langchain/agents"; + +const defaultSystemMessage = `You are a helpful assistant. Help the user answer any questions. + +You have access to the following tools: + +{tools} + +In order to use a tool, you can use and tags. You will then get back a response in the form +For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond: + +searchweather in SF +64 degrees + +When you are done, respond with a final answer between . For example: + +The weather in SF is 64 degrees + +Begin! + +Previous Conversation: +{chat_history} + +Question: {input} +{agent_scratchpad}` + +class XMLAgent_Agents implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + sessionId?: string + + constructor(fields?: { sessionId?: string }) { + this.label = 'XML Agent' + this.name = 'xmlAgent' + this.version = 1.0 + this.type = 'XMLAgent' + this.category = 'Agents' + this.icon = 'xmlagent.svg' + this.description = `Agent that is designed for LLMs that are good for reasoning/writing XML (e.g: Anthropic Claude)` + this.baseClasses = [this.type, ...getBaseClasses(AgentExecutor)] + this.inputs = [ + { + label: 'Tools', + name: 'tools', + type: 'Tool', + list: true + }, + { + label: 'Memory', + name: 'memory', + type: 'BaseChatMemory' + }, + { + label: 'Chat Model', + name: 'model', + type: 'BaseChatModel' + }, + { + label: 'System Message', + name: 'systemMessage', + type: 'string', + warning: 'Prompt must include input variables: {tools}, {chat_history}, {input} and {agent_scratchpad}', + rows: 4, + default: defaultSystemMessage, + additionalParams: true + } + ] + this.sessionId = fields?.sessionId + } + + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const memory = nodeData.inputs?.memory as FlowiseMemory + const executor = await prepareAgent(nodeData, { sessionId: this.sessionId, chatId: options.chatId, input }, options.chatHistory) + + const loggerHandler = new ConsoleCallbackHandler(options.logger) + const callbacks = await additionalCallbacks(nodeData, options) + + let res: ChainValues = {} + let sourceDocuments: ICommonObject[] = [] + + if (options.socketIO && options.socketIOClientId) { + const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId) + res = await executor.invoke({ input }, { callbacks: [loggerHandler, handler, ...callbacks] }) + if (res.sourceDocuments) { + options.socketIO.to(options.socketIOClientId).emit('sourceDocuments', flatten(res.sourceDocuments)) + sourceDocuments = res.sourceDocuments + } + } else { + res = await executor.invoke({ input }, { callbacks: [loggerHandler, ...callbacks] }) + if (res.sourceDocuments) { + sourceDocuments = res.sourceDocuments + } + } + + await memory.addChatMessages( + [ + { + text: input, + type: 'userMessage' + }, + { + text: res?.output, + type: 'apiMessage' + } + ], + this.sessionId + ) + + return sourceDocuments.length ? { text: res?.output, sourceDocuments: flatten(sourceDocuments) } : res?.output + } +} + +const prepareAgent = async ( + nodeData: INodeData, + flowObj: { sessionId?: string; chatId?: string; input?: string }, + chatHistory: IMessage[] = [] +) => { + const model = nodeData.inputs?.model as ChatOpenAI + const memory = nodeData.inputs?.memory as FlowiseMemory + const systemMessage = nodeData.inputs?.systemMessage as string + let tools = nodeData.inputs?.tools + tools = flatten(tools) + const inputKey = memory.inputKey ? memory.inputKey : 'input' + const memoryKey = memory.memoryKey ? memory.memoryKey : 'chat_history' + + let promptMessage = systemMessage ? systemMessage : defaultSystemMessage + if (memory.memoryKey) promptMessage = promptMessage.replaceAll('{chat_history}', `{${memory.memoryKey}}`) + if (memory.inputKey) promptMessage = promptMessage.replaceAll('{input}', `{${memory.inputKey}}`) + + const prompt = ChatPromptTemplate.fromMessages([ + HumanMessagePromptTemplate.fromTemplate(promptMessage), + new MessagesPlaceholder('agent_scratchpad') + ]) + + const missingVariables = ['tools', 'agent_scratchpad'].filter((v) => !prompt.inputVariables.includes(v)) + + if (missingVariables.length > 0) { + throw new Error(`Provided prompt is missing required input variables: ${JSON.stringify(missingVariables)}`) + } + + const llmWithStop = model.bind({ stop: ['', ''] }) + + const messages = (await memory.getChatMessages(flowObj.sessionId, false, chatHistory)) as IMessage[] + let chatHistoryMsgTxt = '' + for (const message of messages) { + if (message.type === 'apiMessage') { + chatHistoryMsgTxt += `\\nAI:${message.message}` + } else if (message.type === 'userMessage') { + chatHistoryMsgTxt += `\\nHuman:${message.message}` + } + } + + const runnableAgent = RunnableSequence.from([ + { + [inputKey]: (i: { input: string; tools: Tool[]; steps: AgentStep[] }) => i.input, + agent_scratchpad: (i: { input: string; tools: Tool[]; steps: AgentStep[] }) => formatLogToMessage(i.steps), + tools: (_: { input: string; tools: Tool[]; steps: AgentStep[] }) => + tools.map((tool: Tool) => `${tool.name}: ${tool.description}`), + [memoryKey]: (_: { input: string; tools: Tool[]; steps: AgentStep[] }) => chatHistoryMsgTxt + }, + prompt, + llmWithStop, + new XMLAgentOutputParser() + ]) + + const executor = AgentExecutor.fromAgentAndTools({ + agent: runnableAgent, + tools, + sessionId: flowObj?.sessionId, + chatId: flowObj?.chatId, + input: flowObj?.input, + isXML: true, + verbose: process.env.DEBUG === 'true' ? true : false + }) + + return executor +} + +module.exports = { nodeClass: XMLAgent_Agents } diff --git a/packages/components/nodes/agents/XMLAgent/xmlagent.svg b/packages/components/nodes/agents/XMLAgent/xmlagent.svg new file mode 100644 index 00000000..d1b5f708 --- /dev/null +++ b/packages/components/nodes/agents/XMLAgent/xmlagent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts index 85586d14..251bd24a 100644 --- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts +++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts @@ -95,6 +95,8 @@ class AWSChatBedrock_ChatModels implements INode { name: 'model', type: 'options', options: [ + { label: 'anthropic.claude-3-sonnet', name: 'anthropic.claude-3-sonnet-20240229-v1:0' }, + { label: 'anthropic.claude-instant-v1', name: 'anthropic.claude-instant-v1' }, { label: 'anthropic.claude-instant-v1', name: 'anthropic.claude-instant-v1' }, { label: 'anthropic.claude-v1', name: 'anthropic.claude-v1' }, { label: 'anthropic.claude-v2', name: 'anthropic.claude-v2' }, diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts index 8b4f7c0e..844e7d25 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/ChatAnthropic.ts @@ -43,6 +43,16 @@ class ChatAnthropic_ChatModels implements INode { name: 'modelName', type: 'options', options: [ + { + label: 'claude-3-opus', + name: 'claude-3-opus-20240229', + description: 'Most powerful model for highly complex tasks' + }, + { + label: 'claude-3-sonnet', + name: 'claude-3-sonnet-20240229', + description: 'Ideal balance of intelligence and speed for enterprise workloads' + }, { label: 'claude-2', name: 'claude-2', diff --git a/packages/components/src/agents.ts b/packages/components/src/agents.ts index 5e4bd9c8..41bc076d 100644 --- a/packages/components/src/agents.ts +++ b/packages/components/src/agents.ts @@ -257,6 +257,8 @@ export class AgentExecutor extends BaseChain { input?: string + isXML?: boolean + /** * How to handle errors raised by the agent's output parser. Defaults to `False`, which raises the error. @@ -277,7 +279,7 @@ export class AgentExecutor extends BaseChain { return this.agent.returnValues } - constructor(input: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }) { + constructor(input: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string; isXML?: boolean }) { let agent: BaseSingleActionAgent | BaseMultiActionAgent if (Runnable.isRunnable(input.agent)) { agent = new RunnableAgent({ runnable: input.agent }) @@ -305,13 +307,17 @@ export class AgentExecutor extends BaseChain { this.sessionId = input.sessionId this.chatId = input.chatId this.input = input.input + this.isXML = input.isXML } - static fromAgentAndTools(fields: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string }): AgentExecutor { + static fromAgentAndTools( + fields: AgentExecutorInput & { sessionId?: string; chatId?: string; input?: string; isXML?: boolean } + ): AgentExecutor { const newInstance = new AgentExecutor(fields) if (fields.sessionId) newInstance.sessionId = fields.sessionId if (fields.chatId) newInstance.chatId = fields.chatId if (fields.input) newInstance.input = fields.input + if (fields.isXML) newInstance.isXML = fields.isXML return newInstance } @@ -405,12 +411,16 @@ export class AgentExecutor extends BaseChain { * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } */ observation = tool - ? // @ts-ignore - await tool.call(action.toolInput, runManager?.getChild(), undefined, { - sessionId: this.sessionId, - chatId: this.chatId, - input: this.input - }) + ? await (tool as any).call( + this.isXML && typeof action.toolInput === 'string' ? { input: action.toolInput } : action.toolInput, + runManager?.getChild(), + undefined, + { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + } + ) : `${action.tool} is not a valid tool, try another one.` } catch (e) { if (e instanceof ToolInputParsingException) { @@ -526,12 +536,16 @@ export class AgentExecutor extends BaseChain { * - tags?: string[] * - flowConfig?: { sessionId?: string, chatId?: string, input?: string } */ - // @ts-ignore - observation = await tool.call(agentAction.toolInput, runManager?.getChild(), undefined, { - sessionId: this.sessionId, - chatId: this.chatId, - input: this.input - }) + observation = await (tool as any).call( + this.isXML && typeof agentAction.toolInput === 'string' ? { input: agentAction.toolInput } : agentAction.toolInput, + runManager?.getChild(), + undefined, + { + sessionId: this.sessionId, + chatId: this.chatId, + input: this.input + } + ) if (observation?.includes(SOURCE_DOCUMENTS_PREFIX)) { const observationArray = observation.split(SOURCE_DOCUMENTS_PREFIX) observation = observationArray[0] diff --git a/packages/server/marketplaces/chatflows/Claude LLM.json b/packages/server/marketplaces/chatflows/Claude LLM.json index 48be286d..fdce533e 100644 --- a/packages/server/marketplaces/chatflows/Claude LLM.json +++ b/packages/server/marketplaces/chatflows/Claude LLM.json @@ -179,6 +179,16 @@ "name": "modelName", "type": "options", "options": [ + { + "label": "claude-3-opus", + "name": "claude-3-opus-20240229", + "description": "Most powerful model for highly complex tasks" + }, + { + "label": "claude-3-sonnet", + "name": "claude-3-sonnet-20240229", + "description": "Ideal balance of intelligence and speed for enterprise workloads" + }, { "label": "claude-2", "name": "claude-2",