sdpy-kit 0.1.2__tar.gz → 0.1.4__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 (42) hide show
  1. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/PKG-INFO +17 -6
  2. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/__init__.py +1 -1
  3. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/base_service.py +62 -5
  4. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/chat_service.py +49 -15
  5. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/embedding_service.py +43 -23
  6. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/image_service.py +3 -1
  7. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/rerank_service.py +3 -1
  8. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/pyproject.toml +18 -5
  9. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/.gitignore +0 -0
  10. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/README.md +0 -0
  11. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/__init__.py +0 -0
  12. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/exceptions.py +0 -0
  13. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/failover.py +0 -0
  14. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/model_builder.py +0 -0
  15. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/model_client.py +0 -0
  16. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/pool.py +0 -0
  17. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/profiles.py +0 -0
  18. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/runner.py +0 -0
  19. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/ai/types.py +0 -0
  20. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/common_kit.py +0 -0
  21. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/config/__init__.py +0 -0
  22. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/config/kit_config.py +0 -0
  23. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/config/log_config.py +0 -0
  24. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/__init__.py +0 -0
  25. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/base_db_kit.py +0 -0
  26. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/crud.py +0 -0
  27. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/engine.py +0 -0
  28. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/exceptions.py +0 -0
  29. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/models/__init__.py +0 -0
  30. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/models/base.py +0 -0
  31. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/mysql_kit.py +0 -0
  32. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/pg_kit.py +0 -0
  33. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/query.py +0 -0
  34. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/session.py +0 -0
  35. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/sqlite_kit.py +0 -0
  36. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/db/transaction.py +0 -0
  37. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/doc_kit.py +0 -0
  38. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/file_kit.py +0 -0
  39. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/mcp_kit.py +0 -0
  40. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/mineru_kit.py +0 -0
  41. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/redis_kit.py +0 -0
  42. {sdpy_kit-0.1.2 → sdpy_kit-0.1.4}/kit/zvec_kit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -17,23 +17,34 @@ Classifier: Topic :: Database
17
17
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Python: >=3.13
20
- Requires-Dist: fastmcp>=4.0.3
21
20
  Requires-Dist: firecrawl-anydoc>=0.2.4
22
21
  Requires-Dist: httpx>=0.28.1
23
22
  Requires-Dist: markdown-it-py>=3.0
24
23
  Requires-Dist: numpy>=2.0
25
24
  Requires-Dist: pimd[pdfa]>=2.2.5
26
- Requires-Dist: psycopg2-binary>=2.9.12
27
25
  Requires-Dist: pydantic-settings>=2.0
28
26
  Requires-Dist: pydantic>=2.0
29
- Requires-Dist: pymysql>=1.2.0
30
27
  Requires-Dist: python-dotenv>=1.2.3
31
- Requires-Dist: redis>=5.0.0
32
28
  Requires-Dist: requests>=2.34.0
33
29
  Requires-Dist: sqlalchemy~=2.0.51
34
30
  Requires-Dist: sqliteai-vector>=1.0.0
35
31
  Requires-Dist: sqlmodel>=0.0.39
36
- Requires-Dist: zvec>=0.6.0
32
+ Provides-Extra: all
33
+ Requires-Dist: fastmcp>=4.0.3; extra == 'all'
34
+ Requires-Dist: psycopg2-binary>=2.9.12; extra == 'all'
35
+ Requires-Dist: pymysql>=1.2.0; extra == 'all'
36
+ Requires-Dist: redis>=5.0.0; extra == 'all'
37
+ Requires-Dist: zvec>=0.6.0; extra == 'all'
38
+ Provides-Extra: mcp
39
+ Requires-Dist: fastmcp>=4.0.3; extra == 'mcp'
40
+ Provides-Extra: mysql
41
+ Requires-Dist: pymysql>=1.2.0; extra == 'mysql'
42
+ Provides-Extra: pg
43
+ Requires-Dist: psycopg2-binary>=2.9.12; extra == 'pg'
44
+ Provides-Extra: redis
45
+ Requires-Dist: redis>=5.0.0; extra == 'redis'
46
+ Provides-Extra: zvec
47
+ Requires-Dist: zvec>=0.6.0; extra == 'zvec'
37
48
  Description-Content-Type: text/markdown
