sdpy-kit 0.2.5__tar.gz → 0.3.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.2.5 → sdpy_kit-0.3.0}/PKG-INFO +15 -2
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/README.md +9 -1
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/__init__.py +1 -1
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/config/kit_config.py +20 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/base_db_kit.py +38 -11
- sdpy_kit-0.3.0/kit/laya_kit.py +302 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/pyproject.toml +7 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/.gitignore +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/base_service.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/chat_service.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/profiles.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/runner.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/ai/types.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/anydoc_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/common_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/__init__.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/crud.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/engine.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/query.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/session.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/sqlite_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/docx_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/file_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/marker_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/mcp_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/md2doc_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/mineru_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/wx_article_kit.py +0 -0
- {sdpy_kit-0.2.5 → sdpy_kit-0.3.0}/kit/zvec_kit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sdpy-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
|
|
5
5
|
Author-email: Clark <changhongyuan@126.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,8 @@ Requires-Dist: docxtpl>=0.20.2; extra == 'all'
|
|
|
29
29
|
Requires-Dist: fastmcp>=4.1.0; extra == 'all'
|
|
30
30
|
Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
|
|
31
31
|
Requires-Dist: httpx>=0.28.1; extra == 'all'
|
|
32
|
+
Requires-Dist: huggingface-hub<2,>=1.5; extra == 'all'
|
|
33
|
+
Requires-Dist: laya>=0.4.6; extra == 'all'
|
|
32
34
|
Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
|
|
33
35
|
Requires-Dist: marker-pdf[full]>=2.0.0; extra == 'all'
|
|
34
36
|
Requires-Dist: numpy>=2.5.4; extra == 'all'
|
|
@@ -46,6 +48,9 @@ Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
|
|
|
46
48
|
Requires-Dist: pikepdf>=10.17.0; extra == 'anydoc'
|
|
47
49
|
Provides-Extra: docx
|
|
48
50
|
Requires-Dist: docxtpl>=0.20.2; extra == 'docx'
|
|
51
|
+
Provides-Extra: laya
|
|
52
|
+
Requires-Dist: huggingface-hub<2,>=1.5; extra == 'laya'
|
|
53
|
+
Requires-Dist: laya>=0.4.6; extra == 'laya'
|
|
49
54
|
Provides-Extra: marker
|
|
50
55
|
Requires-Dist: marker-pdf[full]>=2.0.0; extra == 'marker'
|
|
51
56
|
Provides-Extra: mcp
|
|
@@ -512,7 +517,15 @@ PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
|
|
|
512
517
|
> 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
|
|
513
518
|
> `insert_vectors` 支持二维 numpy 数组与批量提交。
|
|
514
519
|
|
|
515
|
-
#### 0.2.
|
|
520
|
+
#### 0.2.6 变更
|
|
521
|
+
|
|
522
|
+
| 编号 | 变更 | 影响 |
|
|
523
|
+
|------|------|------|
|
|
524
|
+
| D1 | `prepare_statement` 的 `params` 支持**参数行列表**(`list[dict]`):**原样返回该列表** → `session.execute(*prepare_statement(sql, params))` 即批量执行(executemany),与 `build_insert_vectors` 的返回形态直接对接;行键集不一致或行值含序列 → 本地可操作 `ValueError`(与 `execute` 批量形态**同一规则**) | 向后兼容:单字典 / `None` 形态行为不变;行列表形态**不做** `IN` 展开(与批量互斥) |
|
|
525
|
+
| D1 | 文档:修正「自有事务」示例——0.2.5 的示例把 `build_insert_vectors` 返回的**行列表**传给了单字典形态,照抄会抛 `AttributeError: 'list' object has no attribute 'items'` | **纯文档缺陷修正**(0.2.5 示例照抄即崩) |
|
|
526
|
+
| — | 文档:批量执行补充 `rowcount` 备注——忽略型写入(如 SQLite `INSERT OR IGNORE`)的冲突行不计入 `rowcount`(即 `rowcount` = 实际插入数;⚠ 仅 SQLite 实测) | 文档 |
|
|
527
|
+
|
|
528
|
+
#### 0.2.5 变更
|
|
516
529
|
|
|
517
530
|
| 编号 | 变更 | 影响 |
|
|
518
531
|
|------|------|------|
|
|
@@ -442,7 +442,15 @@ PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
|
|
|
442
442
|
> 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
|
|
443
443
|
> `insert_vectors` 支持二维 numpy 数组与批量提交。
|
|
444
444
|
|
|
445
|
-
#### 0.2.
|
|
445
|
+
#### 0.2.6 变更
|
|
446
|
+
|
|
447
|
+
| 编号 | 变更 | 影响 |
|
|
448
|
+
|------|------|------|
|
|
449
|
+
| D1 | `prepare_statement` 的 `params` 支持**参数行列表**(`list[dict]`):**原样返回该列表** → `session.execute(*prepare_statement(sql, params))` 即批量执行(executemany),与 `build_insert_vectors` 的返回形态直接对接;行键集不一致或行值含序列 → 本地可操作 `ValueError`(与 `execute` 批量形态**同一规则**) | 向后兼容:单字典 / `None` 形态行为不变;行列表形态**不做** `IN` 展开(与批量互斥) |
|
|
450
|
+
| D1 | 文档:修正「自有事务」示例——0.2.5 的示例把 `build_insert_vectors` 返回的**行列表**传给了单字典形态,照抄会抛 `AttributeError: 'list' object has no attribute 'items'` | **纯文档缺陷修正**(0.2.5 示例照抄即崩) |
|
|
451
|
+
| — | 文档:批量执行补充 `rowcount` 备注——忽略型写入(如 SQLite `INSERT OR IGNORE`)的冲突行不计入 `rowcount`(即 `rowcount` = 实际插入数;⚠ 仅 SQLite 实测) | 文档 |
|
|
452
|
+
|
|
453
|
+
#### 0.2.5 变更
|
|
446
454
|
|
|
447
455
|
| 编号 | 变更 | 影响 |
|
|
448
456
|
|------|------|------|
|
|
@@ -142,6 +142,26 @@ class Settings(BaseSettings):
|
|
|
142
142
|
description="llama-server 可执行文件路径(None 时沿用系统 LLAMA_CPP_BINARY)",
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
+
# Laya 决策引擎配置(本地小模型,把任意文本变成 choice/score/noul 类型化决策)
|
|
146
|
+
LAYA_MODEL: str = Field(
|
|
147
|
+
default="multilingual",
|
|
148
|
+
description=(
|
|
149
|
+
"生效检查点:multilingual(多语言含中文)/ english / typed-decisions"
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
LAYA_DEVICE: str | None = Field(
|
|
153
|
+
default=None,
|
|
154
|
+
description="推理设备(如 cpu / cuda),None 表示交给 laya 自动选择",
|
|
155
|
+
)
|
|
156
|
+
LAYA_HF_ENDPOINT: str = Field(
|
|
157
|
+
default="https://hf-mirror.com",
|
|
158
|
+
description="检查点权重下载入口(默认国内镜像),置空则使用 HuggingFace 官方源",
|
|
159
|
+
)
|
|
160
|
+
LAYA_HF_DISABLE_XET: bool = Field(
|
|
161
|
+
default=True,
|
|
162
|
+
description="禁用 HF Xet 存储后端(国内镜像不支持,开启会导致权重下载挂起)",
|
|
163
|
+
)
|
|
164
|
+
|
|
145
165
|
class Config:
|
|
146
166
|
env_file = ".env"
|
|
147
167
|
env_file_encoding = "utf-8"
|
|
@@ -202,14 +202,24 @@ _EXPANDING_TYPES = (list, tuple, set, frozenset)
|
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
def prepare_statement(
|
|
205
|
-
sql: str, params: FieldData | None = None
|
|
206
|
-
) -> tuple[TextClause, FieldData | None]:
|
|
207
|
-
"""把 SQL 文本 +
|
|
205
|
+
sql: str, params: FieldData | builtins.list[FieldData] | None = None
|
|
206
|
+
) -> tuple[TextClause, FieldData | builtins.list[FieldData] | None]:
|
|
207
|
+
"""把 SQL 文本 + 参数转为(可直接执行的语句, 参数)
|
|
208
208
|
|
|
209
209
|
供调用方在**自有事务**(`*Kit.transaction()`)内执行原生 SQL,
|
|
210
|
-
与 ``query`` / ``execute``
|
|
211
|
-
``session.execute(*prepare_statement(sql, params))
|
|
212
|
-
|
|
210
|
+
与 ``query`` / ``execute`` 使用**同一套参数规则**(单一事实源)——
|
|
211
|
+
单字典形态下后两者等价于 ``session.execute(*prepare_statement(sql, params))``;
|
|
212
|
+
本函数**不建连接、不改状态、不执行**。
|
|
213
|
+
|
|
214
|
+
两种参数形态(与 ``execute`` 对称):
|
|
215
|
+
|
|
216
|
+
- **单字典**:值为 ``list`` / ``tuple`` / ``set`` / ``frozenset`` 的键按
|
|
217
|
+
SQLAlchemy **expanding** 语义绑定,可直接书写 ``WHERE id IN :ids``
|
|
218
|
+
(三库通用);第二项为归一化后的参数字典;
|
|
219
|
+
- **字典列表**(批量 / executemany):第二项**原样返回该列表**,语句为
|
|
220
|
+
裸 ``text(sql)``(**不做** ``IN`` 展开——批量与展开参数互斥,与
|
|
221
|
+
``execute`` 的批量约束一致);经 ``_validate_batch_rows`` 本地预校验
|
|
222
|
+
(各行键集一致、行值为标量)。
|
|
213
223
|
|
|
214
224
|
典型用法(调用方**无须 import sqlalchemy**)::
|
|
215
225
|
|
|
@@ -221,7 +231,13 @@ def prepare_statement(
|
|
|
221
231
|
with SQLiteKit.transaction() as session:
|
|
222
232
|
session.execute(stmt, bound) # 第二个值必须一并传入
|
|
223
233
|
|
|
224
|
-
|
|
234
|
+
stmt, rows = prepare_statement( # 批量(executemany)
|
|
235
|
+
"INSERT INTO t (a, b) VALUES (:a, :b)", [{"a": 1, "b": 2}]
|
|
236
|
+
)
|
|
237
|
+
with SQLiteKit.transaction() as session:
|
|
238
|
+
session.execute(stmt, rows)
|
|
239
|
+
|
|
240
|
+
三条实现约束(**单字典形态**,与 ``query`` / ``execute`` 现有语义完全一致):
|
|
225
241
|
|
|
226
242
|
1. 只对**确实出现在 SQL 文本中**的参数名做 expanding —— 否则传入"未使用的
|
|
227
243
|
序列参数"会触发 SQLAlchemy 的 ``ArgumentError: This text() construct
|
|
@@ -233,20 +249,31 @@ def prepare_statement(
|
|
|
233
249
|
|
|
234
250
|
Args:
|
|
235
251
|
sql: SQL 语句(使用命名参数 ``:param``)
|
|
236
|
-
params:
|
|
237
|
-
|
|
252
|
+
params: **单个参数字典**(值为 ``list`` / ``tuple`` / ``set`` / ``frozenset``
|
|
253
|
+
的键按 ``IN`` 展开,三库通用)或**参数字典的列表**(批量,行值须为标量)
|
|
238
254
|
|
|
239
255
|
Returns:
|
|
240
|
-
``(语句,
|
|
241
|
-
``
|
|
256
|
+
``(语句, 参数)``:执行时**必须**把第二个值一并传给 ``session.execute``;
|
|
257
|
+
``params`` 为 None 时第二项即 None,为列表时**原样返回该列表**
|
|
242
258
|
|
|
243
259
|
Note:
|
|
244
260
|
``:name::type`` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 ``text()``
|
|
245
261
|
识别**(该写法在支持本函数之前同样不可用),请改用 ``CAST(:name AS type)``。
|
|
246
262
|
|
|
263
|
+
Raises:
|
|
264
|
+
ValueError: 列表形态的行键集不一致或行值含序列类型(来自
|
|
265
|
+
``_validate_batch_rows``,与 ``execute`` 批量形态同一规则)
|
|
266
|
+
|
|
247
267
|
See Also:
|
|
248
268
|
``query`` / ``execute``:kit 自管事务的原生 SQL 入口(同样支持列表参数)。
|
|
249
269
|
"""
|
|
270
|
+
if isinstance(params, builtins.list):
|
|
271
|
+
# 批量形态:语句不做 IN 展开,参数行列表原样返回(由 execute/session
|
|
272
|
+
# 以 executemany 执行);空列表不校验、不报错(构造期无副作用)
|
|
273
|
+
if params:
|
|
274
|
+
_validate_batch_rows(params)
|
|
275
|
+
return text(sql), params
|
|
276
|
+
|
|
250
277
|
statement = text(sql)
|
|
251
278
|
if not params:
|
|
252
279
|
return statement, params
|
|
@@ -0,0 +1,302 @@
|
|
|
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-10-13
|
|
21
|
+
"""laya 决策引擎工具类,把任意文本变成类型化决策
|
|
22
|
+
|
|
23
|
+
laya(https://github.com/NandhaKishorM/laya)是单次前向传播的类型化决策引擎
|
|
24
|
+
(多语言、非自回归),本工具类封装它的三类决策原语:
|
|
25
|
+
|
|
26
|
+
- `choice(state, instructions, criteria)`:候选标签里选一个,`decision` 为选中标签;
|
|
27
|
+
- `score(state, instructions, criteria)`:序数标尺上评分,`decision` 为期望值;
|
|
28
|
+
- `noul(state, instructions)`:判断是否成立,`decision` 为 P(true)(0.0-1.0)。
|
|
29
|
+
|
|
30
|
+
三者都返回 `{decision, answer_confidence, probabilities}`——判断结果 + 置信度;
|
|
31
|
+
需要完整字段(`routing` / `usage`)时用 `predict()` + `brief()`。
|
|
32
|
+
|
|
33
|
+
关键用法(实测得出,务必遵守):
|
|
34
|
+
|
|
35
|
+
- 判断规则必须写进 `instructions`,只靠 `criteria` 描述做语义推断会判错;
|
|
36
|
+
- 候选标签的文字本身会改变输出分布(中文标签与英文标签结果不同);
|
|
37
|
+
- **措辞敏感**:同一意思换同义词会明显改变 `answer_confidence`(如「退款」→「退费」
|
|
38
|
+
使置信度由 0.92 降到 0.57),因此**不要用绝对置信度做门控阈值**;需要稳定请在
|
|
39
|
+
业务侧做输入归一化——本工具类**不改写任何文本**;
|
|
40
|
+
- 上游检查点温度标定无效,置信度未经标定,仅供参考。
|
|
41
|
+
|
|
42
|
+
配置走 `kit.config.Settings`(`.env` / 环境变量):`LAYA_MODEL`(检查点)、
|
|
43
|
+
`LAYA_DEVICE`(推理设备)、`LAYA_HF_ENDPOINT`(权重下载入口)、`LAYA_HF_DISABLE_XET`。
|
|
44
|
+
**HF 相关环境变量必须在导入本模块前生效**(本模块导入即设置并加载 laya),
|
|
45
|
+
如需自定义请在 `.env` 或进程启动前设置。
|
|
46
|
+
|
|
47
|
+
用法:
|
|
48
|
+
from kit.laya_kit import LayaKit
|
|
49
|
+
|
|
50
|
+
result = LayaKit.choice(
|
|
51
|
+
state="我们三月份被重复扣费了两次,请把重复的那笔退款。",
|
|
52
|
+
instructions="这封来信应该由哪个部门处理?",
|
|
53
|
+
criteria={"账务": "账单、付款、退款", "技术": "故障、报错"},
|
|
54
|
+
)
|
|
55
|
+
print(result["decision"], result["answer_confidence"])
|
|
56
|
+
|
|
57
|
+
说明:本模块为可选依赖,需安装 `pip install "sdpy-kit[laya]"`;首次使用会下载
|
|
58
|
+
检查点权重(322M-421M 参数),模型常驻内存,用完可 `LayaKit.unload()` 释放。
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
import os
|
|
62
|
+
import threading
|
|
63
|
+
from typing import Any
|
|
64
|
+
|
|
65
|
+
from kit.config.kit_config import get_settings
|
|
66
|
+
from kit.config.log_config import get_default_logger
|
|
67
|
+
|
|
68
|
+
logger = get_default_logger(__name__)
|
|
69
|
+
|
|
70
|
+
_settings = get_settings()
|
|
71
|
+
|
|
72
|
+
# HuggingFace 相关环境必须在导入 laya / huggingface_hub 之前设置,否则不生效。
|
|
73
|
+
# 统一使用 setdefault:仅当用户未显式设置环境变量时生效,不覆盖显式配置。
|
|
74
|
+
if _settings.LAYA_HF_ENDPOINT:
|
|
75
|
+
os.environ.setdefault("HF_ENDPOINT", _settings.LAYA_HF_ENDPOINT)
|
|
76
|
+
if _settings.LAYA_HF_DISABLE_XET:
|
|
77
|
+
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
import laya
|
|
81
|
+
except ImportError as e:
|
|
82
|
+
raise ImportError(
|
|
83
|
+
"laya_kit 需要可选依赖,请安装:pip install sdpy-kit[laya]"
|
|
84
|
+
) from e
|
|
85
|
+
|
|
86
|
+
# 单问题封装使用的内部问题 id(调用方无需关心)
|
|
87
|
+
_QID: str = "answer"
|
|
88
|
+
|
|
89
|
+
# 决策原语的取值键(与 laya 返回体一致,按优先级取第一个存在的)
|
|
90
|
+
_DECISION_KEYS: tuple[str, ...] = ("choice", "score", "noul")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LayaError(Exception):
|
|
94
|
+
"""laya 决策异常(模型加载失败 / 推理失败 / 返回体不符合预期)"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LayaKit:
|
|
98
|
+
"""laya 决策引擎工具类(无状态,方法均为 `classmethod`)
|
|
99
|
+
|
|
100
|
+
Router 为类级单例(模型加载开销极大,必须复用),配置统一走
|
|
101
|
+
`kit.config.Settings` 的 `LAYA_*` 配置项;**不改写任何输入文本**。
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
_router: Any = None
|
|
105
|
+
_router_lock: threading.Lock = threading.Lock()
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def router(cls) -> Any:
|
|
109
|
+
"""惰性创建并缓存 Router(进程内单例,线程安全)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Any: laya.Router 实例
|
|
113
|
+
"""
|
|
114
|
+
with cls._router_lock:
|
|
115
|
+
if cls._router is None:
|
|
116
|
+
kwargs: dict[str, Any] = {"default": _settings.LAYA_MODEL}
|
|
117
|
+
if _settings.LAYA_DEVICE:
|
|
118
|
+
kwargs["device"] = _settings.LAYA_DEVICE
|
|
119
|
+
logger.info(
|
|
120
|
+
f"加载 laya Router:检查点={_settings.LAYA_MODEL},"
|
|
121
|
+
f"设备={_settings.LAYA_DEVICE or 'auto'}"
|
|
122
|
+
)
|
|
123
|
+
cls._router = laya.Router(**kwargs)
|
|
124
|
+
return cls._router
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def route(
|
|
128
|
+
cls, state: str | dict[str, Any], questions: dict[str, Any] | None = None
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
"""纯路由决策(不加载检查点、不下载权重,可离线)
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
state: 任意文本或结构化 state
|
|
134
|
+
questions: 可选问题定义(None 时只做语言/脚本探测)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict[str, Any]: 路由结果,含 `model` / `reason` / `detection`
|
|
138
|
+
"""
|
|
139
|
+
decision = cls.router().route(state, questions)
|
|
140
|
+
if isinstance(decision, dict):
|
|
141
|
+
return decision
|
|
142
|
+
raw = getattr(decision, "__dict__", None)
|
|
143
|
+
if isinstance(raw, dict):
|
|
144
|
+
return raw
|
|
145
|
+
return {
|
|
146
|
+
"model": getattr(decision, "model", None),
|
|
147
|
+
"reason": getattr(decision, "reason", None),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def predict(
|
|
152
|
+
cls,
|
|
153
|
+
state: str | dict[str, Any],
|
|
154
|
+
questions: dict[str, Any],
|
|
155
|
+
*,
|
|
156
|
+
min_confidence: float | None = None,
|
|
157
|
+
) -> dict[str, Any]:
|
|
158
|
+
"""多问题一次前向传播,返回 laya 原始结果(`answers` / `routing` / `usage`)
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
state: 任意文本或结构化 state
|
|
162
|
+
questions: 问题定义字典(choice / score / noul 可混合)
|
|
163
|
+
min_confidence: 置信度门控阈值,低于该值会在答案里标记 low_confidence;
|
|
164
|
+
注意上游置信度未标定(见模块 docstring),阈值需自行标定
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
dict[str, Any]: laya 原始返回(字段最全,便于调用方自行取用)
|
|
168
|
+
"""
|
|
169
|
+
kwargs: dict[str, Any] = {"model": _settings.LAYA_MODEL}
|
|
170
|
+
if min_confidence is not None:
|
|
171
|
+
kwargs["min_confidence"] = min_confidence
|
|
172
|
+
return cls.router().predict(state, questions, **kwargs)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def brief(cls, raw: dict[str, Any]) -> dict[str, Any]:
|
|
176
|
+
"""把 predict 原始结果压成「判断结果 + 置信度」两部分
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
raw: `predict()` 的原始返回
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
dict[str, Any]: 问题 id → {decision, answer_confidence, probabilities}
|
|
183
|
+
"""
|
|
184
|
+
brief: dict[str, Any] = {}
|
|
185
|
+
for qid, answer in (raw.get("answers") or {}).items():
|
|
186
|
+
if not isinstance(answer, dict):
|
|
187
|
+
brief[qid] = answer
|
|
188
|
+
continue
|
|
189
|
+
decision = next(
|
|
190
|
+
(answer[key] for key in _DECISION_KEYS if key in answer),
|
|
191
|
+
None,
|
|
192
|
+
)
|
|
193
|
+
brief[qid] = {
|
|
194
|
+
"decision": decision,
|
|
195
|
+
"answer_confidence": answer.get("answer_confidence"),
|
|
196
|
+
"probabilities": answer.get("probabilities"),
|
|
197
|
+
}
|
|
198
|
+
return brief
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def choice(
|
|
202
|
+
cls,
|
|
203
|
+
state: str | dict[str, Any],
|
|
204
|
+
instructions: str,
|
|
205
|
+
criteria: dict[str, str],
|
|
206
|
+
*,
|
|
207
|
+
min_confidence: float | None = None,
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
"""单选题:在 `criteria` 给出的候选标签里选一个
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
state: 待判断的文本或结构化 state
|
|
213
|
+
instructions: 判断规则与问题——**规则写这里**,不要只写在 criteria 描述里
|
|
214
|
+
criteria: 候选标签 → 该标签的说明文字
|
|
215
|
+
min_confidence: 置信度门控阈值
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
dict[str, Any]: {decision: 选中标签, answer_confidence, probabilities}
|
|
219
|
+
"""
|
|
220
|
+
question = {
|
|
221
|
+
"type": "choice",
|
|
222
|
+
"instructions": instructions,
|
|
223
|
+
"criteria": criteria,
|
|
224
|
+
}
|
|
225
|
+
return cls._ask(question, state, min_confidence)
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def score(
|
|
229
|
+
cls,
|
|
230
|
+
state: str | dict[str, Any],
|
|
231
|
+
instructions: str,
|
|
232
|
+
criteria: list[str],
|
|
233
|
+
*,
|
|
234
|
+
min_confidence: float | None = None,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""评分题:在 `criteria` 给出的序数标尺上给出期望值
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
state: 待判断的文本或结构化 state
|
|
240
|
+
instructions: 判断规则与问题
|
|
241
|
+
criteria: 从低到高的等级描述列表
|
|
242
|
+
min_confidence: 置信度门控阈值
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
dict[str, Any]: {decision: 序数期望值, answer_confidence, probabilities}
|
|
246
|
+
"""
|
|
247
|
+
question = {
|
|
248
|
+
"type": "score",
|
|
249
|
+
"instructions": instructions,
|
|
250
|
+
"criteria": criteria,
|
|
251
|
+
}
|
|
252
|
+
return cls._ask(question, state, min_confidence)
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def noul(
|
|
256
|
+
cls,
|
|
257
|
+
state: str | dict[str, Any],
|
|
258
|
+
instructions: str,
|
|
259
|
+
*,
|
|
260
|
+
min_confidence: float | None = None,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
"""是否题:返回 P(true)
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
state: 待判断的文本或结构化 state
|
|
266
|
+
instructions: 待判断的命题(如「客户是否明确要求退款?」)
|
|
267
|
+
min_confidence: 置信度门控阈值
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
dict[str, Any]: {decision: P(true), answer_confidence, probabilities}
|
|
271
|
+
"""
|
|
272
|
+
question = {"type": "noul", "instructions": instructions}
|
|
273
|
+
return cls._ask(question, state, min_confidence)
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def _ask(
|
|
277
|
+
cls,
|
|
278
|
+
question: dict[str, Any],
|
|
279
|
+
state: str | dict[str, Any],
|
|
280
|
+
min_confidence: float | None,
|
|
281
|
+
) -> dict[str, Any]:
|
|
282
|
+
"""单问题封装:构造 questions → predict → 取该问的 brief 结果
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
LayaError: laya 未返回该问题的答案时抛出(返回体结构变化)
|
|
286
|
+
"""
|
|
287
|
+
raw = cls.predict(state, {_QID: question}, min_confidence=min_confidence)
|
|
288
|
+
brief = cls.brief(raw)
|
|
289
|
+
if _QID not in brief:
|
|
290
|
+
raise LayaError(f"laya 未返回问句 {_QID} 的答案:{raw!r}")
|
|
291
|
+
return brief[_QID]
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def unload(cls) -> None:
|
|
295
|
+
"""释放 Router 与模型内存(先调用底层 unload,再置空单例)"""
|
|
296
|
+
with cls._router_lock:
|
|
297
|
+
if cls._router is not None:
|
|
298
|
+
unload_fn = getattr(cls._router, "unload", None)
|
|
299
|
+
if callable(unload_fn):
|
|
300
|
+
unload_fn()
|
|
301
|
+
cls._router = None
|
|
302
|
+
logger.info("laya Router 已释放")
|
|
@@ -52,6 +52,7 @@ dynamic = ["version"]
|
|
|
52
52
|
# docx → docx_kit(docxtpl + python-docx,模板动态填充)
|
|
53
53
|
# mineru → mineru_kit(requests)
|
|
54
54
|
# marker → marker_kit(marker-pdf[full],本地模型解析 PDF→Markdown+Images)
|
|
55
|
+
# laya → laya_kit(laya + torch,本地类型化决策引擎 choice/score/noul)
|
|
55
56
|
# 基础版 = config/log + kit.db 核心,SQLite 常规操作用标准库 sqlite3,零额外依赖。
|
|
56
57
|
[project.optional-dependencies]
|
|
57
58
|
pg = ["psycopg2-binary>=2.9.13"]
|
|
@@ -78,6 +79,10 @@ anydoc = [
|
|
|
78
79
|
docx = ["docxtpl>=0.20.2"]
|
|
79
80
|
mineru = ["requests>=2.34.2"]
|
|
80
81
|
marker = ["marker-pdf[full]>=2.0.0"]
|
|
82
|
+
laya = [
|
|
83
|
+
"laya>=0.4.6",
|
|
84
|
+
"huggingface-hub>=1.5,<2",
|
|
85
|
+
]
|
|
81
86
|
all = [
|
|
82
87
|
"psycopg2-binary>=2.9.13",
|
|
83
88
|
"pymysql>=1.2.3",
|
|
@@ -95,6 +100,8 @@ all = [
|
|
|
95
100
|
"docxtpl>=0.20.2",
|
|
96
101
|
"requests>=2.34.2",
|
|
97
102
|
"marker-pdf[full]>=2.0.0",
|
|
103
|
+
"laya>=0.4.6",
|
|
104
|
+
"huggingface-hub>=1.5,<2",
|
|
98
105
|
]
|
|
99
106
|
[tool.hatch.version]
|
|
100
107
|
path = "kit/__init__.py"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|