entari-plugin-llm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entari_plugin_llm-0.1.0/LICENSE +21 -0
- entari_plugin_llm-0.1.0/PKG-INFO +109 -0
- entari_plugin_llm-0.1.0/README.md +1 -0
- entari_plugin_llm-0.1.0/pyproject.toml +77 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/README.md +92 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/__init__.py +35 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/_callback.py +21 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/_jsondata.py +52 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/_types.py +35 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/config.py +105 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/exception.py +1 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/handlers/__init__.py +0 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/handlers/chat.py +53 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/handlers/check.py +36 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/handlers/command.py +188 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/handlers/manager.py +276 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/handlers/utils.py +60 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/json_output.py +56 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/log.py +20 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/model.py +65 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/service.py +311 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/tools/__init__.py +1 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/tools/builtins/image_vision.py +42 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/tools/builtins/webpage_processor.py +47 -0
- entari_plugin_llm-0.1.0/src/entari_plugin_llm/tools/event.py +88 -0
- entari_plugin_llm-0.1.0/tests/__init__.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ARCLET
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: entari-plugin-llm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An Entari Plugin for LLM Chat with Function Call
|
|
5
|
+
Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>, KomoriDev <mute231010@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: arclet.entari[cron,dotenv,reload,yaml]>=0.18.0rc2
|
|
9
|
+
Requires-Dist: docstring-parser>=0.17.0
|
|
10
|
+
Requires-Dist: litellm>=1.83.7
|
|
11
|
+
Requires-Dist: entari-plugin-database>=0.3.1
|
|
12
|
+
Provides-Extra: browser
|
|
13
|
+
Requires-Dist: entari-plugin-browser>=0.5.4; extra == "browser"
|
|
14
|
+
Provides-Extra: google
|
|
15
|
+
Requires-Dist: litellm[google]>=1.83.7; extra == "google"
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# entari-plugin-llm
|
|
19
|
+
|
|
20
|
+
entari-plugin-llm 是一个用于 Entari 框架的 LLM(大语言模型)插件,提供基于 litellm 的对话能力、函数调用(Tool Call)支持、会话管理以及若干内置实用工具(如图像识别、网页处理等)。
|
|
21
|
+
|
|
22
|
+
插件目标是为基于 Arclet Entari 的机器人/服务提供一个可配置、可拓展的 LLM 工具箱,便于在对话中调用函数以完成复杂任务,并支持结构化 JSON 输出、视觉识别等能力。
|
|
23
|
+
|
|
24
|
+
主要特性
|
|
25
|
+
- 基于 litellm 的聊天能力(支持流式与非流式)。
|
|
26
|
+
- 支持“函数调用”机制(Tool Call),可以把插件内的订阅函数自动注册为 LLM 可调用的工具。
|
|
27
|
+
- 会话与上下文持久化(使用 `entari-plugin-database` 提供的数据库模型)。
|
|
28
|
+
- 支持视觉(image)输入的模型调用(当模型支持 vision 时)。
|
|
29
|
+
- 可配置的模型/提示/工具调用策略,通过 `Config` 加载与热重载(ConfigReload)。
|
|
30
|
+
|
|
31
|
+
要求
|
|
32
|
+
- Python >= 3.10
|
|
33
|
+
- 依赖见 `pyproject.toml` 中的 `dependencies`(推荐使用 pdm 或 uv 等现代包管理器)。
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
1. 克隆仓库:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
git clone https://github.com/ArcletProject/entari-plugin-llm.git
|
|
41
|
+
cd entari-plugin-llm
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. 安装依赖(使用 pdm,或使用 pip 在虚拟环境中安装):
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
pdm sync
|
|
48
|
+
# 或者使用 uv:
|
|
49
|
+
uv sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. 运行本地示例(运行 Entari 应用):
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
python main.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
说明:`main.py` 通过 `Entari.load("")` 加载当前目录下的 Entari 配置并启动服务 —— 在实际部署时请提供合适的配置文件与环境变量(例如模型的 API key、base_url 等)。
|
|
59
|
+
|
|
60
|
+
## 基本用法(示例)
|
|
61
|
+
|
|
62
|
+
作为插件使用时,包会在导入时通过 `metadata()` 注册插件信息,并在运行时加载配置、工具与服务。你可以在代码中直接引用导出的服务:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from entari_plugin_llm import llm
|
|
66
|
+
|
|
67
|
+
# 在异步上下文中调用
|
|
68
|
+
resp = await llm.generate("Hello world")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 工具与函数调用(Tool)
|
|
72
|
+
- 插件将符合 arclet.letoderea 订阅器规范的函数自动注册为可被 LLM 调用的工具。
|
|
73
|
+
- 工具的参数与文档由函数的 docstring 与类型注解自动生成 JSON Schema,以便 LLM 在函数调用时进行参数填充。
|
|
74
|
+
|
|
75
|
+
### 配置与热重载
|
|
76
|
+
|
|
77
|
+
插件使用 `Config` 对象管理模型、提示词、上下文长度以及工具调用的最大循环步数。修改 Entari 插件配置并触发 `ConfigReload` 事件可以热重载这些设置。
|
|
78
|
+
|
|
79
|
+
## 项目结构(重要文件与目录)
|
|
80
|
+
|
|
81
|
+
- `src/entari_plugin_llm/` - 插件实现代码
|
|
82
|
+
- `__init__.py` - 插件元信息与自动注册
|
|
83
|
+
- `service.py` - LLM 服务实现(封装了 litellm 的调用、工具调用处理、vision 等)
|
|
84
|
+
- `model.py` - 数据库 ORM 模型(会话与上下文)
|
|
85
|
+
- `handlers/` - Entari 事件处理器(chat、command、check 等)
|
|
86
|
+
- `tools/` - 插件提供的工具注册逻辑与内置工具(如 image_vision、webpage_processor)
|
|
87
|
+
- `main.py` - 一个简单的启动示例
|
|
88
|
+
- `pyproject.toml` - 项目与依赖配置
|
|
89
|
+
|
|
90
|
+
## 开发与调试
|
|
91
|
+
|
|
92
|
+
在开发过程中你可以编辑 `entari.yml` 或插件配置,并使用 Entari 的配置热重载来应用更改。
|
|
93
|
+
|
|
94
|
+
## 贡献
|
|
95
|
+
|
|
96
|
+
欢迎提交 issue 与 PR。请遵循仓库的编码风格与测试规范,尽量在 PR 中包含说明与复现步骤。
|
|
97
|
+
|
|
98
|
+
## 许可证
|
|
99
|
+
|
|
100
|
+
本项目遵循 MIT 许可证(见 `pyproject.toml` 中的 license 字段)。
|
|
101
|
+
|
|
102
|
+
## 联系方式
|
|
103
|
+
|
|
104
|
+
作者: RF-Tar-Railt, KomoriDev(详见 `src/entari_plugin_llm/__init__.py` 中的作者信息)
|
|
105
|
+
|
|
106
|
+
## 更多信息
|
|
107
|
+
|
|
108
|
+
请参阅 `pyproject.toml` 中的依赖与可选依赖(如浏览器、Google provider 等)以获取额外功能支持。
|
|
109
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.\src\entari_plugin_llm\README.md
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "entari-plugin-llm"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "An Entari Plugin for LLM Chat with Function Call"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com" },
|
|
7
|
+
{ name = "KomoriDev", email = "mute231010@gmail.com" },
|
|
8
|
+
]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"arclet.entari[yaml,cron,reload,dotenv]>=0.18.0rc2",
|
|
11
|
+
"docstring-parser>=0.17.0",
|
|
12
|
+
"litellm>=1.83.7",
|
|
13
|
+
"entari-plugin-database>=0.3.1",
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
|
|
18
|
+
[project.license]
|
|
19
|
+
text = "MIT"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
browser = [
|
|
23
|
+
"entari-plugin-browser>=0.5.4",
|
|
24
|
+
]
|
|
25
|
+
google = [
|
|
26
|
+
"litellm[google]>=1.83.7",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"entari-plugin-server>=0.6.2",
|
|
32
|
+
"satori-python-adapter-onebot11>=0.4.2",
|
|
33
|
+
"ruff>=0.14.10",
|
|
34
|
+
"satori-python-adapter-console>=0.5.1",
|
|
35
|
+
"agno>=2.5.8",
|
|
36
|
+
"ddgs>=9.11.2",
|
|
37
|
+
"satori-python-adapter-milky>=0.3.1",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 120
|
|
42
|
+
target-version = "py310"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = [
|
|
46
|
+
"E",
|
|
47
|
+
"W",
|
|
48
|
+
"F",
|
|
49
|
+
"UP",
|
|
50
|
+
"C",
|
|
51
|
+
"T",
|
|
52
|
+
"PYI",
|
|
53
|
+
"PT",
|
|
54
|
+
"Q",
|
|
55
|
+
"I",
|
|
56
|
+
]
|
|
57
|
+
ignore = [
|
|
58
|
+
"C901",
|
|
59
|
+
"T201",
|
|
60
|
+
"E731",
|
|
61
|
+
"E402",
|
|
62
|
+
"PYI055",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pyright]
|
|
66
|
+
pythonVersion = "3.10"
|
|
67
|
+
pythonPlatform = "All"
|
|
68
|
+
typeCheckingMode = "basic"
|
|
69
|
+
|
|
70
|
+
[tool.pdm]
|
|
71
|
+
distribution = true
|
|
72
|
+
|
|
73
|
+
[build-system]
|
|
74
|
+
requires = [
|
|
75
|
+
"pdm-backend",
|
|
76
|
+
]
|
|
77
|
+
build-backend = "pdm.backend"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# entari-plugin-llm
|
|
2
|
+
|
|
3
|
+
entari-plugin-llm 是一个用于 Entari 框架的 LLM(大语言模型)插件,提供基于 litellm 的对话能力、函数调用(Tool Call)支持、会话管理以及若干内置实用工具(如图像识别、网页处理等)。
|
|
4
|
+
|
|
5
|
+
插件目标是为基于 Arclet Entari 的机器人/服务提供一个可配置、可拓展的 LLM 工具箱,便于在对话中调用函数以完成复杂任务,并支持结构化 JSON 输出、视觉识别等能力。
|
|
6
|
+
|
|
7
|
+
主要特性
|
|
8
|
+
- 基于 litellm 的聊天能力(支持流式与非流式)。
|
|
9
|
+
- 支持“函数调用”机制(Tool Call),可以把插件内的订阅函数自动注册为 LLM 可调用的工具。
|
|
10
|
+
- 会话与上下文持久化(使用 `entari-plugin-database` 提供的数据库模型)。
|
|
11
|
+
- 支持视觉(image)输入的模型调用(当模型支持 vision 时)。
|
|
12
|
+
- 可配置的模型/提示/工具调用策略,通过 `Config` 加载与热重载(ConfigReload)。
|
|
13
|
+
|
|
14
|
+
要求
|
|
15
|
+
- Python >= 3.10
|
|
16
|
+
- 依赖见 `pyproject.toml` 中的 `dependencies`(推荐使用 pdm 或 uv 等现代包管理器)。
|
|
17
|
+
|
|
18
|
+
## 快速开始
|
|
19
|
+
|
|
20
|
+
1. 克隆仓库:
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
git clone https://github.com/ArcletProject/entari-plugin-llm.git
|
|
24
|
+
cd entari-plugin-llm
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. 安装依赖(使用 pdm,或使用 pip 在虚拟环境中安装):
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
pdm sync
|
|
31
|
+
# 或者使用 uv:
|
|
32
|
+
uv sync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. 运行本地示例(运行 Entari 应用):
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
python main.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
说明:`main.py` 通过 `Entari.load("")` 加载当前目录下的 Entari 配置并启动服务 —— 在实际部署时请提供合适的配置文件与环境变量(例如模型的 API key、base_url 等)。
|
|
42
|
+
|
|
43
|
+
## 基本用法(示例)
|
|
44
|
+
|
|
45
|
+
作为插件使用时,包会在导入时通过 `metadata()` 注册插件信息,并在运行时加载配置、工具与服务。你可以在代码中直接引用导出的服务:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from entari_plugin_llm import llm
|
|
49
|
+
|
|
50
|
+
# 在异步上下文中调用
|
|
51
|
+
resp = await llm.generate("Hello world")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 工具与函数调用(Tool)
|
|
55
|
+
- 插件将符合 arclet.letoderea 订阅器规范的函数自动注册为可被 LLM 调用的工具。
|
|
56
|
+
- 工具的参数与文档由函数的 docstring 与类型注解自动生成 JSON Schema,以便 LLM 在函数调用时进行参数填充。
|
|
57
|
+
|
|
58
|
+
### 配置与热重载
|
|
59
|
+
|
|
60
|
+
插件使用 `Config` 对象管理模型、提示词、上下文长度以及工具调用的最大循环步数。修改 Entari 插件配置并触发 `ConfigReload` 事件可以热重载这些设置。
|
|
61
|
+
|
|
62
|
+
## 项目结构(重要文件与目录)
|
|
63
|
+
|
|
64
|
+
- `src/entari_plugin_llm/` - 插件实现代码
|
|
65
|
+
- `__init__.py` - 插件元信息与自动注册
|
|
66
|
+
- `service.py` - LLM 服务实现(封装了 litellm 的调用、工具调用处理、vision 等)
|
|
67
|
+
- `model.py` - 数据库 ORM 模型(会话与上下文)
|
|
68
|
+
- `handlers/` - Entari 事件处理器(chat、command、check 等)
|
|
69
|
+
- `tools/` - 插件提供的工具注册逻辑与内置工具(如 image_vision、webpage_processor)
|
|
70
|
+
- `main.py` - 一个简单的启动示例
|
|
71
|
+
- `pyproject.toml` - 项目与依赖配置
|
|
72
|
+
|
|
73
|
+
## 开发与调试
|
|
74
|
+
|
|
75
|
+
在开发过程中你可以编辑 `entari.yml` 或插件配置,并使用 Entari 的配置热重载来应用更改。
|
|
76
|
+
|
|
77
|
+
## 贡献
|
|
78
|
+
|
|
79
|
+
欢迎提交 issue 与 PR。请遵循仓库的编码风格与测试规范,尽量在 PR 中包含说明与复现步骤。
|
|
80
|
+
|
|
81
|
+
## 许可证
|
|
82
|
+
|
|
83
|
+
本项目遵循 MIT 许可证(见 `pyproject.toml` 中的 license 字段)。
|
|
84
|
+
|
|
85
|
+
## 联系方式
|
|
86
|
+
|
|
87
|
+
作者: RF-Tar-Railt, KomoriDev(详见 `src/entari_plugin_llm/__init__.py` 中的作者信息)
|
|
88
|
+
|
|
89
|
+
## 更多信息
|
|
90
|
+
|
|
91
|
+
请参阅 `pyproject.toml` 中的依赖与可选依赖(如浏览器、Google provider 等)以获取额外功能支持。
|
|
92
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from arclet.entari import declare_static, metadata, plugin
|
|
2
|
+
|
|
3
|
+
from .config import Config, _conf
|
|
4
|
+
from .log import _suppress_litellm_logging
|
|
5
|
+
from .tools import LLMToolEvent as LLMToolEvent
|
|
6
|
+
|
|
7
|
+
metadata(
|
|
8
|
+
name="LLM 工具箱",
|
|
9
|
+
author=[
|
|
10
|
+
{"name": "RF-Tar-Railt", "email": "rf_tar_railt@qq.com"},
|
|
11
|
+
{"name": "KomoriDev", "email": "mute231010@gmail.com"},
|
|
12
|
+
],
|
|
13
|
+
version="0.1.0",
|
|
14
|
+
description="一个通用的 LLM 工具箱插件,提供了丰富的工具和模型配置选项,支持多种 LLM 模型,并且可以轻松集成到各种应用场景中。",
|
|
15
|
+
urls={
|
|
16
|
+
"homepage": "https://github.com/ArcletProject/entari-plugin-llm",
|
|
17
|
+
},
|
|
18
|
+
config=Config,
|
|
19
|
+
readme="README.md",
|
|
20
|
+
)
|
|
21
|
+
declare_static()
|
|
22
|
+
_suppress_litellm_logging()
|
|
23
|
+
|
|
24
|
+
for tool in _conf.tools:
|
|
25
|
+
plugin.load_plugin(tool)
|
|
26
|
+
|
|
27
|
+
from .handlers import chat as chat
|
|
28
|
+
from .handlers import check as check
|
|
29
|
+
from .handlers import command as command
|
|
30
|
+
from .service import llm as llm
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"llm",
|
|
34
|
+
"LLMToolEvent",
|
|
35
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from litellm.integrations.custom_logger import CustomLogger
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .service import LLMService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TokenUsageHandler(CustomLogger):
|
|
10
|
+
def __init__(self, service: "LLMService"):
|
|
11
|
+
self.service = service
|
|
12
|
+
|
|
13
|
+
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
|
14
|
+
if "usage" in response_obj:
|
|
15
|
+
self.service.total_tokens += response_obj["usage"].get("total_tokens", 0)
|
|
16
|
+
self.service.total_calls += 1
|
|
17
|
+
|
|
18
|
+
async def async_log_stream_event(self, kwargs, response_obj, start_time, end_time):
|
|
19
|
+
if "usage" in response_obj:
|
|
20
|
+
self.service.total_tokens += response_obj["usage"].get("total_tokens", 0)
|
|
21
|
+
self.service.total_calls += 1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from arclet.entari import local_data
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class LLMState:
|
|
11
|
+
default_model: str | None = None
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_dict(cls, data: dict[str, Any]) -> "LLMState":
|
|
15
|
+
value = data.get("default_model")
|
|
16
|
+
default_model = value if isinstance(value, str) and value else None
|
|
17
|
+
return cls(default_model=default_model)
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
return asdict(self)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _state_path() -> Path:
|
|
24
|
+
return local_data.get_data_file("entari_plugin_llm", "state.json")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_state() -> LLMState:
|
|
28
|
+
path = _state_path()
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return LLMState()
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
return LLMState()
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return LLMState()
|
|
37
|
+
return LLMState.from_dict(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _write_state(data: LLMState) -> None:
|
|
41
|
+
path = _state_path()
|
|
42
|
+
path.write_text(json.dumps(data.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_default_model() -> str | None:
|
|
46
|
+
return _read_state().default_model
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_default_model(model_name: str | None) -> None:
|
|
50
|
+
state = _read_state()
|
|
51
|
+
state.default_model = model_name if model_name else None
|
|
52
|
+
_write_state(state)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any, Literal, TypeAlias, TypedDict
|
|
2
|
+
|
|
3
|
+
from typing_extensions import NotRequired
|
|
4
|
+
|
|
5
|
+
JSON_VALUE: TypeAlias = str | int | float | bool | None
|
|
6
|
+
JSON_TYPE: TypeAlias = dict[str, "JSON_TYPE"] | list["JSON_TYPE"] | JSON_VALUE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SystemMessage(TypedDict):
|
|
10
|
+
role: Literal["system"]
|
|
11
|
+
content: str
|
|
12
|
+
name: NotRequired[str | None]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UserMessage(TypedDict):
|
|
16
|
+
role: Literal["user"]
|
|
17
|
+
content: str | list[dict[str, Any]]
|
|
18
|
+
name: NotRequired[str | None]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AssistantMessage(TypedDict):
|
|
22
|
+
role: Literal["assistant"]
|
|
23
|
+
content: str | None
|
|
24
|
+
tool_calls: NotRequired[list[dict[str, Any]] | None]
|
|
25
|
+
reasoning_content: NotRequired[str | None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolMessage(TypedDict):
|
|
29
|
+
role: Literal["tool"]
|
|
30
|
+
content: str
|
|
31
|
+
tool_call_id: str
|
|
32
|
+
name: NotRequired[str | None]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Message: TypeAlias = SystemMessage | UserMessage | AssistantMessage | ToolMessage
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from arclet.entari import BasicConfModel, plugin_config
|
|
4
|
+
from arclet.entari.config import model_field
|
|
5
|
+
|
|
6
|
+
from ._jsondata import get_default_model
|
|
7
|
+
from .exception import ModelNotFoundError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScopedModel(BasicConfModel):
|
|
11
|
+
name: str
|
|
12
|
+
"""用于 OpenAI API 的模型"""
|
|
13
|
+
alias: str | None = None
|
|
14
|
+
"""模型的别名"""
|
|
15
|
+
api_key: str | None = None
|
|
16
|
+
"""用于使用 OpenAI API 进行身份验证的 API 密钥。如果未设置,则回退到全局 api_key"""
|
|
17
|
+
base_url: str = "https://api.openai.com/v1"
|
|
18
|
+
"""OpenAI API 的接口地址。如果未设置,则回退到全局 base_url"""
|
|
19
|
+
prompt: str = ""
|
|
20
|
+
"""该模型使用的提示词。如果未设置,则回退到全局 prompt"""
|
|
21
|
+
extra: dict[str, Any] = model_field(default_factory=dict)
|
|
22
|
+
"""传递给 LLM API 调用的额外参数"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Config(BasicConfModel, extra="allow"):
|
|
26
|
+
api_key: str | None = None
|
|
27
|
+
"""用于使用 OpenAI API 进行身份验证的全局 API 密钥。用作没有特定键的模型的后备"""
|
|
28
|
+
base_url: str = "https://api.openai.com/v1"
|
|
29
|
+
"""OpenAI API 的全局接口地址。用作没有特定接口地址的模型的后备"""
|
|
30
|
+
prompt: str = ""
|
|
31
|
+
"""全局提示词。用作没有特定提示词的模型的后备"""
|
|
32
|
+
models: list[ScopedModel] = model_field(default_factory=list)
|
|
33
|
+
"""配置模型及其各自设置的列表"""
|
|
34
|
+
toolcall_max_steps: int = 8
|
|
35
|
+
"""单个会话中工具调用的最大步骤数"""
|
|
36
|
+
context_length: int = 50
|
|
37
|
+
"""上下文长度"""
|
|
38
|
+
tools: dict[str, dict[str, Any]] = model_field(default_factory=dict)
|
|
39
|
+
"""工具"""
|
|
40
|
+
|
|
41
|
+
def _reload_tools(self):
|
|
42
|
+
loaded_tools: dict[str, dict[str, Any]] = {}
|
|
43
|
+
|
|
44
|
+
for key, value in self.tools.items():
|
|
45
|
+
if key.startswith("$"):
|
|
46
|
+
loaded_tools[key] = value
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
tool_config = dict(value)
|
|
50
|
+
new_key = key
|
|
51
|
+
|
|
52
|
+
if key.startswith("~"):
|
|
53
|
+
new_key = key[1:]
|
|
54
|
+
if "$disable" not in tool_config or isinstance(tool_config["$disable"], bool):
|
|
55
|
+
tool_config["$disable"] = True
|
|
56
|
+
elif key.startswith("?"):
|
|
57
|
+
new_key = key[1:]
|
|
58
|
+
tool_config["$optional"] = True
|
|
59
|
+
|
|
60
|
+
if key.startswith("::"):
|
|
61
|
+
new_key = new_key.replace("::", "entari_plugin_llm.tools.builtins.")
|
|
62
|
+
|
|
63
|
+
if tool_config.get("$disable"):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
loaded_tools[new_key] = tool_config
|
|
67
|
+
|
|
68
|
+
self.tools = loaded_tools
|
|
69
|
+
|
|
70
|
+
def __post_init__(self):
|
|
71
|
+
self._reload_tools()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_conf = plugin_config(Config)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_model_config(model_name: str | None = None) -> ScopedModel:
|
|
78
|
+
if model_name is None:
|
|
79
|
+
if not _conf.models:
|
|
80
|
+
raise ModelNotFoundError("No models configured.")
|
|
81
|
+
|
|
82
|
+
model_name = get_default_model()
|
|
83
|
+
|
|
84
|
+
for model in _conf.models:
|
|
85
|
+
if model.name == model_name or model.alias == model_name:
|
|
86
|
+
model_cp = ScopedModel(
|
|
87
|
+
name=model.name,
|
|
88
|
+
alias=model.alias,
|
|
89
|
+
api_key=model.api_key,
|
|
90
|
+
base_url=model.base_url,
|
|
91
|
+
prompt=model.prompt,
|
|
92
|
+
extra=model.extra,
|
|
93
|
+
)
|
|
94
|
+
if not model.api_key and _conf.api_key:
|
|
95
|
+
model_cp.api_key = _conf.api_key
|
|
96
|
+
if model.base_url == "https://api.openai.com/v1" and _conf.base_url != "https://api.openai.com/v1":
|
|
97
|
+
model_cp.base_url = _conf.base_url
|
|
98
|
+
if not model.prompt and _conf.prompt:
|
|
99
|
+
model_cp.prompt = _conf.prompt
|
|
100
|
+
return model_cp
|
|
101
|
+
raise ModelNotFoundError(f"Model {model_name} not found in config.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_model_list() -> set[str]:
|
|
105
|
+
return {m.name for m in _conf.models} | {m.alias for m in _conf.models if m.alias}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class ModelNotFoundError(Exception): ...
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
|
|
3
|
+
from arclet.entari import MessageChain, MessageCreatedEvent, Session, filter_
|
|
4
|
+
from arclet.entari.config import config_model_validate
|
|
5
|
+
from arclet.entari.event.config import ConfigReload
|
|
6
|
+
from arclet.entari.event.send import SendResponse
|
|
7
|
+
from arclet.letoderea import BLOCK, on
|
|
8
|
+
from arclet.letoderea.context import Contexts
|
|
9
|
+
|
|
10
|
+
from ..config import Config, _conf
|
|
11
|
+
from ..exception import ModelNotFoundError
|
|
12
|
+
from .manager import LLMSessionManager
|
|
13
|
+
|
|
14
|
+
RECORD = deque(maxlen=16)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@on(SendResponse)
|
|
18
|
+
async def _record(event: SendResponse):
|
|
19
|
+
if event.result and event.session:
|
|
20
|
+
RECORD.append(event.session.event.sn)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@on(MessageCreatedEvent, priority=1000).if_(filter_.to_me)
|
|
24
|
+
async def run_conversation(session: Session, ctx: Contexts):
|
|
25
|
+
if session.event.sn in RECORD:
|
|
26
|
+
return BLOCK
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
answer = await LLMSessionManager.chat(
|
|
30
|
+
session.elements,
|
|
31
|
+
session=session,
|
|
32
|
+
ctx=ctx,
|
|
33
|
+
)
|
|
34
|
+
if answer != "[END_OF_RESPONSE]":
|
|
35
|
+
await session.send(answer)
|
|
36
|
+
except ModelNotFoundError as e:
|
|
37
|
+
await session.send(MessageChain(str(e)))
|
|
38
|
+
except Exception as e:
|
|
39
|
+
await session.send(MessageChain(str(e)))
|
|
40
|
+
return BLOCK
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@on(ConfigReload)
|
|
44
|
+
async def reload_config(event: ConfigReload):
|
|
45
|
+
if event.scope != "plugin":
|
|
46
|
+
return
|
|
47
|
+
if event.key not in ("entari_plugin_llm", "llm"):
|
|
48
|
+
return
|
|
49
|
+
new_conf = config_model_validate(Config, event.value)
|
|
50
|
+
_conf.models = new_conf.models
|
|
51
|
+
_conf.prompt = new_conf.prompt
|
|
52
|
+
_conf.context_length = new_conf.context_length
|
|
53
|
+
_conf.toolcall_max_steps = new_conf.toolcall_max_steps
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from arclet.entari.event.lifespan import Ready
|
|
2
|
+
from arclet.letoderea import on
|
|
3
|
+
|
|
4
|
+
from .._jsondata import get_default_model, set_default_model
|
|
5
|
+
from ..config import _conf
|
|
6
|
+
from ..log import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@on(Ready)
|
|
10
|
+
async def _():
|
|
11
|
+
if not _conf.models:
|
|
12
|
+
set_default_model(None)
|
|
13
|
+
logger.warning("未配置任何模型,已清空本地默认模型配置")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
first_model = _conf.models[0].name
|
|
17
|
+
default_model = get_default_model()
|
|
18
|
+
if not default_model:
|
|
19
|
+
set_default_model(first_model)
|
|
20
|
+
logger.info(f"未检测到本地默认模型,已设置为首个模型: {first_model}")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
matched = next(
|
|
24
|
+
(m for m in _conf.models if m.name == default_model or m.alias == default_model),
|
|
25
|
+
None,
|
|
26
|
+
)
|
|
27
|
+
if matched is None:
|
|
28
|
+
set_default_model(first_model)
|
|
29
|
+
logger.warning(
|
|
30
|
+
f"本地默认模型不存在于当前配置: {default_model},已重置为: {first_model}",
|
|
31
|
+
)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if matched.name != default_model:
|
|
35
|
+
set_default_model(matched.name)
|
|
36
|
+
logger.info(f"已将本地默认模型标准化为模型名: {matched.name}")
|