38
49
 
39
50
  # sdpy-kit
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.1.2"
26
+ __version__ = "0.1.4"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -17,16 +17,25 @@ Project: sdpy
17
17
  Author: Clark Chang
18
18
  """
19
19
 
20
+ import json
21
+ import time
20
22
  from abc import ABC, abstractmethod
21
- from typing import Self, override
23
+ from typing import ClassVar, Final, Self, override
22
24
 
23
- from kit.ai.exceptions import AIResponseError
25
+ from kit.ai.exceptions import AIError, AIResponseError
24
26
  from kit.ai.model_client import ModelClient
25
27
  from kit.ai.profiles import ModelProfile
26
28
  from kit.ai.types import JSONObject, JSONValue
29
+ from kit.config.log_config import get_default_logger
27
30
 
28
31
  __all__ = ["BaseAIService", "safe_get"]
29
32
 
33
+ logger = get_default_logger(__name__)
34
+
35
+ # 日志打印请求参数的截断上限:单字段字符数 / 参数总字符数
36
+ _LOG_FIELD_LIMIT: Final[int] = 200
37
+ _LOG_PAYLOAD_LIMIT: Final[int] = 2000
38
+
30
39
 
31
40
  def safe_get(data: JSONValue, *path: str | int) -> JSONValue:
32
41
  """按层级路径安全取值,支持 dict 键(str)与 list 索引(int)。
@@ -49,6 +58,26 @@ def safe_get(data: JSONValue, *path: str | int) -> JSONValue:
49
58
  return cur
50
59
 
51
60
 
61
+ def _clip(value: JSONValue) -> JSONValue:
62
+ """递归裁剪过长字符串字段(日志打印参数用,单字段上限 _LOG_FIELD_LIMIT)。"""
63
+ if isinstance(value, str):
64
+ if len(value) > _LOG_FIELD_LIMIT:
65
+ return f"{value[:_LOG_FIELD_LIMIT]}…(截断)"
66
+ return value
67
+ if isinstance(value, list):
68
+ return [_clip(item) for item in value]
69
+ if isinstance(value, dict):
70
+ return {key: _clip(item) for key, item in value.items()}
71
+ return value
72
+
73
+
74
+ def _truncate(text: str, limit: int) -> str:
75
+ """超长文本按上限截断,尾部追加截断标记(日志打印参数总长用)。"""
76
+ if len(text) > limit:
77
+ return f"{text[:limit]}…(截断)"
78
+ return text
79
+
80
+
52
81
  class BaseAIService[RequestT, ResponseT](ABC):
53
82
  """AI 服务基类(模板方法 + 泛型)。
54
83
 
@@ -58,6 +87,9 @@ class BaseAIService[RequestT, ResponseT](ABC):
58
87
  类型 ``ResponseT`` 由泛型绑定,保证覆写签名一致、类型自洽。
59
88
  """
60
89
 
90
+ # 日志「模型及描述」中的服务中文名,子类覆写
91
+ _LABEL: ClassVar[str] = "模型"
92
+
61
93
  def __init__(self, profile: ModelProfile) -> None:
62
94
  self._profile: ModelProfile = profile
63
95
  self._client: ModelClient = ModelClient(profile)
@@ -66,6 +98,13 @@ class BaseAIService[RequestT, ResponseT](ABC):
66
98
  def profile(self) -> ModelProfile:
67
99
  return self._profile
68
100
 
101
+ def _log_desc(self, *, suffix: str = "") -> str:
102
+ """日志中的「模型及描述」:服务中文名 + 模型标识 + 实例名。"""
103
+ return (
104
+ f"{self._LABEL} {self._profile.model}"
105
+ f"(实例 {self._profile.name}{suffix})"
106
+ )
107
+
69
108
  @abstractmethod
