sdpy-kit 0.2.6__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.
Files changed (47) hide show
  1. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/PKG-INFO +6 -1
  2. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/__init__.py +1 -1
  3. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/config/kit_config.py +20 -0
  4. sdpy_kit-0.3.0/kit/laya_kit.py +302 -0
  5. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/pyproject.toml +7 -0
  6. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/.gitignore +0 -0
  7. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/README.md +0 -0
  8. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/__init__.py +0 -0
  9. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/base_service.py +0 -0
  10. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/chat_service.py +0 -0
  11. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/embedding_service.py +0 -0
  12. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/exceptions.py +0 -0
  13. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/failover.py +0 -0
  14. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/image_service.py +0 -0
  15. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/model_builder.py +0 -0
  16. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/model_client.py +0 -0
  17. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/pool.py +0 -0
  18. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/profiles.py +0 -0
  19. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/rerank_service.py +0 -0
  20. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/runner.py +0 -0
  21. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/ai/types.py +0 -0
  22. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/anydoc_kit.py +0 -0
  23. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/common_kit.py +0 -0
  24. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/config/__init__.py +0 -0
  25. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/config/log_config.py +0 -0
  26. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/__init__.py +0 -0
  27. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/base_db_kit.py +0 -0
  28. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/crud.py +0 -0
  29. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/engine.py +0 -0
  30. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/exceptions.py +0 -0
  31. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/models/__init__.py +0 -0
  32. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/models/base.py +0 -0
  33. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/mysql_kit.py +0 -0
  34. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/pg_kit.py +0 -0
  35. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/query.py +0 -0
  36. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/session.py +0 -0
  37. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/sqlite_kit.py +0 -0
  38. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/db/transaction.py +0 -0
  39. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/docx_kit.py +0 -0
  40. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/file_kit.py +0 -0
  41. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/marker_kit.py +0 -0
  42. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/mcp_kit.py +0 -0
  43. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/md2doc_kit.py +0 -0
  44. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/mineru_kit.py +0 -0
  45. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/redis_kit.py +0 -0
  46. {sdpy_kit-0.2.6 → sdpy_kit-0.3.0}/kit/wx_article_kit.py +0 -0
  47. {sdpy_kit-0.2.6 → 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.2.6
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
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.2.6"
26
+ __version__ = "0.3.0"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -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"
@@ -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