Skip to content

Commit 11ccd61

Browse files
committed
upload
1 parent 59ccabd commit 11ccd61

6 files changed

Lines changed: 2059 additions & 2 deletions

File tree

.gitignore

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pyenv
86+
# For a library or package, you might want to ignore these files since the code is
87+
# intended to run in multiple environments; otherwise, check them in:
88+
# .python-version
89+
90+
# pipenv
91+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94+
# install all needed dependencies.
95+
#Pipfile.lock
96+
97+
# poetry
98+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99+
# This is especially recommended for binary packages to ensure reproducibility, and is more
100+
# commonly ignored for libraries.
101+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102+
#poetry.lock
103+
104+
# pdm
105+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106+
#pdm.lock
107+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108+
# in version control.
109+
# https://pdm-project.org/#use-with-ide
110+
.pdm.toml
111+
.pdm-python
112+
.pdm-build/
113+
114+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115+
__pypackages__/
116+
117+
# Celery stuff
118+
celerybeat-schedule
119+
celerybeat.pid
120+
121+
# SageMath parsed files
122+
*.sage.py
123+
124+
# Environments
125+
.env
126+
.venv
127+
env/
128+
venv/
129+
ENV/
130+
env.bak/
131+
venv.bak/
132+
133+
# Spyder project settings
134+
.spyderproject
135+
.spyproject
136+
137+
# Rope project settings
138+
.ropeproject
139+
140+
# mkdocs documentation
141+
/site
142+
143+
# mypy
144+
.mypy_cache/
145+
.dmypy.json
146+
dmypy.json
147+
148+
# Pyre type checker
149+
.pyre/
150+
151+
# pytype static type analyzer
152+
.pytype/
153+
154+
# Cython debug symbols
155+
cython_debug/
156+
157+
# PyCharm
158+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160+
# and can be added to the global gitignore or merged into this file. For a more nuclear
161+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162+
#.idea/

