Source code for langchain_core.utils.function_calling
"""Methods for creating function specs in the style of OpenAI Functions"""from__future__importannotationsimportcollectionsimportinspectimportloggingimporttypesimporttypingimportuuidfromtypingimport(TYPE_CHECKING,Any,Callable,Dict,List,Literal,Optional,Set,Tuple,Type,Union,cast,)fromtyping_extensionsimportAnnotated,TypedDict,get_args,get_origin,is_typeddictfromlangchain_core._apiimportdeprecatedfromlangchain_core.messagesimportAIMessage,BaseMessage,HumanMessage,ToolMessagefromlangchain_core.pydantic_v1importBaseModel,Field,create_modelfromlangchain_core.utils.json_schemaimportdereference_refsfromlangchain_core.utils.pydanticimportis_basemodel_subclassifTYPE_CHECKING:fromlangchain_core.toolsimportBaseToollogger=logging.getLogger(__name__)PYTHON_TO_JSON_TYPES={"str":"string","int":"integer","float":"number","bool":"boolean",}
[docs]classFunctionDescription(TypedDict):"""Representation of a callable function to send to an LLM."""name:str"""The name of the function."""description:str"""A description of the function."""parameters:dict"""The parameters of the function."""
[docs]classToolDescription(TypedDict):"""Representation of a callable function to the OpenAI API."""type:Literal["function"]"""The type of the tool."""function:FunctionDescription"""The function description."""
[docs]@deprecated("0.1.16",alternative="langchain_core.utils.function_calling.convert_to_openai_function()",removal="1.0",)defconvert_pydantic_to_openai_function(model:Type[BaseModel],*,name:Optional[str]=None,description:Optional[str]=None,rm_titles:bool=True,)->FunctionDescription:"""Converts a Pydantic model to a function description for the OpenAI API. Args: model: The Pydantic model to convert. name: The name of the function. If not provided, the title of the schema will be used. description: The description of the function. If not provided, the description of the schema will be used. rm_titles: Whether to remove titles from the schema. Defaults to True. Returns: The function description. """ifhasattr(model,"model_json_schema"):schema=model.model_json_schema()# Pydantic 2else:schema=model.schema()# Pydantic 1schema=dereference_refs(schema)schema.pop("definitions",None)title=schema.pop("title","")default_description=schema.pop("description","")return{"name":nameortitle,"description":descriptionordefault_description,"parameters":_rm_titles(schema)ifrm_titleselseschema,}
[docs]@deprecated("0.1.16",alternative="langchain_core.utils.function_calling.convert_to_openai_tool()",removal="1.0",)defconvert_pydantic_to_openai_tool(model:Type[BaseModel],*,name:Optional[str]=None,description:Optional[str]=None,)->ToolDescription:"""Converts a Pydantic model to a function description for the OpenAI API. Args: model: The Pydantic model to convert. name: The name of the function. If not provided, the title of the schema will be used. description: The description of the function. If not provided, the description of the schema will be used. Returns: The tool description. """function=convert_pydantic_to_openai_function(model,name=name,description=description)return{"type":"function","function":function}
def_get_python_function_name(function:Callable)->str:"""Get the name of a Python function."""returnfunction.__name__
[docs]@deprecated("0.1.16",alternative="langchain_core.utils.function_calling.convert_to_openai_function()",removal="1.0",)defconvert_python_function_to_openai_function(function:Callable,)->FunctionDescription:"""Convert a Python function to an OpenAI function-calling API compatible dict. Assumes the Python function has type hints and a docstring with a description. If the docstring has Google Python style argument descriptions, these will be included as well. Args: function: The Python function to convert. Returns: The OpenAI function description. """fromlangchain_core.tools.baseimportcreate_schema_from_functionfunc_name=_get_python_function_name(function)model=create_schema_from_function(func_name,function,filter_args=(),parse_docstring=True,error_on_invalid_docstring=False,include_injected=False,)returnconvert_pydantic_to_openai_function(model,name=func_name,description=model.__doc__,)
def_convert_typed_dict_to_openai_function(typed_dict:Type)->FunctionDescription:visited:Dict={}model=cast(Type[BaseModel],_convert_any_typed_dicts_to_pydantic(typed_dict,visited=visited),)returnconvert_pydantic_to_openai_function(model)_MAX_TYPED_DICT_RECURSION=25def_convert_any_typed_dicts_to_pydantic(type_:Type,*,visited:Dict,depth:int=0,)->Type:iftype_invisited:returnvisited[type_]elifdepth>=_MAX_TYPED_DICT_RECURSION:returntype_elifis_typeddict(type_):typed_dict=type_docstring=inspect.getdoc(typed_dict)annotations_=typed_dict.__annotations__description,arg_descriptions=_parse_google_docstring(docstring,list(annotations_))fields:dict={}forarg,arg_typeinannotations_.items():ifget_origin(arg_type)isAnnotated:annotated_args=get_args(arg_type)new_arg_type=_convert_any_typed_dicts_to_pydantic(annotated_args[0],depth=depth+1,visited=visited)field_kwargs={k:vfork,vinzip(("default","description"),annotated_args[1:])}if(field_desc:=field_kwargs.get("description"))andnotisinstance(field_desc,str):raiseValueError(f"Invalid annotation for field {arg}. Third argument to "f"Annotated must be a string description, received value of "f"type {type(field_desc)}.")elifarg_desc:=arg_descriptions.get(arg):field_kwargs["description"]=arg_descelse:passfields[arg]=(new_arg_type,Field(**field_kwargs))else:new_arg_type=_convert_any_typed_dicts_to_pydantic(arg_type,depth=depth+1,visited=visited)field_kwargs={"default":...}ifarg_desc:=arg_descriptions.get(arg):field_kwargs["description"]=arg_descfields[arg]=(new_arg_type,Field(**field_kwargs))model=create_model(typed_dict.__name__,**fields)model.__doc__=descriptionvisited[typed_dict]=modelreturnmodelelif(origin:=get_origin(type_))and(type_args:=get_args(type_)):subscriptable_origin=_py_38_safe_origin(origin)type_args=tuple(_convert_any_typed_dicts_to_pydantic(arg,depth=depth+1,visited=visited)forargintype_args)returnsubscriptable_origin[type_args]else:returntype_
[docs]@deprecated("0.1.16",alternative="langchain_core.utils.function_calling.convert_to_openai_function()",removal="1.0",)defformat_tool_to_openai_function(tool:BaseTool)->FunctionDescription:"""Format tool into the OpenAI function API. Args: tool: The tool to format. Returns: The function description. """fromlangchain_core.toolsimportsimpleis_simple_oai_tool=isinstance(tool,simple.Tool)andnottool.args_schemaiftool.tool_call_schemaandnotis_simple_oai_tool:returnconvert_pydantic_to_openai_function(tool.tool_call_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="1.0",)defformat_tool_to_openai_tool(tool:BaseTool)->ToolDescription:"""Format tool into the OpenAI function API. Args: tool: The tool to format. Returns: The tool description. """function=format_tool_to_openai_function(tool)return{"type":"function","function":function}
[docs]defconvert_to_openai_function(function:Union[Dict[str,Any],Type,Callable,BaseTool],*,strict:Optional[bool]=None,)->Dict[str,Any]:"""Convert a raw function/class to an OpenAI function. .. versionchanged:: 0.2.29 ``strict`` arg added. Args: function: A dictionary, Pydantic BaseModel class, TypedDict class, a LangChain Tool object, or a Python function. If a dictionary is passed in, it is assumed to already be a valid OpenAI function or a JSON schema with top-level 'title' and 'description' keys specified. strict: If True, model output is guaranteed to exactly match the JSON Schema provided in the function definition. If None, ``strict`` argument will not be included in function definition. .. versionadded:: 0.2.29 Returns: A dict version of the passed in function which is compatible with the OpenAI function-calling API. Raises: ValueError: If function is not in a supported format. """fromlangchain_core.toolsimportBaseTool# already in OpenAI function formatifisinstance(function,dict)andall(kinfunctionforkin("name","description","parameters")):oai_function=function# a JSON schema with title and descriptionelifisinstance(function,dict)andall(kinfunctionforkin("title","description","properties")):function=function.copy()oai_function={"name":function.pop("title"),"description":function.pop("description"),"parameters":function,}elifisinstance(function,type)andis_basemodel_subclass(function):oai_function=cast(Dict,convert_pydantic_to_openai_function(function))elifis_typeddict(function):oai_function=cast(Dict,_convert_typed_dict_to_openai_function(cast(Type,function)))elifisinstance(function,BaseTool):oai_function=cast(Dict,format_tool_to_openai_function(function))elifcallable(function):oai_function=cast(Dict,convert_python_function_to_openai_function(function))else:raiseValueError(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.")ifstrictisnotNone:oai_function["strict"]=strictifstrict:# As of 08/06/24, OpenAI requires that additionalProperties be supplied and# set to False if strict is True.# All properties layer needs 'additionalProperties=False'oai_function["parameters"]=_recursive_set_additional_properties_false(oai_function["parameters"])returnoai_function
[docs]defconvert_to_openai_tool(tool:Union[Dict[str,Any],Type[BaseModel],Callable,BaseTool],*,strict:Optional[bool]=None,)->Dict[str,Any]:"""Convert a raw function/class to an OpenAI tool. .. versionchanged:: 0.2.29 ``strict`` arg added. Args: tool: Either a dictionary, a pydantic.BaseModel class, Python function, or BaseTool. If a dictionary is passed in, it is assumed to already be a valid OpenAI tool, OpenAI function, or a JSON schema with top-level 'title' and 'description' keys specified. strict: If True, model output is guaranteed to exactly match the JSON Schema provided in the function definition. If None, ``strict`` argument will not be included in tool definition. .. versionadded:: 0.2.29 Returns: A dict version of the passed in tool which is compatible with the OpenAI tool-calling API. """ifisinstance(tool,dict)andtool.get("type")=="function"and"function"intool:returntooloai_function=convert_to_openai_function(tool,strict=strict)oai_tool:Dict[str,Any]={"type":"function","function":oai_function}returnoai_tool
[docs]deftool_example_to_messages(input:str,tool_calls:List[BaseModel],tool_outputs:Optional[List[str]]=None)->List[BaseMessage]:"""Convert an example into a list of messages that can be fed into an LLM. This code is an adapter that converts a single example to a list of messages that can be fed into a chat model. The list of messages per example corresponds to: 1) HumanMessage: contains the content from which content should be extracted. 2) AIMessage: contains the extracted information from the model 3) ToolMessage: contains confirmation to the model that the model requested a tool correctly. The ToolMessage is required because some chat models are hyper-optimized for agents rather than for an extraction use case. Arguments: input: string, the user input tool_calls: List[BaseModel], a list of tool calls represented as Pydantic BaseModels tool_outputs: Optional[List[str]], a list of tool call outputs. Does not need to be provided. If not provided, a placeholder value will be inserted. Defaults to None. Returns: A list of messages Examples: .. code-block:: python from typing import List, Optional from langchain_core.pydantic_v1 import BaseModel, Field from langchain_openai import ChatOpenAI class Person(BaseModel): '''Information about a person.''' name: Optional[str] = Field(..., description="The name of the person") hair_color: Optional[str] = Field( ..., description="The color of the person's hair if known" ) height_in_meters: Optional[str] = Field( ..., description="Height in METERs" ) examples = [ ( "The ocean is vast and blue. It's more than 20,000 feet deep.", Person(name=None, height_in_meters=None, hair_color=None), ), ( "Fiona traveled far from France to Spain.", 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=[]fortool_callintool_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_outputsor["You have correctly called this tool."]*len(openai_tool_calls)foroutput,tool_call_dictinzip(tool_outputs,openai_tool_calls):messages.append(ToolMessage(content=output,tool_call_id=tool_call_dict["id"]))# type: ignorereturnmessages
def_parse_google_docstring(docstring:Optional[str],args:List[str],*,error_on_invalid_docstring:bool=False,)->Tuple[str,dict]:"""Parse the function and argument descriptions from the docstring of a function. Assumes the function docstring follows Google Python style guide. """ifdocstring:docstring_blocks=docstring.split("\n\n")iferror_on_invalid_docstring:filtered_annotations={argforarginargsifargnotin("run_manager","callbacks","return")}iffiltered_annotationsand(len(docstring_blocks)<2ornotdocstring_blocks[1].startswith("Args:")):raiseValueError("Found invalid Google-Style docstring.")descriptors=[]args_block=Nonepast_descriptors=Falseforblockindocstring_blocks:ifblock.startswith("Args:"):args_block=blockbreakelifblock.startswith("Returns:")orblock.startswith("Example:"):# Don't break in case Args come afterpast_descriptors=Trueelifnotpast_descriptors:descriptors.append(block)else:continuedescription=" ".join(descriptors)else:iferror_on_invalid_docstring:raiseValueError("Found invalid Google-Style docstring.")description=""args_block=Nonearg_descriptions={}ifargs_block:arg=Noneforlineinargs_block.split("\n")[1:]:if":"inline:arg,desc=line.split(":",maxsplit=1)arg_descriptions[arg.strip()]=desc.strip()elifarg:arg_descriptions[arg.strip()]+=" "+line.strip()returndescription,arg_descriptionsdef_py_38_safe_origin(origin:Type)->Type:origin_union_type_map:Dict[Type,Any]=({types.UnionType:Union}ifhasattr(types,"UnionType")else{})origin_map:Dict[Type,Any]={dict:Dict,list:List,tuple:Tuple,set:Set,collections.abc.Iterable:typing.Iterable,collections.abc.Mapping:typing.Mapping,collections.abc.Sequence:typing.Sequence,collections.abc.MutableMapping:typing.MutableMapping,**origin_union_type_map,}returncast(Type,origin_map.get(origin,origin))def_recursive_set_additional_properties_false(schema:Dict[str,Any],)->Dict[str,Any]:ifisinstance(schema,dict):# Check if 'required' is a key at the current level or if the schema is empty,# in which case additionalProperties still needs to be specified.if"required"inschemaor("properties"inschemaandnotschema["properties"]):schema["additionalProperties"]=False# Recursively check 'properties' and 'items' if they existif"properties"inschema:forvalueinschema["properties"].values():_recursive_set_additional_properties_false(value)if"items"inschema:_recursive_set_additional_properties_false(schema["items"])returnschema