Skip to content

Commit

Permalink
community: replace it with Tencent Cloud SDK (#24172)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
yanchunan and trumanyan authored Jul 31, 2024
1 parent 99eb31e commit 096b66d
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 156 deletions.
173 changes: 59 additions & 114 deletions libs/community/langchain_community/chat_models/hunyuan.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -34,39 +28,36 @@

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}")

return message_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)
Expand All @@ -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)

Expand All @@ -137,8 +98,6 @@ def lc_secrets(self) -> Dict[str, str]:
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
Expand All @@ -149,13 +108,26 @@ def lc_serializable(self) -> bool:
"""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."""
Expand Down Expand Up @@ -193,12 +165,6 @@ def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]:

@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",
Expand All @@ -216,22 +182,19 @@ def validate_environment(cls, values: Dict) -> Dict:
"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(
Expand All @@ -248,13 +211,7 @@ def _generate(
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,
Expand All @@ -266,62 +223,50 @@ def _stream(
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)
if run_manager:
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.")

parameters = {**self._default_params, **kwargs}

headers = parameters.pop("headers", {})
timestamp = parameters.pop("timestamp", int(time.time()))
expired = parameters.pop("expired", timestamp + 24 * 60 * 60)
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`."
)

payload = {
"timestamp": timestamp,
"expired": expired,
"messages": [_convert_message_to_dict(m) for m in messages],
parameters = {**self._default_params, **kwargs}
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:
Expand Down
21 changes: 21 additions & 0 deletions libs/community/tests/integration_tests/chat_models/test_hunyuan.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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")
Expand All @@ -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
Expand Down
Loading

0 comments on commit 096b66d

Please sign in to comment.