Skip to content

Commit 40b62e9

Browse files
authored
Add support for ModelScope API-Inference
Add support for ModelScope API-Inference
2 parents 436c038 + 5d2a987 commit 40b62e9

File tree

9 files changed

+353
-7
lines changed

9 files changed

+353
-7
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
<a href="https://github.com/zhayujie/chatgpt-on-wechat"><img src="https://img.shields.io/github/stars/zhayujie/chatgpt-on-wechat?style=flat-square" alt="Stars"></a> <br/>
77
</p>
88

9-
chatgpt-on-wechat(简称CoW)项目是基于大模型的智能对话机器人,支持微信公众号、企业微信应用、飞书、钉钉接入,可选择GPT3.5/GPT4.0/Claude/Gemini/LinkAI/ChatGLM/KIMI/文心一言/讯飞星火/通义千问/LinkAI,能处理文本、语音和图片,通过插件访问操作系统和互联网等外部资源,支持基于自有知识库定制企业AI应用。
9+
chatgpt-on-wechat(简称CoW)项目是基于大模型的智能对话机器人,支持微信公众号、企业微信应用、飞书、钉钉接入,可选择GPT3.5/GPT4.0/Claude/Gemini/LinkAI/ChatGLM/KIMI/文心一言/讯飞星火/通义千问/LinkAI/ModelScope,能处理文本、语音和图片,通过插件访问操作系统和互联网等外部资源,支持基于自有知识库定制企业AI应用。
1010

1111
# 简介
1212

1313
最新版本支持的功能如下:
1414

1515
-**多端部署:** 有多种部署方式可选择且功能完备,目前已支持微信公众号、企业微信应用、飞书、钉钉等部署方式
16-
-**基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4o-mini, GPT-4o, GPT-4, Claude-3.5, Gemini, 文心一言, 讯飞星火, 通义千问,ChatGLM-4,Kimi(月之暗面), MiniMax, GiteeAI
16+
-**基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4o-mini, GPT-4o, GPT-4, Claude-3.5, Gemini, 文心一言, 讯飞星火, 通义千问,ChatGLM-4,Kimi(月之暗面), MiniMax, GiteeAI, ModelScope(魔搭社区)
1717
-**语音能力:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai(whisper/tts) 等多种语音模型
1818
-**图像能力:** 支持图片生成、图片识别、图生图(如照片修复),可选择 Dall-E-3, stable diffusion, replicate, midjourney, CogView-3, vision模型
1919
-**丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结、文档总结和对话、联网搜索等插件

bot/bot_factory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,9 @@ def create_bot(bot_type):
6868
from bot.minimax.minimax_bot import MinimaxBot
6969
return MinimaxBot()
7070

71+
elif bot_type == const.MODELSCOPE:
72+
from bot.modelscope.modelscope_bot import ModelScopeBot
73+
return ModelScopeBot()
74+
7175

7276
raise RuntimeError

