[docs]classChatUpstage(BaseChatOpenAI):"""ChatUpstage chat model. To use, you should have the environment variable `UPSTAGE_API_KEY` set with your API key or pass it as a named parameter to the constructor. Example: .. code-block:: python from langchain_upstage import ChatUpstage model = ChatUpstage() """@propertydeflc_secrets(self)->Dict[str,str]:return{"upstage_api_key":"UPSTAGE_API_KEY"}@classmethoddefget_lc_namespace(cls)->List[str]:return["langchain","chat_models","upstage"]@propertydeflc_attributes(self)->Dict[str,Any]:attributes:Dict[str,Any]={}ifself.upstage_api_base:attributes["upstage_api_base"]=self.upstage_api_basereturnattributes@propertydef_llm_type(self)->str:"""Return type of chat model."""return"upstage-chat"def_get_ls_params(self,stop:Optional[List[str]]=None,**kwargs:Any)->LangSmithParams:"""Get the parameters used to invoke the model."""params=super()._get_ls_params(stop=stop,**kwargs)params["ls_provider"]="upstage"returnparamsmodel_name:str=Field(default="solar-mini",alias="model")"""Model name to use."""upstage_api_key:SecretStr=Field(default_factory=secret_from_env("UPSTAGE_API_KEY",error_message=("You must specify an api key. ""You can pass it an argument as `api_key=...` or ""set the environment variable `UPSTAGE_API_KEY`."),),alias="api_key",)"""Automatically inferred from env are `UPSTAGE_API_KEY` if not provided."""upstage_api_base:Optional[str]=Field(default_factory=from_env("UPSTAGE_API_BASE",default="https://api.upstage.ai/v1/solar"),alias="base_url",)"""Base URL path for API requests, leave blank if not using a proxy or service emulator."""openai_api_key:Optional[SecretStr]=Field(default=None)"""openai api key is not supported for upstage. use `upstage_api_key` instead."""openai_api_base:Optional[str]=Field(default=None)"""openai api base is not supported for upstage. use `upstage_api_base` instead."""openai_organization:Optional[str]=Field(default=None)"""openai organization is not supported for upstage."""tiktoken_model_name:Optional[str]=None"""tiktoken is not supported for upstage."""tokenizer_name:Optional[str]="upstage/solar-pro-tokenizer""""huggingface tokenizer name. Solar tokenizer is opened in huggingface https://huggingface.co/upstage/solar-pro-tokenizer"""@model_validator(mode="after")defvalidate_environment(self)->Self:"""Validate that api key and python package exists in environment."""ifself.nisnotNoneandself.n<1:raiseValueError("n must be at least 1.")ifself.nisnotNoneandself.n>1andself.streaming:raiseValueError("n must be 1 when streaming.")client_params:dict={"api_key":(self.upstage_api_key.get_secret_value()ifself.upstage_api_keyelseNone),"base_url":self.upstage_api_base,"timeout":self.request_timeout,"default_headers":self.default_headers,"default_query":self.default_query,}ifself.max_retriesisnotNone:client_params["max_retries"]=self.max_retriesifnot(self.clientorNone):sync_specific:dict={"http_client":self.http_client}self.client=openai.OpenAI(**client_params,**sync_specific).chat.completionsifnot(self.async_clientorNone):async_specific:dict={"http_client":self.http_async_client}self.async_client=openai.AsyncOpenAI(**client_params,**async_specific).chat.completionsreturnselfdef_get_tokenizer(self)->Tokenizer:self.tokenizer_name=SOLAR_TOKENIZERS.get(self.model_name,self.tokenizer_name)returnTokenizer.from_pretrained(self.tokenizer_name)
[docs]defget_token_ids(self,text:str)->List[int]:"""Get the tokens present in the text."""tokenizer=self._get_tokenizer()encode=tokenizer.encode(text,add_special_tokens=False)returnencode.ids
[docs]defget_num_tokens_from_messages(self,messages:List[BaseMessage],tools:Sequence[Any]|None=None)->int:"""Calculate num tokens for solar model."""tokenizer=self._get_tokenizer()tokens_per_message=5# <|im_start|>{role}\n{message}<|im_end|>tokens_prefix=1# <|startoftext|>tokens_suffix=3# <|im_start|>assistant\nnum_tokens=0num_tokens+=tokens_prefixmessages_dict=[_convert_message_to_dict(m)forminmessages]formessageinmessages_dict:num_tokens+=tokens_per_messageforkey,valueinmessage.items():# Cast str(value) in case the message value is not a string# This occurs with function messagesnum_tokens+=len(tokenizer.encode(str(value),add_special_tokens=False))# every reply is primed with <|im_start|>assistantnum_tokens+=tokens_suffixreturnnum_tokens
def_create_message_dicts(self,messages:List[BaseMessage],stop:Optional[List[str]])->Tuple[List[Dict[str,Any]],Dict[str,Any]]:params=self._default_paramsifstopisnotNone:params["stop"]=stopmessage_dicts=[_convert_message_to_dict(m)forminmessages]returnmessage_dicts,paramsdef_generate(self,messages:List[BaseMessage],stop:Optional[List[str]]=None,run_manager:Optional[CallbackManagerForLLMRun]=None,**kwargs:Any,)->ChatResult:using_doc_parsing_model=self._using_doc_parsing_model(kwargs)ifusing_doc_parsing_model:document_contents=self._parse_documents(kwargs.pop("file_path"))messages.append(HumanMessage(document_contents))ifself.streaming:stream_iter=self._stream(messages,stop=stop,run_manager=run_manager,**kwargs)returngenerate_from_stream(stream_iter)payload=self._get_request_payload(messages,stop=stop,**kwargs)response=self.client.create(**payload)returnself._create_chat_result(response)asyncdef_agenerate(self,messages:List[BaseMessage],stop:Optional[List[str]]=None,run_manager:Optional[AsyncCallbackManagerForLLMRun]=None,**kwargs:Any,)->ChatResult:using_doc_parsing_model=self._using_doc_parsing_model(kwargs)ifusing_doc_parsing_model:document_contents=self._parse_documents(kwargs.pop("file_path"))messages.append(HumanMessage(document_contents))ifself.streaming:stream_iter=self._astream(messages,stop=stop,run_manager=run_manager,**kwargs)returnawaitagenerate_from_stream(stream_iter)payload=self._get_request_payload(messages,stop=stop,**kwargs)response=awaitself.async_client.create(**payload)returnself._create_chat_result(response)def_using_doc_parsing_model(self,kwargs:Dict[str,Any])->bool:if"file_path"inkwargs:ifself.model_nameinDOC_PARSING_MODEL:returnTrueraiseValueError("file_path is not supported for this model.")returnFalsedef_parse_documents(self,file_path:str)->str:document_contents="Documents:\n"loader=UpstageDocumentParseLoader(api_key=(self.upstage_api_key.get_secret_value()ifself.upstage_api_keyelseNone),file_path=file_path,output_format="text",coordinates=False,)docs=loader.load()ifisinstance(file_path,list):file_titles=[os.path.basename(path)forpathinfile_path]else:file_titles=[os.path.basename(file_path)]fori,docinenumerate(docs):file_title=file_titles[min(i,len(file_titles)-1)]document_contents+=f"{file_title}:\n{doc.page_content}\n\n"returndocument_contentsdef_get_request_payload(self,input_:LanguageModelInput,*,stop:Optional[List[str]]=None,**kwargs:Any,)->dict:messages=self._convert_input(input_).to_messages()ifstopisnotNone:kwargs["stop"]=stopreturn{"messages":[_convert_message_to_dict(m)forminmessages],**self._default_params,**kwargs,}# TODO: Fix typing.@overload# type: ignore[override]defwith_structured_output(self,schema:Optional[_DictOrPydanticClass]=None,*,include_raw:Literal[True]=True,**kwargs:Any,)->Runnable[LanguageModelInput,_AllReturnType]:...@overloaddefwith_structured_output(self,schema:Optional[_DictOrPydanticClass]=None,*,include_raw:Literal[False]=False,**kwargs:Any,)->Runnable[LanguageModelInput,_DictOrPydantic]:...
[docs]defwith_structured_output(self,schema:Optional[_DictOrPydanticClass]=None,*,include_raw:bool=False,**kwargs:Any,)->Runnable[LanguageModelInput,_DictOrPydantic]:"""Model wrapper that returns outputs formatted to match the given schema. Args: schema: The output schema as a dict or a Pydantic class. If a Pydantic class then the model output will be an object of that class. If a dict then the model output will be a dict. With a Pydantic class the returned attributes will be validated, whereas with a dict they will not be. If `method` is "function_calling" and `schema` is a dict, then the dict must match the OpenAI function-calling spec or be a valid JSON schema with top level 'title' and 'description' keys specified. include_raw: If False then only the parsed structured output is returned. If an error occurs during model output parsing it will be raised. If True then both the raw model response (a BaseMessage) and the parsed model response will be returned. If an error occurs during output parsing it will be caught and returned as well. The final output is always a dict with keys "raw", "parsed", and "parsing_error". Returns: A Runnable that takes any ChatModel input and returns as output: If include_raw is True then a dict with keys: raw: BaseMessage parsed: Optional[_DictOrPydantic] parsing_error: Optional[BaseException] If include_raw is False then just _DictOrPydantic is returned, where _DictOrPydantic depends on the schema: If schema is a Pydantic class then _DictOrPydantic is the Pydantic class. If schema is a dict then _DictOrPydantic is a dict. Example: Function-calling, Pydantic schema (method="function_calling", include_raw=False): .. code-block:: python from langchain_upstage import ChatUpstage from pydantic import BaseModel class AnswerWithJustification(BaseModel): '''An answer to the user question along with justification for the answer.''' answer: str justification: str llm = ChatUpstage(model="solar-mini", temperature=0) structured_llm = llm.with_structured_output(AnswerWithJustification) structured_llm.invoke( "What weighs more a pound of bricks or a pound of feathers" ) # -> AnswerWithJustification( # answer='They weigh the same', # justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.' # ) Example: Function-calling, Pydantic schema (method="function_calling", include_raw=True): .. code-block:: python from langchain_upstage import ChatUpstage from pydantic import BaseModel class AnswerWithJustification(BaseModel): '''An answer to the user question along with justification for the answer.''' answer: str justification: str llm = ChatUpstage(model="solar-mini", temperature=0) structured_llm = llm.with_structured_output( AnswerWithJustification, include_raw=True ) structured_llm.invoke( "What weighs more a pound of bricks or a pound of feathers" ) # -> { # 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}), # 'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'), # 'parsing_error': None # } Example: Function-calling, dict schema (method="function_calling", include_raw=False): .. code-block:: python from langchain_upstage import ChatUpstage from langchain_core.utils.function_calling import convert_to_openai_tool from pydantic import BaseModel class AnswerWithJustification(BaseModel): '''An answer to the user question along with justification for the answer.''' answer: str justification: str dict_schema = convert_to_openai_tool(AnswerWithJustification) llm = ChatUpstage(model="solar-mini", temperature=0) structured_llm = llm.with_structured_output(dict_schema) structured_llm.invoke( "What weighs more a pound of bricks or a pound of feathers" ) # -> { # 'answer': 'They weigh the same', # 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.' # } """# noqa: E501ifkwargs:raiseValueError(f"Received unsupported arguments {kwargs}")is_pydantic_schema=_is_pydantic_class(schema)ifschemaisNone:raiseValueError("schema must be specified. Received None.")tool_name=convert_to_openai_tool(schema)["function"]["name"]llm=self.bind_tools([schema],tool_choice=tool_name)ifis_pydantic_schema:output_parser:OutputParserLike=PydanticToolsParser(tools=[cast(type,schema)],first_tool_only=True)else:output_parser=JsonOutputKeyToolsParser(key_name=tool_name,first_tool_only=True)ifinclude_raw:parser_assign=RunnablePassthrough.assign(parsed=itemgetter("raw")|output_parser,parsing_error=lambda_:None)parser_none=RunnablePassthrough.assign(parsed=lambda_:None)parser_with_fallback=parser_assign.with_fallbacks([parser_none],exception_key="parsing_error")returnRunnableMap(raw=llm)|parser_with_fallbackelse:returnllm|output_parser
[docs]defbind_tools(self,tools:Sequence[Union[Dict[str,Any],Type[BaseModel],Callable,BaseTool]],*,tool_choice:Optional[Union[dict,str,Literal["auto"],bool]]=None,**kwargs:Any,)->Runnable[LanguageModelInput,BaseMessage]:"""Bind tool-like objects to this chat model. Assumes model is compatible with Upstage 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": does not call a tool; True: forces tool call (requires `tools` be length 1); False: no effect; 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_choicein("any","required","auto"):tool_choice="auto"eliftool_choice=="none":tool_choice="none"else:tool_choice={"type":"function","function":{"name":tool_choice},}elifisinstance(tool_choice,bool):tool_choice="auto"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)