70
109
  def _build_payload(self, request: RequestT) -> JSONObject:
71
110
  """将请求模型构造为 JSON 请求体。"""
@@ -75,10 +114,28 @@ class BaseAIService[RequestT, ResponseT](ABC):
75
114
  """将 JSON 响应体解析为业务结果。"""
76
115
 
77
116
  async def _call(self, request: RequestT) -> ResponseT:
78
- """统一编排:构造请求体 → 发送 → 解析响应。"""
117
+ """统一编排:构造请求体 → 发送 → 解析响应(含中文 info 日志)。"""
118
+ desc = self._log_desc()
79
119
  payload = self._build_payload(request)
80
- data = await self._client.post_json(payload)
81
- return self._parse_response(data)
120
+ params = _truncate(
121
+ json.dumps(_clip(payload), ensure_ascii=False), _LOG_PAYLOAD_LIMIT
122
+ )
123
+ logger.info(f"********** {desc} ********** Start")
124
+ logger.info(f"请求参数: {params}")
125
+ start = time.perf_counter()
126
+ try:
127
+ data = await self._client.post_json(payload)
128
+ result = self._parse_response(data)
129
+ except AIError as exc:
130
+ logger.error(
131
+ f"********** {desc} ********** "
132
+ f"耗时 {time.perf_counter() - start:.1f}秒(失败:{exc})"
133
+ )
134
+ raise
135
+ logger.info(
136
+ f"********** {desc} ********** 耗时 {time.perf_counter() - start:.1f}秒"
137
+ )
138
+ return result
82
139
 
83
140
  async def close(self) -> None:
84
141
  await self._client.close()
@@ -17,12 +17,20 @@ Project: sdpy
17
17
  Author: Clark Chang
