mirror of
https://github.com/LiteyukiStudio/nonebot-plugin-marshoai.git
synced 2024-11-26 14:55:03 +08:00
Merge pull request #4 from snowykami/main
✨ 添加Marsho AI插件,整合Azure服务,更新配置和元数据,修复startup加载两次的问题
This commit is contained in:
commit
5362b604e7
2
.github/workflows/pypi-publish.yml
vendored
2
.github/workflows/pypi-publish.yml
vendored
@ -3,7 +3,7 @@ name: Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,6 +3,9 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
#nonebot dev
|
||||
.env
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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,
|
||||
@ -190,7 +185,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()
|
||||
|
@ -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())
|
||||
|
14
nonebot_plugin_marshoai/metadata.py
Normal file
14
nonebot_plugin_marshoai/metadata.py
Normal file
@ -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"},
|
||||
)
|
@ -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 ""
|
||||
|
@ -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,21 +22,23 @@ 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
|
||||
|
||||
[tool.pdm.version]
|
||||
source = "file"
|
||||
path = "nonebot_plugin_marshoai/constants.py"
|
||||
source = "scm"
|
||||
tag_filter = "v*"
|
||||
tag_regex = '^v(?:\D*)?(?P<version>([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 = []
|
||||
|
||||
[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"
|
||||
|
Loading…
Reference in New Issue
Block a user