Compare commits
2 Commits
22cb30bde0
...
9f25fdab12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f25fdab12 | ||
|
|
b53a918aaa |
@@ -31,12 +31,33 @@ platforms:
|
||||
max_tokens: 2000
|
||||
max_words: 1000
|
||||
max_context_messages: 20
|
||||
qwen:
|
||||
# 国内: https://dashscope.aliyuncs.com
|
||||
# 海外: https://dashscope-intl.aliyuncs.com
|
||||
url: https://dashscope.aliyuncs.com
|
||||
api_key:
|
||||
model: qwen-plus
|
||||
temperature: 0.7
|
||||
top_p: 0.8
|
||||
max_tokens: 2000
|
||||
max_words: 1000
|
||||
max_context_messages: 20
|
||||
# 是否开启深度思考模式(仅 qwq 系列支持)
|
||||
enable_thinking: false
|
||||
deepseek:
|
||||
url: https://api.deepseek.com
|
||||
api_key:
|
||||
model:
|
||||
max_words: 1000
|
||||
max_context_messages: 20
|
||||
gemini:
|
||||
url: https://generativelanguage.googleapis.com
|
||||
api_key:
|
||||
model: gemini-2.0-flash
|
||||
temperature: 1
|
||||
max_tokens: 2000
|
||||
max_words: 1000
|
||||
max_context_messages: 20
|
||||
openai:
|
||||
url: https://api.openai.com
|
||||
api_key:
|
||||
@@ -52,6 +73,8 @@ platforms:
|
||||
max_words: 1000
|
||||
max_tokens: 2000
|
||||
max_context_messages: 20
|
||||
# 是否开启流式输出(开启后 Element 中消息会逐步更新)
|
||||
streaming: false
|
||||
xai:
|
||||
url: https://api.x.ai
|
||||
api_key:
|
||||
@@ -60,14 +83,7 @@ platforms:
|
||||
max_tokens: 1000
|
||||
max_words: 2000
|
||||
max_context_messages: 20
|
||||
gemini:
|
||||
url: https://generativelanguage.googleapis.com
|
||||
api_key:
|
||||
model: gemini-2.0-flash
|
||||
temperature: 1
|
||||
max_tokens: 2000
|
||||
max_words: 1000
|
||||
max_context_messages: 20
|
||||
|
||||
|
||||
# additional prompt
|
||||
additional_prompt:
|
||||
|
||||
@@ -10,7 +10,7 @@ from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
||||
from maubot_llmplus.local_paltform import Ollama, LmStudio
|
||||
from maubot_llmplus.platforms import Platform
|
||||
from maubot_llmplus.plugin import AbsExtraConfigPlugin, Config
|
||||
from maubot_llmplus.thrid_platform import OpenAi, Anthropic, XAi, Deepseek, Gemini
|
||||
from maubot_llmplus.thrid_platform import OpenAi, Anthropic, XAi, Deepseek, Gemini, Qwen
|
||||
|
||||
|
||||
class AiBotPlugin(AbsExtraConfigPlugin):
|
||||
@@ -124,15 +124,17 @@ class AiBotPlugin(AbsExtraConfigPlugin):
|
||||
await event.mark_read()
|
||||
await self.client.set_typing(event.room_id, timeout=99999)
|
||||
platform = self.get_ai_platform()
|
||||
|
||||
if platform.is_streaming_enabled():
|
||||
await self.client.set_typing(event.room_id, timeout=0)
|
||||
await self._handle_streaming(event, platform)
|
||||
return
|
||||
|
||||
chat_completion = await platform.create_chat_completion(self, event)
|
||||
self.log.debug(
|
||||
f"发送结果 {chat_completion.message}, {chat_completion.model}, {chat_completion.finish_reason}")
|
||||
# ai gpt调用
|
||||
# 关闭typing提示
|
||||
await self.client.set_typing(event.room_id, timeout=0)
|
||||
# 打开typing提示
|
||||
if chat_completion.result:
|
||||
# if hasattr(chat_completion.message, 'content'):
|
||||
resp_content = chat_completion.message['content']
|
||||
response = TextMessageEventContent(msgtype=MessageType.TEXT, body=resp_content, format=Format.HTML,
|
||||
formatted_body=markdown.render(resp_content))
|
||||
@@ -150,6 +152,48 @@ class AiBotPlugin(AbsExtraConfigPlugin):
|
||||
|
||||
return None
|
||||
|
||||
async def _handle_streaming(self, evt: MessageEvent, platform) -> None:
|
||||
# 发送初始占位消息
|
||||
placeholder = TextMessageEventContent(
|
||||
msgtype=MessageType.TEXT, body="▌", format=Format.HTML, formatted_body="▌"
|
||||
)
|
||||
response_event_id = await evt.respond(placeholder, in_thread=self.config['reply_in_thread'])
|
||||
|
||||
accumulated = ""
|
||||
last_edit_len = 0
|
||||
EDIT_THRESHOLD = 50 # 每积累50个字符更新一次消息
|
||||
|
||||
try:
|
||||
async for chunk in platform.create_chat_completion_stream(self, evt):
|
||||
accumulated += chunk
|
||||
if len(accumulated) - last_edit_len >= EDIT_THRESHOLD:
|
||||
display = accumulated + " ▌"
|
||||
new_content = TextMessageEventContent(
|
||||
msgtype=MessageType.TEXT,
|
||||
body=display,
|
||||
format=Format.HTML,
|
||||
formatted_body=markdown.render(display)
|
||||
)
|
||||
new_content.set_edit(response_event_id)
|
||||
await self.client.send_message(evt.room_id, new_content)
|
||||
last_edit_len = len(accumulated)
|
||||
except Exception as e:
|
||||
self.log.exception(f"Streaming error: {e}")
|
||||
if not accumulated:
|
||||
accumulated = f"Streaming error: {e}"
|
||||
|
||||
# 输出最终完整内容
|
||||
if not accumulated:
|
||||
accumulated = "(无响应)"
|
||||
final_content = TextMessageEventContent(
|
||||
msgtype=MessageType.TEXT,
|
||||
body=accumulated,
|
||||
format=Format.HTML,
|
||||
formatted_body=markdown.render(accumulated)
|
||||
)
|
||||
final_content.set_edit(response_event_id)
|
||||
await self.client.send_message(evt.room_id, final_content)
|
||||
|
||||
def get_ai_platform(self) -> Platform:
|
||||
use_platform = self.config.cur_platform
|
||||
if use_platform == 'openai':
|
||||
@@ -162,6 +206,8 @@ class AiBotPlugin(AbsExtraConfigPlugin):
|
||||
return Deepseek(self.config, self.http)
|
||||
if use_platform == 'gemini':
|
||||
return Gemini(self.config, self.http)
|
||||
if use_platform == 'qwen':
|
||||
return Qwen(self.config, self.http)
|
||||
if use_platform == 'local_ai#ollama':
|
||||
return Ollama(self.config, self.http)
|
||||
if use_platform == 'local_ai#lmstudio':
|
||||
@@ -300,7 +346,7 @@ class AiBotPlugin(AbsExtraConfigPlugin):
|
||||
self.config.cur_model = self.config['platforms'][argus.split("#")[0]]['model']
|
||||
await event.react("✅")
|
||||
# 如果是openai或者是claude
|
||||
elif argus == 'openai' or argus == 'anthropic' or argus == 'xai' or argus == 'deepseek' or argus == 'gemini':
|
||||
elif argus == 'openai' or argus == 'anthropic' or argus == 'xai' or argus == 'deepseek' or argus == 'gemini' or argus == 'qwen':
|
||||
if argus == self.config.cur_platform:
|
||||
await event.reply(f"current ai platform has be {argus}")
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Generator
|
||||
from typing import Optional, List, Generator, AsyncIterator
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from maubot import Plugin
|
||||
@@ -55,6 +55,12 @@ class Platform:
|
||||
async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def create_chat_completion_stream(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> AsyncIterator[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_streaming_enabled(self) -> bool:
|
||||
return False
|
||||
|
||||
async def list_models(self) -> List[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@@ -129,10 +129,22 @@ class OpenAi(Platform):
|
||||
|
||||
class Anthropic(Platform):
|
||||
max_tokens: int
|
||||
streaming: bool
|
||||
|
||||
def __init__(self, config: BaseProxyConfig, http: ClientSession) -> None:
|
||||
super().__init__(config, http)
|
||||
self.max_tokens = self.config['max_tokens']
|
||||
self.streaming = self.config.get('streaming', False)
|
||||
|
||||
def is_streaming_enabled(self) -> bool:
|
||||
return self.streaming
|
||||
|
||||
def _build_request(self, full_chat_context: list) -> tuple:
|
||||
endpoint = f"{self.url}/v1/messages"
|
||||
headers = {"x-api-key": self.api_key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
|
||||
req_body = {"model": self.model, "max_tokens": self.max_tokens, "system": self.system_prompt,
|
||||
"messages": full_chat_context}
|
||||
return endpoint, headers, req_body
|
||||
|
||||
async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion:
|
||||
full_chat_context = []
|
||||
@@ -140,10 +152,7 @@ class Anthropic(Platform):
|
||||
chat_context = await maubot_llmplus.platforms.get_chat_context(system_context, plugin, self, evt)
|
||||
full_chat_context.extend(list(chat_context))
|
||||
|
||||
endpoint = f"{self.url}/v1/messages"
|
||||
headers = {"x-api-key": self.api_key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
|
||||
req_body = {"model": self.model, "max_tokens": self.max_tokens, "system": self.system_prompt,
|
||||
"messages": full_chat_context}
|
||||
endpoint, headers, req_body = self._build_request(full_chat_context)
|
||||
|
||||
async with self.http.post(endpoint, headers=headers, data=json.dumps(req_body)) as response:
|
||||
# plugin.log.debug(f"响应内容:{response.status}, {await response.json()}")
|
||||
@@ -162,6 +171,33 @@ class Anthropic(Platform):
|
||||
finish_reason=response_json['stop_reason'],
|
||||
model=response_json['model']
|
||||
)
|
||||
|
||||
async def create_chat_completion_stream(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent):
|
||||
full_chat_context = []
|
||||
system_context = deque()
|
||||
chat_context = await maubot_llmplus.platforms.get_chat_context(system_context, plugin, self, evt)
|
||||
full_chat_context.extend(list(chat_context))
|
||||
|
||||
endpoint, headers, req_body = self._build_request(full_chat_context)
|
||||
req_body["stream"] = True
|
||||
|
||||
async with self.http.post(endpoint, headers=headers, data=json.dumps(req_body)) as response:
|
||||
if response.status != 200:
|
||||
raise ValueError(f"Error: {await response.text()}")
|
||||
async for line_bytes in response.content:
|
||||
line = line_bytes.decode("utf-8").strip()
|
||||
if not line.startswith("data: "):
|
||||
continue
|
||||
data_str = line[6:]
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
if data.get("type") == "content_block_delta":
|
||||
delta = data.get("delta", {})
|
||||
if delta.get("type") == "text_delta":
|
||||
yield delta.get("text", "")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
async def list_models(self) -> List[str]:
|
||||
@@ -324,3 +360,79 @@ class XAi(Platform):
|
||||
|
||||
def get_type(self) -> str:
|
||||
return "xai"
|
||||
|
||||
|
||||
class Qwen(Platform):
|
||||
max_tokens: int
|
||||
temperature: float
|
||||
top_p: float
|
||||
enable_thinking: bool
|
||||
|
||||
def __init__(self, config: BaseProxyConfig, http: ClientSession) -> None:
|
||||
super().__init__(config, http)
|
||||
self.max_tokens = self.config['max_tokens']
|
||||
self.temperature = self.config['temperature']
|
||||
self.top_p = self.config['top_p']
|
||||
self.enable_thinking = self.config['enable_thinking']
|
||||
|
||||
async def create_chat_completion(self, plugin: AbsExtraConfigPlugin, evt: MessageEvent) -> ChatCompletion:
|
||||
full_context = []
|
||||
context = await maubot_llmplus.platforms.get_context(plugin, self, evt)
|
||||
full_context.extend(list(context))
|
||||
|
||||
parameters = {
|
||||
"result_format": "message"
|
||||
}
|
||||
if self.max_tokens:
|
||||
parameters["max_tokens"] = self.max_tokens
|
||||
if self.temperature is not None:
|
||||
parameters["temperature"] = self.temperature
|
||||
if self.top_p is not None:
|
||||
parameters["top_p"] = self.top_p
|
||||
if self.enable_thinking:
|
||||
parameters["enable_thinking"] = True
|
||||
|
||||
request_body = {
|
||||
"model": self.model,
|
||||
"input": {
|
||||
"messages": full_context
|
||||
},
|
||||
"parameters": parameters
|
||||
}
|
||||
|
||||
endpoint = f"{self.url}/api/v1/services/aigc/text-generation/generation"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
}
|
||||
|
||||
async with self.http.post(endpoint, headers=headers, data=json.dumps(request_body)) as response:
|
||||
if response.status != 200:
|
||||
return ChatCompletion(
|
||||
result=False,
|
||||
message={},
|
||||
finish_reason=f"Error: {await response.text()}",
|
||||
model=None
|
||||
)
|
||||
response_json = await response.json()
|
||||
choice = response_json["output"]["choices"][0]
|
||||
return ChatCompletion(
|
||||
result=True,
|
||||
message=choice["message"],
|
||||
finish_reason=choice.get("finish_reason", "stop"),
|
||||
model=response_json.get("model", self.model)
|
||||
)
|
||||
|
||||
async def list_models(self) -> List[str]:
|
||||
models = [
|
||||
"qwen-max", "qwen-max-latest",
|
||||
"qwen-plus", "qwen-plus-latest",
|
||||
"qwen-turbo", "qwen-turbo-latest",
|
||||
"qwen-long",
|
||||
"qwen3-235b-a22b", "qwen3-30b-a3b",
|
||||
"qwq-plus", "qwq-plus-latest",
|
||||
]
|
||||
return [f"- {m}" for m in models]
|
||||
|
||||
def get_type(self) -> str:
|
||||
return "qwen"
|
||||
Reference in New Issue
Block a user