bot/modelscope/modelscope_bot.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
# encoding:utf-8
2+
3+
import time
4+
import json
5+
import openai
6+
import openai.error
7+
from bot.bot import Bot
8+
from bot.session_manager import SessionManager
9+
from bridge.context import ContextType
10+
from bridge.reply import Reply, ReplyType
11+
from common.log import logger
12+
from config import conf, load_config
13+
from .modelscope_session import ModelScopeSession
14+
import requests
15+
16+
17+
# ModelScope对话模型API
18+
class ModelScopeBot(Bot):
19+
def __init__(self):
20+
super().__init__()
21+
self.sessions = SessionManager(ModelScopeSession, model=conf().get("model") or "Qwen/Qwen2.5-7B-Instruct")
22+
model = conf().get("model") or "Qwen/Qwen2.5-7B-Instruct"
23+
if model == "modelscope":
24+
model = "Qwen/Qwen2.5-7B-Instruct"
25+
self.args = {
26+
"model": model, # 对话模型的名称
27+
"temperature": conf().get("temperature", 0.3), # 如果设置,值域须为 [0, 1] 我们推荐 0.3,以达到较合适的效果。
28+
"top_p": conf().get("top_p", 1.0), # 使用默认值
29+
}
30+
self.api_key = conf().get("modelscope_api_key")
31+
self.base_url = conf().get("modelscope_base_url", "https://api-inference.modelscope.cn/v1/chat/completions")
32+
"""
33+
需要获取ModelScope支持API-inference的模型名称列表,请到魔搭社区官网模型中心查看 https://modelscope.cn/models?filter=inference_type&page=1。
34+
或者使用命令 curl https://api-inference.modelscope.cn/v1/models 对模型列表和ID进行获取。查看commend/const.py文件也可以获取模型列表。
35+
获取ModelScope的免费API Key,请到魔搭社区官网用户中心查看获取方式 https://modelscope.cn/docs/model-service/API-Inference/intro。
36+
"""
37+
def reply(self, query, context=None):
38+
# acquire reply content
39+
if context.type == ContextType.TEXT:
40+
logger.info("[MODELSCOPE_AI] query={}".format(query))
41+
42+
session_id = context["session_id"]
43+
reply = None
44+
clear_memory_commands = conf().get("clear_memory_commands", ["#清除记忆"])
45+
if query in clear_memory_commands:
46+
self.sessions.clear_session(session_id)
47+
reply = Reply(ReplyType.INFO, "记忆已清除")
48+
elif query == "#清除所有":
49+
self.sessions.clear_all_session()
50+
reply = Reply(ReplyType.INFO, "所有人记忆已清除")
51+
elif query == "#更新配置":
52+
load_config()
53+
reply = Reply(ReplyType.INFO, "配置已更新")
54+
if reply:
55+
return reply
56+
session = self.sessions.session_query(query, session_id)
57+
logger.debug("[MODELSCOPE_AI] session query={}".format(session.messages))
58+
59+
model = context.get("modelscope_model")
60+
new_args = self.args.copy()
61+
if model:
62+
new_args["model"] = model
63+
64+
if new_args["model"] == "Qwen/QwQ-32B":
65+
reply_content = self.reply_text_stream(session, args=new_args)
66+
else:
67+
reply_content = self.reply_text(session, args=new_args)
68+
69+
logger.debug(
70+
"[MODELSCOPE_AI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(
71+
session.messages,
72+
session_id,
73+
reply_content["content"],
74+
reply_content["completion_tokens"],
75+
)
76+
)
77+
if reply_content["completion_tokens"] == 0 and len(reply_content["content"]) > 0:
78+
# 只有当 content 为空且 completion_tokens 为 0 时才标记为错误
79+
if len(reply_content["content"]) == 0:
80+
reply = Reply(ReplyType.ERROR, reply_content["content"])
81+
else:
82+
reply = Reply(ReplyType.TEXT, reply_content["content"])
83+
elif reply_content["completion_tokens"] > 0:
84+
self.sessions.session_reply(reply_content["content"], session_id, reply_content["total_tokens"])
85+
reply = Reply(ReplyType.TEXT, reply_content["content"])
86+
else:
87+
reply = Reply(ReplyType.ERROR, reply_content["content"])
88+
logger.debug("[MODELSCOPE_AI] reply {} used 0 tokens.".format(reply_content))
89+
return reply
90+
elif context.type == ContextType.IMAGE_CREATE:
91+
ok, retstring = self.create_img(query, 0)
92+
reply = None
93+
if ok:
94+
reply = Reply(ReplyType.IMAGE_URL, retstring)
95+
else:
96+
reply = Reply(ReplyType.ERROR, retstring)
97+
return reply
98+
else:
99+
reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type))
100+
return reply
101+
102+
def reply_text(self, session: ModelScopeSession, args=None, retry_count=0) -> dict:
103+
"""
104+
call openai's ChatCompletion to get the answer
105+
:param session: a conversation session
106+
:param session_id: session id
107+
:param retry_count: retry count
108+
:return: {}
109+
"""
110+
try:
111+
headers = {
112+
"Content-Type": "application/json",
113+
"Authorization": "Bearer " + self.api_key
114+
}
115+
116+
body = args
117+
body["messages"] = session.messages
118+
res = requests.post(
119+
self.base_url,
120+
headers=headers,
121+
data=json.dumps(body)
122+
)
123+
124+
if res.status_code == 200:
125+
response = res.json()
126+
return {
127+
"total_tokens": response["usage"]["total_tokens"],
128+
"completion_tokens": response["usage"]["completion_tokens"],
129+
"content": response["choices"][0]["message"]["content"]
130+
}
131+
else:
132+
response = res.json()
133+
if "errors" in response:
134+
error = response.get("errors")
135+
elif "error" in response:
136+
error = response.get("error")
137+
else:
138+
error = "Unknown error"
139+
logger.error(f"[MODELSCOPE_AI] chat failed, status_code={res.status_code}, "
140+
f"msg={error.get('message')}, type={error.get('type')}")
141+
142+
result = {"completion_tokens": 0, "content": "提问太快啦,请休息一下再问我吧"}
143+
need_retry = False
144+
if res.status_code >= 500:
145+
# server error, need retry
146+
logger.warn(f"[MODELSCOPE_AI] do retry, times={retry_count}")
147+
need_retry = retry_count < 2
148+
elif res.status_code == 401:
149+
result["content"] = "授权失败,请检查API Key是否正确"
150+
elif res.status_code == 429:
151+
result["content"] = "请求过于频繁,请稍后再试"
152+
need_retry = retry_count < 2
153+
else:
154+
need_retry = False
155+
156+
if need_retry:
157+
time.sleep(3)
158+
return self.reply_text(session, args, retry_count + 1)
159+
else:
160+
return result
161+
except Exception as e:
162+
logger.exception(e)
163+
need_retry = retry_count < 2
164+
result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
165+
if need_retry:
166+
return self.reply_text(session, args, retry_count + 1)
167+
else:
168+
return result
169+
170+
def reply_text_stream(self, session: ModelScopeSession, args=None, retry_count=0) -> dict:
171+
"""
172+
call ModelScope's ChatCompletion to get the answer with stream response
173+
:param session: a conversation session
174+
:param session_id: session id
175+
:param retry_count: retry count
176+
:return: {}
177+
"""
178+
try:
179+
headers = {
180+
"Content-Type": "application/json",
181+
"Authorization": "Bearer " + self.api_key
182+
}
183+
184+
body = args
185+
body["messages"] = session.messages
186+
body["stream"] = True # 启用流式响应
187+
188+
res = requests.post(
189+
self.base_url,
190+
headers=headers,
191+
data=json.dumps(body),
192+
stream=True
193+
)
194+
if res.status_code == 200:
195+
content = ""
196+
for line in res.iter_lines():
197+
if line:
198+
decoded_line = line.decode('utf-8')
199+
if decoded_line.startswith("data: "):
200+
try:
201+
json_data = json.loads(decoded_line[6:])
202+
delta_content = json_data.get("choices", [{}])[0].get("delta", {}).get("content", "")
203+
if delta_content:
204+
content += delta_content
205+
except json.JSONDecodeError as e:
206+
pass
207+
return {
208+
"total_tokens": 1, # 流式响应通常不返回token使用情况
209+
"completion_tokens": 1,
210+
"content": content
211+
}
212+
else:
213+
response = res.json()
214+
if "errors" in response:
215+
error = response.get("errors")
216+
elif "error" in response:
217+
error = response.get("error")
218+
else:
219+
error = "Unknown error"
220+
logger.error(f"[MODELSCOPE_AI] chat failed, status_code={res.status_code}, "
221+
f"msg={error.get('message')}, type={error.get('type')}")
222+
223+
result = {"completion_tokens": 0, "content": "提问太快啦,请休息一下再问我吧"}
224+
need_retry = False
225+
if res.status_code >= 500:
226+
# server error, need retry
227+
logger.warn(f"[MODELSCOPE_AI] do retry, times={retry_count}")
228+
need_retry = retry_count < 2
229+
elif res.status_code == 401:
230+
result["content"] = "授权失败,请检查API Key是否正确"
231+
elif res.status_code == 429:
232+
result["content"] = "请求过于频繁,请稍后再试"
233+
need_retry = retry_count < 2
234+
else:
235+
need_retry = False
236+
237+
if need_retry:
238+
time.sleep(3)
239+
return self.reply_text_stream(session, args, retry_count + 1)
240+
else:
241+
return result
242+
except Exception as e:
243+
logger.exception(e)
244+
need_retry = retry_count < 2
245+
result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
246+
if need_retry:
247+
return self.reply_text_stream(session, args, retry_count + 1)
248+
else:
249+
return result
250+
def create_img(self, query, retry_count=0):
251+
try:
252+
logger.info("[ModelScopeImage] image_query={}".format(query))
253+
headers = {
254+
"Content-Type": "application/json; charset=utf-8", # 明确指定编码
255+
"Authorization": f"Bearer {self.api_key}"
256+
}
257+
payload = {
258+
"prompt": query, # required
259+
"n": 1,
260+
"model": conf().get("text_to_image"),
261+
}
262+
url = "https://api-inference.modelscope.cn/v1/images/generations"
263+
264+
# 手动序列化并保留中文(禁用 ASCII 转义)
265+
json_payload = json.dumps(payload, ensure_ascii=False).encode('utf-8')
266+
267+
# 使用 data 参数发送原始字符串(requests 会自动处理编码)
268+
res = requests.post(url, headers=headers, data=json_payload)
269+
270+
response_data = res.json()
271+
image_url = response_data['images'][0]['url']
272+
logger.info("[ModelScopeImage] image_url={}".format(image_url))
273+
return True, image_url
274+
275+
except Exception as e:
276+
logger.error(format(e))
277+
return False, "画图出现问题,请休息一下再问我吧"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from bot.session_manager import Session
2+
from common.log import logger
3+
4+
5+
class ModelScopeSession(Session):
6+
def __init__(self, session_id, system_prompt=None, model="Qwen/Qwen2.5-7B-Instruct"):
7+
super().__init__(session_id, system_prompt)
8+
self.model = model
9+
self.reset()
10+
11+
def discard_exceeding(self, max_tokens, cur_tokens=None):
12+
precise = True
13+
try:
14+
cur_tokens = self.calc_tokens()
15+
except Exception as e:
16+
precise = False
17+
if cur_tokens is None:
18+
raise e
19+
logger.debug("Exception when counting tokens precisely for query: {}".format(e))
20+
while cur_tokens > max_tokens:
21+
if len(self.messages) > 2:
22+
self.messages.pop(1)
23+
elif len(self.messages) == 2 and self.messages[1]["role"] == "assistant":
24+
self.messages.pop(1)
25+
if precise:
26+
cur_tokens = self.calc_tokens()
27+
else:
28+
cur_tokens = cur_tokens - max_tokens
29+
break
30+
elif len(self.messages) == 2 and self.messages[1]["role"] == "user":
31+
logger.warn("user message exceed max_tokens. total_tokens={}".format(cur_tokens))
32+
break
33+
else:
34+
logger.debug("max_tokens={}, total_tokens={}, len(messages)={}".format(max_tokens, cur_tokens,
35+
len(self.messages)))
36+
break
37+
if precise:
38+
cur_tokens = self.calc_tokens()
39+
else:
40+
cur_tokens = cur_tokens - max_tokens
41+
return cur_tokens
42+
43+
def calc_tokens(self):
44+
return num_tokens_from_messages(self.messages, self.model)
45+
46+
47+
def num_tokens_from_messages(messages, model):
48+
tokens = 0
49+
for msg in messages:
50+
tokens += len(msg["content"])
51+
return tokens

bridge/bridge.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ def __init__(self):
4949
if model_type in [const.MOONSHOT, "moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"]:
5050
self.btype["chat"] = const.MOONSHOT
5151

52+
if model_type in [const.MODELSCOPE]:
53+
self.btype["chat"] = const.MODELSCOPE
54+
5255
if model_type in ["abab6.5-chat"]:
5356
self.btype["chat"] = const.MiniMax
5457

0 commit comments

Comments
 (0)