From 096b66db4ae7d998461ff974864e15f7100f0141 Mon Sep 17 00:00:00 2001 From: TrumanYan <36946723@qq.com> Date: Wed, 31 Jul 2024 21:05:38 +0800 Subject: [PATCH] community: replace it with Tencent Cloud SDK (#24172) Description: The old method will be discontinued; use the official SDK for more model options. Issue: None Dependencies: None Twitter handle: None Co-authored-by: trumanyan --- .../chat_models/hunyuan.py | 173 ++++++------------ .../chat_models/test_hunyuan.py | 21 +++ .../unit_tests/chat_models/test_hunyuan.py | 49 +---- 3 files changed, 87 insertions(+), 156 deletions(-) diff --git a/libs/community/langchain_community/chat_models/hunyuan.py b/libs/community/langchain_community/chat_models/hunyuan.py index 45ab9212367..8abe3c245ea 100644 --- a/libs/community/langchain_community/chat_models/hunyuan.py +++ b/libs/community/langchain_community/chat_models/hunyuan.py @@ -1,13 +1,7 @@ -import base64 -import hashlib -import hmac import json import logging -import time from typing import Any, Dict, Iterator, List, Mapping, Optional, Type -from urllib.parse import urlparse -import requests from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models.chat_models import ( BaseChatModel, @@ -34,18 +28,15 @@ from langchain_core.utils import ( logger = logging.getLogger(__name__) -DEFAULT_API_BASE = "https://hunyuan.cloud.tencent.com" -DEFAULT_PATH = "/hyllm/v1/chat/completions" - def _convert_message_to_dict(message: BaseMessage) -> dict: message_dict: Dict[str, Any] if isinstance(message, ChatMessage): - message_dict = {"role": message.role, "content": message.content} + message_dict = {"Role": message.role, "Content": message.content} elif isinstance(message, HumanMessage): - message_dict = {"role": "user", "content": message.content} + message_dict = {"Role": "user", "Content": message.content} elif isinstance(message, AIMessage): - message_dict = {"role": "assistant", "content": message.content} + message_dict = {"Role": "assistant", "Content": message.content} else: raise TypeError(f"Got unknown type {message}") @@ -53,20 +44,20 @@ def _convert_message_to_dict(message: BaseMessage) -> dict: def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: - role = _dict["role"] + role = _dict["Role"] if role == "user": - return HumanMessage(content=_dict["content"]) + return HumanMessage(content=_dict["Content"]) elif role == "assistant": - return AIMessage(content=_dict.get("content", "") or "") + return AIMessage(content=_dict.get("Content", "") or "") else: - return ChatMessage(content=_dict["content"], role=role) + return ChatMessage(content=_dict["Content"], role=role) def _convert_delta_to_message_chunk( _dict: Mapping[str, Any], default_class: Type[BaseMessageChunk] ) -> BaseMessageChunk: - role = _dict.get("role") - content = _dict.get("content") or "" + role = _dict.get("Role") + content = _dict.get("Content") or "" if role == "user" or default_class == HumanMessageChunk: return HumanMessageChunk(content=content) @@ -78,43 +69,13 @@ def _convert_delta_to_message_chunk( return default_class(content=content) # type: ignore[call-arg] -# signature generation -# https://cloud.tencent.com/document/product/1729/97732#532252ce-e960-48a7-8821-940a9ce2ccf3 -def _signature(secret_key: SecretStr, url: str, payload: Dict[str, Any]) -> str: - sorted_keys = sorted(payload.keys()) - - url_info = urlparse(url) - - sign_str = url_info.netloc + url_info.path + "?" - - for key in sorted_keys: - value = payload[key] - - if isinstance(value, list) or isinstance(value, dict): - value = json.dumps(value, separators=(",", ":"), ensure_ascii=False) - elif isinstance(value, float): - value = "%g" % value - - sign_str = sign_str + key + "=" + str(value) + "&" - - sign_str = sign_str[:-1] - - hmacstr = hmac.new( - key=secret_key.get_secret_value().encode("utf-8"), - msg=sign_str.encode("utf-8"), - digestmod=hashlib.sha1, - ).digest() - - return base64.b64encode(hmacstr).decode("utf-8") - - def _create_chat_result(response: Mapping[str, Any]) -> ChatResult: generations = [] - for choice in response["choices"]: - message = _convert_dict_to_message(choice["messages"]) + for choice in response["Choices"]: + message = _convert_dict_to_message(choice["Message"]) generations.append(ChatGeneration(message=message)) - token_usage = response["usage"] + token_usage = response["Usage"] llm_output = {"token_usage": token_usage} return ChatResult(generations=generations, llm_output=llm_output) @@ -137,8 +98,6 @@ class ChatHunyuan(BaseChatModel): def lc_serializable(self) -> bool: return True - hunyuan_api_base: str = Field(default=DEFAULT_API_BASE) - """Hunyuan custom endpoints""" hunyuan_app_id: Optional[int] = None """Hunyuan App ID""" hunyuan_secret_id: Optional[str] = None @@ -149,13 +108,26 @@ class ChatHunyuan(BaseChatModel): """Whether to stream the results or not.""" request_timeout: int = 60 """Timeout for requests to Hunyuan API. Default is 60 seconds.""" - - query_id: Optional[str] = None - """Query id for troubleshooting""" temperature: float = 1.0 """What sampling temperature to use.""" top_p: float = 1.0 """What probability mass to use.""" + model: str = "hunyuan-lite" + """What Model to use. + Optional model: + - hunyuan-lite、 + - hunyuan-standard + - hunyuan-standard-256K + - hunyuan-pro + - hunyuan-code + - hunyuan-role + - hunyuan-functioncall + - hunyuan-vision + """ + stream_moderation: bool = False + """Whether to review the results or not when streaming is true.""" + enable_enhancement: bool = True + """Whether to enhancement the results or not.""" model_kwargs: Dict[str, Any] = Field(default_factory=dict) """Holds any model parameters valid for API call not explicitly specified.""" @@ -193,12 +165,6 @@ class ChatHunyuan(BaseChatModel): @pre_init def validate_environment(cls, values: Dict) -> Dict: - values["hunyuan_api_base"] = get_from_dict_or_env( - values, - "hunyuan_api_base", - "HUNYUAN_API_BASE", - DEFAULT_API_BASE, - ) values["hunyuan_app_id"] = get_from_dict_or_env( values, "hunyuan_app_id", @@ -216,22 +182,19 @@ class ChatHunyuan(BaseChatModel): "HUNYUAN_SECRET_KEY", ) ) - return values @property def _default_params(self) -> Dict[str, Any]: """Get the default parameters for calling Hunyuan API.""" normal_params = { - "app_id": self.hunyuan_app_id, - "secret_id": self.hunyuan_secret_id, - "temperature": self.temperature, - "top_p": self.top_p, + "Temperature": self.temperature, + "TopP": self.top_p, + "Model": self.model, + "Stream": self.streaming, + "StreamModeration": self.stream_moderation, + "EnableEnhancement": self.enable_enhancement, } - - if self.query_id is not None: - normal_params["query_id"] = self.query_id - return {**normal_params, **self.model_kwargs} def _generate( @@ -248,13 +211,7 @@ class ChatHunyuan(BaseChatModel): return generate_from_stream(stream_iter) res = self._chat(messages, **kwargs) - - response = res.json() - - if "error" in response: - raise ValueError(f"Error from Hunyuan api response: {response}") - - return _create_chat_result(response) + return _create_chat_result(json.loads(res.to_json_string())) def _stream( self, @@ -266,19 +223,17 @@ class ChatHunyuan(BaseChatModel): res = self._chat(messages, **kwargs) default_chunk_class = AIMessageChunk - for chunk in res.iter_lines(): - chunk = chunk.decode(encoding="UTF-8", errors="strict").replace( - "data: ", "" - ) + for chunk in res: + chunk = chunk.get("data", "") if len(chunk) == 0: continue response = json.loads(chunk) if "error" in response: raise ValueError(f"Error from Hunyuan api response: {response}") - for choice in response["choices"]: + for choice in response["Choices"]: chunk = _convert_delta_to_message_chunk( - choice["delta"], default_chunk_class + choice["Delta"], default_chunk_class ) default_chunk_class = chunk.__class__ cg_chunk = ChatGenerationChunk(message=chunk) @@ -286,42 +241,32 @@ class ChatHunyuan(BaseChatModel): run_manager.on_llm_new_token(chunk.content, chunk=cg_chunk) yield cg_chunk - def _chat(self, messages: List[BaseMessage], **kwargs: Any) -> requests.Response: + def _chat(self, messages: List[BaseMessage], **kwargs: Any) -> Any: if self.hunyuan_secret_key is None: raise ValueError("Hunyuan secret key is not set.") + try: + from tencentcloud.common import credential + from tencentcloud.hunyuan.v20230901 import hunyuan_client, models + except ImportError: + raise ImportError( + "Could not import tencentcloud python package. " + "Please install it with `pip install tencentcloud-sdk-python`." + ) + parameters = {**self._default_params, **kwargs} - - headers = parameters.pop("headers", {}) - timestamp = parameters.pop("timestamp", int(time.time())) - expired = parameters.pop("expired", timestamp + 24 * 60 * 60) - - payload = { - "timestamp": timestamp, - "expired": expired, - "messages": [_convert_message_to_dict(m) for m in messages], + cred = credential.Credential( + self.hunyuan_secret_id, str(self.hunyuan_secret_key.get_secret_value()) + ) + client = hunyuan_client.HunyuanClient(cred, "") + req = models.ChatCompletionsRequest() + params = { + "Messages": [_convert_message_to_dict(m) for m in messages], **parameters, } - - if self.streaming: - payload["stream"] = 1 - - url = self.hunyuan_api_base + DEFAULT_PATH - - res = requests.post( - url=url, - timeout=self.request_timeout, - headers={ - "Content-Type": "application/json", - "Authorization": _signature( - secret_key=self.hunyuan_secret_key, url=url, payload=payload - ), - **headers, - }, - json=payload, - stream=self.streaming, - ) - return res + req.from_json_string(json.dumps(params)) + resp = client.ChatCompletions(req) + return resp @property def _llm_type(self) -> str: diff --git a/libs/community/tests/integration_tests/chat_models/test_hunyuan.py b/libs/community/tests/integration_tests/chat_models/test_hunyuan.py index f3972391a49..91e9e184a7a 100644 --- a/libs/community/tests/integration_tests/chat_models/test_hunyuan.py +++ b/libs/community/tests/integration_tests/chat_models/test_hunyuan.py @@ -1,8 +1,10 @@ +import pytest from langchain_core.messages import AIMessage, HumanMessage from langchain_community.chat_models.hunyuan import ChatHunyuan +@pytest.mark.requires("tencentcloud-sdk-python") def test_chat_hunyuan() -> None: chat = ChatHunyuan() message = HumanMessage(content="Hello") @@ -11,6 +13,7 @@ def test_chat_hunyuan() -> None: assert isinstance(response.content, str) +@pytest.mark.requires("tencentcloud-sdk-python") def test_chat_hunyuan_with_temperature() -> None: chat = ChatHunyuan(temperature=0.6) message = HumanMessage(content="Hello") @@ -19,6 +22,24 @@ def test_chat_hunyuan_with_temperature() -> None: assert isinstance(response.content, str) +@pytest.mark.requires("tencentcloud-sdk-python") +def test_chat_hunyuan_with_model_name() -> None: + chat = ChatHunyuan(model="hunyuan-standard") + message = HumanMessage(content="Hello") + response = chat.invoke([message]) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) + + +@pytest.mark.requires("tencentcloud-sdk-python") +def test_chat_hunyuan_with_stream() -> None: + chat = ChatHunyuan(streaming=True) + message = HumanMessage(content="Hello") + response = chat.invoke([message]) + assert isinstance(response, AIMessage) + assert isinstance(response.content, str) + + def test_extra_kwargs() -> None: chat = ChatHunyuan(temperature=0.88, top_p=0.7) assert chat.temperature == 0.88 diff --git a/libs/community/tests/unit_tests/chat_models/test_hunyuan.py b/libs/community/tests/unit_tests/chat_models/test_hunyuan.py index ca8fafa0abc..e3f13b2d725 100644 --- a/libs/community/tests/unit_tests/chat_models/test_hunyuan.py +++ b/libs/community/tests/unit_tests/chat_models/test_hunyuan.py @@ -8,27 +8,25 @@ from langchain_core.messages import ( HumanMessageChunk, SystemMessage, ) -from langchain_core.pydantic_v1 import SecretStr from langchain_community.chat_models.hunyuan import ( _convert_delta_to_message_chunk, _convert_dict_to_message, _convert_message_to_dict, - _signature, ) def test__convert_message_to_dict_human() -> None: message = HumanMessage(content="foo") result = _convert_message_to_dict(message) - expected_output = {"role": "user", "content": "foo"} + expected_output = {"Role": "user", "Content": "foo"} assert result == expected_output def test__convert_message_to_dict_ai() -> None: message = AIMessage(content="foo") result = _convert_message_to_dict(message) - expected_output = {"role": "assistant", "content": "foo"} + expected_output = {"Role": "assistant", "Content": "foo"} assert result == expected_output @@ -47,68 +45,35 @@ def test__convert_message_to_dict_function() -> None: def test__convert_dict_to_message_human() -> None: - message_dict = {"role": "user", "content": "foo"} + message_dict = {"Role": "user", "Content": "foo"} result = _convert_dict_to_message(message_dict) expected_output = HumanMessage(content="foo") assert result == expected_output def test__convert_dict_to_message_ai() -> None: - message_dict = {"role": "assistant", "content": "foo"} + message_dict = {"Role": "assistant", "Content": "foo"} result = _convert_dict_to_message(message_dict) expected_output = AIMessage(content="foo") assert result == expected_output def test__convert_dict_to_message_other_role() -> None: - message_dict = {"role": "system", "content": "foo"} + message_dict = {"Role": "system", "Content": "foo"} result = _convert_dict_to_message(message_dict) expected_output = ChatMessage(role="system", content="foo") assert result == expected_output def test__convert_delta_to_message_assistant() -> None: - delta = {"role": "assistant", "content": "foo"} + delta = {"Role": "assistant", "Content": "foo"} result = _convert_delta_to_message_chunk(delta, AIMessageChunk) expected_output = AIMessageChunk(content="foo") assert result == expected_output def test__convert_delta_to_message_human() -> None: - delta = {"role": "user", "content": "foo"} + delta = {"Role": "user", "Content": "foo"} result = _convert_delta_to_message_chunk(delta, HumanMessageChunk) expected_output = HumanMessageChunk(content="foo") assert result == expected_output - - -def test__signature() -> None: - secret_key = SecretStr("YOUR_SECRET_KEY") - url = "https://hunyuan.cloud.tencent.com/hyllm/v1/chat/completions" - - result = _signature( - secret_key=secret_key, - url=url, - payload={ - "app_id": "YOUR_APP_ID", - "secret_id": "YOUR_SECRET_ID", - "query_id": "test_query_id_cb5d8156-0ce2-45af-86b4-d02f5c26a142", - "messages": [ - { - "role": "user", - "content": "You are a helpful assistant that translates English" - " to French.Translate this sentence from English to" - " French. I love programming.", - } - ], - "temperature": 0.0, - "top_p": 0.8, - "stream": 1, - "timestamp": 1697738378, - "expired": 1697824778, - }, - ) - - # The signature was generated by the demo provided by Huanyuan. - # https://hunyuan-sdk-1256237915.cos.ap-guangzhou.myqcloud.com/python.zip - expected_output = "MXBvqNCXyxJWfEyBwk1pYBVnxzo=" - assert result == expected_output