Source code for langchain_core.output_parsers.openai_tools
import copy
import json
from json import JSONDecodeError
from typing import Any, Dict, List, Optional, Type
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import AIMessage, InvalidToolCall
from langchain_core.output_parsers import BaseCumulativeTransformOutputParser
from langchain_core.outputs import ChatGeneration, Generation
from langchain_core.pydantic_v1 import BaseModel, ValidationError
from langchain_core.utils.json import parse_partial_json
[docs]def parse_tool_call(
raw_tool_call: Dict[str, Any],
*,
partial: bool = False,
strict: bool = False,
return_id: bool = True,
) -> Optional[Dict[str, Any]]:
"""解析单个工具调用。"""
if "function" not in raw_tool_call:
return None
if partial:
try:
function_args = parse_partial_json(
raw_tool_call["function"]["arguments"], strict=strict
)
except (JSONDecodeError, TypeError): # None args raise TypeError
return None
else:
try:
function_args = json.loads(
raw_tool_call["function"]["arguments"], strict=strict
)
except JSONDecodeError as e:
raise OutputParserException(
f"Function {raw_tool_call['function']['name']} arguments:\n\n"
f"{raw_tool_call['function']['arguments']}\n\nare not valid JSON. "
f"Received JSONDecodeError {e}"
)
parsed = {
"name": raw_tool_call["function"]["name"] or "",
"args": function_args or {},
}
if return_id:
parsed["id"] = raw_tool_call.get("id")
return parsed
[docs]def make_invalid_tool_call(
raw_tool_call: Dict[str, Any],
error_msg: Optional[str],
) -> InvalidToolCall:
"""从原始工具调用创建一个InvalidToolCall。"""
return InvalidToolCall(
name=raw_tool_call["function"]["name"],
args=raw_tool_call["function"]["arguments"],
id=raw_tool_call.get("id"),
error=error_msg,
)
[docs]def parse_tool_calls(
raw_tool_calls: List[dict],
*,
partial: bool = False,
strict: bool = False,
return_id: bool = True,
) -> List[Dict[str, Any]]:
"""解析工具调用列表。"""
final_tools: List[Dict[str, Any]] = []
exceptions = []
for tool_call in raw_tool_calls:
try:
parsed = parse_tool_call(
tool_call, partial=partial, strict=strict, return_id=return_id
)
if parsed:
final_tools.append(parsed)
except OutputParserException as e:
exceptions.append(str(e))
continue
if exceptions:
raise OutputParserException("\n\n".join(exceptions))
return final_tools
[docs]class JsonOutputToolsParser(BaseCumulativeTransformOutputParser[Any]):
"""从OpenAI响应中解析工具。"""
strict: bool = False
"""是否允许非符合JSON规范的字符串。
参见:https://docs.python.org/3/library/json.html#encoders-and-decoders
在解析输出可能包含Unicode字符或换行符时很有用。"""
return_id: bool = False
"""是否返回工具调用ID。"""
first_tool_only: bool = False
"""是否仅返回第一个工具调用。
如果为False,则结果将是工具调用的列表,如果未找到工具调用,则为空列表。
如果为True,并且找到多个工具调用,则仅返回第一个工具调用,其他工具调用将被忽略。
如果未找到工具调用,则返回None。"""
[docs] def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any:
generation = result[0]
if not isinstance(generation, ChatGeneration):
raise OutputParserException(
"This output parser can only be used with a chat generation."
)
message = generation.message
if isinstance(message, AIMessage) and message.tool_calls:
tool_calls = [dict(tc) for tc in message.tool_calls]
for tool_call in tool_calls:
if not self.return_id:
_ = tool_call.pop("id")
else:
try:
raw_tool_calls = copy.deepcopy(message.additional_kwargs["tool_calls"])
except KeyError:
return []
tool_calls = parse_tool_calls(
raw_tool_calls,
partial=partial,
strict=self.strict,
return_id=self.return_id,
)
# for backwards compatibility
for tc in tool_calls:
tc["type"] = tc.pop("name")
if self.first_tool_only:
return tool_calls[0] if tool_calls else None
return tool_calls
[docs]class JsonOutputKeyToolsParser(JsonOutputToolsParser):
"""从OpenAI响应中解析工具。"""
key_name: str
"""返回工具的类型。"""
[docs] def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any:
parsed_result = super().parse_result(result, partial=partial)
if self.first_tool_only:
single_result = (
parsed_result
if parsed_result and parsed_result["type"] == self.key_name
else None
)
if self.return_id:
return single_result
elif single_result:
return single_result["args"]
else:
return None
parsed_result = [res for res in parsed_result if res["type"] == self.key_name]
if not self.return_id:
parsed_result = [res["args"] for res in parsed_result]
return parsed_result
[docs]class PydanticToolsParser(JsonOutputToolsParser):
"""从OpenAI响应中解析工具。"""
tools: List[Type[BaseModel]]
# TODO: Support more granular streaming of objects. Currently only streams once all
# Pydantic object fields are present.
[docs] def parse_result(self, result: List[Generation], *, partial: bool = False) -> Any:
json_results = super().parse_result(result, partial=partial)
if not json_results:
return None if self.first_tool_only else []
json_results = [json_results] if self.first_tool_only else json_results
name_dict = {tool.__name__: tool for tool in self.tools}
pydantic_objects = []
for res in json_results:
try:
if not isinstance(res["args"], dict):
raise ValueError(
f"Tool arguments must be specified as a dict, received: "
f"{res['args']}"
)
pydantic_objects.append(name_dict[res["type"]](**res["args"]))
except (ValidationError, ValueError) as e:
if partial:
continue
else:
raise e
if self.first_tool_only:
return pydantic_objects[0] if pydantic_objects else None
else:
return pydantic_objects