18
18
  """
19
19
 
20
+ import json
21
+ import time
20
22
  from collections.abc import AsyncIterator
21
23
  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
24
+ from typing import ClassVar, cast, override
25
+
26
+ from kit.ai.base_service import (
27
+ _LOG_PAYLOAD_LIMIT,
28
+ BaseAIService,
29
+ _clip,
30
+ _truncate,
31
+ safe_get,
32
+ )
33
+ from kit.ai.exceptions import AIError, AIResponseError
26
34
  from kit.ai.types import JSONObject
27
35
  from kit.config.log_config import get_default_logger
28
36
 
@@ -46,6 +54,8 @@ class ChatRequest:
46
54
  class ChatService(BaseAIService[ChatRequest, str]):
47
55
  """对话类模型服务(全异步)。"""
48
56
 
57
+ _LABEL: ClassVar[str] = "对话模型"
58
+
49
59
  @override
50
60
  def _build_payload(self, request: ChatRequest) -> JSONObject:
51
61
  payload: dict[str, object] = {
@@ -97,14 +107,38 @@ class ChatService(BaseAIService[ChatRequest, str]):
97
107
  messages, temperature=temperature, max_tokens=max_tokens, stream=True
98
108
  )
99
109
  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
110
+ desc = self._log_desc()
111
+ params = _truncate(
112
+ json.dumps(_clip(payload), ensure_ascii=False), _LOG_PAYLOAD_LIMIT
113
+ )
114
+ # 惰性 Start:异步生成器体在首次 __anext__ 前不执行,不消费不打日志
115
+ logger.info(f"********** {desc} ********** Start")
116
+ logger.info(f"请求参数: {params}")
117
+ start = time.perf_counter()
118
+ note = ""
119
+ failed = False
120
+ try:
121
+ async for parsed in self._client.stream_json(payload):
122
+ if parsed is None:
123
+ continue
124
+ try:
125
+ delta = safe_get(parsed, "choices", 0, "delta", "content")
126
+ except AIResponseError:
127
+ # 流式结束帧等无 delta 的正常帧,跳过
128
+ logger.debug("流式帧缺少 delta.content,跳过")
129
+ continue
130
+ if isinstance(delta, str) and delta:
131
+ yield delta
132
+ except AIError as exc:
133
+ note = f"(失败:{exc})"
134
+ failed = True
135
+ raise
136
+ except BaseException: # noqa: BLE001 - 中断路径(提前 break/取消)也需收尾日志
137
+ note = "(中断)"
138
+ raise
139
+ finally:
140
+ log = logger.error if failed else logger.info
141
+ log(
142
+ f"********** {desc} ********** "
143
+ f"耗时 {time.perf_counter() - start:.1f}秒{note}"
144
+ )
@@ -19,6 +19,7 @@ Author: Clark Chang
19
19
 
20
20
  import asyncio
21
21
  import itertools
22
+ import time
22
23
  from dataclasses import dataclass
23
24
  from typing import cast, override
24
25
 
@@ -26,9 +27,12 @@ from kit.ai.base_service import BaseAIService, safe_get
26
27
  from kit.ai.exceptions import AIError, AIRequestError, AIResponseError
27
28
  from kit.ai.profiles import ModelProfile
28
29
  from kit.ai.types import JSONObject
30
+ from kit.config.log_config import get_default_logger
29
31
 
30
32
  __all__ = ["EmbeddingRequest", "EmbeddingService"]
31
33
 
34
+ logger = get_default_logger(__name__)
35
+
32
36
  _DEFAULT_CONCURRENCY = 4
33
37
 
34
38
 
@@ -106,28 +110,44 @@ class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
106
110
  return []
107
111
 
108
112
  batch = batch_size or self._profile.batch_size
109
- semaphore = asyncio.Semaphore(concurrency)
110
- results: list[list[float] | None] = [None] * len(texts)
111
- failures: list[str] = []
112
-
113
- async def _one(batch_index: int, chunk: tuple[str, ...]) -> None:
114
- async with semaphore:
115
- try:
116
- vectors = await self._call(EmbeddingRequest(list(chunk)))
117
- except AIError as exc:
118
- failures.append(f"批次 {batch_index}: {exc}")
119
- return
120
- start = batch_index * batch
121
- for offset, vec in enumerate(vectors):
122
- results[start + offset] = vec
123
-
124
113
  batches = list(itertools.batched(texts, batch, strict=False))
125
- await asyncio.gather(*(_one(i, chunk) for i, chunk in enumerate(batches)))
126
-
127
- if failures:
128
- raise AIRequestError(
129
- f"有 {len(failures)}/{len(batches)} 批向量化失败: "
130
- + " | ".join(failures)
114
+ desc = self._log_desc(suffix=",整批汇总")
115
+ logger.info(f"********** {desc} ********** Start")
116
+ logger.info(
117
+ f"请求参数: 共 {len(texts)} 条文本,分 {len(batches)} 批"
118
+ f"(批大小 {batch},并发 {concurrency})"
119
+ )
120
+ start = time.perf_counter()
121
+ note = ""
122
+ try:
123
+ semaphore = asyncio.Semaphore(concurrency)
124
+ results: list[list[float] | None] = [None] * len(texts)
125
+ failures: list[str] = []
126
+
127
+ async def _one(batch_index: int, chunk: tuple[str, ...]) -> None:
128
+ async with semaphore:
129
+ try:
130
+ vectors = await self._call(EmbeddingRequest(list(chunk)))
131
+ except AIError as exc:
132
+ failures.append(f"批次 {batch_index}: {exc}")
133
+ return
134
+ start = batch_index * batch
135
+ for offset, vec in enumerate(vectors):
136
+ results[start + offset] = vec
137
+
138
+ await asyncio.gather(*(_one(i, chunk) for i, chunk in enumerate(batches)))
139
+
140
+ if failures:
141
+ note = f"(失败:{len(failures)}/{len(batches)} 批)"
142
+ raise AIRequestError(
143
+ f"有 {len(failures)}/{len(batches)} 批向量化失败: "
144
+ + " | ".join(failures)
145
+ )
146
+
147
+ return [vec for vec in results if vec is not None]
148
+ finally:
149
+ log = logger.error if failures else logger.info
150
+ log(
151
+ f"********** {desc} ********** "
152
+ f"耗时 {time.perf_counter() - start:.1f}秒{note}"
131
153
  )
132
-
133
- return [vec for vec in results if vec is not None]
@@ -18,7 +18,7 @@ Author: Clark Chang
18
18
  """
