Source code for langchain_google_vertexai.functions_utils
from__future__importannotationsimportjsonimportloggingfromtypingimport(Any,Callable,Dict,List,Literal,Optional,Sequence,Type,TypedDict,Union,cast,)importgoogle.cloud.aiplatform_v1beta1.typesasgapicimportvertexai.generative_modelsasvertexai# type: ignorefromgoogle.cloud.aiplatform_v1beta1.typesimport(ToolConfigasGapicToolConfig,)fromlangchain_core.exceptionsimportOutputParserExceptionfromlangchain_core.output_parsersimportBaseOutputParserfromlangchain_core.outputsimportChatGeneration,Generationfromlangchain_core.toolsimportBaseToolfromlangchain_core.toolsimporttoolascallable_as_lc_toolfromlangchain_core.utils.function_callingimport(FunctionDescription,convert_to_openai_tool,)fromlangchain_core.utils.json_schemaimportdereference_refsfrompydanticimportBaseModellogger=logging.getLogger(__name__)_FunctionDeclarationLike=Union[BaseTool,Type[BaseModel],FunctionDescription,Callable,vertexai.FunctionDeclaration,Dict[str,Any],]_GoogleSearchRetrievalLike=Union[gapic.GoogleSearchRetrieval,Dict[str,Any],]_RetrievalLike=Union[gapic.Retrieval,Dict[str,Any]]class_ToolDictLike(TypedDict):function_declarations:Optional[List[_FunctionDeclarationLike]]google_search_retrieval:Optional[_GoogleSearchRetrievalLike]retrieval:Optional[_RetrievalLike]_ToolType=Union[gapic.Tool,vertexai.Tool,_ToolDictLike,_FunctionDeclarationLike]_ToolsType=Sequence[_ToolType]_ALLOWED_SCHEMA_FIELDS=[]_ALLOWED_SCHEMA_FIELDS.extend([f.nameforfingapic.Schema()._pb.DESCRIPTOR.fields])_ALLOWED_SCHEMA_FIELDS.extend([fforfingapic.Schema.to_dict(gapic.Schema(),preserving_proto_field_name=False).keys()])_ALLOWED_SCHEMA_FIELDS_SET=set(_ALLOWED_SCHEMA_FIELDS)def_format_json_schema_to_gapic_v1(schema:Dict[str,Any])->Dict[str,Any]:"""Format a JSON schema from a Pydantic V1 BaseModel to gapic."""converted_schema:Dict[str,Any]={}forkey,valueinschema.items():ifkey=="definitions":continueelifkey=="items":converted_schema["items"]=_format_json_schema_to_gapic_v1(value)elifkey=="properties":if"properties"notinconverted_schema:converted_schema["properties"]={}forpkey,pvalueinvalue.items():converted_schema["properties"][pkey]=_format_json_schema_to_gapic_v1(pvalue)continueelifkeyin["type","_type"]:converted_schema["type"]=str(value).upper()elifkey=="allOf":iflen(value)>1:logger.warning("Only first value for 'allOf' key is supported. "f"Got {len(value)}, ignoring other than first value!")return_format_json_schema_to_gapic_v1(value[0])elifkeynotin_ALLOWED_SCHEMA_FIELDS_SET:logger.warning(f"Key '{key}' is not supported in schema, ignoring")else:converted_schema[key]=valuereturnconverted_schemadef_format_json_schema_to_gapic(schema:Dict[str,Any],parent_key:Optional[str]=None,required_fields:Optional[list]=None,)->Dict[str,Any]:"""Format a JSON schema from a Pydantic V2 BaseModel to gapic."""converted_schema:Dict[str,Any]={}forkey,valueinschema.items():ifkey=="definitions":continueelifkey=="items":converted_schema["items"]=_format_json_schema_to_gapic(value,parent_key,required_fields)elifkey=="properties":if"properties"notinconverted_schema:converted_schema["properties"]={}forpkey,pvalueinvalue.items():converted_schema["properties"][pkey]=_format_json_schema_to_gapic(pvalue,pkey,schema.get("required",[]))continueelifkeyin["type","_type"]:converted_schema["type"]=str(value).upper()elifkey=="allOf":iflen(value)>1:logger.warning("Only first value for 'allOf' key is supported. "f"Got {len(value)}, ignoring other than first value!")return_format_json_schema_to_gapic(value[0],parent_key,required_fields)elifkey=="anyOf":iflen(value)==2andany(v.get("type")=="null"forvinvalue):non_null_type=next(vforvinvalueifv.get("type")!="null")converted_schema.update(_format_json_schema_to_gapic(non_null_type,parent_key,required_fields))# Remove the field from required if it existsifrequired_fieldsandparent_keyinrequired_fields:required_fields.remove(parent_key)continueelifkeynotin_ALLOWED_SCHEMA_FIELDS_SET:logger.warning(f"Key '{key}' is not supported in schema, ignoring")else:converted_schema[key]=valuereturnconverted_schemadef_dict_to_gapic_schema(schema:Dict[str,Any],pydantic_version:str="v1")->gapic.Schema:dereferenced_schema=dereference_refs(schema)ifpydantic_version=="v1":formatted_schema=_format_json_schema_to_gapic_v1(dereferenced_schema)else:formatted_schema=_format_json_schema_to_gapic(dereferenced_schema)json_schema=json.dumps(formatted_schema)returngapic.Schema.from_json(json_schema)def_format_base_tool_to_function_declaration(tool:BaseTool,)->gapic.FunctionDeclaration:"Format tool into the Vertex function API."ifnottool.args_schema:returngapic.FunctionDeclaration(name=tool.name,description=tool.description,parameters=gapic.Schema(type=gapic.Type.OBJECT,properties={"__arg1":gapic.Schema(type=gapic.Type.STRING),},required=["__arg1"],),)ifhasattr(tool.args_schema,"model_json_schema"):schema=tool.args_schema.model_json_schema()pydantic_version="v2"else:schema=tool.args_schema.schema()# type: ignore[attr-defined]pydantic_version="v1"parameters=_dict_to_gapic_schema(schema,pydantic_version=pydantic_version)returngapic.FunctionDeclaration(name=tool.nameorschema.get("title"),description=tool.descriptionorschema.get("description"),parameters=parameters,)def_format_pydantic_to_function_declaration(pydantic_model:Type[BaseModel],)->gapic.FunctionDeclaration:ifhasattr(pydantic_model,"model_json_schema"):schema=pydantic_model.model_json_schema()pydantic_version="v2"else:schema=pydantic_model.schema()pydantic_version="v1"returngapic.FunctionDeclaration(name=schema["title"],description=schema.get("description",""),parameters=_dict_to_gapic_schema(schema,pydantic_version=pydantic_version),)def_format_dict_to_function_declaration(tool:Union[FunctionDescription,Dict[str,Any]],)->gapic.FunctionDeclaration:# Ensure we send "anyOf" parameters through pydantic v2 schema parsingpydantic_version=Noneifisinstance(tool,dict):properties=tool.get("parameters",{}).get("properties",{}).values()forpropertyinproperties:if"anyOf"inproperty:pydantic_version="v2"ifpydantic_version:parameters=_dict_to_gapic_schema(tool.get("parameters",{}),pydantic_version=pydantic_version)else:parameters=_dict_to_gapic_schema(tool.get("parameters",{}))returngapic.FunctionDeclaration(name=tool.get("name"),description=tool.get("description"),parameters=parameters,)def_format_vertex_to_function_declaration(tool:vertexai.FunctionDeclaration,)->gapic.FunctionDeclaration:tool_dict=tool.to_dict()return_format_dict_to_function_declaration(tool_dict)def_format_to_gapic_function_declaration(tool:_FunctionDeclarationLike,)->gapic.FunctionDeclaration:"Format tool into the Vertex function declaration."ifisinstance(tool,BaseTool):return_format_base_tool_to_function_declaration(tool)elifisinstance(tool,type)andissubclass(tool,BaseModel):return_format_pydantic_to_function_declaration(tool)elifcallable(tool)andnot(isinstance(tool,type)andhasattr(tool,"__annotations__")):return_format_base_tool_to_function_declaration(callable_as_lc_tool()(tool))elifisinstance(tool,vertexai.FunctionDeclaration):return_format_vertex_to_function_declaration(tool)elifisinstance(tool,dict)or(isinstance(tool,type)andhasattr(tool,"__annotations__")):# this could come from# 'langchain_core.utils.function_calling.convert_to_openai_tool'function=convert_to_openai_tool(cast(dict,tool))["function"]return_format_dict_to_function_declaration(cast(FunctionDescription,function))else:raiseValueError(f"Unsupported tool call type {tool}")def_format_to_gapic_tool(tools:_ToolsType)->gapic.Tool:gapic_tool=gapic.Tool()fortoolintools:ifany(fingapic_toolforfin["google_search_retrieval","retrieval"]):raiseValueError("Providing multiple retrieval, google_search_retrieval"" or mixing with function_declarations is not supported")ifisinstance(tool,(gapic.Tool,vertexai.Tool)):rt:gapic.Tool=(toolifisinstance(tool,gapic.Tool)elsetool._raw_tool# type: ignore)if"retrieval"inrt:gapic_tool.retrieval=rt.retrievalif"google_search_retrieval"inrt:gapic_tool.google_search_retrieval=rt.google_search_retrievalif"function_declarations"inrt:gapic_tool.function_declarations.extend(rt.function_declarations)if"google_search"inrt:gapic_tool.google_search=rt.google_searchelifisinstance(tool,dict):# not _ToolDictLikeifnotany(fintoolforfin["function_declarations","google_search_retrieval","retrieval",]):fd=_format_to_gapic_function_declaration(tool)gapic_tool.function_declarations.append(fd)continue# _ToolDictLiketool=cast(_ToolDictLike,tool)if"function_declarations"intool:function_declarations=tool["function_declarations"]ifnotisinstance(tool["function_declarations"],list):raiseValueError("function_declarations should be a list"f"got '{type(function_declarations)}'")iffunction_declarations:fds=[_format_to_gapic_function_declaration(fd)forfdinfunction_declarations]gapic_tool.function_declarations.extend(fds)if"google_search_retrieval"intool:gapic_tool.google_search_retrieval=gapic.GoogleSearchRetrieval(tool["google_search_retrieval"])if"retrieval"intool:gapic_tool.retrieval=gapic.Retrieval(tool["retrieval"])else:fd=_format_to_gapic_function_declaration(tool)gapic_tool.function_declarations.append(fd)returngapic_tool
[docs]classPydanticFunctionsOutputParser(BaseOutputParser):"""Parse an output as a pydantic object. This parser is used to parse the output of a ChatModel that uses Google Vertex function format to invoke functions. The parser extracts the function call invocation and matches them to the pydantic schema provided. An exception will be raised if the function call does not match the provided schema. Example: ... code-block:: python message = AIMessage( content="This is a test message", additional_kwargs={ "function_call": { "name": "cookie", "arguments": json.dumps({"name": "value", "age": 10}), } }, ) chat_generation = ChatGeneration(message=message) class Cookie(BaseModel): name: str age: int class Dog(BaseModel): species: str # Full output parser = PydanticOutputFunctionsParser( pydantic_schema={"cookie": Cookie, "dog": Dog} ) result = parser.parse_result([chat_generation]) """pydantic_schema:Union[Type[BaseModel],Dict[str,Type[BaseModel]]]
[docs]defparse_result(self,result:List[Generation],*,partial:bool=False)->BaseModel:ifnotisinstance(result[0],ChatGeneration):raiseValueError("This output parser only works on ChatGeneration output")message=result[0].messagefunction_call=message.additional_kwargs.get("function_call",{})iffunction_call:function_name=function_call["name"]tool_input=function_call.get("arguments",{})ifisinstance(self.pydantic_schema,dict):schema=self.pydantic_schema[function_name]else:schema=self.pydantic_schemareturnschema(**json.loads(tool_input))else:raiseOutputParserException(f"Could not parse function call: {message}")
[docs]defparse(self,text:str)->BaseModel:raiseValueError("Can only parse messages")
class_FunctionCallingConfigDict(TypedDict):mode:Union[gapic.FunctionCallingConfig.Mode,int]allowed_function_names:Optional[List[str]]class_ToolConfigDict(TypedDict):function_calling_config:_FunctionCallingConfigDict_ToolChoiceType=Union[dict,List[str],str,Literal["auto","none","any"],Literal[True]]def_format_tool_config(tool_config:_ToolConfigDict)->Union[gapic.ToolConfig,None]:if"function_calling_config"notintool_config:raiseValueError("Invalid ToolConfig, missing 'function_calling_config' key. Received:\n\n"f"{tool_config=}")returngapic.ToolConfig(function_calling_config=gapic.FunctionCallingConfig(**tool_config["function_calling_config"]))def_tool_choice_to_tool_config(tool_choice:_ToolChoiceType,all_names:List[str],)->Optional[GapicToolConfig]:allowed_function_names:Optional[List[str]]=Noneiftool_choiceisTrueortool_choice=="any":mode=gapic.FunctionCallingConfig.Mode.ANYallowed_function_names=all_nameseliftool_choice=="auto":mode=gapic.FunctionCallingConfig.Mode.AUTOeliftool_choice=="none":mode=gapic.FunctionCallingConfig.Mode.NONEelifisinstance(tool_choice,str):mode=gapic.FunctionCallingConfig.Mode.ANYallowed_function_names=[tool_choice]elifisinstance(tool_choice,list):mode=gapic.FunctionCallingConfig.Mode.ANYallowed_function_names=tool_choiceelifisinstance(tool_choice,dict):if"mode"intool_choice:mode=tool_choice["mode"]allowed_function_names=tool_choice.get("allowed_function_names")elif"function_calling_config"intool_choice:mode=tool_choice["function_calling_config"]["mode"]allowed_function_names=tool_choice["function_calling_config"].get("allowed_function_names")elif("type"intool_choiceandtool_choice["type"]=="function"and"function"intool_choiceand"name"intool_choice["function"]):mode=gapic.FunctionCallingConfig.Mode.ANYallowed_function_names=[tool_choice["function"]["name"]]else:raiseValueError(f"Unrecognized tool choice format:\n\n{tool_choice=}\n\nShould match "f"VertexAI ToolConfig or FunctionCallingConfig format.")else:raiseValueError(f"Unrecognized tool choice format:\n\n{tool_choice=}")tool_config=_ToolConfigDict(function_calling_config=_FunctionCallingConfigDict(mode=mode,allowed_function_names=allowed_function_names,))return_format_tool_config(tool_config)