sdpy-kit 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.
- sdpy_kit-0.1.0/.gitignore +95 -0
- sdpy_kit-0.1.0/PKG-INFO +128 -0
- sdpy_kit-0.1.0/README.md +97 -0
- sdpy_kit-0.1.0/kit/__init__.py +68 -0
- sdpy_kit-0.1.0/kit/ai/__init__.py +47 -0
- sdpy_kit-0.1.0/kit/ai/base_service.py +94 -0
- sdpy_kit-0.1.0/kit/ai/chat_service.py +110 -0
- sdpy_kit-0.1.0/kit/ai/embedding_service.py +133 -0
- sdpy_kit-0.1.0/kit/ai/exceptions.py +57 -0
- sdpy_kit-0.1.0/kit/ai/failover.py +244 -0
- sdpy_kit-0.1.0/kit/ai/image_service.py +99 -0
- sdpy_kit-0.1.0/kit/ai/model_builder.py +119 -0
- sdpy_kit-0.1.0/kit/ai/model_client.py +214 -0
- sdpy_kit-0.1.0/kit/ai/pool.py +411 -0
- sdpy_kit-0.1.0/kit/ai/profiles.py +373 -0
- sdpy_kit-0.1.0/kit/ai/rerank_service.py +93 -0
- sdpy_kit-0.1.0/kit/ai/runner.py +580 -0
- sdpy_kit-0.1.0/kit/ai/types.py +28 -0
- sdpy_kit-0.1.0/kit/common_kit.py +221 -0
- sdpy_kit-0.1.0/kit/config/__init__.py +44 -0
- sdpy_kit-0.1.0/kit/config/kit_config.py +147 -0
- sdpy_kit-0.1.0/kit/config/log_config.py +134 -0
- sdpy_kit-0.1.0/kit/db/__init__.py +70 -0
- sdpy_kit-0.1.0/kit/db/base_db_kit.py +406 -0
- sdpy_kit-0.1.0/kit/db/crud.py +361 -0
- sdpy_kit-0.1.0/kit/db/engine.py +230 -0
- sdpy_kit-0.1.0/kit/db/exceptions.py +34 -0
- sdpy_kit-0.1.0/kit/db/models/__init__.py +33 -0
- sdpy_kit-0.1.0/kit/db/models/base.py +56 -0
- sdpy_kit-0.1.0/kit/db/mysql_kit.py +89 -0
- sdpy_kit-0.1.0/kit/db/pg_kit.py +87 -0
- sdpy_kit-0.1.0/kit/db/session.py +85 -0
- sdpy_kit-0.1.0/kit/db/sqlite_kit.py +284 -0
- sdpy_kit-0.1.0/kit/db/transaction.py +109 -0
- sdpy_kit-0.1.0/kit/doc_kit.py +1502 -0
- sdpy_kit-0.1.0/kit/file_kit.py +429 -0
- sdpy_kit-0.1.0/kit/mcp_kit.py +547 -0
- sdpy_kit-0.1.0/kit/mineru_kit.py +918 -0
- sdpy_kit-0.1.0/kit/redis_kit.py +392 -0
- sdpy_kit-0.1.0/kit/zvec_kit.py +671 -0
- sdpy_kit-0.1.0/pyproject.toml +49 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# 忽略匹配下列规则的Git 提交 V2.1.0
|
|
2
|
+
### gradle ###
|
|
3
|
+
.gradle
|
|
4
|
+
/build/
|
|
5
|
+
!gradle/wrapper/gradle-wrapper.jar
|
|
6
|
+
|
|
7
|
+
### STS ###
|
|
8
|
+
.settings/
|
|
9
|
+
.apt_generated
|
|
10
|
+
.classpath
|
|
11
|
+
.factorypath
|
|
12
|
+
.project
|
|
13
|
+
.settings
|
|
14
|
+
.springBeans
|
|
15
|
+
bin/
|
|
16
|
+
|
|
17
|
+
### IntelliJ IDEA ###
|
|
18
|
+
.idea
|
|
19
|
+
*.iws
|
|
20
|
+
*.iml
|
|
21
|
+
*.ipr
|
|
22
|
+
*.lock
|
|
23
|
+
rebel.xml
|
|
24
|
+
|
|
25
|
+
### NetBeans ###
|
|
26
|
+
nbproject/private/
|
|
27
|
+
build/
|
|
28
|
+
nbbuild/
|
|
29
|
+
nbdist/
|
|
30
|
+
.nb-gradle/
|
|
31
|
+
|
|
32
|
+
### maven ###
|
|
33
|
+
target/
|
|
34
|
+
*.war
|
|
35
|
+
*.ear
|
|
36
|
+
*.zip
|
|
37
|
+
*.tar
|
|
38
|
+
*.tar.gz
|
|
39
|
+
|
|
40
|
+
### logs ####
|
|
41
|
+
/logs/
|
|
42
|
+
*.log
|
|
43
|
+
|
|
44
|
+
### temp ignore ###
|
|
45
|
+
*.cache
|
|
46
|
+
*.diff
|
|
47
|
+
*.patch
|
|
48
|
+
*.tmp
|
|
49
|
+
*.java~
|
|
50
|
+
*.properties~
|
|
51
|
+
*.xml~
|
|
52
|
+
|
|
53
|
+
### system ignore ###
|
|
54
|
+
.DS_Store
|
|
55
|
+
Thumbs.db
|
|
56
|
+
Servers
|
|
57
|
+
.metadata
|
|
58
|
+
# 根目录 data/ 运行时数据(zvec 向量库、SQLite 测试库等,不入库)
|
|
59
|
+
data/
|
|
60
|
+
# AI 文生图 e2e 测试产物(生成的海报图片,不入库)
|
|
61
|
+
tests/image/
|
|
62
|
+
# AMiner 检索测试产物(md 报告,不入库)
|
|
63
|
+
tests/aminer/
|
|
64
|
+
# SQLite 测试库(运行时生成,不入库)
|
|
65
|
+
metadata_log.db
|
|
66
|
+
upload
|
|
67
|
+
gen_code
|
|
68
|
+
|
|
69
|
+
### node ###
|
|
70
|
+
node_modules
|
|
71
|
+
__pycache__/
|
|
72
|
+
.vscode/
|
|
73
|
+
.venv/
|
|
74
|
+
.trae/
|
|
75
|
+
.codebuddy/
|
|
76
|
+
.qoder/
|
|
77
|
+
|
|
78
|
+
### python dependencies ###
|
|
79
|
+
# 项目统一使用 uv + pyproject.toml 管理依赖,禁止 requirements.txt
|
|
80
|
+
requirements.txt
|
|
81
|
+
requirements-dev.txt
|
|
82
|
+
requirements*.txt
|
|
83
|
+
|
|
84
|
+
### secrets ###
|
|
85
|
+
# 环境变量文件含 API 密钥等敏感信息,禁止入库
|
|
86
|
+
.env
|
|
87
|
+
.env.*
|
|
88
|
+
docs/current-task/
|
|
89
|
+
research/
|
|
90
|
+
|
|
91
|
+
### sdpy-kit 发布工程 ###
|
|
92
|
+
# kit/ 为发布脚本从主工程同步的副本(构建时生成,不入库)
|
|
93
|
+
publish/sdpy-kit/kit/
|
|
94
|
+
publish/sdpy-kit/dist/
|
|
95
|
+
publish/sdpy-kit/*.egg-info/
|
sdpy_kit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sdpy-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
|
|
5
|
+
Author-email: Clark <changhongyuan@126.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Requires-Dist: fastmcp>=4.0.3
|
|
15
|
+
Requires-Dist: firecrawl-anydoc>=0.2.4
|
|
16
|
+
Requires-Dist: httpx>=0.28.1
|
|
17
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
18
|
+
Requires-Dist: pimd[pdfa]>=2.2.5
|
|
19
|
+
Requires-Dist: psycopg2-binary>=2.9.12
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Requires-Dist: pymysql>=1.2.0
|
|
23
|
+
Requires-Dist: python-dotenv>=1.2.3
|
|
24
|
+
Requires-Dist: redis>=5.0.0
|
|
25
|
+
Requires-Dist: requests>=2.34.0
|
|
26
|
+
Requires-Dist: sqlalchemy~=2.0.51
|
|
27
|
+
Requires-Dist: sqliteai-vector>=1.0.0
|
|
28
|
+
Requires-Dist: sqlmodel>=0.0.39
|
|
29
|
+
Requires-Dist: zvec>=0.6.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# sdpy-kit
|
|
33
|
+
|
|
34
|
+
SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:统一数据库访问层(PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致)+ AI 多模型服务池(chat / embedding / rerank / image,多厂商候选链与故障转移)+ MCP 客户端 + Redis / 文件 / 文档 / 向量库 / MinerU 工具。
|
|
35
|
+
|
|
36
|
+
> 要求 Python 3.13+
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install sdpy-kit
|
|
42
|
+
# 或
|
|
43
|
+
uv add sdpy-kit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 配置(.env)
|
|
47
|
+
|
|
48
|
+
kit 通过 pydantic-settings 读取项目根目录 `.env`(默认值见下,均可省略):
|
|
49
|
+
|
|
50
|
+
```dotenv
|
|
51
|
+
# 日志
|
|
52
|
+
LOG_LEVEL=INFO
|
|
53
|
+
LOG_TO_CONSOLE=true
|
|
54
|
+
LOG_TO_FILE=false
|
|
55
|
+
|
|
56
|
+
# 数据库(三选一,切换只改 DEFAULT_DB_TYPE)
|
|
57
|
+
DEFAULT_DB_TYPE=sqlite # postgresql / mysql / sqlite
|
|
58
|
+
DB_HOST=localhost
|
|
59
|
+
DB_PORT=5432
|
|
60
|
+
DB_DATABASE=postgres
|
|
61
|
+
DB_USER=admin
|
|
62
|
+
DB_PASSWORD=
|
|
63
|
+
MYSQL_PORT=3306
|
|
64
|
+
MYSQL_DATABASE=mydb
|
|
65
|
+
MYSQL_USER=root
|
|
66
|
+
SQLITE_DB_PATH=./data/app.db
|
|
67
|
+
|
|
68
|
+
# Redis
|
|
69
|
+
REDIS_HOST=localhost
|
|
70
|
+
REDIS_PORT=6379
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## AI / MCP 功能的档案文件
|
|
74
|
+
|
|
75
|
+
AI 与 MCP 功能需要在项目根目录提供两个 JSON 档案文件:
|
|
76
|
+
|
|
77
|
+
- `config/ai_models.json` —— chat / embedding / rerank / image 四类型多厂商模型档案
|
|
78
|
+
- `config/mcp_servers.json` —— MCP 服务器档案
|
|
79
|
+
|
|
80
|
+
文件缺失时,首次使用相应功能会报错,**报错信息中附带可直接复制的最小模板**;
|
|
81
|
+
其中 `api_key` / `token_env` 字段填写环境变量名,真实密钥配置在 `.env`。
|
|
82
|
+
|
|
83
|
+
## 快速开始
|
|
84
|
+
|
|
85
|
+
### 日志与配置
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from kit.config import get_settings, get_default_logger
|
|
89
|
+
|
|
90
|
+
settings = get_settings()
|
|
91
|
+
logger = get_default_logger(__name__)
|
|
92
|
+
logger.info("hello %s", settings.APP_NAME)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 数据库(SQLiteKit / PgKit / MySqlKit API 完全一致,切换只改 import)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from sqlmodel import SQLModel, Field
|
|
99
|
+
from kit.db.sqlite_kit import SQLiteKit
|
|
100
|
+
|
|
101
|
+
class Item(SQLModel, table=True):
|
|
102
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
103
|
+
name: str
|
|
104
|
+
|
|
105
|
+
SQLiteKit.connect()
|
|
106
|
+
SQLiteKit.create_tables([Item])
|
|
107
|
+
item = SQLiteKit.create(Item, {"name": "demo"})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
约定:`SQLiteKit/PgKit/MySqlKit` 方法自动提交;`CRUDBase`(配合 `session_scope`)不自动提交;事务用 `kit.transactional`。
|
|
111
|
+
|
|
112
|
+
## 模块总览
|
|
113
|
+
|
|
114
|
+
| 模块 | 内容 |
|
|
115
|
+
|------|------|
|
|
116
|
+
| `kit.db` | 统一数据库访问层:PgKit / MySqlKit / SQLiteKit、CRUDBase、transactional |
|
|
117
|
+
| `kit.ai` | AI 服务池:chat / embedding / rerank / image,多厂商候选链 + 故障转移 |
|
|
118
|
+
| `kit.mcp_kit` | 通用 MCP 客户端(streamable-http / sse,档案驱动多服务器) |
|
|
119
|
+
| `kit.redis_kit` | Redis 工具 |
|
|
120
|
+
| `kit.file_kit` | 文件工具 |
|
|
121
|
+
| `kit.doc_kit` | 文档转换(基于 firecrawl-anydoc / pimd) |
|
|
122
|
+
| `kit.zvec_kit` | zvec 本地向量库 |
|
|
123
|
+
| `kit.mineru_kit` | MinerU 文档精准解析 API |
|
|
124
|
+
| `kit.common_kit` | 通用工具 |
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
sdpy_kit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# sdpy-kit
|
|
2
|
+
|
|
3
|
+
SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:统一数据库访问层(PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致)+ AI 多模型服务池(chat / embedding / rerank / image,多厂商候选链与故障转移)+ MCP 客户端 + Redis / 文件 / 文档 / 向量库 / MinerU 工具。
|
|
4
|
+
|
|
5
|
+
> 要求 Python 3.13+
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install sdpy-kit
|
|
11
|
+
# 或
|
|
12
|
+
uv add sdpy-kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 配置(.env)
|
|
16
|
+
|
|
17
|
+
kit 通过 pydantic-settings 读取项目根目录 `.env`(默认值见下,均可省略):
|
|
18
|
+
|
|
19
|
+
```dotenv
|
|
20
|
+
# 日志
|
|
21
|
+
LOG_LEVEL=INFO
|
|
22
|
+
LOG_TO_CONSOLE=true
|
|
23
|
+
LOG_TO_FILE=false
|
|
24
|
+
|
|
25
|
+
# 数据库(三选一,切换只改 DEFAULT_DB_TYPE)
|
|
26
|
+
DEFAULT_DB_TYPE=sqlite # postgresql / mysql / sqlite
|
|
27
|
+
DB_HOST=localhost
|
|
28
|
+
DB_PORT=5432
|
|
29
|
+
DB_DATABASE=postgres
|
|
30
|
+
DB_USER=admin
|
|
31
|
+
DB_PASSWORD=
|
|
32
|
+
MYSQL_PORT=3306
|
|
33
|
+
MYSQL_DATABASE=mydb
|
|
34
|
+
MYSQL_USER=root
|
|
35
|
+
SQLITE_DB_PATH=./data/app.db
|
|
36
|
+
|
|
37
|
+
# Redis
|
|
38
|
+
REDIS_HOST=localhost
|
|
39
|
+
REDIS_PORT=6379
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## AI / MCP 功能的档案文件
|
|
43
|
+
|
|
44
|
+
AI 与 MCP 功能需要在项目根目录提供两个 JSON 档案文件:
|
|
45
|
+
|
|
46
|
+
- `config/ai_models.json` —— chat / embedding / rerank / image 四类型多厂商模型档案
|
|
47
|
+
- `config/mcp_servers.json` —— MCP 服务器档案
|
|
48
|
+
|
|
49
|
+
文件缺失时,首次使用相应功能会报错,**报错信息中附带可直接复制的最小模板**;
|
|
50
|
+
其中 `api_key` / `token_env` 字段填写环境变量名,真实密钥配置在 `.env`。
|
|
51
|
+
|
|
52
|
+
## 快速开始
|
|
53
|
+
|
|
54
|
+
### 日志与配置
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from kit.config import get_settings, get_default_logger
|
|
58
|
+
|
|
59
|
+
settings = get_settings()
|
|
60
|
+
logger = get_default_logger(__name__)
|
|
61
|
+
logger.info("hello %s", settings.APP_NAME)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 数据库(SQLiteKit / PgKit / MySqlKit API 完全一致,切换只改 import)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from sqlmodel import SQLModel, Field
|
|
68
|
+
from kit.db.sqlite_kit import SQLiteKit
|
|
69
|
+
|
|
70
|
+
class Item(SQLModel, table=True):
|
|
71
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
72
|
+
name: str
|
|
73
|
+
|
|
74
|
+
SQLiteKit.connect()
|
|
75
|
+
SQLiteKit.create_tables([Item])
|
|
76
|
+
item = SQLiteKit.create(Item, {"name": "demo"})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
约定:`SQLiteKit/PgKit/MySqlKit` 方法自动提交;`CRUDBase`(配合 `session_scope`)不自动提交;事务用 `kit.transactional`。
|
|
80
|
+
|
|
81
|
+
## 模块总览
|
|
82
|
+
|
|
83
|
+
| 模块 | 内容 |
|
|
84
|
+
|------|------|
|
|
85
|
+
| `kit.db` | 统一数据库访问层:PgKit / MySqlKit / SQLiteKit、CRUDBase、transactional |
|
|
86
|
+
| `kit.ai` | AI 服务池:chat / embedding / rerank / image,多厂商候选链 + 故障转移 |
|
|
87
|
+
| `kit.mcp_kit` | 通用 MCP 客户端(streamable-http / sse,档案驱动多服务器) |
|
|
88
|
+
| `kit.redis_kit` | Redis 工具 |
|
|
89
|
+
| `kit.file_kit` | 文件工具 |
|
|
90
|
+
| `kit.doc_kit` | 文档转换(基于 firecrawl-anydoc / pimd) |
|
|
91
|
+
| `kit.zvec_kit` | zvec 本地向量库 |
|
|
92
|
+
| `kit.mineru_kit` | MinerU 文档精准解析 API |
|
|
93
|
+
| `kit.common_kit` | 通用工具 |
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2026-08-23
|
|
21
|
+
"""Kit 模块——统一数据库访问层 + 工具集
|
|
22
|
+
|
|
23
|
+
数据库访问通过 kit.db 子包提供(基于 SQLModel)。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
from kit.db import (
|
|
29
|
+
MYSQL,
|
|
30
|
+
POSTGRESQL,
|
|
31
|
+
SQLITE,
|
|
32
|
+
CRUDBase,
|
|
33
|
+
DBError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
close_all_engines,
|
|
36
|
+
close_engine,
|
|
37
|
+
create_tables,
|
|
38
|
+
get_engine,
|
|
39
|
+
get_session,
|
|
40
|
+
list_tables,
|
|
41
|
+
session_scope,
|
|
42
|
+
table_exists,
|
|
43
|
+
transactional,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# 引擎管理
|
|
48
|
+
"get_engine",
|
|
49
|
+
"create_tables",
|
|
50
|
+
"list_tables",
|
|
51
|
+
"table_exists",
|
|
52
|
+
"close_all_engines",
|
|
53
|
+
"close_engine",
|
|
54
|
+
# 常量
|
|
55
|
+
"POSTGRESQL",
|
|
56
|
+
"MYSQL",
|
|
57
|
+
"SQLITE",
|
|
58
|
+
# 会话
|
|
59
|
+
"session_scope",
|
|
60
|
+
"get_session",
|
|
61
|
+
# 异常
|
|
62
|
+
"DBError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
# CRUD
|
|
65
|
+
"CRUDBase",
|
|
66
|
+
# 事务
|
|
67
|
+
"transactional",
|
|
68
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2026-08-21
|
|
21
|
+
"""kit/ai - AI 模型服务模块(纯异步,Python 3.13+)
|
|
22
|
+
|
|
23
|
+
提供开箱即用的各类模型基础服务:
|
|
24
|
+
- ChatService: 对话模型
|
|
25
|
+
- EmbeddingService: 文本嵌入向量
|
|
26
|
+
- RerankService: 重排序
|
|
27
|
+
- ImageService: 文生图
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from kit.ai.chat_service import ChatRequest, ChatService
|
|
31
|
+
from kit.ai.embedding_service import EmbeddingRequest, EmbeddingService
|
|
32
|
+
from kit.ai.image_service import ImageRequest, ImageService
|
|
33
|
+
from kit.ai.model_builder import ModelBuilder
|
|
34
|
+
from kit.ai.rerank_service import RerankHit, RerankRequest, RerankService
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ChatRequest",
|
|
38
|
+
"ChatService",
|
|
39
|
+
"EmbeddingRequest",
|
|
40
|
+
"EmbeddingService",
|
|
41
|
+
"ImageRequest",
|
|
42
|
+
"ImageService",
|
|
43
|
+
"ModelBuilder",
|
|
44
|
+
"RerankHit",
|
|
45
|
+
"RerankRequest",
|
|
46
|
+
"RerankService",
|
|
47
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from typing import Self, override
|
|
22
|
+
|
|
23
|
+
from kit.ai.exceptions import AIResponseError
|
|
24
|
+
from kit.ai.model_client import ModelClient
|
|
25
|
+
from kit.ai.profiles import ModelProfile
|
|
26
|
+
from kit.ai.types import JSONObject, JSONValue
|
|
27
|
+
|
|
28
|
+
__all__ = ["BaseAIService", "safe_get"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def safe_get(data: JSONValue, *path: str | int) -> JSONValue:
|
|
32
|
+
"""按层级路径安全取值,支持 dict 键(str)与 list 索引(int)。
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
AIResponseError: 路径任一环节缺失或类型不符。
|
|
36
|
+
"""
|
|
37
|
+
cur: JSONValue = data
|
|
38
|
+
for key in path:
|
|
39
|
+
if isinstance(key, int):
|
|
40
|
+
if not isinstance(cur, list) or key < 0 or key >= len(cur):
|
|
41
|
+
raise AIResponseError(f"响应缺少索引 {path}: {str(data)[:200]}")
|
|
42
|
+
cur = cur[key]
|
|
43
|
+
else:
|
|
44
|
+
if not isinstance(cur, dict) or key not in cur:
|
|
45
|
+
raise AIResponseError(
|
|
46
|
+
f"响应缺少字段 {'.'.join(map(str, path))}: {str(data)[:200]}"
|
|
47
|
+
)
|
|
48
|
+
cur = cur[key]
|
|
49
|
+
return cur
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BaseAIService[RequestT, ResponseT](ABC):
|
|
53
|
+
"""AI 服务基类(模板方法 + 泛型)。
|
|
54
|
+
|
|
55
|
+
子类实现同步的 ``_build_payload``(请求模型→JSON 请求体)与
|
|
56
|
+
``_parse_response``(JSON 响应体→业务结果),公共的「构造请求、
|
|
57
|
+
发送、解析」由 ``_call`` 统一编排。请求类型 ``RequestT`` 与结果
|
|
58
|
+
类型 ``ResponseT`` 由泛型绑定,保证覆写签名一致、类型自洽。
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, profile: ModelProfile) -> None:
|
|
62
|
+
self._profile: ModelProfile = profile
|
|
63
|
+
self._client: ModelClient = ModelClient(profile)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def profile(self) -> ModelProfile:
|
|
67
|
+
return self._profile
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def _build_payload(self, request: RequestT) -> JSONObject:
|
|
71
|
+
"""将请求模型构造为 JSON 请求体。"""
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def _parse_response(self, data: JSONObject) -> ResponseT:
|
|
75
|
+
"""将 JSON 响应体解析为业务结果。"""
|
|
76
|
+
|
|
77
|
+
async def _call(self, request: RequestT) -> ResponseT:
|
|
78
|
+
"""统一编排:构造请求体 → 发送 → 解析响应。"""
|
|
79
|
+
payload = self._build_payload(request)
|
|
80
|
+
data = await self._client.post_json(payload)
|
|
81
|
+
return self._parse_response(data)
|
|
82
|
+
|
|
83
|
+
async def close(self) -> None:
|
|
84
|
+
await self._client.close()
|
|
85
|
+
|
|
86
|
+
async def __aenter__(self) -> Self:
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
async def __aexit__(self, *exc_info: object) -> None:
|
|
90
|
+
await self.close()
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
def __str__(self) -> str:
|
|
94
|
+
return f"{type(self).__name__}({self._profile.name})"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from collections.abc import AsyncIterator
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import cast, override
|
|
23
|
+
|
|
24
|
+
from kit.ai.base_service import BaseAIService, safe_get
|
|
25
|
+
from kit.ai.exceptions import AIResponseError
|
|
26
|
+
from kit.ai.types import JSONObject
|
|
27
|
+
from kit.config.log_config import get_default_logger
|
|
28
|
+
|
|
29
|
+
__all__ = ["ChatRequest", "ChatService"]
|
|
30
|
+
|
|
31
|
+
logger = get_default_logger(__name__)
|
|
32
|
+
|
|
33
|
+
Message = dict[str, str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class ChatRequest:
|
|
38
|
+
"""对话请求模型。"""
|
|
39
|
+
|
|
40
|
+
messages: list[Message]
|
|
41
|
+
temperature: float | None = None
|
|
42
|
+
max_tokens: int | None = None
|
|
43
|
+
stream: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChatService(BaseAIService[ChatRequest, str]):
|
|
47
|
+
"""对话类模型服务(全异步)。"""
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def _build_payload(self, request: ChatRequest) -> JSONObject:
|
|
51
|
+
payload: dict[str, object] = {
|
|
52
|
+
"model": self._profile.model,
|
|
53
|
+
"messages": request.messages,
|
|
54
|
+
"stream": request.stream,
|
|
55
|
+
}
|
|
56
|
+
if request.temperature is not None:
|
|
57
|
+
payload["temperature"] = request.temperature
|
|
58
|
+
if request.max_tokens is not None:
|
|
59
|
+
payload["max_tokens"] = request.max_tokens
|
|
60
|
+
return cast(JSONObject, payload)
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
def _parse_response(self, data: JSONObject) -> str:
|
|
64
|
+
content = safe_get(data, "choices", 0, "message", "content")
|
|
65
|
+
if not isinstance(content, str):
|
|
66
|
+
raise AIResponseError(f"content 类型异常: {type(content).__name__}")
|
|
67
|
+
return content
|
|
68
|
+
|
|
69
|
+
async def ask(
|
|
70
|
+
self,
|
|
71
|
+
messages: list[Message],
|
|
72
|
+
*,
|
|
73
|
+
temperature: float | None = None,
|
|
74
|
+
max_tokens: int | None = None,
|
|
75
|
+
) -> str:
|
|
76
|
+
request = ChatRequest(messages, temperature=temperature, max_tokens=max_tokens)
|
|
77
|
+
return await self._call(request)
|
|
78
|
+
|
|
79
|
+
async def complete(
|
|
80
|
+
self,
|
|
81
|
+
prompt: str,
|
|
82
|
+
*,
|
|
83
|
+
temperature: float | None = None,
|
|
84
|
+
max_tokens: int | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
messages: list[Message] = [{"role": "user", "content": prompt}]
|
|
87
|
+
return await self.ask(messages, temperature=temperature, max_tokens=max_tokens)
|
|
88
|
+
|
|
89
|
+
async def stream(
|
|
90
|
+
self,
|
|
91
|
+
messages: list[Message],
|
|
92
|
+
*,
|
|
93
|
+
temperature: float | None = None,
|
|
94
|
+
max_tokens: int | None = None,
|
|
95
|
+
) -> AsyncIterator[str]:
|
|
96
|
+
request = ChatRequest(
|
|
97
|
+
messages, temperature=temperature, max_tokens=max_tokens, stream=True
|
|
98
|
+
)
|
|
99
|
+
payload = self._build_payload(request)
|
|
100
|
+
async for parsed in self._client.stream_json(payload):
|
|
101
|
+
if parsed is None:
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
delta = safe_get(parsed, "choices", 0, "delta", "content")
|
|
105
|
+
except AIResponseError:
|
|
106
|
+
# 流式结束帧等无 delta 的正常帧,跳过
|
|
107
|
+
logger.debug("流式帧缺少 delta.content,跳过")
|
|
108
|
+
continue
|
|
109
|
+
if isinstance(delta, str) and delta:
|
|
110
|
+
yield delta
|