大家好,我是雨飞。最近在尝试智普的API实现Agent功能,由于langchain本身的接口只支持OpenAI的function call和tools,因此做了一些修改。
langchain原生的实现,可以看官方的文档,更新太频繁了,就感觉昨天还能用的东西,今天就不行了。
下面这个是核心的代码,实现了简单的Agent功能,通过调用不同的工具去回答问题。agent_output_parser 函数,自己做了重构和实现。
代码中,实现了两个简单的工具,获取天气、计算两个数的和。
python
import requests
import datetime
from langchain.agents import tool
from zhipu_llm import ChatZhipuAI
from langchain_core.utils.function_calling import convert_to_openai_tool
from langchain.schema.agent import AgentFinish
from langchain.prompts import MessagesPlaceholder
from langchain.prompts import ChatPromptTemplate
from agent_output_parser import OpenAIToolsAgentOutputParser,format_to_openai_tool_messages
zhipuai_api_key=""
glm3= "glm-3-turbo"
glm4="glm-4"
chat_zhipu = ChatZhipuAI(
temperature=0.8,
api_key=zhipuai_api_key,
model=glm3
)
@tool
def get_current_temperature(latitude: float, longitude: float):
"""Fetch current temperature for given coordinates."""
BASE_URL = "https://api.open-meteo.com/v1/forecast"
# Parameters for the request
params = {
'latitude': latitude,
'longitude': longitude,
'hourly': 'temperature_2m',
'forecast_days': 1,
}
# Make the request
response = requests.get(BASE_URL, params=params)
if response.status_code == 200:
results = response.json()
else:
raise Exception(f"API Request failed with status code: {response.status_code}")
current_utc_time = datetime.datetime.utcnow()
time_list = [datetime.datetime.fromisoformat(time_str.replace('Z', '+00:00')) for time_str in
results['hourly']['time']]
temperature_list = results['hourly']['temperature_2m']
closest_time_index = min(range(len(time_list)), key=lambda i: abs(time_list[i] - current_utc_time))
current_temperature = temperature_list[closest_time_index]
return f'The current temperature is {current_temperature}°C'
@tool
def sum_nums(s1: float, s2: float):
"""Sum the result for given inputs."""
return "The result is {a}".format(a = s1+s2)
prompt = ChatPromptTemplate.from_messages([
("system", "You are helpful but sassy assistant"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
tools =[get_current_temperature,sum_nums]
zhipu_tools = [convert_to_openai_tool(f) for f in tools]
model = chat_zhipu.bind(tools=zhipu_tools)
chain = prompt | model | OpenAIToolsAgentOutputParser()
def run_agent(user_input):
intermediate_steps = []
tool_mappings = {
"get_current_temperature": get_current_temperature,
"sum_nums":sum_nums
}
while True:
mes = format_to_openai_tool_messages(intermediate_steps)
result = chain.invoke({
"input": user_input,
"agent_scratchpad": mes
})
if isinstance(result, AgentFinish):
return result
tool = tool_mappings[result.tool]
observation = tool.run(result.tool_input)
intermediate_steps.append((result, observation))
print((result,observation))
result = run_agent("what is the weather is NewYork?")
print(result)
print(result.messages[0].content)
result2 = run_agent("what is the result of 19+23?")
print(result2)
print(result2.messages[0].content)
agent_output_parser.py 这个类的代码,这个类是参考了 OpenAI 的 functions 的 Agent 类的实现,修改了最后返回的对象。因为是工具调用,最终要返回的是 ToolMessage,而 functions 最终返回的是FunctionMessage。
python
# !/usr/bin env python3
import json
from json import JSONDecodeError
from typing import List, Union,Sequence, Tuple
from langchain_core.agents import AgentAction, AgentActionMessageLog, AgentFinish
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import (
AIMessage,
BaseMessage,
ToolMessage
)
from langchain_core.outputs import ChatGeneration, Generation
from langchain.agents.agent import AgentOutputParser
class OpenAIToolsAgentOutputParser(AgentOutputParser):
"""Parses a message into agent action/finish.
Is meant to be used with OpenAI models, as it relies on the specific
function_call parameter from OpenAI to convey what tools to use.
If a function_call parameter is passed, then that is used to get
the tool and tool input.
If one is not passed, then the AIMessage is assumed to be the final output.
"""
@property
def _type(self) -> str:
return "openai-functions-agent"
@staticmethod
def _parse_ai_message(message: BaseMessage) -> Union[AgentAction, AgentFinish]:
"""Parse an AI message."""
if not isinstance(message, AIMessage):
raise TypeError(f"Expected an AI message got {type(message)}")
function_call = message.additional_kwargs.get("tool_calls", [{}])
function_call = function_call[0].get("function",{})
if function_call:
function_name = function_call["name"]
try:
if len(function_call["arguments"].strip()) == 0:
# OpenAI returns an empty string for functions containing no args
_tool_input = {}
else:
# otherwise it returns a json object
_tool_input = json.loads(function_call["arguments"], strict=False)
except JSONDecodeError:
raise OutputParserException(
f"Could not parse tool input: {function_call} because "
f"the `arguments` is not valid JSON."
)
# HACK HACK HACK:
# The code that encodes tool input into Open AI uses a special variable
# name called `__arg1` to handle old style tools that do not expose a
# schema and expect a single string argument as an input.
# We unpack the argument here if it exists.
# Open AI does not support passing in a JSON array as an argument.
if "__arg1" in _tool_input:
tool_input = _tool_input["__arg1"]
else:
tool_input = _tool_input
content_msg = f"responded: {message.content}\n" if message.content else "\n"
log = f"\nInvoking: `{function_name}` with `{tool_input}`\n{content_msg}\n"
return AgentActionMessageLog(
tool=function_name,
tool_input=tool_input,
log=log,
message_log=[message],
)
return AgentFinish(
return_values={"output": message.content}, log=str(message.content)
)
def parse_result(
self, result: List[Generation], *, partial: bool = False
) -> Union[AgentAction, AgentFinish]:
if not isinstance(result[0], ChatGeneration):
raise ValueError("This output parser only works on ChatGeneration output")
message = result[0].message
return self._parse_ai_message(message)
def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
raise ValueError("Can only parse messages")
def format_to_openai_tool_messages(
intermediate_steps: Sequence[Tuple[AgentAction, str]],
) -> List[BaseMessage]:
"""Convert (AgentAction, tool output) tuples into FunctionMessages.
Args:
intermediate_steps: Steps the LLM has taken to date, along with observations
Returns:
list of messages to send to the LLM for the next prediction
"""
messages = []
for agent_action, observation in intermediate_steps:
messages.extend(_convert_agent_action_to_messages(agent_action, observation))
return messages
def _convert_agent_action_to_messages(
agent_action: AgentAction, observation: str
) -> List[BaseMessage]:
"""Convert an agent action to a message.
This code is used to reconstruct the original AI message from the agent action.
Args:
agent_action: Agent action to convert.
Returns:
AIMessage that corresponds to the original tool invocation.
"""
if isinstance(agent_action, AgentActionMessageLog):
return list(agent_action.message_log) + [
_create_function_message(agent_action, observation)
]
else:
return [AIMessage(content=agent_action.log)]
def _create_function_message(
agent_action: AgentAction, observation: str
) -> ToolMessage:
"""Convert agent action and observation into a function message.
Args:
agent_action: the tool invocation request from the agent
observation: the result of the tool invocation
Returns:
FunctionMessage that corresponds to the original tool invocation
"""
if not isinstance(observation, str):
try:
content = json.dumps(observation, ensure_ascii=False)
except Exception:
content = str(observation)
else:
content = observation
return ToolMessage(
tool_call_id=agent_action.tool,
content=content,
)
智普大模型调用部分,也就是 zhipu_llm 这个模块的文件,
可以参考这篇文章里的代码,代码比较长,就不再贴了。
另外,也可以使用 langchain 的 AgentExecutor 封装部分代码去实现 Agent 的功能,示例如下:
python
from langchain.agents import AgentExecutor
def agent_builder():
agent = (
{"input":lambda x:x["input"],
"agent_scratchpad":lambda x:format_to_openai_tool_messages(x["intermediate_steps"])
}
)| prompt | model | OpenAIToolsAgentOutputParser()
agent_executor = AgentExecutor(agent=agent,tools=tools,verbose=True)
return agent_executor
query = "how many letters in the word educa?"
agent = agent_builder()
result = agent.invoke({"input":query})
print(result)