sdpy-kit 0.1.0__py3-none-any.whl
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.
- kit/__init__.py +68 -0
- kit/ai/__init__.py +47 -0
- kit/ai/base_service.py +94 -0
- kit/ai/chat_service.py +110 -0
- kit/ai/embedding_service.py +133 -0
- kit/ai/exceptions.py +57 -0
- kit/ai/failover.py +244 -0
- kit/ai/image_service.py +99 -0
- kit/ai/model_builder.py +119 -0
- kit/ai/model_client.py +214 -0
- kit/ai/pool.py +411 -0
- kit/ai/profiles.py +373 -0
- kit/ai/rerank_service.py +93 -0
- kit/ai/runner.py +580 -0
- kit/ai/types.py +28 -0
- kit/common_kit.py +221 -0
- kit/config/__init__.py +44 -0
- kit/config/kit_config.py +147 -0
- kit/config/log_config.py +134 -0
- kit/db/__init__.py +70 -0
- kit/db/base_db_kit.py +406 -0
- kit/db/crud.py +361 -0
- kit/db/engine.py +230 -0
- kit/db/exceptions.py +34 -0
- kit/db/models/__init__.py +33 -0
- kit/db/models/base.py +56 -0
- kit/db/mysql_kit.py +89 -0
- kit/db/pg_kit.py +87 -0
- kit/db/session.py +85 -0
- kit/db/sqlite_kit.py +284 -0
- kit/db/transaction.py +109 -0
- kit/doc_kit.py +1502 -0
- kit/file_kit.py +429 -0
- kit/mcp_kit.py +547 -0
- kit/mineru_kit.py +918 -0
- kit/redis_kit.py +392 -0
- kit/zvec_kit.py +671 -0
- sdpy_kit-0.1.0.dist-info/METADATA +128 -0
- sdpy_kit-0.1.0.dist-info/RECORD +40 -0
- sdpy_kit-0.1.0.dist-info/WHEEL +4 -0
kit/__init__.py
ADDED
|
@@ -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
|
+
]
|
kit/ai/__init__.py
ADDED
|
@@ -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
|
+
]
|
kit/ai/base_service.py
ADDED
|
@@ -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})"
|
kit/ai/chat_service.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
import asyncio
|
|
21
|
+
import itertools
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import cast, override
|
|
24
|
+
|
|
25
|
+
from kit.ai.base_service import BaseAIService, safe_get
|
|
26
|
+
from kit.ai.exceptions import AIError, AIRequestError, AIResponseError
|
|
27
|
+
from kit.ai.profiles import ModelProfile
|
|
28
|
+
from kit.ai.types import JSONObject
|
|
29
|
+
|
|
30
|
+
__all__ = ["EmbeddingRequest", "EmbeddingService"]
|
|
31
|
+
|
|
32
|
+
_DEFAULT_CONCURRENCY = 4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class EmbeddingRequest:
|
|
37
|
+
"""向量化请求模型(单批)。"""
|
|
38
|
+
|
|
39
|
+
texts: list[str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
|
|
43
|
+
"""向量化服务(全异步)。
|
|
44
|
+
|
|
45
|
+
公网 API 有限流限速前提,通过分批 + 可选并发(信号量)控制压力。
|
|
46
|
+
信号量在运行中的事件循环内惰性创建,避免构造期绑定循环的隐患。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, profile: ModelProfile) -> None:
|
|
50
|
+
super().__init__(profile)
|
|
51
|
+
if profile.dimension is None:
|
|
52
|
+
raise ValueError(f"模型 {profile.model} 未配置 dimension,无法用于向量化")
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def dimension(self) -> int:
|
|
56
|
+
dim = self._profile.dimension
|
|
57
|
+
if not isinstance(dim, int):
|
|
58
|
+
raise AIResponseError("dimension 在构造期已校验非空,不应缺失")
|
|
59
|
+
return dim
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def _build_payload(self, request: EmbeddingRequest) -> JSONObject:
|
|
63
|
+
return cast(
|
|
64
|
+
JSONObject,
|
|
65
|
+
{
|
|
66
|
+
"model": self._profile.model,
|
|
67
|
+
"input": request.texts,
|
|
68
|
+
"encoding_format": "float",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
def _parse_response(self, data: JSONObject) -> list[list[float]]:
|
|
74
|
+
items = safe_get(data, "data")
|
|
75
|
+
if not isinstance(items, list):
|
|
76
|
+
raise AIResponseError(f"向量化响应 data 必须是数组: {str(data)[:200]}")
|
|
77
|
+
vectors: list[list[float]] = []
|
|
78
|
+
for item in items:
|
|
79
|
+
if not isinstance(item, dict):
|
|
80
|
+
raise AIResponseError(f"向量化响应条目非法: {str(item)[:200]}")
|
|
81
|
+
embedding = item.get("embedding")
|
|
82
|
+
if not isinstance(embedding, list):
|
|
83
|
+
raise AIResponseError(f"向量化响应 embedding 非法: {str(item)[:200]}")
|
|
84
|
+
floats: list[float] = []
|
|
85
|
+
for value in embedding:
|
|
86
|
+
if not isinstance(value, (int, float)):
|
|
87
|
+
raise AIResponseError(
|
|
88
|
+
f"向量化响应 embedding 元素非法: {str(item)[:200]}"
|
|
89
|
+
)
|
|
90
|
+
floats.append(float(value))
|
|
91
|
+
vectors.append(floats)
|
|
92
|
+
return vectors
|
|
93
|
+
|
|
94
|
+
async def embed_text(self, text: str) -> list[float]:
|
|
95
|
+
vectors = await self._call(EmbeddingRequest([text]))
|
|
96
|
+
return vectors[0]
|
|
97
|
+
|
|
98
|
+
async def embed_texts(
|
|
99
|
+
self,
|
|
100
|
+
texts: list[str],
|
|
101
|
+
*,
|
|
102
|
+
batch_size: int | None = None,
|
|
103
|
+
concurrency: int = _DEFAULT_CONCURRENCY,
|
|
104
|
+
) -> list[list[float]]:
|
|
105
|
+
if not texts:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
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
|
+
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)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return [vec for vec in results if vec is not None]
|
kit/ai/exceptions.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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 统一异常体系
|
|
22
|
+
|
|
23
|
+
继承链:
|
|
24
|
+
AIError 基类
|
|
25
|
+
├── AIConfigError 档案文件缺失/非法、档案不存在、密钥未配置、必填字段缺失
|
|
26
|
+
├── AIRequestError 网络异常、HTTP >= 400
|
|
27
|
+
│ └── AITimeoutError 请求超时(可用于重试策略)
|
|
28
|
+
└── AIResponseError 响应结构解析失败、流式数据无法解析
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AIError(Exception):
|
|
33
|
+
"""AI 模型服务基类异常"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AIConfigError(AIError):
|
|
37
|
+
"""模型档案配置异常(文件缺失/非法、档案不存在、密钥未配置、必填字段缺失)"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AIRequestError(AIError):
|
|
41
|
+
"""模型服务请求异常(网络异常、HTTP >= 400)。
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
status_code: HTTP 状态码;非 HTTP 错误(超时、连接失败等)为 None。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, message: str, *, status_code: int | None = None) -> None:
|
|
48
|
+
super().__init__(message)
|
|
49
|
+
self.status_code: int | None = status_code
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AITimeoutError(AIRequestError):
|
|
53
|
+
"""模型服务请求超时异常(可用于重试策略)"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AIResponseError(AIError):
|
|
57
|
+
"""模型服务响应异常(响应结构解析失败、流式数据无法解析)"""
|