diff --git a/.gitignore b/.gitignore index 1ef6584..83e06bf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +#nonebot dev +.env + # C extensions *.so diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3e99ede --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/nonebot_plugin_marshoai/__init__.py b/nonebot_plugin_marshoai/__init__.py index f5867b7..ba2b790 100644 --- a/nonebot_plugin_marshoai/__init__.py +++ b/nonebot_plugin_marshoai/__init__.py @@ -1,23 +1,15 @@ from nonebot.plugin import PluginMetadata, inherit_supported_adapters, require + require("nonebot_plugin_alconna") require("nonebot_plugin_localstore") from .azure import * from nonebot import get_driver, logger -from .config import ConfigModel, config -from .constants import USAGE +from .config import config +from .metadata import metadata import nonebot_plugin_localstore as store __author__ = "Asankilp" -__plugin_meta__ = PluginMetadata( - name="Marsho AI插件", - description="接入Azure服务的AI聊天插件", - usage=USAGE, - type="application", - config=ConfigModel, - homepage="https://github.com/LiteyukiStudio/nonebot-plugin-marshoai", - supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), - extra={"License":"MIT","Author":"Asankilp"} -) +__plugin_meta__ = metadata driver = get_driver() diff --git a/nonebot_plugin_marshoai/azure.py b/nonebot_plugin_marshoai/azure.py index fe57308..f19e82a 100644 --- a/nonebot_plugin_marshoai/azure.py +++ b/nonebot_plugin_marshoai/azure.py @@ -12,14 +12,16 @@ from azure.ai.inference.aio import ChatCompletionsClient from azure.ai.inference.models import ( UserMessage, AssistantMessage, + ContentItem, TextContentItem, ImageContentItem, ImageUrl, CompletionsFinishReason, ) from azure.core.credentials import AzureKeyCredential -from typing import Optional -from .__init__ import __plugin_meta__ +from typing import Any, Optional + +from .metadata import metadata from .config import config from .models import MarshoContext from .constants import * @@ -126,15 +128,12 @@ async def nickname(event: Event, name=None): @marsho_cmd.handle() async def marsho(target: MsgTarget, event: Event, text: Optional[UniMsg] = None): if not text: - await UniMessage( - __plugin_meta__.usage + "\n当前使用的模型:" + model_name - ).send() + await UniMessage(metadata.usage + "\n当前使用的模型:" + model_name).send() await marsho_cmd.finish(INTRODUCTION) return try: - is_support_image_model = model_name.lower() in SUPPORT_IMAGE_MODELS - usermsg = [] if is_support_image_model else "" + user_id = event.get_user_id() nicknames = await get_nicknames() nickname = nicknames.get(user_id, "") @@ -146,22 +145,18 @@ async def marsho(target: MsgTarget, event: Event, text: Optional[UniMsg] = None) await UniMessage( "*你未设置自己的昵称。推荐使用'nickname [昵称]'命令设置昵称来获得个性化(可能)回答。" ).send() + + usermsg: list[ContentItem] = [] for i in text: - if i.type == "image": - if is_support_image_model: - imgurl = i.data["url"] - picmsg = ImageContentItem( - image_url=ImageUrl(url=str(await get_image_b64(imgurl))) + if i.type == "text": + usermsg += [TextContentItem(text=i.data["text"] + nickname_prompt)] + elif i.type == "image" and model_name.lower() in SUPPORT_IMAGE_MODELS: + usermsg.append( + ImageContentItem( + image_url=ImageUrl(url=str(await get_image_b64(i.data["url"]))) ) - usermsg.append(picmsg) - else: - await UniMessage("*此模型不支持图片处理。").send() - elif i.type == "text": - clean_text = i.data["text"] - if is_support_image_model: - usermsg.append(TextContentItem(text=clean_text + nickname_prompt)) - else: - usermsg += str(clean_text + nickname_prompt) + ) + response = await make_chat( client=client, model_name=model_name, diff --git a/nonebot_plugin_marshoai/azure_onebot.py b/nonebot_plugin_marshoai/azure_onebot.py index 3aaddc8..82c866c 100644 --- a/nonebot_plugin_marshoai/azure_onebot.py +++ b/nonebot_plugin_marshoai/azure_onebot.py @@ -1,8 +1,5 @@ from nonebot import on_type from nonebot.rule import to_me -from nonebot.adapters.onebot.v11 import PokeNotifyEvent -poke_notify = on_type( - (PokeNotifyEvent,), - rule=to_me() - ) +from nonebot.adapters.onebot.v11 import PokeNotifyEvent # type: ignore +poke_notify = on_type((PokeNotifyEvent,), rule=to_me()) diff --git a/nonebot_plugin_marshoai/metadata.py b/nonebot_plugin_marshoai/metadata.py new file mode 100644 index 0000000..72a8a0d --- /dev/null +++ b/nonebot_plugin_marshoai/metadata.py @@ -0,0 +1,14 @@ +from nonebot.plugin import PluginMetadata, inherit_supported_adapters +from .config import ConfigModel, config +from .constants import USAGE + +metadata = PluginMetadata( + name="Marsho AI插件", + description="接入Azure服务的AI聊天插件", + usage=USAGE, + type="application", + config=ConfigModel, + homepage="https://github.com/LiteyukiStudio/nonebot-plugin-marshoai", + supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), + extra={"License": "MIT", "Author": "Asankilp"}, +) diff --git a/nonebot_plugin_marshoai/util.py b/nonebot_plugin_marshoai/util.py index dc37f4f..845bebf 100644 --- a/nonebot_plugin_marshoai/util.py +++ b/nonebot_plugin_marshoai/util.py @@ -2,55 +2,67 @@ import base64 import mimetypes import os import json +from typing import Any import httpx import nonebot_plugin_localstore as store from datetime import datetime -from zhDateTime import DateTime +from zhDateTime import DateTime # type: ignore from azure.ai.inference.aio import ChatCompletionsClient from azure.ai.inference.models import SystemMessage from .config import config + + async def get_image_b64(url): headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } - + async with httpx.AsyncClient() as client: response = await client.get(url, headers=headers) if response.status_code == 200: # 获取图片数据 image_data = response.content - content_type = response.headers.get('Content-Type') + content_type = response.headers.get("Content-Type") if not content_type: content_type = mimetypes.guess_type(url)[0] - image_format = content_type.split('/')[1] if content_type else 'jpeg' - base64_image = base64.b64encode(image_data).decode('utf-8') + image_format = content_type.split("/")[1] if content_type else "jpeg" + base64_image = base64.b64encode(image_data).decode("utf-8") data_url = f"data:{content_type};base64,{base64_image}" return data_url else: return None + 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 + top_p=config.marshoai_top_p, ) + + def get_praises(): - praises_file = store.get_plugin_data_file("praises.json") # 夸赞名单文件使用localstore存储 + praises_file = store.get_plugin_data_file( + "praises.json" + ) # 夸赞名单文件使用localstore存储 if not os.path.exists(praises_file): init_data = { "like": [ - {"name":"Asankilp","advantages":"赋予了Marsho猫娘人格,使用vim与vscode为Marsho写了许多代码,使Marsho更加可爱"} - ] - } - with open(praises_file,"w",encoding="utf-8") as f: - json.dump(init_data,f,ensure_ascii=False,indent=4) - with open(praises_file,"r",encoding="utf-8") as f: + { + "name": "Asankilp", + "advantages": "赋予了Marsho猫娘人格,使用vim与vscode为Marsho写了许多代码,使Marsho更加可爱", + } + ] + } + with open(praises_file, "w", encoding="utf-8") as f: + json.dump(init_data, f, ensure_ascii=False, indent=4) + with open(praises_file, "r", encoding="utf-8") as f: data = json.load(f) return data + def build_praises(): praises = get_praises() result = ["你喜欢以下几个人物,他们有各自的优点:"] @@ -58,41 +70,47 @@ def build_praises(): result.append(f"名字:{item['name']},优点:{item['advantages']}") return "\n".join(result) -async def save_context_to_json(name: str, context: str): + +async def save_context_to_json(name: str, context: Any): context_dir = store.get_plugin_data_dir() / "contexts" os.makedirs(context_dir, exist_ok=True) file_path = os.path.join(context_dir, f"{name}.json") - with open(file_path, 'w', encoding='utf-8') as json_file: + with open(file_path, "w", encoding="utf-8") as json_file: json.dump(context, json_file, ensure_ascii=False, indent=4) + async def load_context_from_json(name: str): context_dir = store.get_plugin_data_dir() / "contexts" os.makedirs(context_dir, exist_ok=True) file_path = os.path.join(context_dir, f"{name}.json") try: - with open(file_path, 'r', encoding='utf-8') as json_file: + with open(file_path, "r", encoding="utf-8") as json_file: return json.load(json_file) except FileNotFoundError: return [] + + async def set_nickname(user_id: str, name: str): filename = store.get_plugin_data_file("nickname.json") if not os.path.exists(filename): data = {} else: - with open(filename,'r', encoding='utf-8') as f: + with open(filename, "r", encoding="utf-8") as f: data = json.load(f) data[user_id] = name - with open(filename, 'w', encoding='utf-8') as f: + with open(filename, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=4) + async def get_nicknames(): filename = store.get_plugin_data_file("nickname.json") try: - with open(filename, 'r', encoding='utf-8') as f: + with open(filename, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {} + def get_prompt(): prompts = "" prompts += config.marshoai_additional_prompt @@ -100,14 +118,17 @@ def get_prompt(): 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:] #库更新之前使用切片 + 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() + spell = SystemMessage(content=marsho_prompt + prompts).as_dict() return spell + def suggest_solution(errinfo: str) -> str: suggestions = { "content_filter": "消息已被内容过滤器过滤。请调整聊天内容后重试。", @@ -116,11 +137,11 @@ def suggest_solution(errinfo: str) -> str: "content_length_limit": "请求体过大。请重置上下文。", "unauthorized": "Azure凭据无效。请联系Bot管理员。", "invalid type: parameter messages.content is of type array but should be of type string.": "聊天请求体包含此模型不支持的数据类型。请重置上下文。", - "At most 1 image(s) may be provided in one request.": "此模型只能在上下文中包含1张图片。如果此前的聊天已经发送过图片,请重置上下文。" + "At most 1 image(s) may be provided in one request.": "此模型只能在上下文中包含1张图片。如果此前的聊天已经发送过图片,请重置上下文。", } for key, suggestion in suggestions.items(): if key in errinfo: return f"\n{suggestion}" - + return "" diff --git a/pyproject.toml b/pyproject.toml index 42dd9e0..4158c12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "azure-ai-inference>=1.0.0b4", "zhDatetime>=1.1.1", "aiohttp>=3.9", - "httpx>=0.27.0" + "httpx>=0.27.0", ] license = { text = "MIT" } @@ -22,6 +22,9 @@ Homepage = "https://github.com/LiteyukiStudio/nonebot-plugin-marshoai" [tool.nonebot] plugins = ["nonebot_plugin_marshoai"] +adapters = [ + { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }, +] [tool.pdm] distribution = true @@ -34,9 +37,7 @@ path = "nonebot_plugin_marshoai/constants.py" includes = [] [tool.pdm.dev-dependencies] -dev = [ - "nb-cli>=1.4.2", -] +dev = ["nb-cli>=1.4.2"] [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend"