infomankit 0.3.23__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.
- infoman/__init__.py +1 -0
- infoman/cli/README.md +378 -0
- infoman/cli/__init__.py +7 -0
- infoman/cli/commands/__init__.py +3 -0
- infoman/cli/commands/init.py +312 -0
- infoman/cli/scaffold.py +634 -0
- infoman/cli/templates/Makefile.template +132 -0
- infoman/cli/templates/app/__init__.py.template +3 -0
- infoman/cli/templates/app/app.py.template +4 -0
- infoman/cli/templates/app/models_base.py.template +18 -0
- infoman/cli/templates/app/models_entity_init.py.template +11 -0
- infoman/cli/templates/app/models_schemas_init.py.template +11 -0
- infoman/cli/templates/app/repository_init.py.template +11 -0
- infoman/cli/templates/app/routers_init.py.template +15 -0
- infoman/cli/templates/app/services_init.py.template +11 -0
- infoman/cli/templates/app/static_index.html.template +39 -0
- infoman/cli/templates/app/static_main.js.template +31 -0
- infoman/cli/templates/app/static_style.css.template +111 -0
- infoman/cli/templates/app/utils_init.py.template +11 -0
- infoman/cli/templates/config/.env.dev.template +43 -0
- infoman/cli/templates/config/.env.prod.template +43 -0
- infoman/cli/templates/config/README.md.template +28 -0
- infoman/cli/templates/docker/.dockerignore.template +60 -0
- infoman/cli/templates/docker/Dockerfile.template +47 -0
- infoman/cli/templates/docker/README.md.template +240 -0
- infoman/cli/templates/docker/docker-compose.yml.template +81 -0
- infoman/cli/templates/docker/mysql_custom.cnf.template +42 -0
- infoman/cli/templates/docker/mysql_init.sql.template +15 -0
- infoman/cli/templates/project/.env.example.template +1 -0
- infoman/cli/templates/project/.gitignore.template +60 -0
- infoman/cli/templates/project/Makefile.template +38 -0
- infoman/cli/templates/project/README.md.template +137 -0
- infoman/cli/templates/project/deploy.sh.template +97 -0
- infoman/cli/templates/project/main.py.template +10 -0
- infoman/cli/templates/project/manage.sh.template +97 -0
- infoman/cli/templates/project/pyproject.toml.template +47 -0
- infoman/cli/templates/project/service.sh.template +203 -0
- infoman/config/__init__.py +25 -0
- infoman/config/base.py +67 -0
- infoman/config/db_cache.py +237 -0
- infoman/config/db_relation.py +181 -0
- infoman/config/db_vector.py +39 -0
- infoman/config/jwt.py +16 -0
- infoman/config/llm.py +16 -0
- infoman/config/log.py +627 -0
- infoman/config/mq.py +26 -0
- infoman/config/settings.py +65 -0
- infoman/llm/__init__.py +0 -0
- infoman/llm/llm.py +297 -0
- infoman/logger/__init__.py +57 -0
- infoman/logger/context.py +191 -0
- infoman/logger/core.py +358 -0
- infoman/logger/filters.py +157 -0
- infoman/logger/formatters.py +138 -0
- infoman/logger/handlers.py +276 -0
- infoman/logger/metrics.py +160 -0
- infoman/performance/README.md +583 -0
- infoman/performance/__init__.py +19 -0
- infoman/performance/cli.py +215 -0
- infoman/performance/config.py +166 -0
- infoman/performance/reporter.py +519 -0
- infoman/performance/runner.py +303 -0
- infoman/performance/standards.py +222 -0
- infoman/service/__init__.py +8 -0
- infoman/service/app.py +67 -0
- infoman/service/core/__init__.py +0 -0
- infoman/service/core/auth.py +105 -0
- infoman/service/core/lifespan.py +132 -0
- infoman/service/core/monitor.py +57 -0
- infoman/service/core/response.py +37 -0
- infoman/service/exception/__init__.py +7 -0
- infoman/service/exception/error.py +274 -0
- infoman/service/exception/exception.py +25 -0
- infoman/service/exception/handler.py +238 -0
- infoman/service/infrastructure/__init__.py +8 -0
- infoman/service/infrastructure/base.py +212 -0
- infoman/service/infrastructure/db_cache/__init__.py +8 -0
- infoman/service/infrastructure/db_cache/manager.py +194 -0
- infoman/service/infrastructure/db_relation/__init__.py +41 -0
- infoman/service/infrastructure/db_relation/manager.py +300 -0
- infoman/service/infrastructure/db_relation/manager_pro.py +408 -0
- infoman/service/infrastructure/db_relation/mysql.py +52 -0
- infoman/service/infrastructure/db_relation/pgsql.py +54 -0
- infoman/service/infrastructure/db_relation/sqllite.py +25 -0
- infoman/service/infrastructure/db_vector/__init__.py +40 -0
- infoman/service/infrastructure/db_vector/manager.py +201 -0
- infoman/service/infrastructure/db_vector/qdrant.py +322 -0
- infoman/service/infrastructure/mq/__init__.py +15 -0
- infoman/service/infrastructure/mq/manager.py +178 -0
- infoman/service/infrastructure/mq/nats/__init__.py +0 -0
- infoman/service/infrastructure/mq/nats/nats_client.py +57 -0
- infoman/service/infrastructure/mq/nats/nats_event_router.py +25 -0
- infoman/service/launch.py +284 -0
- infoman/service/middleware/__init__.py +7 -0
- infoman/service/middleware/base.py +41 -0
- infoman/service/middleware/logging.py +51 -0
- infoman/service/middleware/rate_limit.py +301 -0
- infoman/service/middleware/request_id.py +21 -0
- infoman/service/middleware/white_list.py +24 -0
- infoman/service/models/__init__.py +8 -0
- infoman/service/models/base.py +441 -0
- infoman/service/models/type/embed.py +70 -0
- infoman/service/routers/__init__.py +18 -0
- infoman/service/routers/health_router.py +311 -0
- infoman/service/routers/monitor_router.py +44 -0
- infoman/service/utils/__init__.py +8 -0
- infoman/service/utils/cache/__init__.py +0 -0
- infoman/service/utils/cache/cache.py +192 -0
- infoman/service/utils/module_loader.py +10 -0
- infoman/service/utils/parse.py +10 -0
- infoman/service/utils/resolver/__init__.py +8 -0
- infoman/service/utils/resolver/base.py +47 -0
- infoman/service/utils/resolver/resp.py +102 -0
- infoman/service/vector/__init__.py +20 -0
- infoman/service/vector/base.py +56 -0
- infoman/service/vector/qdrant.py +125 -0
- infoman/service/vector/service.py +67 -0
- infoman/utils/__init__.py +2 -0
- infoman/utils/decorators/__init__.py +8 -0
- infoman/utils/decorators/cache.py +137 -0
- infoman/utils/decorators/retry.py +99 -0
- infoman/utils/decorators/safe_execute.py +99 -0
- infoman/utils/decorators/timing.py +99 -0
- infoman/utils/encryption/__init__.py +8 -0
- infoman/utils/encryption/aes.py +66 -0
- infoman/utils/encryption/ecc.py +108 -0
- infoman/utils/encryption/rsa.py +112 -0
- infoman/utils/file/__init__.py +0 -0
- infoman/utils/file/handler.py +22 -0
- infoman/utils/hash/__init__.py +0 -0
- infoman/utils/hash/hash.py +61 -0
- infoman/utils/http/__init__.py +8 -0
- infoman/utils/http/client.py +62 -0
- infoman/utils/http/info.py +94 -0
- infoman/utils/http/result.py +19 -0
- infoman/utils/notification/__init__.py +8 -0
- infoman/utils/notification/feishu.py +35 -0
- infoman/utils/text/__init__.py +8 -0
- infoman/utils/text/extractor.py +111 -0
- infomankit-0.3.23.dist-info/METADATA +632 -0
- infomankit-0.3.23.dist-info/RECORD +143 -0
- infomankit-0.3.23.dist-info/WHEEL +4 -0
- infomankit-0.3.23.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/6/25 21:51
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import traceback
|
|
12
|
+
from typing import Any, Optional, Type, Union
|
|
13
|
+
from pydantic import BaseModel, ValidationError
|
|
14
|
+
from infoman.service.utils.resolver.base import BaseResp
|
|
15
|
+
from infoman.logger import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_data_from_text(text) -> (bool, Any):
|
|
19
|
+
try:
|
|
20
|
+
result = json.loads(text)
|
|
21
|
+
result_code = result.get("code")
|
|
22
|
+
if result_code != 200:
|
|
23
|
+
return False, result.get("message")
|
|
24
|
+
return True, result.get("data")
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.info(f"get_data_from_text error: {traceback.format_exc()}")
|
|
27
|
+
return False, None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_data_model_from_text(
|
|
31
|
+
text: str, model: Optional[Type[BaseModel]] = None
|
|
32
|
+
) -> (bool, Any):
|
|
33
|
+
"""
|
|
34
|
+
从文本中解析JSON数据,并可选地使用模型验证数据。
|
|
35
|
+
|
|
36
|
+
参数:
|
|
37
|
+
text (str): 要解析的JSON文本
|
|
38
|
+
model (Optional[Type[BaseModel]]): 可选的Pydantic模型,用于验证数据
|
|
39
|
+
|
|
40
|
+
返回:
|
|
41
|
+
tuple: (bool, Any) 第一个元素表示是否成功,第二个元素是数据或错误信息
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
result = json.loads(text)
|
|
45
|
+
|
|
46
|
+
result_code = result.get("code")
|
|
47
|
+
if result_code != "000000":
|
|
48
|
+
return False, result.get("message")
|
|
49
|
+
|
|
50
|
+
data = result.get("data")
|
|
51
|
+
if model is not None:
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
try:
|
|
54
|
+
validated_data = model.parse_obj(data)
|
|
55
|
+
return True, validated_data
|
|
56
|
+
except ValidationError as e:
|
|
57
|
+
logger.info(f"Data validation error: {e}")
|
|
58
|
+
return False, f"Data validation failed: {e}"
|
|
59
|
+
elif isinstance(data, list):
|
|
60
|
+
data_list = []
|
|
61
|
+
for one in data:
|
|
62
|
+
try:
|
|
63
|
+
validated_data = model.parse_obj(one)
|
|
64
|
+
data_list.append(validated_data)
|
|
65
|
+
except ValidationError as e:
|
|
66
|
+
logger.info(f"Data validation error: {e}")
|
|
67
|
+
return True, data_list
|
|
68
|
+
return True, data
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.info(f"get_data_from_text error: {traceback.format_exc()}")
|
|
72
|
+
return False, None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_model_from_text(
|
|
76
|
+
text: str, model: Optional[Type[BaseModel]] = None
|
|
77
|
+
) -> (bool, Any):
|
|
78
|
+
try:
|
|
79
|
+
result = json.loads(text)
|
|
80
|
+
result_code = result.get("code")
|
|
81
|
+
if result_code != "000000":
|
|
82
|
+
return False, result.get("message")
|
|
83
|
+
|
|
84
|
+
data = result.get("data")
|
|
85
|
+
if isinstance(data, list):
|
|
86
|
+
try:
|
|
87
|
+
validated_data = model.parse_obj(result)
|
|
88
|
+
return True, validated_data
|
|
89
|
+
except ValidationError as e:
|
|
90
|
+
logger.info(f"Data validation error: {e}")
|
|
91
|
+
|
|
92
|
+
if model is not None:
|
|
93
|
+
try:
|
|
94
|
+
validated_data = model.parse_obj(data)
|
|
95
|
+
return True, validated_data
|
|
96
|
+
except ValidationError as e:
|
|
97
|
+
logger.info(f"Data validation error: {e}")
|
|
98
|
+
return False, "data is null"
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.info(f"get_data_from_text error: {traceback.format_exc()}")
|
|
102
|
+
return False, None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Vector store integration entry.
|
|
5
|
+
|
|
6
|
+
Exposes a light-weight abstraction and a Qdrant implementation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .base import VectorClient, VectorPoint, VectorFilter
|
|
10
|
+
from .qdrant import QdrantVectorClient
|
|
11
|
+
from .service import VectorService, get_vector_service
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"VectorClient",
|
|
15
|
+
"VectorPoint",
|
|
16
|
+
"VectorFilter",
|
|
17
|
+
"QdrantVectorClient",
|
|
18
|
+
"VectorService",
|
|
19
|
+
"get_vector_service",
|
|
20
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Iterable, Mapping, Protocol, Sequence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class VectorPoint:
|
|
12
|
+
id: str | int
|
|
13
|
+
vector: Sequence[float]
|
|
14
|
+
payload: Mapping[str, Any] | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class VectorFilter:
|
|
19
|
+
# Generic equality filter; concrete impl can translate
|
|
20
|
+
must: dict[str, Any] | None = None
|
|
21
|
+
must_not: dict[str, Any] | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VectorClient(Protocol):
|
|
25
|
+
async def ensure_collection(
|
|
26
|
+
self,
|
|
27
|
+
name: str,
|
|
28
|
+
vector_size: int,
|
|
29
|
+
distance: str = "cosine",
|
|
30
|
+
on_disk: bool = False,
|
|
31
|
+
) -> None: ...
|
|
32
|
+
|
|
33
|
+
async def upsert(
|
|
34
|
+
self,
|
|
35
|
+
collection: str,
|
|
36
|
+
points: Iterable[VectorPoint],
|
|
37
|
+
wait: bool = False,
|
|
38
|
+
) -> None: ...
|
|
39
|
+
|
|
40
|
+
async def delete(
|
|
41
|
+
self,
|
|
42
|
+
collection: str,
|
|
43
|
+
ids: Iterable[str | int] | None = None,
|
|
44
|
+
filt: VectorFilter | None = None,
|
|
45
|
+
wait: bool = False,
|
|
46
|
+
) -> None: ...
|
|
47
|
+
|
|
48
|
+
async def search(
|
|
49
|
+
self,
|
|
50
|
+
collection: str,
|
|
51
|
+
query_vector: Sequence[float],
|
|
52
|
+
limit: int = 10,
|
|
53
|
+
filt: VectorFilter | None = None,
|
|
54
|
+
with_vectors: bool = False,
|
|
55
|
+
) -> list[dict[str, Any]]: ...
|
|
56
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Iterable, Sequence
|
|
7
|
+
|
|
8
|
+
from .base import VectorClient, VectorPoint, VectorFilter
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from qdrant_client import AsyncQdrantClient
|
|
12
|
+
from qdrant_client.models import (
|
|
13
|
+
Distance,
|
|
14
|
+
VectorParams,
|
|
15
|
+
PointStruct,
|
|
16
|
+
Filter as QFilter,
|
|
17
|
+
FieldCondition,
|
|
18
|
+
MatchAny,
|
|
19
|
+
MatchValue,
|
|
20
|
+
)
|
|
21
|
+
except Exception: # pragma: no cover - optional dependency at import time
|
|
22
|
+
AsyncQdrantClient = None # type: ignore
|
|
23
|
+
Distance = VectorParams = PointStruct = object # type: ignore
|
|
24
|
+
QFilter = FieldCondition = MatchAny = MatchValue = object # type: ignore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_DIST_MAP = {
|
|
28
|
+
"cosine": Distance.COSINE if hasattr(Distance, "COSINE") else "cosine",
|
|
29
|
+
"dot": Distance.DOT if hasattr(Distance, "DOT") else "dot",
|
|
30
|
+
"euclid": Distance.EUCLID if hasattr(Distance, "EUCLID") else "euclid",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _to_qfilter(f: VectorFilter | None) -> QFilter | None:
|
|
35
|
+
if f is None:
|
|
36
|
+
return None
|
|
37
|
+
must = []
|
|
38
|
+
must_not = []
|
|
39
|
+
if f.must:
|
|
40
|
+
for k, v in f.must.items():
|
|
41
|
+
if isinstance(v, (list, tuple, set)):
|
|
42
|
+
must.append(FieldCondition(key=k, match=MatchAny(any=list(v))))
|
|
43
|
+
else:
|
|
44
|
+
must.append(FieldCondition(key=k, match=MatchValue(value=v)))
|
|
45
|
+
if f.must_not:
|
|
46
|
+
for k, v in f.must_not.items():
|
|
47
|
+
if isinstance(v, (list, tuple, set)):
|
|
48
|
+
must_not.append(FieldCondition(key=k, match=MatchAny(any=list(v))))
|
|
49
|
+
else:
|
|
50
|
+
must_not.append(FieldCondition(key=k, match=MatchValue(value=v)))
|
|
51
|
+
return QFilter(must=must or None, must_not=must_not or None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class QdrantVectorClient(VectorClient):
|
|
55
|
+
def __init__(self, client: Any):
|
|
56
|
+
if AsyncQdrantClient is not None and not isinstance(client, AsyncQdrantClient):
|
|
57
|
+
# Allow duck typing (useful in tests), but warn in docs
|
|
58
|
+
pass
|
|
59
|
+
self.client = client
|
|
60
|
+
|
|
61
|
+
async def ensure_collection(
|
|
62
|
+
self, name: str, vector_size: int, distance: str = "cosine", on_disk: bool = False
|
|
63
|
+
) -> None:
|
|
64
|
+
dist = _DIST_MAP.get(distance, _DIST_MAP["cosine"])
|
|
65
|
+
exists = await self.client.collection_exists(collection_name=name)
|
|
66
|
+
if exists:
|
|
67
|
+
return
|
|
68
|
+
await self.client.create_collection(
|
|
69
|
+
collection_name=name,
|
|
70
|
+
vectors_config=VectorParams(size=vector_size, distance=dist, on_disk=on_disk),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def upsert(self, collection: str, points: Iterable[VectorPoint], wait: bool = False) -> None:
|
|
74
|
+
payloads = []
|
|
75
|
+
qpoints: list[PointStruct] = []
|
|
76
|
+
for p in points:
|
|
77
|
+
payload = dict(p.payload) if p.payload else None
|
|
78
|
+
payloads.append(payload)
|
|
79
|
+
qpoints.append(PointStruct(id=p.id, vector=list(p.vector), payload=payload))
|
|
80
|
+
if not qpoints:
|
|
81
|
+
return
|
|
82
|
+
await self.client.upsert(collection_name=collection, points=qpoints, wait=wait)
|
|
83
|
+
|
|
84
|
+
async def delete(
|
|
85
|
+
self,
|
|
86
|
+
collection: str,
|
|
87
|
+
ids: Iterable[str | int] | None = None,
|
|
88
|
+
filt: VectorFilter | None = None,
|
|
89
|
+
wait: bool = False,
|
|
90
|
+
) -> None:
|
|
91
|
+
if ids:
|
|
92
|
+
await self.client.delete(collection_name=collection, points=list(ids), wait=wait)
|
|
93
|
+
return
|
|
94
|
+
qf = _to_qfilter(filt)
|
|
95
|
+
if qf is not None:
|
|
96
|
+
await self.client.delete(collection_name=collection, filter=qf, wait=wait)
|
|
97
|
+
|
|
98
|
+
async def search(
|
|
99
|
+
self,
|
|
100
|
+
collection: str,
|
|
101
|
+
query_vector: Sequence[float],
|
|
102
|
+
limit: int = 10,
|
|
103
|
+
filt: VectorFilter | None = None,
|
|
104
|
+
with_vectors: bool = False,
|
|
105
|
+
) -> list[dict[str, Any]]:
|
|
106
|
+
qf = _to_qfilter(filt)
|
|
107
|
+
res = await self.client.search(
|
|
108
|
+
collection_name=collection,
|
|
109
|
+
query_vector=list(query_vector),
|
|
110
|
+
limit=limit,
|
|
111
|
+
filter=qf,
|
|
112
|
+
with_vectors=with_vectors,
|
|
113
|
+
)
|
|
114
|
+
results: list[dict[str, Any]] = []
|
|
115
|
+
for r in res:
|
|
116
|
+
results.append(
|
|
117
|
+
{
|
|
118
|
+
"id": r.id,
|
|
119
|
+
"score": r.score,
|
|
120
|
+
"payload": getattr(r, "payload", None),
|
|
121
|
+
"vector": getattr(r, "vector", None) if with_vectors else None,
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
return results
|
|
125
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
|
|
10
|
+
from .base import VectorPoint, VectorFilter
|
|
11
|
+
from . import VectorClient
|
|
12
|
+
from infoman.service.models.type.embed import (
|
|
13
|
+
EmbedModel,
|
|
14
|
+
EmbedCollection,
|
|
15
|
+
EmbedDataType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VectorService:
|
|
20
|
+
"""High-level vector operations bound to Embed* config.
|
|
21
|
+
|
|
22
|
+
This provides convenience methods using `EmbedModel` and `EmbedDataType`
|
|
23
|
+
to compute collection names and dimensions.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, vector_client: VectorClient):
|
|
27
|
+
self.vc = vector_client
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def collection_name(data_type: EmbedDataType, model: EmbedModel) -> str:
|
|
31
|
+
return EmbedCollection.collection_name(data_type, model)
|
|
32
|
+
|
|
33
|
+
async def ensure_collection(self, data_type: EmbedDataType, model: EmbedModel) -> str:
|
|
34
|
+
name = self.collection_name(data_type, model)
|
|
35
|
+
await self.vc.ensure_collection(name, model.value.dem, distance="cosine")
|
|
36
|
+
return name
|
|
37
|
+
|
|
38
|
+
async def upsert(
|
|
39
|
+
self,
|
|
40
|
+
data_type: EmbedDataType,
|
|
41
|
+
model: EmbedModel,
|
|
42
|
+
points: list[VectorPoint],
|
|
43
|
+
wait: bool = False,
|
|
44
|
+
) -> str:
|
|
45
|
+
name = self.collection_name(data_type, model)
|
|
46
|
+
await self.vc.upsert(name, points, wait=wait)
|
|
47
|
+
return name
|
|
48
|
+
|
|
49
|
+
async def search(
|
|
50
|
+
self,
|
|
51
|
+
data_type: EmbedDataType,
|
|
52
|
+
model: EmbedModel,
|
|
53
|
+
query_vector: Sequence[float],
|
|
54
|
+
limit: int = 10,
|
|
55
|
+
filt: VectorFilter | None = None,
|
|
56
|
+
with_vectors: bool = False,
|
|
57
|
+
) -> list[dict[str, Any]]:
|
|
58
|
+
name = self.collection_name(data_type, model)
|
|
59
|
+
return await self.vc.search(name, query_vector, limit, filt, with_vectors)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_vector_service(app: FastAPI) -> VectorService | None:
|
|
63
|
+
vc = getattr(app.state, "vector_client", None)
|
|
64
|
+
if vc is None:
|
|
65
|
+
return None
|
|
66
|
+
return VectorService(vc)
|
|
67
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/6/22 12:11
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
import functools
|
|
11
|
+
from typing import Callable, TypeVar, Any, Optional, Union, Dict, List, Tuple
|
|
12
|
+
from infoman.logger import logger
|
|
13
|
+
from inspect import iscoroutinefunction
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cache(
|
|
20
|
+
ttl: Optional[int] = 60, # 缓存生存时间(秒)
|
|
21
|
+
max_size: int = 128, # 最大缓存条目数
|
|
22
|
+
key_func: Optional[Callable] = None, # 自定义缓存键生成函数
|
|
23
|
+
) -> Callable[[F], F]:
|
|
24
|
+
"""
|
|
25
|
+
缓存装饰器 - 缓存函数结果以提高性能
|
|
26
|
+
|
|
27
|
+
参数:
|
|
28
|
+
ttl: 缓存生存时间(秒),None表示永不过期
|
|
29
|
+
max_size: 最大缓存条目数
|
|
30
|
+
key_func: 自定义缓存键生成函数
|
|
31
|
+
|
|
32
|
+
示例:
|
|
33
|
+
@cache(ttl=300) # 缓存5分钟
|
|
34
|
+
def get_user_data(user_id):
|
|
35
|
+
# 获取用户数据的代码
|
|
36
|
+
"""
|
|
37
|
+
cache_dict: Dict[str, Tuple[Any, Optional[float]]] = {}
|
|
38
|
+
|
|
39
|
+
def make_key(*args, **kwargs) -> str:
|
|
40
|
+
"""生成缓存键"""
|
|
41
|
+
if key_func:
|
|
42
|
+
return str(key_func(*args, **kwargs))
|
|
43
|
+
|
|
44
|
+
# 默认键生成逻辑
|
|
45
|
+
key_parts = [str(arg) for arg in args]
|
|
46
|
+
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
|
47
|
+
return ":".join(key_parts)
|
|
48
|
+
|
|
49
|
+
def is_expired(timestamp: Optional[float]) -> bool:
|
|
50
|
+
"""检查缓存是否过期"""
|
|
51
|
+
if timestamp is None: # 永不过期
|
|
52
|
+
return False
|
|
53
|
+
return time.time() > timestamp
|
|
54
|
+
|
|
55
|
+
def cleanup_cache() -> None:
|
|
56
|
+
"""清理过期缓存"""
|
|
57
|
+
if ttl is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
expired_keys = [
|
|
61
|
+
k
|
|
62
|
+
for k, (_, exp_time) in cache_dict.items()
|
|
63
|
+
if exp_time is not None and is_expired(exp_time)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for k in expired_keys:
|
|
67
|
+
del cache_dict[k]
|
|
68
|
+
|
|
69
|
+
def decorator(func: F) -> F:
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
async def async_wrapper(*args, **kwargs):
|
|
72
|
+
# 清理过期缓存
|
|
73
|
+
cleanup_cache()
|
|
74
|
+
|
|
75
|
+
# 生成缓存键
|
|
76
|
+
key = make_key(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
# 检查缓存
|
|
79
|
+
if key in cache_dict:
|
|
80
|
+
value, exp_time = cache_dict[key]
|
|
81
|
+
if not is_expired(exp_time):
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
# 缓存未命中,执行函数
|
|
85
|
+
result = await func(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
# 更新缓存
|
|
88
|
+
if len(cache_dict) >= max_size:
|
|
89
|
+
# 简单的LRU策略:删除第一个条目
|
|
90
|
+
if cache_dict:
|
|
91
|
+
cache_dict.pop(next(iter(cache_dict)))
|
|
92
|
+
|
|
93
|
+
expiration = (time.time() + ttl) if ttl is not None else None
|
|
94
|
+
cache_dict[key] = (result, expiration)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
@functools.wraps(func)
|
|
99
|
+
def sync_wrapper(*args, **kwargs):
|
|
100
|
+
# 清理过期缓存
|
|
101
|
+
cleanup_cache()
|
|
102
|
+
|
|
103
|
+
# 生成缓存键
|
|
104
|
+
key = make_key(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
# 检查缓存
|
|
107
|
+
if key in cache_dict:
|
|
108
|
+
value, exp_time = cache_dict[key]
|
|
109
|
+
if not is_expired(exp_time):
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
# 缓存未命中,执行函数
|
|
113
|
+
result = func(*args, **kwargs)
|
|
114
|
+
|
|
115
|
+
# 更新缓存
|
|
116
|
+
if len(cache_dict) >= max_size:
|
|
117
|
+
# 简单的LRU策略:删除第一个条目
|
|
118
|
+
if cache_dict:
|
|
119
|
+
cache_dict.pop(next(iter(cache_dict)))
|
|
120
|
+
|
|
121
|
+
expiration = (time.time() + ttl) if ttl is not None else None
|
|
122
|
+
cache_dict[key] = (result, expiration)
|
|
123
|
+
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
# 添加清除缓存的辅助方法
|
|
127
|
+
def clear_cache():
|
|
128
|
+
cache_dict.clear()
|
|
129
|
+
|
|
130
|
+
if iscoroutinefunction(func):
|
|
131
|
+
async_wrapper.clear_cache = clear_cache
|
|
132
|
+
return async_wrapper
|
|
133
|
+
else:
|
|
134
|
+
sync_wrapper.clear_cache = clear_cache
|
|
135
|
+
return sync_wrapper
|
|
136
|
+
|
|
137
|
+
return decorator
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/6/22 12:10
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
import asyncio
|
|
11
|
+
import functools
|
|
12
|
+
from typing import Callable, TypeVar, Any, Optional, Union, Dict, List, Tuple
|
|
13
|
+
from infoman.logger import logger
|
|
14
|
+
from inspect import iscoroutinefunction
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def retry(
|
|
21
|
+
max_attempts: int = 3,
|
|
22
|
+
delay_seconds: float = 1.0,
|
|
23
|
+
backoff_factor: float = 2.0,
|
|
24
|
+
exceptions: Tuple[Exception, ...] = (Exception,),
|
|
25
|
+
logger_name: Optional[str] = None,
|
|
26
|
+
) -> Callable[[F], F]:
|
|
27
|
+
"""
|
|
28
|
+
重试装饰器 - 在发生特定异常时自动重试函数
|
|
29
|
+
|
|
30
|
+
参数:
|
|
31
|
+
max_attempts: 最大尝试次数
|
|
32
|
+
delay_seconds: 初始延迟时间(秒)
|
|
33
|
+
backoff_factor: 退避因子(每次重试后延迟时间的乘数)
|
|
34
|
+
exceptions: 触发重试的异常类型
|
|
35
|
+
logger_name: 自定义日志记录器名称
|
|
36
|
+
|
|
37
|
+
示例:
|
|
38
|
+
@retry(max_attempts=5, exceptions=(ConnectionError, TimeoutError))
|
|
39
|
+
async def connect_to_service():
|
|
40
|
+
# 连接服务的代码
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def decorator(func: F) -> F:
|
|
44
|
+
@functools.wraps(func)
|
|
45
|
+
async def async_wrapper(*args, **kwargs):
|
|
46
|
+
attempt = 1
|
|
47
|
+
current_delay = delay_seconds
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
return await func(*args, **kwargs)
|
|
52
|
+
except exceptions as e:
|
|
53
|
+
if attempt >= max_attempts:
|
|
54
|
+
logger.error(
|
|
55
|
+
f"Function {func.__name__} failed after {max_attempts} attempts. "
|
|
56
|
+
f"Last error: {str(e)}"
|
|
57
|
+
)
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
logger.warning(
|
|
61
|
+
f"Attempt {attempt}/{max_attempts} for {func.__name__} failed: {str(e)}. "
|
|
62
|
+
f"Retrying in {current_delay:.2f}s"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
await asyncio.sleep(current_delay)
|
|
66
|
+
attempt += 1
|
|
67
|
+
current_delay *= backoff_factor
|
|
68
|
+
|
|
69
|
+
@functools.wraps(func)
|
|
70
|
+
def sync_wrapper(*args, **kwargs):
|
|
71
|
+
attempt = 1
|
|
72
|
+
current_delay = delay_seconds
|
|
73
|
+
|
|
74
|
+
while True:
|
|
75
|
+
try:
|
|
76
|
+
return func(*args, **kwargs)
|
|
77
|
+
except exceptions as e:
|
|
78
|
+
if attempt >= max_attempts:
|
|
79
|
+
logger.error(
|
|
80
|
+
f"Function {func.__name__} failed after {max_attempts} attempts. "
|
|
81
|
+
f"Last error: {str(e)}"
|
|
82
|
+
)
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
logger.warning(
|
|
86
|
+
f"Attempt {attempt}/{max_attempts} for {func.__name__} failed: {str(e)}. "
|
|
87
|
+
f"Retrying in {current_delay:.2f}s"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
time.sleep(current_delay)
|
|
91
|
+
attempt += 1
|
|
92
|
+
current_delay *= backoff_factor
|
|
93
|
+
|
|
94
|
+
if iscoroutinefunction(func):
|
|
95
|
+
return async_wrapper
|
|
96
|
+
else:
|
|
97
|
+
return sync_wrapper
|
|
98
|
+
|
|
99
|
+
return decorator
|