From acdf1d1ed28e4f4d74a112b51e59b39361c95b50 Mon Sep 17 00:00:00 2001 From: Asankilp Date: Thu, 24 Oct 2024 16:45:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=89=E5=AE=9E=E7=8E=B0=E6=88=B3?= =?UTF-8?q?=E4=B8=80=E6=88=B3=E5=93=8D=E5=BA=94=E5=92=8C=E5=85=B6=E5=AE=83?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 ++++++++++++ bot.py | 2 +- marshoai/__plugin__.py | 65 +++++++++++++++++++++++++++++---------- marshoai/checkers.py | 28 +++++++++++++++++ marshoai/extra_segment.py | 8 +++++ marshoai/util.py | 20 ++++++------ 6 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 marshoai/checkers.py create mode 100644 marshoai/extra_segment.py diff --git a/README.md b/README.md index 73a5b31..c6b04a7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,26 @@ _✨ 使用 Azure OpenAI 推理服务的聊天机器人(施工中) ✨_ 发送`marsho`指令可以获取使用说明 +#### 👉 戳一戳 +当 nonebot 连接到支持的 OneBot v11 实现端时,可以接收头像双击戳一戳消息并进行响应。详见`MARSHOAI_POKE_SUFFIX`配置项。 +## ⚙️ 配置 + +在 `bot.py` 所在目录的`.env`文件中添加下表中的配置 + +| 配置项 | 必填 | 默认值 | 说明 | +| :---------------: | :--: | :----: | :----------------------------------------------------------: | +| MARSHOAI_TOKEN | 是 | 无 | 调用 API 必需的访问 token | +| MARSHOAI_DEFAULT_MODEL | 否 | `gpt-4o-mini` | Marsho 默认调用的模型 | +| MARSHOAI_PROMPT | 否 | 猫娘 Marsho 人设提示词 | Marsho 的基本系统提示词 | +| MARSHOAI_ADDITIONAL_PROMPT | 否 | 无 | Marsho 的扩展系统提示词 | +| MARSHOAI_POKE_SUFFIX | 否 | `揉了揉你的猫耳` | 对 Marsho 所连接的 OneBot 用户进行双击戳一戳时,构建的聊天内容。此配置项为空字符串时,戳一戳响应功能会被禁用。例如,默认值构建的聊天内容将为`*[昵称]揉了揉你的猫耳`。 | +| MARSHOAI_ENABLE_PRAISES | 否 | `true` | 是否启用夸赞名单功能(未实现) | +| MARSHOAI_ENABLE_TIME_PROMPT | 否 | `true` | 是否启用实时更新的日期与时间(精确到秒)与农历日期系统提示词 | +| MARSHOAI_AZURE_ENDPOINT | 否 | `https://models.inference.ai.azure.com` | 调用 Azure OpenAI 服务的 API 终结点 | +| MARSHOAI_TEMPERATURE | 否 | 无 | 进行推理时的温度参数 | +| MARSHOAI_TOP_P | 否 | 无 | 进行推理时的核采样参数 | +| MARSHOAI_MAX_TOKENS | 否 | 无 | 返回消息的最大 token 数 | + ## © 版权说明 "Marsho" logo 由 [@Asankilp](https://github.com/Asankilp) 绘制,基于 [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 许可下提供。 "Melobot" logo 由 [@mldkouo](https://github.com/mldkouo) 绘制,版权归属于 [@Meloland](https://github.com/meloland)。 \ No newline at end of file diff --git a/bot.py b/bot.py index 502bc21..4d392a1 100644 --- a/bot.py +++ b/bot.py @@ -22,5 +22,5 @@ if __name__ == "__main__": .add_io(ForwardWebSocketIO("ws://127.0.0.1:8081")) .add_adapter(Adapter()) .load_plugin("./marshoai") - .run() + .run(debug=True) ) diff --git a/marshoai/__plugin__.py b/marshoai/__plugin__.py index e602f29..31a6213 100644 --- a/marshoai/__plugin__.py +++ b/marshoai/__plugin__.py @@ -2,17 +2,18 @@ import traceback from azure.ai.inference.aio import ChatCompletionsClient from azure.ai.inference.models import UserMessage, TextContentItem, ImageContentItem, ImageUrl, CompletionsFinishReason from melobot import Plugin, send_text -from melobot.protocols.onebot.v11 import on_start_match, on_message, on_command +from melobot.protocols.onebot.v11 import on_start_match, on_message, on_command, on_notice, on_event, Adapter from melobot.protocols.onebot.v11.handle import Args -from melobot.protocols.onebot.v11.utils import MsgChecker, LevelRole, MsgCheckerFactory, StartMatcher, ParseArgs -from melobot.protocols.onebot.v11.adapter.event import MessageEvent +from melobot.protocols.onebot.v11.utils import MsgChecker, LevelRole, MsgCheckerFactory, StartMatcher, ParseArgs, Parser +from melobot.protocols.onebot.v11.adapter.event import MessageEvent, PokeNotifyEvent, GroupMessageEvent, PrivateMessageEvent +from melobot.protocols.onebot.v11.adapter.segment import PokeSegment from azure.core.credentials import AzureKeyCredential +from typing import Union from .constants import * from .config import Config from .util import * from .models import MarshoContext - - +from .checkers import superuser_checker, PokeMarshoChecker config = Config() model_name = config.marshoai_default_model context = MarshoContext() @@ -22,11 +23,8 @@ client = ChatCompletionsClient( endpoint=endpoint, credential=AzureKeyCredential(token) ) -checker_ft = MsgCheckerFactory( - owner= config.owner, - super_users=config.superusers -) -superuser_checker: MsgChecker = checker_ft.get_base(LevelRole.SU) + + @on_command(checker=superuser_checker, cmd_start="/", cmd_sep=" ", targets="changemodel") async def changemodel(args: ParseArgs = Args()): @@ -35,13 +33,25 @@ async def changemodel(args: ParseArgs = Args()): await send_text("已切换") @on_start_match("reset") -async def reset(event: MessageEvent): +async def reset_group(event: GroupMessageEvent): + context.reset(event.group_id, event.is_private) + await send_text("上下文已重置") + +@on_start_match("reset") +async def reset_private(event: PrivateMessageEvent): context.reset(event.user_id, event.is_private) await send_text("上下文已重置") @on_start_match("marsho") -async def marsho(event: MessageEvent): +async def marsho_group(event: GroupMessageEvent): + await marsho_main(event, True) + +@on_start_match("marsho") +async def marsho_private(event: PrivateMessageEvent): + await marsho_main(event, False) + +async def marsho_main(event: Union[GroupMessageEvent, PrivateMessageEvent], is_group: bool): if event.text.lstrip("marsho") == "": await send_text(USAGE) await send_text(INTRODUCTION) @@ -51,6 +61,7 @@ async def marsho(event: MessageEvent): is_support_image_model = model_name.lower() in SUPPORT_IMAGE_MODELS usermsg = [] if is_support_image_model else "" user_id = event.sender.user_id + target_id = event.group_id if is_group else event.user_id nickname_prompt = "" marsho_string_removed = False for i in event.get_segments("image"): @@ -76,11 +87,11 @@ async def marsho(event: MessageEvent): response = await make_chat( client=client, model_name=model_name, - msg=context.build(event.user_id, event.is_private)+[UserMessage(content=usermsg)]) + msg=context.build(target_id, event.is_private)+[UserMessage(content=usermsg)]) choice = response.choices[0] if choice["finish_reason"] == CompletionsFinishReason.STOPPED: # 当对话成功时,将dict的上下文添加到上下文类中 - context.append(UserMessage(content=usermsg).as_dict(), event.user_id, event.is_private) - context.append(choice.message.as_dict(), event.user_id, event.is_private) + context.append(UserMessage(content=usermsg).as_dict(), target_id, event.is_private) + context.append(choice.message.as_dict(), target_id, event.is_private) elif choice["finish_reason"] == CompletionsFinishReason.CONTENT_FILTERED: await send_text("*已被内容过滤器过滤。请调整聊天内容后重试。") return @@ -90,6 +101,28 @@ async def marsho(event: MessageEvent): traceback.print_exc() return +@on_event(checker=PokeMarshoChecker()) +async def poke(event: PokeNotifyEvent, adapter: Adapter): # 尚未实现私聊戳一戳 QwQ + #await adapter.send_custom(str(event.user_id),group_id=event.group_id) + user_id = event.user_id + # nicknames = await get_nicknames() + # nickname = nicknames.get(user_id, "") + nickname = "" + try: + if config.marshoai_poke_suffix != "": + response = await make_chat( + client=client, + model_name=model_name, + msg=[get_prompt(),UserMessage(content=f"*{nickname}{config.marshoai_poke_suffix}")] + ) + choice = response.choices[0] + if choice["finish_reason"] == CompletionsFinishReason.STOPPED: + await adapter.send_custom(" "+str(choice.message.content),group_id=event.group_id) + except Exception as e: + await adapter.send_custom(str(e)+suggest_solution(str(e)),group_id=event.group_id) + traceback.print_exc() + return + class MarshoAI(Plugin): version = VERSION - flows = [changemodel,marsho,reset] + flows = [changemodel,marsho_group,marsho_private,reset_group,reset_private,poke] diff --git a/marshoai/checkers.py b/marshoai/checkers.py new file mode 100644 index 0000000..e4474df --- /dev/null +++ b/marshoai/checkers.py @@ -0,0 +1,28 @@ +from melobot.protocols.onebot.v11.adapter.event import Event, MessageEvent, PokeNotifyEvent +from melobot.protocols.onebot.v11.utils import MsgChecker, LevelRole, MsgCheckerFactory, StartMatcher, ParseArgs, Checker +from melobot.protocols.onebot.v11.adapter.segment import PokeRecvSegment + +from .config import Config +from .extra_segment import TouchSegment +config = Config() + +superuser_checker_ft = MsgCheckerFactory( + owner= config.owner, + super_users=config.superusers +) +superuser_checker: MsgChecker = superuser_checker_ft.get_base(LevelRole.SU) # 超级用户检查器 + +class PokeMarshoChecker(Checker): + """ + 戳一戳 Bot 检查器,戳一戳对象为 Bot 自身时检查通过 + """ + def __init__(self) -> None: + super().__init__() + async def check(self, event: PokeNotifyEvent) -> bool: + try: + if event.target_id == event.self_id: + return True + else: + return False + except AttributeError: + return False \ No newline at end of file diff --git a/marshoai/extra_segment.py b/marshoai/extra_segment.py new file mode 100644 index 0000000..2294b16 --- /dev/null +++ b/marshoai/extra_segment.py @@ -0,0 +1,8 @@ +from melobot.protocols.onebot.v11.adapter.segment import Segment +from typing import Literal +from typing_extensions import TypedDict + +class _TouchData(TypedDict): + id: str + +TouchSegment = Segment.add_type(Literal['touch'], _TouchData) \ No newline at end of file diff --git a/marshoai/util.py b/marshoai/util.py index 0e595f9..6048da7 100644 --- a/marshoai/util.py +++ b/marshoai/util.py @@ -33,10 +33,10 @@ async def get_image_b64(url): async def make_chat(client: ChatCompletionsClient, msg, model_name: str): return await client.complete( messages=msg, - model=model_name - # temperature=config.marshoai_temperature, - # max_tokens=config.marshoai_max_tokens, - # top_p=config.marshoai_top_p + model=model_name, + temperature=config.marshoai_temperature, + max_tokens=config.marshoai_max_tokens, + top_p=config.marshoai_top_p ) # def get_praises(): # praises_file = store.get_plugin_data_file("praises.json") # 夸赞名单文件使用localstore存储 @@ -96,15 +96,15 @@ async def make_chat(client: ChatCompletionsClient, msg, model_name: str): def get_prompt(): prompts = "" - # prompts += config.marshoai_additional_prompt + prompts += config.marshoai_additional_prompt # if config.marshoai_enable_praises: # praises_prompt = build_praises() # prompts += praises_prompt - # if config.marshoai_enable_time_prompt: - # current_time = datetime.now().strftime('%Y.%m.%d %H:%M:%S') - # current_lunar_date = DateTime.now().to_lunar().date_hanzify()[5:] #库更新之前使用切片 - # time_prompt = f"现在的时间是{current_time},农历{current_lunar_date}。" - # prompts += time_prompt + if config.marshoai_enable_time_prompt: + current_time = datetime.now().strftime('%Y.%m.%d %H:%M:%S') + current_lunar_date = DateTime.now().to_lunar().date_hanzify()[5:] #库更新之前使用切片 + time_prompt = f"现在的时间是{current_time},农历{current_lunar_date}。" + prompts += time_prompt marsho_prompt = config.marshoai_prompt spell = SystemMessage(content=marsho_prompt+prompts).as_dict() return spell