README.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,55 @@
1-
# nonebot-plugin-gemini
2-
✨ Gemini AI 对话 NoneBot 插件 ✨
1+
# Nonebot Plugin Gemini
2+
Google Gemini AI 对话插件
3+
4+
## 功能
5+
| 命令 | 用途 | 示例 |
6+
| --- | --- | --- |
7+
| gemini <文本/图像> | 单次调用 Gemini 并获取回复 | gemini 编写一个NoneBot2的echo插件 |
8+
| geminichat [可选]<文本> | 开启一轮与 Gemini 的对话 | geminichat |
9+
| 结束对话 | 结束本轮对话 | 结束对话 |
10+
11+
> 如果你配置了命令头,请在使用命令时将命令头加上
12+
13+
## 安装方法
14+
<details open>
15+
<summary>使用 nb-cli 安装</summary>
16+
在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
17+
18+
nb plugin install nonebot-plugin-gemini
19+
20+
</details>
21+
22+
<details>
23+
<summary>使用包管理器安装</summary>
24+
在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
25+
26+
<details>
27+
<summary>pip</summary>
28+
29+
pip install nonebot-plugin-gemini
30+
</details>
31+
<details>
32+
<summary>pdm</summary>
33+
34+
pdm add nonebot-plugin-gemini
35+
</details>
36+
<details>
37+
<summary>poetry</summary>
38+
39+
poetry add nonebot-plugin-gemini
40+
</details>
41+
<details>
42+
<summary>conda</summary>
43+
44+
conda install nonebot-plugin-gemini
45+
</details>
46+
47+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分追加写入
48+
49+
plugins = ["nonebot_plugin_gemini"]
50+
51+
</details>
52+
53+
54+
## 配置
55+
[Google AI Studio](https://makersuite.google.com/app/apikey) 获取 `GOOGLE_API_KEY` 后,在 .env 文件 或 环境变量 中添加 `GOOGLE_API_KEY`

nonebot_plugin_gemini/__init__.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import os
2+
import aiohttp
3+
import google.generativeai as genai
4+
5+
from io import BytesIO
6+
from PIL import Image as PILImage
7+
from nonebot.typing import T_State
8+
from nonebot.matcher import Matcher
9+
from nonebot.plugin import PluginMetadata
10+
from nonebot.adapters import Message, Event, Bot
11+
from nonebot import require, get_driver, on_command
12+
from nonebot.params import CommandArg, ArgPlainText
13+
from google.generativeai.generative_models import ChatSession
14+
15+
from .config import Config
16+
17+
require("nonebot_plugin_alconna")
18+
require("nonebot_plugin_htmlrender")
19+
20+
from nonebot_plugin_alconna import UniMessage, Text, Image
21+
from nonebot_plugin_htmlrender import md_to_pic
22+
23+
24+
__plugin_meta__ = PluginMetadata(
25+
name="nonebot-plugin-gemini",
26+
description="Gemini AI 对话",
27+
usage="gemini [文本/图片] -Gemini 生成回复\ngeminichat (可选)[文本] -开始 Gemini 对话\n结束对话 -结束 Gemini 对话",
28+
type="application",
29+
homepage="https://github.com/zhaomaoniu/nonebot-plugin-gemini",
30+
config=Config,
31+
supported_adapters=None,
32+
)
33+
34+
35+
plugin_config = Config.parse_obj(get_driver().config)
36+
37+
38+
GOOGLE_API_KEY = plugin_config.google_api_key or os.environ.get("GOOGLE_API_KEY", None)
39+
40+
41+
if GOOGLE_API_KEY is None:
42+
raise ValueError("GOOGLE_API_KEY 未配置, nonebot-plugin-gemini 无法运行")
43+
44+
45+
genai.configure(api_key=GOOGLE_API_KEY)
46+
47+
models = {
48+
"gemini-pro": genai.GenerativeModel("gemini-pro"),
49+
"gemini-pro-vision": genai.GenerativeModel("gemini-pro-vision"),
50+
}
51+
52+
53+
async def to_markdown(text: str) -> bytes:
54+
text = text.replace("•", " *")
55+
return await md_to_pic(text, width=800)
56+
57+
58+
async def to_pil_image(image: Image) -> PILImage:
59+
if image.raw is not None:
60+
return PILImage.open(
61+
image.raw.getvalue() if isinstance(image.raw, BytesIO) else image.raw
62+
)
63+
64+
try:
65+
return PILImage.open(image.raw_bytes)
66+
except ValueError:
67+
pass
68+
69+
if image.path is not None:
70+
return PILImage.open(image.path)
71+
72+
if image.url is not None:
73+
async with aiohttp.ClientSession() as session:
74+
async with session.get(image.url) as resp:
75+
data = await resp.read()
76+
return PILImage.open(BytesIO(data))
77+
78+
raise ValueError("无法获取图片")
79+
80+
81+
chat = on_command("gemini", priority=10, block=True)
82+
conversation = on_command("geminichat", priority=5, block=True)
83+
84+
85+
@chat.handle()
86+
async def _(event: Event, bot: Bot, message: Message = CommandArg()):
87+
uni_message = await UniMessage.generate(message=message, event=event, bot=bot)
88+
89+
msg = []
90+
model = "gemini-pro"
91+
92+
for seg in uni_message:
93+
if isinstance(seg, Text):
94+
msg.append(seg.text)
95+
96+
elif isinstance(seg, Image):
97+
model = "gemini-pro-vision"
98+
msg.append(await to_pil_image(seg))
99+
100+
if not msg:
101+
await chat.finish("未获取到有效输入,输入应为文本或图片")
102+
103+
try:
104+
resp = await models[model].generate_content_async(msg)
105+
except Exception as e:
106+
await chat.finish(f"{type(e).__name__}: {e}")
107+
108+
try:
109+
result = resp.text
110+
except ValueError:
111+
result = "\n---\n".join(
112+
[part.text for part in resp.candidates[0].content.parts]
113+
)
114+
115+
await chat.finish(
116+
await UniMessage(Image(raw=await to_markdown(result))).export()
117+
if len(result) > 500
118+
else result.strip()
119+
)
120+
121+
122+
@conversation.handle()
123+
async def start_conversation(
124+
state: T_State, matcher: Matcher, args: Message = CommandArg()
125+
):
126+
if args.extract_plain_text() != "":
127+
matcher.set_arg(key="msg", message=args)
128+
129+
state["gemini_chat_session"] = models["gemini-pro"].start_chat(history=[])
130+
131+
132+
@conversation.got("msg", prompt="对话开始")
133+
async def got_message(state: T_State, msg: str = ArgPlainText()):
134+
if msg in ["结束", "结束对话", "结束会话", "stop", "quit"]:
135+
await conversation.finish("对话结束")
136+
137+
chat_session: ChatSession = state["gemini_chat_session"]
138+
139+
try:
140+
resp = await chat_session.send_message_async(msg)
141+
except Exception as e:
142+
await conversation.finish(f"发生意外错误,对话已结束\n{type(e).__name__}: {e}")
143+
144+
try:
145+
result = resp.text
146+
except ValueError:
147+
result = "\n---\n".join(
148+
[part.text for part in resp.candidates[0].content.parts]
149+
)
150+
151+
await conversation.reject(
152+
await UniMessage(Image(raw=await to_markdown(result))).export()
153+
if len(result) > 500
154+
else result.strip()
155+
)

nonebot_plugin_gemini/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing import Optional
2+
from pydantic import BaseModel
3+
4+
5+
class Config(BaseModel):
6+
google_api_key: Optional[str] = None

0 commit comments

Comments
 (0)