Skip to content

Commit

Permalink
support react
Browse files Browse the repository at this point in the history
  • Loading branch information
gulixin0922 committed Jul 9, 2024
1 parent 9316b1b commit 99acbf8
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
get_openai_functions_agent_executor,
get_qwen_local_functions_agent_executor
)
from bisheng_langchain.gpts.agent_types.llm_react_agent import get_react_agent_executor


__all__ = [
"get_openai_functions_agent_executor",
"get_qwen_local_functions_agent_executor"
"get_qwen_local_functions_agent_executor",
"get_react_agent_executor"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import operator
from typing import Annotated, Sequence, TypedDict, Union
from langchain.tools import BaseTool
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
from langchain_core.language_models import LanguageModelLike
from langgraph.graph import END, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt.tool_executor import ToolExecutor
from langgraph.utils import RunnableCallable
from langchain.agents import create_structured_chat_agent
from bisheng_langchain.gpts.prompts.react_agent_prompt import react_agent_prompt


def get_react_agent_executor(
tools: list[BaseTool],
llm: LanguageModelLike,
system_message: str,
interrupt_before_action: bool,
**kwargs
):
prompt = react_agent_prompt
prompt = prompt.partial(assistant_message=system_message)
agent = create_structured_chat_agent(llm, tools, prompt)
agent_executer = create_agent_executor(agent, tools)
return agent_executer


def _get_agent_state(input_schema=None):
if input_schema is None:

class AgentState(TypedDict):
# The input string
input: str
# The list of previous messages in the conversation
chat_history: Sequence[BaseMessage]
# The outcome of a given call to the agent
# Needs `None` as a valid type, since this is what this will start as
agent_outcome: Union[AgentAction, AgentFinish, None]
# List of actions and corresponding observations
# Here we annotate this with `operator.add` to indicate that operations to
# this state should be ADDED to the existing values (not overwrite it)
intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]

else:

class AgentState(input_schema):
# The outcome of a given call to the agent
# Needs `None` as a valid type, since this is what this will start as
agent_outcome: Union[AgentAction, AgentFinish, None]
# List of actions and corresponding observations
# Here we annotate this with `operator.add` to indicate that operations to
# this state should be ADDED to the existing values (not overwrite it)
intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]

return AgentState


def create_agent_executor(
agent_runnable, tools, input_schema=None
) -> CompiledStateGraph:
"""This is a helper function for creating a graph that works with LangChain Agents.
Args:
agent_runnable (RunnableLike): The agent runnable.
tools (list): A list of tools to be used by the agent.
input_schema (dict, optional): The input schema for the agent. Defaults to None.
Returns:
The `CompiledStateGraph` object.
"""

if isinstance(tools, ToolExecutor):
tool_executor = tools
else:
tool_executor = ToolExecutor(tools)

state = _get_agent_state(input_schema)

# Define logic that will be used to determine which conditional edge to go down

def should_continue(data):
# If the agent outcome is an AgentFinish, then we return `exit` string
# This will be used when setting up the graph to define the flow
if isinstance(data["agent_outcome"], AgentFinish):
return "end"
# Otherwise, an AgentAction is returned
# Here we return `continue` string
# This will be used when setting up the graph to define the flow
else:
return "continue"

def run_agent(data, config):
agent_outcome = agent_runnable.invoke(data, config)
return {"agent_outcome": agent_outcome}

async def arun_agent(data, config):
agent_outcome = await agent_runnable.ainvoke(data, config)
return {"agent_outcome": agent_outcome}

# Define the function to execute tools
def execute_tools(data, config):
# Get the most recent agent_outcome - this is the key added in the `agent` above
agent_action = data["agent_outcome"]
if not isinstance(agent_action, list):
agent_action = [agent_action]
output = tool_executor.batch(agent_action, config, return_exceptions=True)
return {
"intermediate_steps": [
(action, str(out)) for action, out in zip(agent_action, output)
]
}

async def aexecute_tools(data, config):
# Get the most recent agent_outcome - this is the key added in the `agent` above
agent_action = data["agent_outcome"]
if not isinstance(agent_action, list):
agent_action = [agent_action]
output = await tool_executor.abatch(
agent_action, config, return_exceptions=True
)
return {
"intermediate_steps": [
(action, str(out)) for action, out in zip(agent_action, output)
]
}

# Define a new graph
workflow = StateGraph(state)

# Define the two nodes we will cycle between
workflow.add_node("agent", RunnableCallable(run_agent, arun_agent))
workflow.add_node("tools", RunnableCallable(execute_tools, aexecute_tools))

# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")

# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "tools",
# Otherwise we finish.
"end": END,
},
)

# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("tools", "agent")

# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
return workflow.compile()


if __name__ == "__main__":
pass
25 changes: 13 additions & 12 deletions src/bisheng-langchain/bisheng_langchain/gpts/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ def __init__(self, yaml_path) -> None:

