From 22aa268ed48ec9067d688b11ff7b8d269f4c8bd9 Mon Sep 17 00:00:00 2001 From: morvanzhou Date: Thu, 20 Jun 2024 19:31:27 +0800 Subject: [PATCH] feat(llm): - add moonshot - add summary --- src/retk/config.py | 3 + src/retk/core/ai/llm/__init__.py | 6 +- src/retk/core/ai/llm/api/__init__.py | 6 + src/retk/core/ai/llm/{ => api}/aliyun.py | 21 +- src/retk/core/ai/llm/{ => api}/baidu.py | 10 +- src/retk/core/ai/llm/{ => api}/base.py | 6 +- src/retk/core/ai/llm/api/moonshot.py | 27 +++ src/retk/core/ai/llm/{ => api}/openai.py | 52 +++-- src/retk/core/ai/llm/{ => api}/tencent.py | 14 +- src/retk/core/ai/llm/{ => api}/xfyun.py | 6 +- src/retk/core/ai/llm/knowledge/__init__.py | 52 +++++ .../core/ai/llm/knowledge/system_extend.md | 25 ++ .../core/ai/llm/knowledge/system_summary.md | 23 ++ tests/{test_llm.py => test_ai_llm_api.py} | 159 ++++++++----- tests/test_ai_llm_knowledge.py | 219 ++++++++++++++++++ 15 files changed, 524 insertions(+), 105 deletions(-) create mode 100644 src/retk/core/ai/llm/api/__init__.py rename src/retk/core/ai/llm/{ => api}/aliyun.py (85%) rename src/retk/core/ai/llm/{ => api}/baidu.py (93%) rename src/retk/core/ai/llm/{ => api}/base.py (97%) create mode 100644 src/retk/core/ai/llm/api/moonshot.py rename src/retk/core/ai/llm/{ => api}/openai.py (66%) rename src/retk/core/ai/llm/{ => api}/tencent.py (94%) rename src/retk/core/ai/llm/{ => api}/xfyun.py (97%) create mode 100644 src/retk/core/ai/llm/knowledge/__init__.py create mode 100644 src/retk/core/ai/llm/knowledge/system_extend.md create mode 100644 src/retk/core/ai/llm/knowledge/system_summary.md rename tests/{test_llm.py => test_ai_llm_api.py} (79%) create mode 100644 tests/test_ai_llm_knowledge.py diff --git a/src/retk/config.py b/src/retk/config.py index def284d..85d77b6 100644 --- a/src/retk/config.py +++ b/src/retk/config.py @@ -62,6 +62,9 @@ class Settings(BaseSettings): XFYUN_API_SECRET: str = Field(env='XFYUN_API_SECRET', default="") XFYUN_API_KEY: str = Field(env='XFYUN_API_KEY', default="") + # moonshot api + MOONSHOT_API_KEY: str = Field(env='MOONSHOT_API_KEY', default="") + # Email client settings RETHINK_EMAIL: str = Field(env='RETHINK_EMAIL', default="") RETHINK_EMAIL_PASSWORD: str = Field(env='RETHINK_EMAIL_PASSWORD', default="") diff --git a/src/retk/core/ai/llm/__init__.py b/src/retk/core/ai/llm/__init__.py index 6c45499..d6cad5e 100644 --- a/src/retk/core/ai/llm/__init__.py +++ b/src/retk/core/ai/llm/__init__.py @@ -1,5 +1 @@ -from .aliyun import Aliyun -from .baidu import Baidu -from .openai import OpenAI -from .tencent import Tencent -from .xfyun import XfYun +from . import api, knowledge diff --git a/src/retk/core/ai/llm/api/__init__.py b/src/retk/core/ai/llm/api/__init__.py new file mode 100644 index 0000000..c31cb3b --- /dev/null +++ b/src/retk/core/ai/llm/api/__init__.py @@ -0,0 +1,6 @@ +from .aliyun import AliyunService, AliyunModelEnum +from .baidu import BaiduService, BaiduModelEnum +from .moonshot import MoonshotService, MoonshotModelEnum +from .openai import OpenaiService, OpenaiModelEnum +from .tencent import TencentService, TencentModelEnum +from .xfyun import XfYunService, XfYunModelEnum diff --git a/src/retk/core/ai/llm/aliyun.py b/src/retk/core/ai/llm/api/aliyun.py similarity index 85% rename from src/retk/core/ai/llm/aliyun.py rename to src/retk/core/ai/llm/api/aliyun.py index 27203b9..40edd1d 100644 --- a/src/retk/core/ai/llm/aliyun.py +++ b/src/retk/core/ai/llm/api/aliyun.py @@ -4,18 +4,21 @@ from retk import config, const from retk.logger import logger -from .base import BaseLLM, MessagesType +from .base import BaseLLMService, MessagesType, NoAPIKeyError +# https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-thousand-questions-metering-and-billing class AliyunModelEnum(str, Enum): - QWEN1_5_0_5B = "qwen1.5-0.5b-chat" - QWEN1_8B = "qwen-1.8b-chat" - BAICHUAN7BV1 = "baichuan-7b-v1" - LLAMA3_70B = "llama3-70b-instruct" - CHATGLM3_6B = "chatglm3-6b" + QWEN1_5_05B = "qwen1.5-0.5b-chat" # free + QWEN_2B = "qwen-1.8b-chat" # free + BAICHUAN7BV1 = "baichuan-7b-v1" # free + QWEN_LONG = "qwen-long" # in 0.0005/1000, out 0.002/1000 + QWEN_TURBO = "qwen-turbo" # in 0.002/1000, out 0.006/1000 + QWEN_PLUS = "qwen-plus" # in 0.004/1000, out 0.012/1000 + QWEN_MAX = "qwen-max" # in 0.04/1000, out 0.12/1000 -class Aliyun(BaseLLM): +class AliyunService(BaseLLMService): def __init__( self, top_p: float = 0.9, @@ -27,11 +30,11 @@ def __init__( top_p=top_p, temperature=temperature, timeout=timeout, - default_model=AliyunModelEnum.QWEN1_5_0_5B.value, + default_model=AliyunModelEnum.QWEN1_5_05B.value, ) self.api_key = config.get_settings().ALIYUN_DASHSCOPE_API_KEY if self.api_key == "": - raise ValueError("Aliyun API key is empty") + raise NoAPIKeyError("Aliyun API key is empty") def get_headers(self, stream: bool) -> Dict[str, str]: h = { diff --git a/src/retk/core/ai/llm/baidu.py b/src/retk/core/ai/llm/api/baidu.py similarity index 93% rename from src/retk/core/ai/llm/baidu.py rename to src/retk/core/ai/llm/api/baidu.py index 7ce7313..0cc5ee2 100644 --- a/src/retk/core/ai/llm/baidu.py +++ b/src/retk/core/ai/llm/api/baidu.py @@ -5,7 +5,7 @@ from retk import config, const, httpx_helper from retk.logger import logger -from .base import BaseLLM, MessagesType +from .base import BaseLLMService, MessagesType, NoAPIKeyError class BaiduModelEnum(str, Enum): @@ -17,7 +17,7 @@ class BaiduModelEnum(str, Enum): YI_34B_CHAT = "yi_34b_chat" -class Baidu(BaseLLM): +class BaiduService(BaseLLMService): def __init__( self, top_p: float = 0.9, @@ -34,7 +34,7 @@ def __init__( self.api_key = config.get_settings().BAIDU_QIANFAN_API_KEY self.secret_key = config.get_settings().BAIDU_QIANFAN_SECRET_KEY if self.api_key == "" or self.secret_key == "": - raise ValueError("Baidu api key or key is empty") + raise NoAPIKeyError("Baidu api key or key is empty") self.headers = { "Content-Type": "application/json", @@ -69,6 +69,10 @@ async def set_token(self, req_id: str = None): @staticmethod def get_payload(messages: MessagesType, stream: bool) -> bytes: + if messages[0]["role"] == "system": + messages[0]["role"] = "user" + if messages[1]["role"] == "user": + messages.insert(1, {"role": "assistant", "content": "明白。"}) return json.dumps( { "messages": messages, diff --git a/src/retk/core/ai/llm/base.py b/src/retk/core/ai/llm/api/base.py similarity index 97% rename from src/retk/core/ai/llm/base.py rename to src/retk/core/ai/llm/api/base.py index 0491f76..3209cc3 100644 --- a/src/retk/core/ai/llm/base.py +++ b/src/retk/core/ai/llm/api/base.py @@ -9,7 +9,11 @@ MessagesType = List[Dict[Literal["role", "content"], str]] -class BaseLLM(ABC): +class NoAPIKeyError(Exception): + pass + + +class BaseLLMService(ABC): default_timeout = 60. def __init__( diff --git a/src/retk/core/ai/llm/api/moonshot.py b/src/retk/core/ai/llm/api/moonshot.py new file mode 100644 index 0000000..b2f4db3 --- /dev/null +++ b/src/retk/core/ai/llm/api/moonshot.py @@ -0,0 +1,27 @@ +from enum import Enum + +from retk import config +from .openai import OpenaiLLMStyle + + +class MoonshotModelEnum(str, Enum): + V1_8K = "moonshot-v1-8k" + V1_32K = "moonshot-v1-32k" + V1_128K = "moonshot-v1-128k" + + +class MoonshotService(OpenaiLLMStyle): + def __init__( + self, + top_p: float = 0.9, + temperature: float = 0.7, + timeout: float = 60., + ): + super().__init__( + api_key=config.get_settings().MOONSHOT_API_KEY, + endpoint="https://api.moonshot.cn/v1/chat/completions", + default_model=MoonshotModelEnum.V1_8K.value, + top_p=top_p, + temperature=temperature, + timeout=timeout, + ) diff --git a/src/retk/core/ai/llm/openai.py b/src/retk/core/ai/llm/api/openai.py similarity index 66% rename from src/retk/core/ai/llm/openai.py rename to src/retk/core/ai/llm/api/openai.py index ad25a5c..fdefc7c 100644 --- a/src/retk/core/ai/llm/openai.py +++ b/src/retk/core/ai/llm/api/openai.py @@ -4,10 +4,10 @@ from retk import config, const from retk.logger import logger -from .base import BaseLLM, MessagesType +from .base import BaseLLMService, MessagesType, NoAPIKeyError -class OpenAIModelEnum(str, Enum): +class OpenaiModelEnum(str, Enum): GPT4 = "gpt-4" GPT4_TURBO_PREVIEW = "gpt-4-turbo-preview" GPT4_32K = "gpt-4-32k" @@ -15,23 +15,26 @@ class OpenAIModelEnum(str, Enum): GPT35_TURBO_16K = "gpt-3.5-turbo-16k" -class OpenAI(BaseLLM): +class OpenaiLLMStyle(BaseLLMService): def __init__( self, + api_key: str, + endpoint: str, + default_model: str, top_p: float = 0.9, temperature: float = 0.7, timeout: float = 60., ): super().__init__( - endpoint="https://api.openai.com/v1/chat/completions", + endpoint=endpoint, top_p=top_p, temperature=temperature, timeout=timeout, - default_model=OpenAIModelEnum.GPT35_TURBO.value, + default_model=default_model, ) - self.api_key = config.get_settings().OPENAI_API_KEY + self.api_key = api_key if self.api_key == "": - raise ValueError("OpenAI API key is empty") + raise NoAPIKeyError(f"{self.__class__.__name__} API key is empty") self.headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}", @@ -43,7 +46,7 @@ def get_payload(self, model: Optional[str], messages: MessagesType, stream: bool return json.dumps({ "model": model, "messages": messages, - "max_tokens": 100, + # "max_tokens": 100, "temperature": self.temperature, "top_p": self.top_p, "stream": stream, @@ -66,7 +69,7 @@ async def complete( return "", code if rj.get("error") is not None: return rj["error"]["message"], const.CodeEnum.LLM_SERVICE_ERROR - logger.info(f"ReqId={req_id} OpenAI model usage: {rj['usage']}") + logger.info(f"ReqId={req_id} {self.__class__.__name__} model usage: {rj['usage']}") return rj["choices"][0]["message"]["content"], code async def stream_complete( @@ -86,19 +89,38 @@ async def stream_complete( yield b, code continue txt = "" - lines = b.splitlines() + lines = filter(lambda s: s != b"", b.split("\n\n".encode("utf-8"))) for line in lines: - json_str = line.decode("utf-8").strip() - if json_str == "": - continue + json_str = line.decode("utf-8")[5:].strip() try: json_data = json.loads(json_str) except json.JSONDecodeError: - logger.error(f"ReqId={req_id} OpenAI model stream error: json={json_str}") + logger.error(f"ReqId={req_id} {self.__class__.__name__} model stream error: json={json_str}") continue choice = json_data["choices"][0] if choice["finish_reason"] is not None: - logger.info(f"ReqId={req_id} OpenAI model usage: {json_data['usage']}") + try: + usage = json_data["usage"] + except KeyError: + usage = choice["usage"] + logger.info(f"ReqId={req_id} {self.__class__.__name__} model usage: {usage}") break txt += choice["delta"]["content"] yield txt.encode("utf-8"), code + + +class OpenaiService(OpenaiLLMStyle): + def __init__( + self, + top_p: float = 0.9, + temperature: float = 0.7, + timeout: float = 60., + ): + super().__init__( + api_key=config.get_settings().OPENAI_API_KEY, + endpoint="https://api.openai.com/v1/chat/completions", + default_model=OpenaiModelEnum.GPT35_TURBO.value, + top_p=top_p, + temperature=temperature, + timeout=timeout, + ) diff --git a/src/retk/core/ai/llm/tencent.py b/src/retk/core/ai/llm/api/tencent.py similarity index 94% rename from src/retk/core/ai/llm/tencent.py rename to src/retk/core/ai/llm/api/tencent.py index 1e41ba8..afe6b56 100644 --- a/src/retk/core/ai/llm/tencent.py +++ b/src/retk/core/ai/llm/api/tencent.py @@ -8,7 +8,7 @@ from retk import config, const from retk.logger import logger -from .base import BaseLLM, MessagesType +from .base import BaseLLMService, MessagesType, NoAPIKeyError Headers = TypedDict("Headers", { "Authorization": str, @@ -22,10 +22,10 @@ class TencentModelEnum(str, Enum): - HUNYUAN_PRO = "hunyuan-pro" - HUNYUAN_STANDARD = "hunyuan-standard" - HUNYUAN_STANDARD_256K = "hunyuan-standard-256K" - HUNYUAN_LITE = "hunyuan-lite" + HUNYUAN_PRO = "hunyuan-pro" # in 0.03/1000, out 0.10/1000 + HUNYUAN_STANDARD = "hunyuan-standard" # in 0.0045/1000, out 0.005/1000 + HUNYUAN_STANDARD_256K = "hunyuan-standard-256K" # in 0.015/1000, out 0.06/1000 + HUNYUAN_LITE = "hunyuan-lite" # free # 计算签名摘要函数 @@ -33,7 +33,7 @@ def sign(key, msg): return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() -class Tencent(BaseLLM): +class TencentService(BaseLLMService): service = "hunyuan" host = "hunyuan.tencentcloudapi.com" version = "2023-09-01" @@ -54,7 +54,7 @@ def __init__( self.secret_id = config.get_settings().HUNYUAN_SECRET_ID self.secret_key = config.get_settings().HUNYUAN_SECRET_KEY if self.secret_id == "" or self.secret_key == "": - raise ValueError("Tencent secret id or key is empty") + raise NoAPIKeyError("Tencent secret id or key is empty") def get_auth(self, action: str, payload: bytes, timestamp: int, content_type: str) -> str: algorithm = "TC3-HMAC-SHA256" diff --git a/src/retk/core/ai/llm/xfyun.py b/src/retk/core/ai/llm/api/xfyun.py similarity index 97% rename from src/retk/core/ai/llm/xfyun.py rename to src/retk/core/ai/llm/api/xfyun.py index 4a26487..4399282 100644 --- a/src/retk/core/ai/llm/xfyun.py +++ b/src/retk/core/ai/llm/api/xfyun.py @@ -14,7 +14,7 @@ from retk import config, const from retk.logger import logger -from .base import BaseLLM, MessagesType +from .base import BaseLLMService, MessagesType, NoAPIKeyError class XfYunModelEnum(str, Enum): @@ -32,7 +32,7 @@ class XfYunModelEnum(str, Enum): } -class XfYun(BaseLLM): +class XfYunService(BaseLLMService): def __init__( self, top_p: float = 0.9, @@ -51,7 +51,7 @@ def __init__( self.api_key = _s.XFYUN_API_KEY self.app_id = _s.XFYUN_APP_ID if self.api_secret == "" or self.api_key == "" or self.app_id == "": - raise ValueError("XfYun api secret or key is empty") + raise NoAPIKeyError("XfYun api secret or key is empty") def get_url(self, model: Optional[str], req_id: str = None) -> str: if model is None: diff --git a/src/retk/core/ai/llm/knowledge/__init__.py b/src/retk/core/ai/llm/knowledge/__init__.py new file mode 100644 index 0000000..75260b9 --- /dev/null +++ b/src/retk/core/ai/llm/knowledge/__init__.py @@ -0,0 +1,52 @@ +from pathlib import Path +from typing import Tuple + +from retk import const +from ..api.base import BaseLLMService, MessagesType + +system_summary_prompt = (Path(__file__).parent / "system_summary.md").read_text(encoding="utf-8") +system_extend_prompt = (Path(__file__).parent / "system_extend.md").read_text(encoding="utf-8") + + +async def _send( + llm_service: BaseLLMService, + model: str, + system_prompt: str, + query: str, + req_id: str, +) -> Tuple[str, const.CodeEnum]: + _msgs: MessagesType = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": query}, + ] + return await llm_service.complete(messages=_msgs, model=model, req_id=req_id) + + +async def summary( + llm_service: BaseLLMService, + model: str, + query: str, + req_id: str = None, +) -> Tuple[str, const.CodeEnum]: + return await _send( + llm_service=llm_service, + model=model, + system_prompt=system_summary_prompt, + query=query, + req_id=req_id, + ) + + +async def extend( + llm_service: BaseLLMService, + model: str, + query: str, + req_id: str = None, +) -> Tuple[str, const.CodeEnum]: + return await _send( + llm_service=llm_service, + model=model, + system_prompt=system_extend_prompt, + query=query, + req_id=req_id, + ) diff --git a/src/retk/core/ai/llm/knowledge/system_extend.md b/src/retk/core/ai/llm/knowledge/system_extend.md new file mode 100644 index 0000000..0b7c177 --- /dev/null +++ b/src/retk/core/ai/llm/knowledge/system_extend.md @@ -0,0 +1,25 @@ +你是一个博学多才的人,拥有非常丰富的知识,而且会融会贯通不同知识, +也会对相似知识之间的关系做通熟易懂的类比。 + +接下来,我将展示我最近接触到的一些知识点, +请依据你的内在丰富的知识网络,帮我推荐出一条我会感兴趣的 **新知识**。 + +下面是一个推荐新知识的案例: + +# 我展示的知识点 + +标题:幼儿因果关系认知的局限性 + +知识点: + +1. **特点**:2-4岁的儿童由于脑部发育阶段的特性,无法推理长线的因果关系。 +2. **原因**:这种现象的一个原因是前额叶的发展不足,无法模拟和推理未来发生的事情。 +3. **长短期反馈**:他们无法理解一段时间后的结果,例如不吃饭会导致晚上肚子饿。尽管如此,他们可以理解短期反馈,例如挥手打人或者给脸色会有直接的结果。 + +# 你需要返回的新知识 + +儿童发展中的同理心培养 + +- 富有同理心的小孩能理解和感受他人情感,有助于儿童建立良好的人际关系和社交技巧。 +- 儿童的同理心发展分为不同阶段,从2岁开始,他们能够感知到他人的情感,而4-5岁时,他们开始能够理解他人的观点和需求。 +- 家长和教育者可以通过共情、角色扮演、讲述故事、以及引导儿童关注他人的感受等方法,帮助儿童培养同理心。 diff --git a/src/retk/core/ai/llm/knowledge/system_summary.md b/src/retk/core/ai/llm/knowledge/system_summary.md new file mode 100644 index 0000000..94e26b1 --- /dev/null +++ b/src/retk/core/ai/llm/knowledge/system_summary.md @@ -0,0 +1,23 @@ +你是一个博学多才的人,拥有非常丰富的知识,十分善于用简练的语言总结复杂的概念。 + +接下来,我将展示我最近接触到的一些知识和信息,请帮我提炼总结这段认知的关键信息,总结一个简短标题,并简短罗列出知识点来。 + +比如下面的这个例子: + +# 我展示的信息 + +小孩建立长线的因果关系 + +因为脑部发育阶段的特性,2-4岁的儿童没办法推理比较长线的因果关系,比如不吃饭,晚上会肚子饿。其中的一个原因是前额叶的发展不够,没办法模拟和推理未来发生的事情,也就没办法思考一段时间后的结果。 + +但是短期反馈还是有的,比如挥手要打人或者给脸色的时候能有直接的映射结果,这点他们理解 + +# 你需要返回的总结格式 + +标题:幼儿因果关系认知的局限性 + +知识点: + +1. **特点**:2-4岁的儿童由于脑部发育阶段的特性,无法推理长线的因果关系。 +2. **原因**:这种现象的一个原因是前额叶的发展不足,无法模拟和推理未来发生的事情。 +3. **长短期反馈**:他们无法理解一段时间后的结果,例如不吃饭会导致晚上肚子饿。尽管如此,他们可以理解短期反馈,例如挥手打人或者给脸色会有直接的结果。 diff --git a/tests/test_llm.py b/tests/test_ai_llm_api.py similarity index 79% rename from tests/test_llm.py rename to tests/test_ai_llm_api.py index f38c2de..afe44f3 100644 --- a/tests/test_llm.py +++ b/tests/test_ai_llm_api.py @@ -7,6 +7,7 @@ from retk import const, config from retk.core.ai import llm +from retk.core.ai.llm.api.base import NoAPIKeyError from . import utils @@ -35,6 +36,30 @@ async def mock_baidu_post(url, *args, **kwargs): raise ValueError(f"Unexpected URL: {url}") +def skip_no_api_key(fn): + async def wrapper(*args): + try: + return await fn(*args) + except NoAPIKeyError: + pass + + return wrapper + + +def clear_all_api_key(): + c = config.get_settings() + c.HUNYUAN_SECRET_ID = "" + c.HUNYUAN_SECRET_KEY = "" + c.ALIYUN_DASHSCOPE_API_KEY = "" + c.BAIDU_QIANFAN_API_KEY = "" + c.BAIDU_QIANFAN_SECRET_KEY = "" + c.OPENAI_API_KEY = "" + c.XFYUN_APP_ID = "" + c.XFYUN_API_SECRET = "" + c.XFYUN_API_KEY = "" + c.MOONSHOT_API_KEY = "" + + class ChatBotTest(unittest.IsolatedAsyncioTestCase): @classmethod def setUpClass(cls): @@ -47,104 +72,89 @@ def tearDownClass(cls) -> None: utils.drop_env(".env.test.local") def tearDown(self): - c = config.get_settings() - c.HUNYUAN_SECRET_ID = "" - c.HUNYUAN_SECRET_KEY = "" - c.ALIYUN_DASHSCOPE_API_KEY = "" - c.BAIDU_QIANFAN_API_KEY = "" - c.BAIDU_QIANFAN_SECRET_KEY = "" - c.OPENAI_API_KEY = "" - c.XFYUN_APP_ID = "" - c.XFYUN_API_SECRET = "" - c.XFYUN_API_KEY = "" + clear_all_api_key() + @skip_no_api_key async def test_hunyuan_complete(self): - try: - m = llm.Tencent() - except ValueError: - return + m = llm.api.TencentService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) print(text) + @skip_no_api_key async def test_hunyuan_stream_complete(self): - try: - m = llm.Tencent() - except ValueError: - return + m = llm.api.TencentService() async for b, code in m.stream_complete([{"role": "user", "content": "你是谁"}]): self.assertEqual(const.CodeEnum.OK, code) s = b.decode("utf-8") print(s) + @skip_no_api_key async def test_aliyun_complete(self): - try: - m = llm.Aliyun() - except ValueError: - return + m = llm.api.AliyunService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) print(text) + @skip_no_api_key async def test_aliyun_stream_complete(self): - try: - m = llm.Aliyun() - except ValueError: - return + m = llm.api.AliyunService() async for b, code in m.stream_complete([{"role": "user", "content": "你是谁"}]): self.assertEqual(const.CodeEnum.OK, code) print(b.decode("utf-8")) + @skip_no_api_key async def test_baidu_complete(self): - try: - m = llm.Baidu() - except ValueError: - return + m = llm.api.BaiduService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) print(text) + @skip_no_api_key async def test_baidu_stream_complete(self): - try: - m = llm.Baidu() - except ValueError: - return + m = llm.api.BaiduService() async for b, code in m.stream_complete([{"role": "user", "content": "你是谁"}]): self.assertEqual(const.CodeEnum.OK, code) print(b.decode("utf-8")) + @skip_no_api_key async def test_openai_complete(self): - try: - m = llm.OpenAI() - except ValueError: - return + m = llm.api.OpenaiService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) print(text) + @skip_no_api_key async def test_openai_stream_complete(self): - try: - m = llm.OpenAI() - except ValueError: - return + m = llm.api.OpenaiService() async for b, code in m.stream_complete([{"role": "user", "content": "你是谁"}]): self.assertEqual(const.CodeEnum.OK, code) print(b.decode("utf-8")) + @skip_no_api_key async def test_xfyun_complete(self): - try: - m = llm.XfYun() - except ValueError: - return + m = llm.api.XfYunService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) print(text) + @skip_no_api_key async def test_xfyun_stream_complete(self): - try: - m = llm.XfYun() - except ValueError: - return + m = llm.api.XfYunService() + async for b, code in m.stream_complete([{"role": "user", "content": "你是谁"}]): + self.assertEqual(const.CodeEnum.OK, code) + print(b.decode("utf-8")) + + @skip_no_api_key + async def test_moonshot_complete(self): + m = llm.api.MoonshotService() + text, code = await m.complete([{"role": "user", "content": "你是谁"}]) + self.assertEqual(const.CodeEnum.OK, code, msg=text) + print(text) + + @skip_no_api_key + async def test_moonshot_stream_complete(self): + m = llm.api.MoonshotService() async for b, code in m.stream_complete([{"role": "user", "content": "你是谁"}]): self.assertEqual(const.CodeEnum.OK, code) print(b.decode("utf-8")) @@ -161,7 +171,7 @@ def test_hunyuan_authorization(self): payload = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") config.get_settings().HUNYUAN_SECRET_ID = self.sid config.get_settings().HUNYUAN_SECRET_KEY = self.skey - m = llm.Tencent() + m = llm.api.TencentService() auth = m.get_auth( action="ChatCompletions", payload=payload, @@ -189,7 +199,7 @@ async def test_hunyuan_auth_failed(self, mock_post): ) config.get_settings().HUNYUAN_SECRET_ID = self.sid config.get_settings().HUNYUAN_SECRET_KEY = self.skey - m = llm.Tencent() + m = llm.api.TencentService() self.assertEqual("hunyuan-lite", m.default_model) text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.LLM_SERVICE_ERROR, code, msg=text) @@ -220,7 +230,7 @@ async def test_hunyuan_complete_mock(self, mock_post): ) config.get_settings().HUNYUAN_SECRET_ID = self.sid config.get_settings().HUNYUAN_SECRET_KEY = self.skey - m = llm.Tencent() + m = llm.api.TencentService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) self.assertEqual("我是一个AI助手。", text) @@ -250,17 +260,15 @@ async def test_aliyun_complete_mock(self, mock_post): } ) config.get_settings().ALIYUN_DASHSCOPE_API_KEY = self.skey - m = llm.Aliyun() + m = llm.api.AliyunService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) self.assertEqual("我是一个AI助手。", text) mock_post.assert_called_once() + @skip_no_api_key async def test_baidu_token(self): - try: - m = llm.Baidu() - except ValueError: - return + m = llm.api.BaiduService() await m.set_token() self.assertNotEqual("", m.token) self.assertGreater(m.token_expires_at, datetime.now().timestamp()) @@ -269,7 +277,7 @@ async def test_baidu_token(self): async def test_baidu_complete_mock(self, mock_post): config.get_settings().BAIDU_QIANFAN_API_KEY = self.sid config.get_settings().BAIDU_QIANFAN_SECRET_KEY = self.skey - m = llm.Baidu() + m = llm.api.BaiduService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) self.assertEqual("我是一个AI助手。", text) @@ -297,7 +305,7 @@ async def test_openai_complete_mock(self, mock_post): } ) config.get_settings().OPENAI_API_KEY = self.sid - m = llm.OpenAI() + m = llm.api.OpenaiService() text, code = await m.complete([{"role": "user", "content": "你是谁"}]) self.assertEqual(const.CodeEnum.OK, code, msg=text) self.assertEqual("我是一个AI助手。", text) @@ -309,7 +317,7 @@ def test_xfyun_auth(self, mock_format_date_time: Mock): config.get_settings().XFYUN_API_KEY = "addd2272b6d8b7c8abdd79531420ca3b" config.get_settings().XFYUN_API_SECRET = "MjlmNzkzNmZkMDQ2OTc0ZDdmNGE2ZTZi" config.get_settings().XFYUN_APP_ID = "testappid" - m = llm.XfYun() + m = llm.api.XfYunService() url = m.get_url( model="v1.1", ) @@ -352,8 +360,35 @@ def get_mock_messages(): # 设置 websockets.connect 的返回值 mock_connect.return_value.__aenter__.return_value = mock_ws - m = llm.XfYun() + m = llm.api.XfYunService() async for result in m.stream_complete([{"role": "user", "content": "你是谁"}]): # 对返回的结果进行断言 self.assertEqual(result, (b"mocked_content", const.CodeEnum.OK)) + + @patch("httpx.AsyncClient.post", new_callable=AsyncMock) + async def test_moonshot_complete_mock(self, mock_post): + mock_post.return_value = Response( + status_code=200, + json={ + "choices": [ + { + "message": { + "role": "assistant", + "content": "我是一个AI助手。" + }, + } + ], + "usage": { + "prompt_tokens": 3, + "completion_tokens": 14, + "total_tokens": 17 + } + } + ) + config.get_settings().MOONSHOT_API_KEY = self.sid + m = llm.api.MoonshotService() + text, code = await m.complete([{"role": "user", "content": "你是谁"}]) + self.assertEqual(const.CodeEnum.OK, code, msg=text) + self.assertEqual("我是一个AI助手。", text) + mock_post.assert_called_once() diff --git a/tests/test_ai_llm_knowledge.py b/tests/test_ai_llm_knowledge.py new file mode 100644 index 0000000..ffc6edf --- /dev/null +++ b/tests/test_ai_llm_knowledge.py @@ -0,0 +1,219 @@ +import unittest + +from retk import const +from retk.core.ai import llm +from . import utils +from .test_ai_llm_api import skip_no_api_key, clear_all_api_key + +md_source = """\ +广东猪脚饭特点 + +广州猪脚饭超越沙县小吃兰州拉面等,成为广东第一中式快餐。 + +原因是 + +1. 广东人口分布有很多外来人口,猪脚饭兼容了很多口味 +2. 工艺简单,大量的预制工作,较低出餐时间,出餐快。适合快节奏的打工人群 +3. 因为出餐快,所以不用招人,省人力成本 + +![IMG6992.png](https://files.rethink.run/userData/RroFuzYSd8NGoKRL5zrrkZ/3a4344ccd6ba477e59ddf1f7f67e98bd.png) + +更值得一提的是猪脚饭在广东便宜,其它地方贵,原因之一是可以从香港走私猪脚,因为外国人不吃,所以产能过剩 + +【猪脚饭如何成为广东的快餐之王?【食录】-哔哩哔哩】 https://b23.tv/YUlg1nN +""" + +md_summary = """\ +标题:广东猪脚饭的快餐特色与成功因素 + +知识点: +1. **市场接受度**:广东猪脚饭因兼容多种口味,受到广泛欢迎,超越沙县小吃和兰州拉面成为广东最受欢迎的中式快餐。 +2. **人口结构**:广东的外来人口众多,猪脚饭满足了不同地域人群的口味需求。 +3. **工艺优势**:猪脚饭的制作工艺简单,预制工作量大,出餐速度快,适合快节奏生活。 +4. **成本效益**:快速出餐减少了人力成本,提高了经营效率。 +5. **价格因素**:猪脚饭在广东价格低廉,部分原因是可能通过香港走私猪脚,利用外国人不吃猪脚导致的产能过剩。 +""" + + +class LLMKnowledgeExtendTest(unittest.IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + cls.sid = "testid" + cls.skey = "testkey" + utils.set_env(".env.test.local") + + @classmethod + def tearDownClass(cls) -> None: + utils.drop_env(".env.test.local") + + def tearDown(self): + clear_all_api_key() + + @skip_no_api_key + async def test_summary(self): + for service, model in [ + (llm.api.TencentService(), llm.api.TencentModelEnum.HUNYUAN_LITE), + (llm.api.AliyunService(), llm.api.AliyunModelEnum.QWEN1_5_05B), + (llm.api.BaiduService(), llm.api.BaiduModelEnum.ERNIE_SPEED_8K), + # (llm.api.OpenaiService(), llm.api.OpenaiModelEnum.GPT4), + (llm.api.XfYunService(), llm.api.XfYunModelEnum.SPARK_LITE), + (llm.api.MoonshotService(), llm.api.MoonshotModelEnum.V1_8K), # 这个总结比较好 + ]: + text, code = await llm.knowledge.summary( + llm_service=service, + model=model.value, + query=md_source, + ) + self.assertEqual(const.CodeEnum.OK, code, msg=text) + print(f"{service.__class__.__name__} {model.value}\n{text}\n\n") + + # summary + """ + TencentService hunyuan-lite + # 广东猪脚饭特点 + + 广州猪脚饭超越沙县小吃、兰州拉面等,成为广东第一中式快餐。 + + 原因: + + 1. **口味多样**:广东人口分布有很多外来人口,猪脚饭兼容了很多口味,适应不同人群的需求。 + + 2. **工艺简单**:猪脚饭的制作工艺相对简单,大量预制工作,使得出餐时间较短,满足快节奏打工人群的需求。 + + 3. **低成本**:猪脚饭出餐快,不需要招人,节省人力成本,降低了餐厅运营成本。 + + 4. **价格优势**:广东地区的猪脚饭价格相对较低,其中一个原因是可以从香港走私猪脚,因为外国人不吃,导致产能过剩。 + + 综上所述,广东猪脚饭凭借多样化的口味、简单的制作工艺、低成本和高性价比,成功成为广东的快餐之王。 + + + AliyunService qwen1.5-0.5b-chat + 标题:广东猪脚饭为何能成为中式快餐之王? + + 知识点: + 1. **特点**:广大的劳动力和价格优势使猪脚饭成为了广东地区著名的中式快餐之一。 + 2. **原因**:广州猪脚饭的独特之处在于其符合当地人的口味,简单工艺、快速出餐和无需招工,且具有良好的性价比。 + 3. **香港影响**:广东猪脚饭与其他食品如沙县小吃、兰州拉面等有着密切联系,且上海等地的饮食文化对广东菜的影响也不可忽视。 + + 请务必确认所有内容是准确无误的,如有疑问欢迎与我交流。 + + + BaiduService ernie_speed + 标题:广东猪脚饭的特点与成为快餐之王的缘由 + + 知识点: + + 1. 特点: + - 广泛适应性:猪脚饭在广东受到欢迎,因为它兼容多种口味,适应了广东地区大量外来人口的口味需求。 + - 出餐快速:猪脚饭制作工艺简单,预制工作量大,出餐时间短,适合快节奏的工作环境。 + - 价格优势:在广东地区,猪脚饭价格相对便宜,部分原因是由于可以从香港走私猪脚,由于外国人不吃猪脚,导致猪脚产能过剩。 + + 2. 成为快餐之王的原因: + - 多样化口味:适应多种口味需求,满足不同人群的需求。 + - 制作效率:简单的制作工艺和大量的预制工作使得出餐速度极快。 + - 成本低廉:低人力成本和原材料成本的优势,使得猪脚饭价格更具竞争力。 + - 地域文化:广东地区的饮食文化和外来人口的融合,为猪脚饭提供了广阔的市场空间。 + + 附加信息: + + - 视频链接:【猪脚饭如何成为广东的快餐之王?【食录】】(可通过此链接了解更多关于猪脚饭的信息)。 + - 其它相关介绍:除了文中提到的特点外,猪脚饭还有丰富的营养价值和美味口感,是广东地区非常受欢迎的中式快餐之一。 + + + XfYunService v1.1 + 广东猪脚饭的特点有以下几点:1. 猪脚饭是一种传统的粤菜,口感鲜美,营养丰富。2. 猪脚饭的制作工艺简单,出餐速度快,适合快节奏的打工人群。3. + + + MoonshotService moonshot-v1-8k + 标题:广东猪脚饭成为快餐之王的原因 + + 知识点: + 1. **口味兼容**:广东猪脚饭适合多种口味,适应了广东外来人口的饮食习惯。 + 2. **工艺简易**:猪脚饭制作流程简单,有大量预制工作,便于快速出餐。 + 3. **快节奏适应**:快速出餐的特点迎合了快节奏的打工人群需求。 + 4. **成本节省**:出餐快减少了人力需求,降低了人力成本。 + 5. **价格优势**:广东猪脚饭价格便宜,部分原因是可能通过香港走私猪脚以降低成本。 + + """ + + @skip_no_api_key + async def test_extend(self): + for service, model in [ + # (llm.api.TencentService(), llm.api.TencentModelEnum.HUNYUAN_PRO), + (llm.api.AliyunService(), llm.api.AliyunModelEnum.QWEN_PLUS), + # (llm.api.BaiduService(), llm.api.BaiduModelEnum.ERNIE_SPEED_8K), + # (llm.api.OpenaiService(), llm.api.OpenaiModelEnum.GPT4), + # (llm.api.XfYunService(), llm.api.XfYunModelEnum.SPARK_LITE), + # (llm.api.MoonshotService(), llm.api.MoonshotModelEnum.V1_8K), # 这个延伸比较好 + ]: + text, code = await llm.knowledge.extend( + llm_service=service, + model=model.value, + query=md_summary, + ) + self.assertEqual(const.CodeEnum.OK, code, msg=text) + print(f"{service.__class__.__name__} {model.value}\n{text}\n\n") + + # extend + """ + TencentService hunyuan-pro + 基于你提供的知识点,我为你推荐以下新知识: + + **新知识**:中国地方特色美食文化的传播与影响 + + * **背景**:随着全球化和人口流动的加速,中国各地的特色美食文化得以广泛传播。广东猪脚饭的成功不仅反映了其独特的快餐特色,也体现了中国地方美食在传播过程中的影响力。 + * **传播途径**:现代社交媒体、美食节目、以及外出就餐等途径使得人们更容易接触到不同地区的特色美食。 + * **文化交融**:在传播过程中,地方特色美食往往会与当地文化和其他美食文化发生交融,形成新的风味和吃法。 + * **经济影响**:特色美食的传播不仅丰富了人们的饮食选择,也为相关产业链带来了经济效益。例如,猪脚饭的成功带动了相关食材(如猪脚)的生产和销售。 + * **社会影响**:美食文化的传播有助于加强地区间的文化交流和理解,增进人们对不同生活方式的认识和尊重。 + + 这一新知识将帮助你更全面地了解中国地方特色美食文化的传播过程及其所带来的多方面影响。 + + + AliyunService qwen1.5-0.5b-chat + 广东猪脚饭的快餐特色与成功因素: + + 1. 市场接受度高:广东的猪脚饭在多个地区都有着广泛的受欢迎程度,这得益于其多元化的口味选择,适应不同地域人们的口味需求。 + 2. 人口结构多样:广东本地的人口丰富多样,使得广东猪脚饭具有了较好的多元化口味,满足了不同地域人群的需求。 + 3. 工艺优势:广东的猪脚饭的制作工艺相对简单,符合中国菜的特点,预制工作量大,易于加工,出餐速度快,保证了效率。 + 4. 成本效益:快速出餐节省了人力成本,提高了经营效率,降低了运营成本。 + 5. 价格因素:广东猪脚饭的价格在短时间内下降了很多,主要是由于采用了快速出餐的方式,影响了商品的销量。 + + AliyunService qwen-plus + 新知识:预制菜产业的发展与影响 + + - 预制菜是餐饮行业中的一种趋势,它包括预先准备好的食材、半成品或成品菜肴,方便快捷地供消费者加热或简单烹饪后食用。 + - **效率提升**:预制菜大大缩短了餐厅的烹饪时间,提高了服务速度,尤其适合快餐业和外卖市场的需求。 + - **风味标准化**:预制菜使得菜品的味道更加统一,有助于连锁餐饮品牌保持品质一致性。 + - **食品安全**:预制菜生产过程中严格的质量控制提升了食品安全标准,降低了餐饮业的食品安全风险。 + - **饮食文化变化**:预制菜的流行也反映了现代人生活节奏加快,对便利性需求增加的社会现象,同时也引发关于食物新鲜度和营养流失的讨论。 + - **产业链影响**:预制菜产业的发展带动了上游食材供应链、冷链物流及包装材料等相关行业的发展。 + + + BaiduService ernie_speed + 基于上述知识点,为您推荐的新知识是: + + **广东猪脚饭连锁经营与品牌发展策略** + + 1. **连锁经营趋势**:随着市场需求的增长,广东猪脚饭逐渐兴起连锁经营的模式,这种模式带来了品牌效应和市场占有率的提高。 + 2. **品牌建设重要性**:在竞争激烈的市场环境下,建立品牌成为猪脚饭行业发展的关键。通过品牌建设,可以提升消费者忠诚度,增加市场议价能力。 + 3. **创新与差异化策略**:在保持传统口味的基础上,部分猪脚饭品牌开始尝试新的菜品、配料和口味创新,以吸引年轻消费者群体。同时,注重提供优质的服务和环境设施,打造独特的用餐体验。 + 4. **市场拓展与营销策略**:随着互联网的普及,广东猪脚饭品牌开始利用社交媒体、短视频等渠道进行营销,扩大品牌影响力。同时,通过合作、加盟等方式拓展市场,提高市场份额。 + 5. **食品安全与质量控制**:随着连锁经营和品牌建设的深入,食品安全和质量控制成为关键挑战。品牌需要建立严格的食品安全管理体系,确保食材的新鲜和卫生,提高消费者的信任度。 + + + XfYunService v1.1 + 广东猪脚饭的成功因素有很多,其中包括市场接受度、人口结构、工艺优势、成本效益和价格因素等。广东猪脚饭因兼容多种口味,受到广泛欢迎,超越沙县小吃和兰州拉面成为广东最受欢迎的中式快餐。广东的外来人口众多,猪脚饭满足了不同地域人群的口味需求。猪脚饭的制作工艺简单,预制工作量大,出餐速度快,适合快节奏生活。快速出餐减少了人力成本,提高了经营效率。 + + + MoonshotService moonshot-v1-8k + # 你需要返回的新知识 + + 广东早茶文化与粤式饮食特色 + + - **文化背景**:广东早茶不仅仅是一种饮食习惯,它代表了广东人悠闲的生活方式和社交文化。早茶通常在上午进行,人们聚在一起品尝点心、喝茶,享受悠闲时光。 + - **粤式饮食特色**:粤菜是中国八大菜系之一,以其精致的烹饪技艺、丰富的食材选择和清淡的口味而闻名。广东早茶中的点心种类繁多,如虾饺、烧卖、肠粉等,每一道都是对食材和工艺的极致追求。 + - **社交功能**:早茶不仅仅是满足口腹之欲,更是亲朋好友相聚、商务洽谈的社交场所。这种饮食文化体现了广东人重视人际关系和社交活动的特点。 + - **经济发展**:随着广东经济的快速发展,早茶文化也得到了进一步的推广和发展。现代的早茶店不仅提供传统的点心,还融入了创新元素,满足不同顾客的需求。 + - **旅游吸引力**:广东早茶作为一种独特的饮食文化,吸引了大量国内外游客前来体验。它不仅丰富了广东的旅游产品,也为当地经济发展做出了贡献。 + """