ingestlib 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.
- ingestlib/__init__.py +4 -0
- ingestlib/config.py +231 -0
- ingestlib/foundations/__init__.py +10 -0
- ingestlib/foundations/llm/__init__.py +48 -0
- ingestlib/foundations/llm/bedrock/__init__.py +39 -0
- ingestlib/foundations/llm/bedrock/embedding.py +127 -0
- ingestlib/foundations/llm/bedrock/factory.py +101 -0
- ingestlib/foundations/llm/bedrock/nova.py +302 -0
- ingestlib/foundations/llm/bedrock/rerank.py +75 -0
- ingestlib/foundations/llm/jina/__init__.py +4 -0
- ingestlib/foundations/llm/jina/rerank.py +83 -0
- ingestlib/foundations/ocr/__init__.py +6 -0
- ingestlib/foundations/ocr/models.py +125 -0
- ingestlib/foundations/ocr/paddle_vl.py +272 -0
- ingestlib/operations/__init__.py +25 -0
- ingestlib/operations/classify/__init__.py +16 -0
- ingestlib/operations/classify/chunker.py +74 -0
- ingestlib/operations/classify/classifier.py +219 -0
- ingestlib/operations/classify/models.py +36 -0
- ingestlib/operations/parse/__init__.py +33 -0
- ingestlib/operations/parse/assembler.py +90 -0
- ingestlib/operations/parse/detector.py +28 -0
- ingestlib/operations/parse/enricher.py +124 -0
- ingestlib/operations/parse/loaders/__init__.py +39 -0
- ingestlib/operations/parse/loaders/office.py +117 -0
- ingestlib/operations/parse/loaders/pdf.py +185 -0
- ingestlib/operations/parse/models.py +162 -0
- ingestlib/operations/parse/pipeline.py +147 -0
- ingestlib/operations/parse/reviewer.py +136 -0
- ingestlib/operations/split/__init__.py +17 -0
- ingestlib/operations/split/models.py +90 -0
- ingestlib/operations/split/pages.py +182 -0
- ingestlib/operations/split/sections.py +119 -0
- ingestlib/operations/split/segmenter.py +172 -0
- ingestlib/operations/split/splitter.py +169 -0
- ingestlib/services/__init__.py +23 -0
- ingestlib/services/ingest/__init__.py +9 -0
- ingestlib/services/ingest/ingestor.py +152 -0
- ingestlib/services/ingest/models.py +30 -0
- ingestlib/services/retrieve/__init__.py +10 -0
- ingestlib/services/retrieve/models.py +43 -0
- ingestlib/services/retrieve/retriever.py +97 -0
- ingestlib/storage/__init__.py +49 -0
- ingestlib/storage/artifacts.py +337 -0
- ingestlib/storage/base.py +102 -0
- ingestlib/storage/pinecone/__init__.py +21 -0
- ingestlib/storage/pinecone/client.py +167 -0
- ingestlib/storage/pinecone/store.py +263 -0
- ingestlib/storage/qdrant/__init__.py +21 -0
- ingestlib/storage/qdrant/client.py +136 -0
- ingestlib/storage/qdrant/store.py +259 -0
- ingestlib/storage/s3/__init__.py +4 -0
- ingestlib/storage/s3/client.py +97 -0
- ingestlib/utils/__init__.py +7 -0
- ingestlib/utils/files.py +14 -0
- ingestlib/utils/logger.py +127 -0
- ingestlib-0.1.0.dist-info/METADATA +248 -0
- ingestlib-0.1.0.dist-info/RECORD +60 -0
- ingestlib-0.1.0.dist-info/WHEEL +4 -0
- ingestlib-0.1.0.dist-info/licenses/LICENSE +21 -0
ingestlib/__init__.py
ADDED
ingestlib/config.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Typed configuration for ingestlib — loaded lazily on first use.
|
|
2
|
+
|
|
3
|
+
Importing ingestlib never touches the filesystem; config loads when the
|
|
4
|
+
first operation needs it. Discovery order for config.yaml:
|
|
5
|
+
|
|
6
|
+
1. INGESTLIB_CONFIG environment variable (explicit path)
|
|
7
|
+
2. config.yaml in the current working directory, then each parent
|
|
8
|
+
|
|
9
|
+
Secrets never live in config.yaml: a .env sitting next to the discovered
|
|
10
|
+
config file is loaded automatically (JINA_API_KEY, PINECONE_API_KEY,
|
|
11
|
+
QDRANT_URL/_API_KEY), and AWS credentials resolve via the profile field
|
|
12
|
+
against ~/.aws/credentials — the standard boto3 chain.
|
|
13
|
+
"""
|
|
14
|
+
import os
|
|
15
|
+
import threading
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
from dotenv import load_dotenv
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_CONFIG_ENV_VAR = "INGESTLIB_CONFIG"
|
|
24
|
+
_CONFIG_FILENAME = "config.yaml"
|
|
25
|
+
|
|
26
|
+
_lock = threading.Lock()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class AWSConfig:
|
|
31
|
+
profile: str
|
|
32
|
+
region: str
|
|
33
|
+
account_id: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class BedrockConfig:
|
|
38
|
+
llm_model_id: str
|
|
39
|
+
embedding_model_id: str
|
|
40
|
+
rerank_model_id: str
|
|
41
|
+
rerank_region: str # separate region for rerank (amazon.rerank-v1:0 not in us-east-1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class JinaConfig:
|
|
46
|
+
api_key: str # from JINA_API_KEY env var
|
|
47
|
+
base_url: str
|
|
48
|
+
rerank_model_id: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class PaddleVLConfig:
|
|
53
|
+
backend: str # mlx-vlm-server (Apple Silicon) | vllm-server (NVIDIA)
|
|
54
|
+
server_url: str # VLM inference server URL
|
|
55
|
+
api_model_name: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class S3Config:
|
|
60
|
+
bucket: str # globally unique bucket name for pipeline artifacts
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class PineconeConfig:
|
|
65
|
+
api_key: str # from PINECONE_API_KEY env var
|
|
66
|
+
index_name: str
|
|
67
|
+
sparse_index_name: str # lexical half of hybrid search (created on first upsert)
|
|
68
|
+
sparse_model_id: str # Pinecone-hosted sparse embedding model (no corpus state)
|
|
69
|
+
cloud: str # serverless cloud provider (aws | gcp | azure)
|
|
70
|
+
region: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class QdrantConfig:
|
|
75
|
+
api_key: str # from QDRANT_API_KEY env var ("" for a local/unsecured server)
|
|
76
|
+
url: str # from QDRANT_URL env var (e.g. http://localhost:6333 or a cloud URL)
|
|
77
|
+
collection_name: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class IngestConfig:
|
|
82
|
+
vector_store: str # which connector the services default to (pinecone | qdrant)
|
|
83
|
+
aws: AWSConfig
|
|
84
|
+
bedrock: BedrockConfig
|
|
85
|
+
jina: JinaConfig
|
|
86
|
+
paddle_vl: PaddleVLConfig
|
|
87
|
+
s3: S3Config
|
|
88
|
+
pinecone: PineconeConfig
|
|
89
|
+
qdrant: QdrantConfig
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _find_config_path() -> Path:
|
|
93
|
+
"""Locate config.yaml: INGESTLIB_CONFIG first, then CWD and its parents."""
|
|
94
|
+
explicit = os.environ.get(_CONFIG_ENV_VAR)
|
|
95
|
+
if explicit:
|
|
96
|
+
path = Path(explicit).expanduser()
|
|
97
|
+
if not path.is_file():
|
|
98
|
+
raise FileNotFoundError(
|
|
99
|
+
f"{_CONFIG_ENV_VAR} points to {path}, which does not exist"
|
|
100
|
+
)
|
|
101
|
+
return path
|
|
102
|
+
cwd = Path.cwd()
|
|
103
|
+
for directory in (cwd, *cwd.parents):
|
|
104
|
+
candidate = directory / _CONFIG_FILENAME
|
|
105
|
+
if candidate.is_file():
|
|
106
|
+
return candidate
|
|
107
|
+
raise FileNotFoundError(
|
|
108
|
+
f"{_CONFIG_FILENAME} not found in {cwd} or any parent directory — "
|
|
109
|
+
f"copy config.example.yaml from https://github.com/LangModule/ingestlib "
|
|
110
|
+
f"into your project, or set {_CONFIG_ENV_VAR}=/path/to/config.yaml"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _load_config() -> IngestConfig:
|
|
115
|
+
config_path = _find_config_path()
|
|
116
|
+
# secrets conventionally sit next to the config file
|
|
117
|
+
load_dotenv(config_path.parent / ".env")
|
|
118
|
+
|
|
119
|
+
with open(config_path, "r") as f:
|
|
120
|
+
data = yaml.safe_load(f)
|
|
121
|
+
|
|
122
|
+
aws_data = data["aws"]
|
|
123
|
+
aws_config = AWSConfig(
|
|
124
|
+
profile=aws_data["profile"],
|
|
125
|
+
region=aws_data["region"],
|
|
126
|
+
account_id=str(aws_data["account_id"]),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
bedrock_data = data["bedrock"]
|
|
130
|
+
bedrock_config = BedrockConfig(
|
|
131
|
+
llm_model_id=bedrock_data["llm_model_id"],
|
|
132
|
+
embedding_model_id=bedrock_data["embedding_model_id"],
|
|
133
|
+
rerank_model_id=bedrock_data["rerank_model_id"],
|
|
134
|
+
rerank_region=bedrock_data["rerank_region"],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
jina_data = data["jina"]
|
|
138
|
+
jina_config = JinaConfig(
|
|
139
|
+
api_key=os.environ.get("JINA_API_KEY", ""),
|
|
140
|
+
base_url=jina_data.get("base_url", "https://api.jina.ai/v1"),
|
|
141
|
+
rerank_model_id=jina_data["rerank_model_id"],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
paddle_vl_data = data["paddle_vl"]
|
|
145
|
+
paddle_vl_config = PaddleVLConfig(
|
|
146
|
+
backend=paddle_vl_data.get("backend", "mlx-vlm-server"),
|
|
147
|
+
server_url=paddle_vl_data.get("server_url", "http://localhost:8111/"),
|
|
148
|
+
api_model_name=paddle_vl_data["api_model_name"],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
s3_data = data.get("s3", {})
|
|
152
|
+
s3_config = S3Config(
|
|
153
|
+
bucket=s3_data.get("bucket", f"ingestlib-{aws_config.account_id}"),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
pinecone_data = data.get("pinecone", {})
|
|
157
|
+
index_name = pinecone_data.get("index_name", "ingestlib")
|
|
158
|
+
pinecone_config = PineconeConfig(
|
|
159
|
+
api_key=os.environ.get("PINECONE_API_KEY", ""),
|
|
160
|
+
index_name=index_name,
|
|
161
|
+
sparse_index_name=pinecone_data.get("sparse_index_name", f"{index_name}-sparse"),
|
|
162
|
+
sparse_model_id=pinecone_data.get("sparse_model_id", "pinecone-sparse-english-v0"),
|
|
163
|
+
cloud=pinecone_data.get("cloud", "aws"),
|
|
164
|
+
region=pinecone_data.get("region", "us-east-1"),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
qdrant_data = data.get("qdrant", {})
|
|
168
|
+
qdrant_config = QdrantConfig(
|
|
169
|
+
api_key=os.environ.get("QDRANT_API_KEY", ""),
|
|
170
|
+
url=os.environ.get("QDRANT_URL", "http://localhost:6333"),
|
|
171
|
+
collection_name=qdrant_data.get("collection_name", "ingestlib"),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return IngestConfig(
|
|
175
|
+
vector_store=data.get("vector_store", "pinecone"),
|
|
176
|
+
aws=aws_config,
|
|
177
|
+
bedrock=bedrock_config,
|
|
178
|
+
jina=jina_config,
|
|
179
|
+
paddle_vl=paddle_vl_config,
|
|
180
|
+
s3=s3_config,
|
|
181
|
+
pinecone=pinecone_config,
|
|
182
|
+
qdrant=qdrant_config,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
_config: IngestConfig | None = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_config() -> IngestConfig:
|
|
190
|
+
"""Full typed configuration, loaded lazily from config.yaml + .env and cached."""
|
|
191
|
+
global _config
|
|
192
|
+
if _config is None:
|
|
193
|
+
with _lock:
|
|
194
|
+
if _config is None:
|
|
195
|
+
_config = _load_config()
|
|
196
|
+
return _config
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_aws_config() -> AWSConfig:
|
|
200
|
+
"""AWS profile/region/account settings."""
|
|
201
|
+
return get_config().aws
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_bedrock_config() -> BedrockConfig:
|
|
205
|
+
"""Bedrock model IDs and rerank region."""
|
|
206
|
+
return get_config().bedrock
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_jina_config() -> JinaConfig:
|
|
210
|
+
"""Jina reranker endpoint and API key."""
|
|
211
|
+
return get_config().jina
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_paddle_vl_config() -> PaddleVLConfig:
|
|
215
|
+
"""PaddleOCR-VL inference server settings."""
|
|
216
|
+
return get_config().paddle_vl
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_s3_config() -> S3Config:
|
|
220
|
+
"""S3 artifact bucket settings."""
|
|
221
|
+
return get_config().s3
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_pinecone_config() -> PineconeConfig:
|
|
225
|
+
"""Pinecone index settings and API key."""
|
|
226
|
+
return get_config().pinecone
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_qdrant_config() -> QdrantConfig:
|
|
230
|
+
"""Qdrant endpoint, API key, and collection settings."""
|
|
231
|
+
return get_config().qdrant
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Foundation layers the operations are built on.
|
|
2
|
+
|
|
3
|
+
Sub-packages:
|
|
4
|
+
llm — Bedrock Nova (chat / thinking / structured output), multimodal
|
|
5
|
+
embeddings, and reranking (Jina primary, Amazon retained)
|
|
6
|
+
ocr — PaddleOCR-VL engine (layout + recognition) and the shared region types
|
|
7
|
+
|
|
8
|
+
Operations (parse / classify / split) compose these; nothing here knows the
|
|
9
|
+
operations exist.
|
|
10
|
+
"""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Public LLM surface.
|
|
2
|
+
|
|
3
|
+
- LLM/embedding primitives come from Bedrock (Nova family).
|
|
4
|
+
- Rerank is exposed twice with explicit provider suffixes — no ambiguous default:
|
|
5
|
+
aws_rerank / aws_arerank → amazon.rerank-v1:0 (quota-limited, keep for later)
|
|
6
|
+
jina_rerank / jina_arerank → Jina Reranker API (primary)
|
|
7
|
+
"""
|
|
8
|
+
from ingestlib.foundations.llm.bedrock import (
|
|
9
|
+
Image,
|
|
10
|
+
achat,
|
|
11
|
+
achat_structured,
|
|
12
|
+
achat_with_thinking,
|
|
13
|
+
aembed_image,
|
|
14
|
+
aembed_text,
|
|
15
|
+
chat,
|
|
16
|
+
chat_structured,
|
|
17
|
+
chat_with_thinking,
|
|
18
|
+
embed_image,
|
|
19
|
+
embed_text,
|
|
20
|
+
get_llm,
|
|
21
|
+
get_llm_with_thinking,
|
|
22
|
+
reset_clients,
|
|
23
|
+
)
|
|
24
|
+
from ingestlib.foundations.llm.bedrock import arerank as aws_arerank
|
|
25
|
+
from ingestlib.foundations.llm.bedrock import rerank as aws_rerank
|
|
26
|
+
from ingestlib.foundations.llm.jina import arerank as jina_arerank
|
|
27
|
+
from ingestlib.foundations.llm.jina import rerank as jina_rerank
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Image",
|
|
31
|
+
"get_llm",
|
|
32
|
+
"get_llm_with_thinking",
|
|
33
|
+
"chat",
|
|
34
|
+
"chat_with_thinking",
|
|
35
|
+
"achat",
|
|
36
|
+
"chat_structured",
|
|
37
|
+
"achat_structured",
|
|
38
|
+
"achat_with_thinking",
|
|
39
|
+
"embed_text",
|
|
40
|
+
"embed_image",
|
|
41
|
+
"aembed_text",
|
|
42
|
+
"aembed_image",
|
|
43
|
+
"aws_rerank",
|
|
44
|
+
"aws_arerank",
|
|
45
|
+
"jina_rerank",
|
|
46
|
+
"jina_arerank",
|
|
47
|
+
"reset_clients",
|
|
48
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Bedrock backend: Nova LLM/embedding + amazon.rerank-v1:0. Clients built in factory.py."""
|
|
2
|
+
from ingestlib.foundations.llm.bedrock.embedding import (
|
|
3
|
+
aembed_image,
|
|
4
|
+
aembed_text,
|
|
5
|
+
embed_image,
|
|
6
|
+
embed_text,
|
|
7
|
+
)
|
|
8
|
+
from ingestlib.foundations.llm.bedrock.factory import reset_clients
|
|
9
|
+
from ingestlib.foundations.llm.bedrock.nova import (
|
|
10
|
+
Image,
|
|
11
|
+
achat,
|
|
12
|
+
achat_structured,
|
|
13
|
+
achat_with_thinking,
|
|
14
|
+
chat,
|
|
15
|
+
chat_structured,
|
|
16
|
+
chat_with_thinking,
|
|
17
|
+
get_llm,
|
|
18
|
+
get_llm_with_thinking,
|
|
19
|
+
)
|
|
20
|
+
from ingestlib.foundations.llm.bedrock.rerank import arerank, rerank
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Image",
|
|
24
|
+
"get_llm",
|
|
25
|
+
"get_llm_with_thinking",
|
|
26
|
+
"chat",
|
|
27
|
+
"chat_with_thinking",
|
|
28
|
+
"achat",
|
|
29
|
+
"chat_structured",
|
|
30
|
+
"achat_structured",
|
|
31
|
+
"achat_with_thinking",
|
|
32
|
+
"embed_text",
|
|
33
|
+
"embed_image",
|
|
34
|
+
"aembed_text",
|
|
35
|
+
"aembed_image",
|
|
36
|
+
"rerank",
|
|
37
|
+
"arerank",
|
|
38
|
+
"reset_clients",
|
|
39
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Nova 2 multimodal embeddings — text and image embed primitives (sync and async)."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from ingestlib.config import get_bedrock_config
|
|
9
|
+
from ingestlib.foundations.llm.bedrock.factory import get_runtime_client
|
|
10
|
+
from ingestlib.utils.logger import get_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
EmbeddingPurpose = Literal["GENERIC_INDEX", "GENERIC_RETRIEVAL", "DOCUMENT_RETRIEVAL"]
|
|
16
|
+
EmbeddingDimension = Literal[256, 384, 1024, 3072]
|
|
17
|
+
ImageFormat = Literal["jpeg", "png", "webp", "gif"]
|
|
18
|
+
ImageDetailLevel = Literal["STANDARD_IMAGE", "DOCUMENT_IMAGE"]
|
|
19
|
+
|
|
20
|
+
DEFAULT_DIMENSION: EmbeddingDimension = 1024
|
|
21
|
+
SUPPORTED_DIMENSIONS: tuple[int, ...] = (256, 384, 1024, 3072)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_dimension(dimension: int) -> None:
|
|
25
|
+
if dimension not in SUPPORTED_DIMENSIONS:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"dimension must be one of {SUPPORTED_DIMENSIONS}, got {dimension}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _invoke(body: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
client = get_runtime_client()
|
|
33
|
+
cfg = get_bedrock_config()
|
|
34
|
+
response = client.invoke_model(
|
|
35
|
+
modelId=cfg.embedding_model_id,
|
|
36
|
+
body=json.dumps(body),
|
|
37
|
+
contentType="application/json",
|
|
38
|
+
accept="application/json",
|
|
39
|
+
)
|
|
40
|
+
return json.loads(response["body"].read())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _embed(
|
|
44
|
+
*,
|
|
45
|
+
content: dict[str, Any],
|
|
46
|
+
purpose: EmbeddingPurpose,
|
|
47
|
+
dimension: EmbeddingDimension,
|
|
48
|
+
) -> list[float]:
|
|
49
|
+
_validate_dimension(dimension)
|
|
50
|
+
body = {
|
|
51
|
+
"taskType": "SINGLE_EMBEDDING",
|
|
52
|
+
"singleEmbeddingParams": {
|
|
53
|
+
"embeddingPurpose": purpose,
|
|
54
|
+
"embeddingDimension": dimension,
|
|
55
|
+
**content,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
return _invoke(body)["embeddings"][0]["embedding"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def embed_text(
|
|
62
|
+
text: str,
|
|
63
|
+
purpose: EmbeddingPurpose = "GENERIC_INDEX",
|
|
64
|
+
dimension: EmbeddingDimension = DEFAULT_DIMENSION,
|
|
65
|
+
) -> list[float]:
|
|
66
|
+
"""Embed text → vector of `dimension` floats (truncates overlong input at the end)."""
|
|
67
|
+
logger.info(
|
|
68
|
+
"embed_text: purpose=%s dim=%d input_len=%d", purpose, dimension, len(text),
|
|
69
|
+
)
|
|
70
|
+
t0 = time.perf_counter()
|
|
71
|
+
result = _embed(
|
|
72
|
+
content={"text": {"truncationMode": "END", "value": text}},
|
|
73
|
+
purpose=purpose,
|
|
74
|
+
dimension=dimension,
|
|
75
|
+
)
|
|
76
|
+
logger.info("embed_text done: %.2fs returned_dim=%d", time.perf_counter() - t0, len(result))
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def embed_image(
|
|
81
|
+
data: bytes,
|
|
82
|
+
format: ImageFormat,
|
|
83
|
+
purpose: EmbeddingPurpose = "GENERIC_INDEX",
|
|
84
|
+
dimension: EmbeddingDimension = DEFAULT_DIMENSION,
|
|
85
|
+
detail_level: ImageDetailLevel = "STANDARD_IMAGE",
|
|
86
|
+
) -> list[float]:
|
|
87
|
+
"""Embed an image → vector of `dimension` floats. DOCUMENT_IMAGE detail for doc pages."""
|
|
88
|
+
logger.info(
|
|
89
|
+
"embed_image: purpose=%s dim=%d format=%s detail_level=%s size=%d bytes",
|
|
90
|
+
purpose, dimension, format, detail_level, len(data),
|
|
91
|
+
)
|
|
92
|
+
t0 = time.perf_counter()
|
|
93
|
+
result = _embed(
|
|
94
|
+
content={
|
|
95
|
+
"image": {
|
|
96
|
+
"format": format,
|
|
97
|
+
"detailLevel": detail_level,
|
|
98
|
+
"source": {"bytes": base64.b64encode(data).decode()},
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
purpose=purpose,
|
|
102
|
+
dimension=dimension,
|
|
103
|
+
)
|
|
104
|
+
logger.info("embed_image done: %.2fs returned_dim=%d", time.perf_counter() - t0, len(result))
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def aembed_text(
|
|
109
|
+
text: str,
|
|
110
|
+
purpose: EmbeddingPurpose = "GENERIC_INDEX",
|
|
111
|
+
dimension: EmbeddingDimension = DEFAULT_DIMENSION,
|
|
112
|
+
) -> list[float]:
|
|
113
|
+
"""Async embed_text() — runs the sync Bedrock client in a worker thread."""
|
|
114
|
+
return await asyncio.to_thread(embed_text, text, purpose, dimension)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def aembed_image(
|
|
118
|
+
data: bytes,
|
|
119
|
+
format: ImageFormat,
|
|
120
|
+
purpose: EmbeddingPurpose = "GENERIC_INDEX",
|
|
121
|
+
dimension: EmbeddingDimension = DEFAULT_DIMENSION,
|
|
122
|
+
detail_level: ImageDetailLevel = "STANDARD_IMAGE",
|
|
123
|
+
) -> list[float]:
|
|
124
|
+
"""Async embed_image() — runs the sync Bedrock client in a worker thread."""
|
|
125
|
+
return await asyncio.to_thread(
|
|
126
|
+
embed_image, data, format, purpose, dimension, detail_level
|
|
127
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Shared boto3 session, Bedrock clients, and generation-keyed model cache."""
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
from botocore.config import Config
|
|
6
|
+
from botocore.exceptions import ProfileNotFound
|
|
7
|
+
|
|
8
|
+
from ingestlib.config import get_aws_config, get_bedrock_config
|
|
9
|
+
from ingestlib.utils.logger import get_logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
_lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
_session: boto3.Session | None = None
|
|
16
|
+
_runtime_client = None # bedrock-runtime (LLM inference + embeddings)
|
|
17
|
+
_rerank_agent_client = None # bedrock-agent-runtime in cfg.rerank_region (for rerank)
|
|
18
|
+
_client_generation: int = 0
|
|
19
|
+
_model_cache: dict[str, tuple[object, int]] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_clients() -> None:
|
|
23
|
+
global _session, _runtime_client, _rerank_agent_client, _client_generation
|
|
24
|
+
|
|
25
|
+
aws = get_aws_config()
|
|
26
|
+
bedrock = get_bedrock_config()
|
|
27
|
+
logger.info(
|
|
28
|
+
"building Bedrock clients: profile=%r region=%s rerank_region=%s",
|
|
29
|
+
aws.profile, aws.region, bedrock.rerank_region,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
profile = (aws.profile or "").strip()
|
|
33
|
+
try:
|
|
34
|
+
_session = (
|
|
35
|
+
boto3.Session(profile_name=profile, region_name=aws.region)
|
|
36
|
+
if profile
|
|
37
|
+
else boto3.Session(region_name=aws.region)
|
|
38
|
+
)
|
|
39
|
+
except ProfileNotFound:
|
|
40
|
+
logger.warning("profile %r not found, falling back to default session", profile)
|
|
41
|
+
_session = boto3.Session(region_name=aws.region)
|
|
42
|
+
|
|
43
|
+
retry_cfg = Config(
|
|
44
|
+
retries={"total_max_attempts": 6, "mode": "standard"},
|
|
45
|
+
connect_timeout=10,
|
|
46
|
+
read_timeout=3600,
|
|
47
|
+
)
|
|
48
|
+
_runtime_client = _session.client(
|
|
49
|
+
"bedrock-runtime", region_name=aws.region, config=retry_cfg
|
|
50
|
+
)
|
|
51
|
+
_rerank_agent_client = _session.client(
|
|
52
|
+
"bedrock-agent-runtime", region_name=bedrock.rerank_region, config=retry_cfg
|
|
53
|
+
)
|
|
54
|
+
_client_generation += 1
|
|
55
|
+
_model_cache.clear()
|
|
56
|
+
logger.debug("clients built (generation=%d)", _client_generation)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ensure() -> None:
|
|
60
|
+
if _runtime_client is None:
|
|
61
|
+
_build_clients()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_runtime_client():
|
|
65
|
+
"""Return the shared boto3 bedrock-runtime client (LLM + embeddings)."""
|
|
66
|
+
with _lock:
|
|
67
|
+
_ensure()
|
|
68
|
+
return _runtime_client
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_rerank_agent_client():
|
|
72
|
+
"""Return the boto3 bedrock-agent-runtime client bound to cfg.rerank_region."""
|
|
73
|
+
with _lock:
|
|
74
|
+
_ensure()
|
|
75
|
+
return _rerank_agent_client
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def reset_clients() -> None:
|
|
79
|
+
"""Force client recreation on the next call (e.g. after credential rotation)."""
|
|
80
|
+
global _session, _runtime_client, _rerank_agent_client
|
|
81
|
+
with _lock:
|
|
82
|
+
logger.info("resetting Bedrock clients (next call will rebuild)")
|
|
83
|
+
_session = None
|
|
84
|
+
_runtime_client = None
|
|
85
|
+
_rerank_agent_client = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_model(key: str) -> object | None:
|
|
89
|
+
"""Return a cached model instance if still valid for the current client generation."""
|
|
90
|
+
with _lock:
|
|
91
|
+
_ensure()
|
|
92
|
+
entry = _model_cache.get(key)
|
|
93
|
+
if entry is not None and entry[1] == _client_generation:
|
|
94
|
+
return entry[0]
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cache_model(key: str, model: object) -> None:
|
|
99
|
+
"""Store a model instance keyed to the current client generation."""
|
|
100
|
+
with _lock:
|
|
101
|
+
_model_cache[key] = (model, _client_generation)
|