[docs]classChatMlflow(BaseChatModel):"""`MLflow` chat models API. To use, you should have the `mlflow[genai]` python package installed. For more information, see https://mlflow.org/docs/latest/llms/deployments. Example: .. code-block:: python from langchain_community.chat_models import ChatMlflow chat = ChatMlflow( target_uri="http://localhost:5000", endpoint="chat", temperature=0.1, ) """endpoint:str"""The endpoint to use."""target_uri:str"""The target URI to use."""temperature:float=0.0"""The sampling temperature."""n:int=1"""The number of completion choices to generate."""stop:Optional[List[str]]=None"""The stop sequence."""max_tokens:Optional[int]=None"""The maximum number of tokens to generate."""extra_params:dict=Field(default_factory=dict)"""Any extra parameters to pass to the endpoint."""_client:Any=PrivateAttr()def__init__(self,**kwargs:Any):super().__init__(**kwargs)self._validate_uri()try:frommlflow.deploymentsimportget_deploy_clientself._client=get_deploy_client(self.target_uri)exceptImportErrorase:raiseImportError("Failed to create the client. "f"Please run `pip install mlflow{self._mlflow_extras}` to install ""required dependencies.")frome@propertydef_mlflow_extras(self)->str:return"[genai]"def_validate_uri(self)->None:ifself.target_uri=="databricks":returnallowed=["http","https","databricks"]ifurlparse(self.target_uri).schemenotinallowed:raiseValueError(f"Invalid target URI: {self.target_uri}. "f"The scheme must be one of {allowed}.")@propertydef_default_params(self)->Dict[str,Any]:params:Dict[str,Any]={"target_uri":self.target_uri,"endpoint":self.endpoint,"temperature":self.temperature,"n":self.n,"stop":self.stop,"max_tokens":self.max_tokens,"extra_params":self.extra_params,}returnparamsdef_prepare_inputs(self,messages:List[BaseMessage],stop:Optional[List[str]]=None,**kwargs:Any,)->Dict[str,Any]:message_dicts=[ChatMlflow._convert_message_to_dict(message)formessageinmessages]data:Dict[str,Any]={"messages":message_dicts,"temperature":self.temperature,"n":self.n,**self.extra_params,**kwargs,}ifstop:=self.stoporstop:data["stop"]=stopifself.max_tokensisnotNone:data["max_tokens"]=self.max_tokensreturndatadef_generate(self,messages:List[BaseMessage],stop:Optional[List[str]]=None,run_manager:Optional[CallbackManagerForLLMRun]=None,**kwargs:Any,)->ChatResult:data=self._prepare_inputs(messages,stop,**kwargs,)resp=self._client.predict(endpoint=self.endpoint,inputs=data)returnChatMlflow._create_chat_result(resp)
[docs]defstream(self,input:LanguageModelInput,config:Optional[RunnableConfig]=None,*,stop:Optional[List[str]]=None,**kwargs:Any,)->Iterator[BaseMessageChunk]:# We need to override `stream` to handle the case# that `self._client` does not implement `predict_stream`ifnothasattr(self._client,"predict_stream"):# MLflow deployment client does not implement streaming,# so use default implementationyieldcast(BaseMessageChunk,self.invoke(input,config=config,stop=stop,**kwargs))else:yield fromsuper().stream(input,config,stop=stop,**kwargs)
def_stream(self,messages:List[BaseMessage],stop:Optional[List[str]]=None,run_manager:Optional[CallbackManagerForLLMRun]=None,**kwargs:Any,)->Iterator[ChatGenerationChunk]:data=self._prepare_inputs(messages,stop,**kwargs,)# TODO: check if `_client.predict_stream` is available.chunk_iter=self._client.predict_stream(endpoint=self.endpoint,inputs=data)first_chunk_role=Noneforchunkinchunk_iter:ifchunk["choices"]:choice=chunk["choices"][0]chunk_delta=choice["delta"]iffirst_chunk_roleisNone:first_chunk_role=chunk_delta.get("role")chunk_message=ChatMlflow._convert_delta_to_message_chunk(chunk_delta,first_chunk_role)generation_info={}iffinish_reason:=choice.get("finish_reason"):generation_info["finish_reason"]=finish_reasoniflogprobs:=choice.get("logprobs"):generation_info["logprobs"]=logprobschunk=ChatGenerationChunk(message=chunk_message,generation_info=generation_infoorNone)ifrun_manager:run_manager.on_llm_new_token(chunk.text,chunk=chunk,logprobs=logprobs)yieldchunkelse:# Handle the case where choices are empty if neededcontinue@propertydef_identifying_params(self)->Dict[str,Any]:returnself._default_paramsdef_get_invocation_params(self,stop:Optional[List[str]]=None,**kwargs:Any)->Dict[str,Any]:"""Get the parameters used to invoke the model FOR THE CALLBACKS."""return{**self._default_params,**super()._get_invocation_params(stop=stop,**kwargs),}@propertydef_llm_type(self)->str:"""Return type of chat model."""return"mlflow-chat"@staticmethoddef_convert_dict_to_message(_dict:Mapping[str,Any])->BaseMessage:role=_dict["role"]content=cast(str,_dict.get("content"))ifrole=="user":returnHumanMessage(content=content)elifrole=="assistant":content=contentor""additional_kwargs:Dict={}tool_calls=[]invalid_tool_calls=[]ifraw_tool_calls:=_dict.get("tool_calls"):additional_kwargs["tool_calls"]=raw_tool_callsforraw_tool_callinraw_tool_calls:try:tool_calls.append(parse_tool_call(raw_tool_call,return_id=True))exceptExceptionase:invalid_tool_calls.append(make_invalid_tool_call(raw_tool_call,str(e)))returnAIMessage(content=content,additional_kwargs=additional_kwargs,id=_dict.get("id"),tool_calls=tool_calls,invalid_tool_calls=invalid_tool_calls,)elifrole=="system":returnSystemMessage(content=content)else:returnChatMessage(content=content,role=role)@staticmethoddef_convert_delta_to_message_chunk(_dict:Mapping[str,Any],default_role:str)->BaseMessageChunk:role=_dict.get("role",default_role)content=_dict.get("content")or""ifrole=="user":returnHumanMessageChunk(content=content)elifrole=="assistant":additional_kwargs:Dict={}tool_call_chunks=[]ifraw_tool_calls:=_dict.get("tool_calls"):additional_kwargs["tool_calls"]=raw_tool_callstry:tool_call_chunks=[tool_call_chunk(name=rtc["function"].get("name"),args=rtc["function"].get("arguments"),id=rtc.get("id"),index=rtc["index"],)forrtcinraw_tool_calls]exceptKeyError:passreturnAIMessageChunk(content=content,additional_kwargs=additional_kwargs,id=_dict.get("id"),tool_call_chunks=tool_call_chunks,)elifrole=="system":returnSystemMessageChunk(content=content)elifrole=="tool":returnToolMessageChunk(content=content,tool_call_id=_dict["tool_call_id"],id=_dict.get("id"))else:returnChatMessageChunk(content=content,role=role)@staticmethoddef_raise_functions_not_supported()->None:raiseValueError("Function messages are not supported by Databricks. Please"" create a feature request at https://github.com/mlflow/mlflow/issues.")@staticmethoddef_convert_message_to_dict(message:BaseMessage)->dict:message_dict={"content":message.content}if(name:=message.nameormessage.additional_kwargs.get("name"))isnotNone:message_dict["name"]=nameifisinstance(message,ChatMessage):message_dict["role"]=message.roleelifisinstance(message,HumanMessage):message_dict["role"]="user"elifisinstance(message,AIMessage):message_dict["role"]="assistant"ifmessage.tool_callsormessage.invalid_tool_calls:message_dict["tool_calls"]=[_lc_tool_call_to_openai_tool_call(tc)fortcinmessage.tool_calls]+[_lc_invalid_tool_call_to_openai_tool_call(tc)fortcinmessage.invalid_tool_calls]# type: ignore[assignment]elif"tool_calls"inmessage.additional_kwargs:message_dict["tool_calls"]=message.additional_kwargs["tool_calls"]tool_call_supported_props={"id","type","function"}message_dict["tool_calls"]=[{k:vfork,vintool_call.items()# type: ignore[union-attr]ifkintool_call_supported_props}fortool_callinmessage_dict["tool_calls"]]else:pass# If tool calls present, content null value should be None not empty string.if"tool_calls"inmessage_dict:message_dict["content"]=message_dict["content"]orNone# type: ignore[assignment]elifisinstance(message,SystemMessage):message_dict["role"]="system"elifisinstance(message,ToolMessage):message_dict["role"]="tool"message_dict["tool_call_id"]=message.tool_call_idsupported_props={"content","role","tool_call_id"}message_dict={k:vfork,vinmessage_dict.items()ifkinsupported_props}elifisinstance(message,FunctionMessage):raiseValueError("Function messages are not supported by Databricks. Please"" create a feature request at https://github.com/mlflow/mlflow/issues.")else:raiseValueError(f"Got unknown message type: {message}")if"function_call"inmessage.additional_kwargs:ChatMlflow._raise_functions_not_supported()returnmessage_dict@staticmethoddef_create_chat_result(response:Mapping[str,Any])->ChatResult:generations=[]forchoiceinresponse["choices"]:message=ChatMlflow._convert_dict_to_message(choice["message"])usage=choice.get("usage",{})gen=ChatGeneration(message=message,generation_info=usage,)generations.append(gen)usage=response.get("usage",{})returnChatResult(generations=generations,llm_output=usage)
[docs]defbind_tools(self,tools:Sequence[Union[Dict[str,Any],Type[BaseModel],Callable,BaseTool]],*,tool_choice:Optional[Union[dict,str,Literal["auto","none","required","any"],bool]]=None,**kwargs:Any,)->Runnable[LanguageModelInput,BaseMessage]:"""Bind tool-like objects to this chat model. Assumes model is compatible with OpenAI tool-calling API. Args: tools: A list of tool definitions to bind to this chat model. Can be a dictionary, pydantic model, callable, or BaseTool. Pydantic models, callables, and BaseTools will be automatically converted to their schema dictionary representation. tool_choice: Which tool to require the model to call. Options are: name of the tool (str): calls corresponding tool; "auto": automatically selects a tool (including no tool); "none": model does not generate any tool calls and instead must generate a standard assistant message; "required": the model picks the most relevant tool in tools and must generate a tool call; or a dict of the form: {"type": "function", "function": {"name": <<tool_name>>}}. **kwargs: Any additional parameters to pass to the :class:`~langchain.runnable.Runnable` constructor. """formatted_tools=[convert_to_openai_tool(tool)fortoolintools]iftool_choice:ifisinstance(tool_choice,str):# tool_choice is a tool/function nameiftool_choicenotin("auto","none","required"):tool_choice={"type":"function","function":{"name":tool_choice},}elifisinstance(tool_choice,dict):tool_names=[formatted_tool["function"]["name"]forformatted_toolinformatted_tools]ifnotany(tool_name==tool_choice["function"]["name"]fortool_nameintool_names):raiseValueError(f"Tool choice {tool_choice} was specified, but the only "f"provided tools were {tool_names}.")else:raiseValueError(f"Unrecognized tool_choice type. Expected str, bool or dict. "f"Received: {tool_choice}")kwargs["tool_choice"]=tool_choicereturnsuper().bind(tools=formatted_tools,**kwargs)