Skip to content

Commit fbf6830

Browse files
authored
Merge pull request #1763 from FlowiseAI/feature/ReAct-Agents
Feature/Update mrkl agents
2 parents d1fdd8b + 15afb8a commit fbf6830

File tree

4 files changed

+340
-100
lines changed

4 files changed

+340
-100
lines changed

packages/components/nodes/agents/MRKLAgentChat/MRKLAgentChat.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { flatten } from 'lodash'
2-
import { AgentExecutor, createReactAgent } from 'langchain/agents'
2+
import { AgentExecutor } from 'langchain/agents'
33
import { pull } from 'langchain/hub'
44
import { Tool } from '@langchain/core/tools'
55
import type { PromptTemplate } from '@langchain/core/prompts'
66
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
77
import { additionalCallbacks } from '../../../src/handler'
8-
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
8+
import { FlowiseMemory, ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
99
import { getBaseClasses } from '../../../src/utils'
10+
import { createReactAgent } from '../../../src/agents'
1011

1112
class MRKLAgentChat_Agents implements INode {
1213
label: string
@@ -18,11 +19,12 @@ class MRKLAgentChat_Agents implements INode {
1819
category: string
1920
baseClasses: string[]
2021
inputs: INodeParams[]
22+
sessionId?: string
2123

22-
constructor() {
24+
constructor(fields?: { sessionId?: string }) {
2325
this.label = 'ReAct Agent for Chat Models'
2426
this.name = 'mrklAgentChat'
25-
this.version = 2.0
27+
this.version = 3.0
2628
this.type = 'AgentExecutor'
2729
this.category = 'Agents'
2830
this.icon = 'agent.svg'
@@ -39,15 +41,22 @@ class MRKLAgentChat_Agents implements INode {
3941
label: 'Chat Model',
4042
name: 'model',
4143
type: 'BaseChatModel'
44+
},
45+
{
46+
label: 'Memory',
47+
name: 'memory',
48+
type: 'BaseChatMemory'
4249
}
4350
]
51+
this.sessionId = fields?.sessionId
4452
}
4553

4654
async init(): Promise<any> {
4755
return null
4856
}
4957

5058
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
59+
const memory = nodeData.inputs?.memory as FlowiseMemory
5160
const model = nodeData.inputs?.model as BaseChatModel
5261
let tools = nodeData.inputs?.tools as Tool[]
5362
tools = flatten(tools)
@@ -68,10 +77,25 @@ class MRKLAgentChat_Agents implements INode {
6877

6978
const callbacks = await additionalCallbacks(nodeData, options)
7079

71-
const result = await executor.invoke({
72-
input,
73-
callbacks
74-
})
80+
const prevChatHistory = options.chatHistory
81+
const chatHistory = ((await memory.getChatMessages(this.sessionId, false, prevChatHistory)) as IMessage[]) ?? []
82+
const chatHistoryString = chatHistory.map((hist) => hist.message).join('\\n')
83+
84+
const result = await executor.invoke({ input, chat_history: chatHistoryString }, { callbacks })
85+
86+
await memory.addChatMessages(
87+
[
88+
{
89+
text: input,
90+
type: 'userMessage'
91+
},
92+
{
93+
text: result?.output,
94+
type: 'apiMessage'
95+
}
96+
],
97+
this.sessionId
98+
)
7599

76100
return result?.output
77101
}

packages/components/nodes/agents/MRKLAgentLLM/MRKLAgentLLM.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { flatten } from 'lodash'
2-
import { AgentExecutor, createReactAgent } from 'langchain/agents'
2+
import { AgentExecutor } from 'langchain/agents'
33
import { pull } from 'langchain/hub'
44
import { Tool } from '@langchain/core/tools'
55
import type { PromptTemplate } from '@langchain/core/prompts'
66
import { BaseLanguageModel } from 'langchain/base_language'
77
import { additionalCallbacks } from '../../../src/handler'
88
import { getBaseClasses } from '../../../src/utils'
99
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
10+
import { createReactAgent } from '../../../src/agents'
1011

1112
class MRKLAgentLLM_Agents implements INode {
1213
label: string
@@ -68,10 +69,7 @@ class MRKLAgentLLM_Agents implements INode {
6869

6970
const callbacks = await additionalCallbacks(nodeData, options)
7071

71-
const result = await executor.invoke({
72-
input,
73-
callbacks
74-
})
72+
const result = await executor.invoke({ input }, { callbacks })
7573

7674
return result?.output
7775
}

packages/components/src/agents.ts

+121-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ import { ChainValues } from '@langchain/core/utils/types'
33
import { AgentStep, AgentAction } from '@langchain/core/agents'
44
import { BaseMessage, FunctionMessage, AIMessage } from '@langchain/core/messages'
55
import { OutputParserException } from '@langchain/core/output_parsers'
6+
import { BaseLanguageModel } from '@langchain/core/language_models/base'
67
import { CallbackManager, CallbackManagerForChainRun, Callbacks } from '@langchain/core/callbacks/manager'
7-
import { ToolInputParsingException, Tool } from '@langchain/core/tools'
8-
import { Runnable } from '@langchain/core/runnables'
8+
import { ToolInputParsingException, Tool, StructuredToolInterface } from '@langchain/core/tools'
9+
import { Runnable, RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables'
910
import { Serializable } from '@langchain/core/load/serializable'
11+
import { renderTemplate } from '@langchain/core/prompts'
1012
import { BaseChain, SerializedLLMChain } from 'langchain/chains'
11-
import { AgentExecutorInput, BaseSingleActionAgent, BaseMultiActionAgent, RunnableAgent, StoppingMethod } from 'langchain/agents'
13+
import {
14+
CreateReactAgentParams,
15+
AgentExecutorInput,
16+
AgentActionOutputParser,
17+
BaseSingleActionAgent,
18+
BaseMultiActionAgent,
19+
RunnableAgent,
20+
StoppingMethod
21+
} from 'langchain/agents'
22+
import { formatLogToString } from 'langchain/agents/format_scratchpad/log'
1223

1324
export const SOURCE_DOCUMENTS_PREFIX = '\n\n----FLOWISE_SOURCE_DOCUMENTS----\n\n'
1425
type AgentFinish = {
@@ -647,3 +658,110 @@ export const formatAgentSteps = (steps: AgentStep[]): BaseMessage[] =>
647658
return [new AIMessage(action.log)]
648659
}
649660
})
661+
662+
const renderTextDescription = (tools: StructuredToolInterface[]): string => {
663+
return tools.map((tool) => `${tool.name}: ${tool.description}`).join('\n')
664+
}
665+
666+
export const createReactAgent = async ({ llm, tools, prompt }: CreateReactAgentParams) => {
667+
const missingVariables = ['tools', 'tool_names', 'agent_scratchpad'].filter((v) => !prompt.inputVariables.includes(v))
668+
if (missingVariables.length > 0) {
669+
throw new Error(`Provided prompt is missing required input variables: ${JSON.stringify(missingVariables)}`)
670+
}
671+
const toolNames = tools.map((tool) => tool.name)
672+
const partialedPrompt = await prompt.partial({
673+
tools: renderTextDescription(tools),
674+
tool_names: toolNames.join(', ')
675+
})
676+
// TODO: Add .bind to core runnable interface.
677+
const llmWithStop = (llm as BaseLanguageModel).bind({
678+
stop: ['\nObservation:']
679+
})
680+
const agent = RunnableSequence.from([
681+
RunnablePassthrough.assign({
682+
//@ts-ignore
683+
agent_scratchpad: (input: { steps: AgentStep[] }) => formatLogToString(input.steps)
684+
}),
685+
partialedPrompt,
686+
llmWithStop,
687+
new ReActSingleInputOutputParser({
688+
toolNames
689+
})
690+
])
691+
return agent
692+
}
693+
694+
class ReActSingleInputOutputParser extends AgentActionOutputParser {
695+
lc_namespace = ['langchain', 'agents', 'react']
696+
697+
private toolNames: string[]
698+
private FINAL_ANSWER_ACTION = 'Final Answer:'
699+
private FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE = 'Parsing LLM output produced both a final answer and a parse-able action:'
700+
private FORMAT_INSTRUCTIONS = `Use the following format:
701+
702+
Question: the input question you must answer
703+
Thought: you should always think about what to do
704+
Action: the action to take, should be one of [{tool_names}]
705+
Action Input: the input to the action
706+
Observation: the result of the action
707+
... (this Thought/Action/Action Input/Observation can repeat N times)
708+
Thought: I now know the final answer
709+
Final Answer: the final answer to the original input question`
710+
711+
constructor(fields: { toolNames: string[] }) {
712+
super(...arguments)
713+
this.toolNames = fields.toolNames
714+
}
715+
716+
/**
717+
* Parses the given text into an AgentAction or AgentFinish object. If an
718+
* output fixing parser is defined, uses it to parse the text.
719+
* @param text Text to parse.
720+
* @returns Promise that resolves to an AgentAction or AgentFinish object.
721+
*/
722+
async parse(text: string): Promise<AgentAction | AgentFinish> {
723+
const includesAnswer = text.includes(this.FINAL_ANSWER_ACTION)
724+
const regex = /Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)/
725+
const actionMatch = text.match(regex)
726+
if (actionMatch) {
727+
if (includesAnswer) {
728+
throw new Error(`${this.FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE}: ${text}`)
729+
}
730+
731+
const action = actionMatch[1]
732+
const actionInput = actionMatch[2]
733+
const toolInput = actionInput.trim().replace(/"/g, '')
734+
735+
return {
736+
tool: action,
737+
toolInput,
738+
log: text
739+
}
740+
}
741+
742+
if (includesAnswer) {
743+
const finalAnswerText = text.split(this.FINAL_ANSWER_ACTION)[1].trim()
744+
return {
745+
returnValues: {
746+
output: finalAnswerText
747+
},
748+
log: text
749+
}
750+
}
751+
752+
// Instead of throwing Error, we return a AgentFinish object
753+
return { returnValues: { output: text }, log: text }
754+
}
755+
756+
/**
757+
* Returns the format instructions as a string. If the 'raw' option is
758+
* true, returns the raw FORMAT_INSTRUCTIONS.
759+
* @param options Options for getting the format instructions.
760+
* @returns Format instructions as a string.
761+
*/
762+
getFormatInstructions(): string {
763+
return renderTemplate(this.FORMAT_INSTRUCTIONS, 'f-string', {
764+
tool_names: this.toolNames.join(', ')
765+
})
766+
}
767+
}

0 commit comments

Comments
 (0)