Source code for langchain_core.utils.function_calling
"""在OpenAI Functions风格中创建函数规范的方法"""
from __future__ import annotations
import inspect
import uuid
from types import FunctionType, MethodType
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Tuple,
Type,
Union,
cast,
)
from typing_extensions import TypedDict
from langchain_core._api import deprecated
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
ToolMessage,
)
from langchain_core.pydantic_v1 import BaseModel
from langchain_core.utils.json_schema import dereference_refs
if TYPE_CHECKING:
from langchain_core.tools import BaseTool
PYTHON_TO_JSON_TYPES = {
"str": "string",
"int": "integer",
"float": "number",
"bool": "boolean",
}
[docs]class FunctionDescription(TypedDict):
"""将可调用函数的表示发送到LLM。"""
name: str
"""函数的名称。"""
description: str
"""一个函数的描述。"""
parameters: dict
"""函数的参数。"""
[docs]class ToolDescription(TypedDict):
"""代表一个可调用函数给OpenAI API。"""
type: Literal["function"]
function: FunctionDescription
def _rm_titles(kv: dict, prev_key: str = "") -> dict:
new_kv = {}
for k, v in kv.items():
if k == "title":
if isinstance(v, dict) and prev_key == "properties" and "title" in v.keys():
new_kv[k] = _rm_titles(v, k)
else:
continue
elif isinstance(v, dict):
new_kv[k] = _rm_titles(v, k)
else:
new_kv[k] = v
return new_kv
[docs]@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.3.0",
)
def convert_pydantic_to_openai_function(
model: Type[BaseModel],
*,
name: Optional[str] = None,
description: Optional[str] = None,
rm_titles: bool = True,
) -> FunctionDescription:
"""将Pydantic模型转换为OpenAI API的函数描述。"""
schema = dereference_refs(model.schema())
schema.pop("definitions", None)
title = schema.pop("title", "")
default_description = schema.pop("description", "")
return {
"name": name or title,
"description": description or default_description,
"parameters": _rm_titles(schema) if rm_titles else schema,
}
[docs]@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_tool()",
removal="0.3.0",
)
def convert_pydantic_to_openai_tool(
model: Type[BaseModel],
*,
name: Optional[str] = None,
description: Optional[str] = None,
) -> ToolDescription:
"""将Pydantic模型转换为OpenAI API的函数描述。"""
function = convert_pydantic_to_openai_function(
model, name=name, description=description
)
return {"type": "function", "function": function}
def _get_python_function_name(function: Callable) -> str:
"""获取Python函数的名称。"""
return function.__name__
def _parse_python_function_docstring(function: Callable) -> Tuple[str, dict]:
"""从函数的文档字符串中解析函数和参数描述。
假设函数文档字符串遵循Google Python风格指南。
"""
docstring = inspect.getdoc(function)
if docstring:
docstring_blocks = docstring.split("\n\n")
descriptors = []
args_block = None
past_descriptors = False
for block in docstring_blocks:
if block.startswith("Args:"):
args_block = block
break
elif block.startswith("Returns:") or block.startswith("Example:"):
# Don't break in case Args come after
past_descriptors = True
elif not past_descriptors:
descriptors.append(block)
else:
continue
description = " ".join(descriptors)
else:
description = ""
args_block = None
arg_descriptions = {}
if args_block:
arg = None
for line in args_block.split("\n")[1:]:
if ":" in line:
arg, desc = line.split(":", maxsplit=1)
arg_descriptions[arg.strip()] = desc.strip()
elif arg:
arg_descriptions[arg.strip()] += " " + line.strip()
return description, arg_descriptions
def _get_python_function_arguments(function: Callable, arg_descriptions: dict) -> dict:
"""获取描述Python函数参数的JsonSchema。
假设所有函数参数都是基本类型(int,float,str,bool)或是pydantic.BaseModel的子类。
"""
properties = {}
annotations = inspect.getfullargspec(function).annotations
for arg, arg_type in annotations.items():
if arg == "return":
continue
if isinstance(arg_type, type) and issubclass(arg_type, BaseModel):
# Mypy error:
# "type" has no attribute "schema"
properties[arg] = arg_type.schema() # type: ignore[attr-defined]
elif (
hasattr(arg_type, "__name__")
and getattr(arg_type, "__name__") in PYTHON_TO_JSON_TYPES
):
properties[arg] = {"type": PYTHON_TO_JSON_TYPES[arg_type.__name__]}
elif (
hasattr(arg_type, "__dict__")
and getattr(arg_type, "__dict__").get("__origin__", None) == Literal
):
properties[arg] = {
"enum": list(arg_type.__args__), # type: ignore
"type": PYTHON_TO_JSON_TYPES[arg_type.__args__[0].__class__.__name__], # type: ignore
}
if arg in arg_descriptions:
if arg not in properties:
properties[arg] = {}
properties[arg]["description"] = arg_descriptions[arg]
return properties
def _get_python_function_required_args(function: Callable) -> List[str]:
"""获取Python函数所需的参数。"""
spec = inspect.getfullargspec(function)
required = spec.args[: -len(spec.defaults)] if spec.defaults else spec.args
required += [k for k in spec.kwonlyargs if k not in (spec.kwonlydefaults or {})]
is_function_type = isinstance(function, FunctionType)
is_method_type = isinstance(function, MethodType)
if required and is_function_type and required[0] == "self":
required = required[1:]
elif required and is_method_type and required[0] == "cls":
required = required[1:]
return required
[docs]@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.3.0",
)
def convert_python_function_to_openai_function(
function: Callable,
) -> Dict[str, Any]:
"""将一个Python函数转换为与OpenAI函数调用API兼容的字典。
假设Python函数具有类型提示和带有描述的文档字符串。如果文档字符串具有Google Python风格的参数描述,这些描述也将被包括在内。
"""
description, arg_descriptions = _parse_python_function_docstring(function)
return {
"name": _get_python_function_name(function),
"description": description,
"parameters": {
"type": "object",
"properties": _get_python_function_arguments(function, arg_descriptions),
"required": _get_python_function_required_args(function),
},
}
[docs]@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.3.0",
)
def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
"""将工具格式化为OpenAI函数API。"""
if tool.args_schema:
return convert_pydantic_to_openai_function(
tool.args_schema, name=tool.name, description=tool.description
)
else:
return {
"name": tool.name,
"description": tool.description,
"parameters": {
# This is a hack to get around the fact that some tools
# do not expose an args_schema, and expect an argument
# which is a string.
# And Open AI does not support an array type for the
# parameters.
"properties": {
"__arg1": {"title": "__arg1", "type": "string"},
},
"required": ["__arg1"],
"type": "object",
},
}
[docs]@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_tool()",
removal="0.3.0",
)
def format_tool_to_openai_tool(tool: BaseTool) -> ToolDescription:
"""将工具格式化为OpenAI函数API。"""
function = format_tool_to_openai_function(tool)
return {"type": "function", "function": function}
[docs]def convert_to_openai_function(
function: Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool],
) -> Dict[str, Any]:
"""将原始函数/类转换为OpenAI函数。
参数:
function:可以是字典、pydantic.BaseModel类或Python函数。
如果传入的是字典,则假定它已经是一个有效的OpenAI函数或带有顶级'title'和'description'键的JSON模式。
返回:
传入函数的字典版本,与OpenAI函数调用API兼容。
"""
from langchain_core.tools import BaseTool
# already in OpenAI function format
if isinstance(function, dict) and all(
k in function for k in ("name", "description", "parameters")
):
return function
# a JSON schema with title and description
elif isinstance(function, dict) and all(
k in function for k in ("title", "description", "properties")
):
function = function.copy()
return {
"name": function.pop("title"),
"description": function.pop("description"),
"parameters": function,
}
elif isinstance(function, type) and issubclass(function, BaseModel):
return cast(Dict, convert_pydantic_to_openai_function(function))
elif isinstance(function, BaseTool):
return cast(Dict, format_tool_to_openai_function(function))
elif callable(function):
return convert_python_function_to_openai_function(function)
else:
raise ValueError(
f"Unsupported function\n\n{function}\n\nFunctions must be passed in"
" as Dict, pydantic.BaseModel, or Callable. If they're a dict they must"
" either be in OpenAI function format or valid JSON schema with top-level"
" 'title' and 'description' keys."
)
[docs]def convert_to_openai_tool(
tool: Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool],
) -> Dict[str, Any]:
"""将原始函数/类转换为OpenAI工具。
参数:
tool:可以是字典、pydantic.BaseModel类、Python函数或BaseTool。如果传入字典,则假定它已经是有效的OpenAI工具、OpenAI函数或带有顶级'title'和'description'键的JSON模式。
返回:
传入工具的字典版本,与OpenAI工具调用API兼容。
"""
if isinstance(tool, dict) and tool.get("type") == "function" and "function" in tool:
return tool
function = convert_to_openai_function(tool)
return {"type": "function", "function": function}
[docs]def tool_example_to_messages(
input: str, tool_calls: List[BaseModel], tool_outputs: Optional[List[str]] = None
) -> List[BaseMessage]:
"""将一个示例转换为可以输入到LLM中的消息列表。
这段代码是一个适配器,将单个示例转换为可以输入到聊天模型中的消息列表。
每个示例的消息列表对应于:
1) HumanMessage: 包含应从中提取内容的内容。
2) AIMessage: 包含从模型中提取的信息
3) ToolMessage: 包含确认消息,告知模型已正确请求了一个工具。
ToolMessage是必需的,因为一些聊天模型是针对代理而不是用于提取的情况进行了超优化。
参数:
input: 字符串,用户输入
tool_calls: List[BaseModel],表示为Pydantic BaseModel的工具调用列表
tool_outputs: Optional[List[str]],工具调用输出的列表。
不需要提供。如果未提供,将插入一个占位符值。
返回:
一个消息列表
示例:
.. code-block:: python
from typing import List, Optional
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI
class Person(BaseModel):
'''关于一个人的信息。'''
name: Optional[str] = Field(..., description="人的姓名")
hair_color: Optional[str] = Field(
..., description="如果已知,人的头发颜色"
)
height_in_meters: Optional[str] = Field(
..., description="以米为单位的身高"
)
examples = [
(
"海洋辽阔而蔚蓝。它超过了20,000英尺的深度。",
Person(name=None, height_in_meters=None, hair_color=None),
),
(
"Fiona从法国远行到了西班牙。",
Person(name="Fiona", height_in_meters=None, hair_color=None),
),
]
messages = []
for txt, tool_call in examples:
messages.extend(
tool_example_to_messages(txt, [tool_call])
)
"""
messages: List[BaseMessage] = [HumanMessage(content=input)]
openai_tool_calls = []
for tool_call in tool_calls:
openai_tool_calls.append(
{
"id": str(uuid.uuid4()),
"type": "function",
"function": {
# The name of the function right now corresponds to the name
# of the pydantic model. This is implicit in the API right now,
# and will be improved over time.
"name": tool_call.__class__.__name__,
"arguments": tool_call.json(),
},
}
)
messages.append(
AIMessage(content="", additional_kwargs={"tool_calls": openai_tool_calls})
)
tool_outputs = tool_outputs or ["You have correctly called this tool."] * len(
openai_tool_calls
)
for output, tool_call_dict in zip(tool_outputs, openai_tool_calls):
messages.append(ToolMessage(content=output, tool_call_id=tool_call_dict["id"])) # type: ignore
return messages