19
19
 
20
20
  from dataclasses import dataclass
21
- from typing import override
21
+ from typing import ClassVar, override
22
22
 
23
23
  from kit.ai.base_service import BaseAIService, safe_get
24
24
  from kit.ai.exceptions import AIResponseError
@@ -52,6 +52,8 @@ class ImageService(BaseAIService[ImageRequest, str]):
52
52
  返回 Base64 数据,调用方按返回值是否以 ``http`` 开头区分形式。
53
53
  """
54
54
 
55
+ _LABEL: ClassVar[str] = "图像生成模型"
56
+
55
57
  @override
56
58
  def _build_payload(self, request: ImageRequest) -> JSONObject:
57
59
  payload: JSONObject = {
@@ -18,7 +18,7 @@ Author: Clark Chang
18
18
  """
19
19
 
20
20
  from dataclasses import dataclass
21
- from typing import cast, override
21
+ from typing import ClassVar, cast, override
22
22
 
23
23
  from kit.ai.base_service import BaseAIService, safe_get
24
24
  from kit.ai.exceptions import AIResponseError
@@ -47,6 +47,8 @@ class RerankHit:
47
47
  class RerankService(BaseAIService[RerankRequest, list[RerankHit]]):
48
48
  """重排序服务(全异步)。"""
49
49
 
50
+ _LABEL: ClassVar[str] = "重排序模型"
51
+
50
52
  @override
51
53
  def _build_payload(self, request: RerankRequest) -> JSONObject:
52
54
  payload: dict[str, object] = {
@@ -34,21 +34,34 @@ dependencies = [
34
34
  "numpy>=2.0",
35
35
  "sqlalchemy~=2.0.51",
36
36
  "sqlmodel>=0.0.39",
37
- "pymysql>=1.2.0",
38
- "psycopg2-binary>=2.9.12",
39
- "redis>=5.0.0",
40
- "zvec>=0.6.0",
41
37
  "sqliteai-vector>=1.0.0",
42
38
  "firecrawl-anydoc>=0.2.4",
43
39
  "pimd[pdfa]>=2.2.5",
44
40
  "markdown-it-py>=3.0",
45
- "fastmcp>=4.0.3",
46
41
  "httpx>=0.28.1",
47
42
  "requests>=2.34.0",
48
43
  "python-dotenv>=1.2.3",
49
44
  ]
50
45
  dynamic = ["version"]
51
46
 
47
+ # 关键节点:可选后端按 extras 拆分(0.1.4)。fastmcp/zvec/redis/psycopg2/pymysql
48
+ # 仅在对应 Kit(mcp_kit/zvec_kit/redis_kit/pg_kit/mysql_kit)被实际使用时才需
49
+ # 安装,且均为延迟导入——不装 extras 时 import kit 与基础 Kit 全部可用。
50
+ # 单机 SQLite 场景安装基础版即可,避免拖入重依赖(原全量安装多占约 60MB)。
51
+ [project.optional-dependencies]
52
+ pg = ["psycopg2-binary>=2.9.12"]
53
+ mysql = ["pymysql>=1.2.0"]
54
+ redis = ["redis>=5.0.0"]
55
+ zvec = ["zvec>=0.6.0"]
56
+ mcp = ["fastmcp>=4.0.3"]
57
+ all = [
58
+ "psycopg2-binary>=2.9.12",
59
+ "pymysql>=1.2.0",
60
+ "redis>=5.0.0",
61
+ "zvec>=0.6.0",
62
+ "fastmcp>=4.0.3",
63
+ ]
64
+
52
65
  [tool.hatch.version]
53
66
  path = "kit/__init__.py"
54
67
 
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