# init agent executor
agent_executor_params = self.assistant_params['agent_executor']
agent_executor_type = agent_executor_params.pop('type')
self.agent_executor_type = agent_executor_params.pop('type')
self.assistant = ConfigurableAssistant(
agent_executor_type=agent_executor_type,
agent_executor_type=self.agent_executor_type,
tools=tools,
llm=llm,
assistant_message=assistant_message,
Expand All @@ -119,22 +119,23 @@ def run(self, query, chat_history=[], chat_round=5):
inputs.append(HumanMessage(content=chat_history[i]))
inputs.append(AIMessage(content=chat_history[i+1]))
inputs.append(HumanMessage(content=query))
result = asyncio.run(self.assistant.ainvoke(inputs))
if self.agent_executor_type == 'get_react_agent_executor':
result = asyncio.run(self.assistant.ainvoke({"input": inputs[-1].content, "chat_history": inputs[:-1]}))
else:
result = asyncio.run(self.assistant.ainvoke(inputs))
return result


if __name__ == "__main__":
from langchain.globals import set_debug

# set_debug(True)
chat_history = []
query = "请简要分析中科创达软件股份有限公司2019年聘任、解聘会计师事务的情况。"
# chat_history = ['你好', '你好,有什么可以帮助你吗?', '福蓉科技股价多少?', '福蓉科技(股票代码:300049)的当前股价为48.67元。']
# query = '去年这个时候的股价是多少?'
# bisheng_assistant = BishengAssistant("config/base_scene.yaml")
# chat_history = []
# query = "600519、300750股价多少?"
chat_history = ['你好', '你好,有什么可以帮助你吗?', '福蓉科技股价多少?', '福蓉科技(股票代码:300049)的当前股价为48.67元。']
query = '今天是什么时候?去年这个时候的股价是多少?'
bisheng_assistant = BishengAssistant("config/base_scene.yaml")
# bisheng_assistant = BishengAssistant("config/knowledge_scene.yaml")
bisheng_assistant = BishengAssistant("config/rag_scene.yaml")
# bisheng_assistant = BishengAssistant("config/rag_scene.yaml")
result = bisheng_assistant.run(query, chat_history=chat_history)
for r in result:
print(f'------------------')
print(type(r), r)
print(result)
29 changes: 15 additions & 14 deletions src/bisheng-langchain/bisheng_langchain/gpts/config/base_scene.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
assistant:
# prompt_type: 'ASSISTANT_PROMPT_DEFAULT'
# llm:
# type: 'ChatOpenAI'
# model: 'gpt-4-0125-preview'
# openai_api_key: ''
# openai_proxy: 'http://118.195.232.223:39995'
# temperature: 0.0

prompt_type: 'ASSISTANT_PROMPT_COHERE'
prompt_type: 'ASSISTANT_PROMPT_DEFAULT'
llm:
type: 'ChatOpenAI'
model: 'command-r-plus-104b'
openai_api_base: 'http://34.87.129.78:9100/v1'
model: 'gpt-4-0125-preview'
openai_api_key: ''
openai_proxy: ''
temperature: 0.3
openai_proxy: 'http://118.195.232.223:39995'
temperature: 0.0

# prompt_type: 'ASSISTANT_PROMPT_COHERE'
# llm:
# type: 'ChatOpenAI'
# model: 'command-r-plus-104b'
# openai_api_base: 'http://34.87.129.78:9100/v1'
# openai_api_key: ''
# openai_proxy: ''
# temperature: 0.3

tools:
- type: "sina_realtime_info"
Expand All @@ -39,8 +39,9 @@ assistant:
- type: macro_china_shrzgm

agent_executor:
type: 'get_openai_functions_agent_executor'
# type: 'get_openai_functions_agent_executor'
# type: 'get_qwen_local_functions_agent_executor'
type: 'get_react_agent_executor'
interrupt_before_action: False
recursion_limit: 50

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import List, Union
from langchain_core.prompts.chat import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
SystemMessagePromptTemplate,
MessagesPlaceholder
)
from langchain_core.prompts.prompt import PromptTemplate
from langchain_core.messages import FunctionMessage, SystemMessage, ToolMessage, AIMessage, HumanMessage, ChatMessage


system_temp = """
{assistant_message}
Respond to the human as helpfully and accurately as possible. You have access to the following tools:
{tools}
Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).
Valid "action" values: "Final Answer" or {tool_names}
Provide only ONE action per $JSON_BLOB, as shown:
```
{{{{
"action": $TOOL_NAME,
"action_input": $INPUT
}}}}
```
Follow this format:
Question: input question to answer
Thought: consider previous and subsequent steps
Action:
```
$JSON_BLOB
```
Observation: action result
... (repeat Thought/Action/Observation N times)
Thought: I know what to respond
Action:
```
{{{{
"action": "Final Answer",
"action_input": "Final response to human"
}}}}
Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation
"""

human_temp = """Question: {input}
Thought: {agent_scratchpad}
(reminder to respond in a JSON blob no matter what)"""


react_agent_prompt = ChatPromptTemplate(
input_variables=['agent_scratchpad', 'input', 'tool_names', 'tools', 'assistant_message'],
optional_variables=['chat_history'],
input_types={'chat_history': List[Union[AIMessage, HumanMessage, ChatMessage, SystemMessage, FunctionMessage, ToolMessage]]},
messages=[
SystemMessagePromptTemplate.from_template(system_temp),
MessagesPlaceholder(variable_name='chat_history', optional=True),
HumanMessagePromptTemplate.from_template(human_temp)
]
)
2 changes: 1 addition & 1 deletion src/bisheng-langchain/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pydantic==1.10.13
pymupdf==1.23.8
shapely==2.0.2
filetype==1.2.0
langgraph==0.0.30
langgraph==0.1.5
openai==1.14.3
langchain-openai==0.1.0
llama-index==0.9.48
Expand Down

0 comments on commit 99acbf8

Please sign in to comment.