From 9dc595107992843a9b339abfc93d6a8d9ab45488 Mon Sep 17 00:00:00 2001 From: snowykami Date: Tue, 5 Nov 2024 19:34:47 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E8=AD=A6=E5=91=8A=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=BF=BD=E7=95=A5=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot_plugin_marshoai/azure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nonebot_plugin_marshoai/azure.py b/nonebot_plugin_marshoai/azure.py index 7789f2d..fe57308 100644 --- a/nonebot_plugin_marshoai/azure.py +++ b/nonebot_plugin_marshoai/azure.py @@ -190,7 +190,7 @@ async def marsho(target: MsgTarget, event: Event, text: Optional[UniMsg] = None) with contextlib.suppress(ImportError): # 优化先不做() - import nonebot.adapters.onebot.v11 + import nonebot.adapters.onebot.v11 # type: ignore from .azure_onebot import poke_notify @poke_notify.handle() From 8ac81e358f61585b78165552e88b93aef7ee61b2 Mon Sep 17 00:00:00 2001 From: snowykami Date: Tue, 5 Nov 2024 20:22:06 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0Marsho=20AI?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=EF=BC=8C=E6=95=B4=E5=90=88Azure=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=EF=BC=8C=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE=EF=BC=8C=E4=BF=AE=E5=A4=8Dstartup?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=B8=A4=E6=AC=A1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++ .vscode/settings.json | 7 +++ nonebot_plugin_marshoai/__init__.py | 16 ++---- nonebot_plugin_marshoai/azure.py | 37 ++++++------- nonebot_plugin_marshoai/azure_onebot.py | 7 +-- nonebot_plugin_marshoai/metadata.py | 14 +++++ nonebot_plugin_marshoai/util.py | 71 ++++++++++++++++--------- pyproject.toml | 9 ++-- 8 files changed, 97 insertions(+), 67 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 nonebot_plugin_marshoai/metadata.py 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" From 6df5f5b6c581caee275b62778dbd85599e4278ec Mon Sep 17 00:00:00 2001 From: snowykami Date: Tue, 5 Nov 2024 20:23:30 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20=E6=9B=B4=E6=96=B0PyPI=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=8C=E9=99=90=E5=88=B6?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E5=8C=B9=E9=85=8D=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=89=8D=E7=BC=80=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=BA=90=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pypi-publish.yml | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index f411f3d..714292d 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -3,7 +3,7 @@ name: Publish on: push: tags: - - '*' + - 'v*' workflow_dispatch: jobs: diff --git a/pyproject.toml b/pyproject.toml index 4158c12..cfd3942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,9 @@ adapters = [ distribution = true [tool.pdm.version] -source = "file" -path = "nonebot_plugin_marshoai/constants.py" +source = "scm" +tag_filter = "v*" +tag_regex = '^v(?:\D*)?(?P([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$' [tool.pdm.build] includes = [] From c818705e4b773b188d48f27762af4b45b994ed8a Mon Sep 17 00:00:00 2001 From: snowykami Date: Tue, 5 Nov 2024 20:26:02 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=A8=20=E6=9B=B4=E6=96=B0pyproject.tom?= =?UTF-8?q?l=EF=BC=8C=E6=B3=A8=E9=87=8A=E6=8E=89OneBot=20V11=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=99=A8=E9=85=8D=E7=BD=AE=E4=BB=A5=E4=BE=BF=E4=BA=8E?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfd3942..aa10d9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +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" }, -] +# adapters = [ +# { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }, +# ] 测试用 [tool.pdm] distribution = true