embed-client 1.0.0.1__py3-none-any.whl → 1.0.1.1__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.
- embed_client/async_client.py +324 -46
- embed_client/example_async_usage.py +128 -32
- embed_client/example_async_usage_ru.py +34 -8
- {embed_client-1.0.0.1.dist-info → embed_client-1.0.1.1.dist-info}/METADATA +1 -1
- embed_client-1.0.1.1.dist-info/RECORD +8 -0
- embed_client-1.0.0.1.dist-info/RECORD +0 -8
- {embed_client-1.0.0.1.dist-info → embed_client-1.0.1.1.dist-info}/WHEEL +0 -0
- {embed_client-1.0.0.1.dist-info → embed_client-1.0.1.1.dist-info}/top_level.txt +0 -0
embed_client/async_client.py
CHANGED
@@ -4,11 +4,15 @@ Async client for Embedding Service API (OpenAPI 3.0.2)
|
|
4
4
|
- 100% type-annotated
|
5
5
|
- English docstrings and examples
|
6
6
|
- Ready for PyPi
|
7
|
+
- Supports new API format with body, embedding, and chunks
|
7
8
|
"""
|
8
9
|
|
9
10
|
from typing import Any, Dict, List, Optional, Union
|
10
11
|
import aiohttp
|
12
|
+
import asyncio
|
11
13
|
import os
|
14
|
+
import json
|
15
|
+
import logging
|
12
16
|
|
13
17
|
class EmbeddingServiceError(Exception):
|
14
18
|
"""Base exception for EmbeddingServiceAsyncClient."""
|
@@ -29,28 +33,73 @@ class EmbeddingServiceAPIError(EmbeddingServiceError):
|
|
29
33
|
super().__init__(f"API error: {error}")
|
30
34
|
self.error = error
|
31
35
|
|
36
|
+
class EmbeddingServiceConfigError(EmbeddingServiceError):
|
37
|
+
"""Raised for configuration errors (invalid base_url, port, etc.)."""
|
38
|
+
|
39
|
+
class EmbeddingServiceTimeoutError(EmbeddingServiceError):
|
40
|
+
"""Raised when request times out."""
|
41
|
+
|
42
|
+
class EmbeddingServiceJSONError(EmbeddingServiceError):
|
43
|
+
"""Raised when JSON parsing fails."""
|
44
|
+
|
32
45
|
class EmbeddingServiceAsyncClient:
|
33
46
|
"""
|
34
47
|
Asynchronous client for the Embedding Service API.
|
48
|
+
|
49
|
+
Supports both old and new API formats:
|
50
|
+
- Old format: {"result": {"success": true, "data": {"embeddings": [...]}}}
|
51
|
+
- New format: {"result": {"success": true, "data": [{"body": "text", "embedding": [...], "chunks": [...]}]}}
|
52
|
+
|
35
53
|
Args:
|
36
54
|
base_url (str): Base URL of the embedding service (e.g., "http://localhost").
|
37
55
|
port (int): Port of the embedding service (e.g., 8001).
|
56
|
+
timeout (float): Request timeout in seconds (default: 30).
|
38
57
|
Raises:
|
39
|
-
|
58
|
+
EmbeddingServiceConfigError: If base_url or port is invalid.
|
40
59
|
"""
|
41
|
-
def __init__(self, base_url: Optional[str] = None, port: Optional[int] = None):
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
60
|
+
def __init__(self, base_url: Optional[str] = None, port: Optional[int] = None, timeout: float = 30.0):
|
61
|
+
# Validate and set base_url
|
62
|
+
try:
|
63
|
+
self.base_url = base_url or os.getenv("EMBEDDING_SERVICE_BASE_URL", "http://localhost")
|
64
|
+
if not self.base_url:
|
65
|
+
raise EmbeddingServiceConfigError("base_url must be provided.")
|
66
|
+
if not isinstance(self.base_url, str):
|
67
|
+
raise EmbeddingServiceConfigError("base_url must be a string.")
|
68
|
+
|
69
|
+
# Validate URL format
|
70
|
+
if not (self.base_url.startswith("http://") or self.base_url.startswith("https://")):
|
71
|
+
raise EmbeddingServiceConfigError("base_url must start with http:// or https://")
|
72
|
+
except (TypeError, AttributeError) as e:
|
73
|
+
raise EmbeddingServiceConfigError(f"Invalid base_url configuration: {e}") from e
|
74
|
+
|
75
|
+
# Validate and set port
|
76
|
+
try:
|
77
|
+
port_env = os.getenv("EMBEDDING_SERVICE_PORT", "8001")
|
78
|
+
self.port = port if port is not None else int(port_env)
|
79
|
+
if self.port is None:
|
80
|
+
raise EmbeddingServiceConfigError("port must be provided.")
|
81
|
+
if not isinstance(self.port, int) or self.port <= 0 or self.port > 65535:
|
82
|
+
raise EmbeddingServiceConfigError("port must be a valid integer between 1 and 65535.")
|
83
|
+
except (ValueError, TypeError) as e:
|
84
|
+
raise EmbeddingServiceConfigError(f"Invalid port configuration: {e}") from e
|
85
|
+
|
86
|
+
# Validate timeout
|
87
|
+
try:
|
88
|
+
self.timeout = float(timeout)
|
89
|
+
if self.timeout <= 0:
|
90
|
+
raise EmbeddingServiceConfigError("timeout must be positive.")
|
91
|
+
except (ValueError, TypeError) as e:
|
92
|
+
raise EmbeddingServiceConfigError(f"Invalid timeout configuration: {e}") from e
|
93
|
+
|
48
94
|
self._session: Optional[aiohttp.ClientSession] = None
|
49
95
|
|
50
96
|
def _make_url(self, path: str, base_url: Optional[str] = None, port: Optional[int] = None) -> str:
|
51
|
-
|
52
|
-
|
53
|
-
|
97
|
+
try:
|
98
|
+
url = (base_url or self.base_url).rstrip("/")
|
99
|
+
port_val = port if port is not None else self.port
|
100
|
+
return f"{url}:{port_val}{path}"
|
101
|
+
except Exception as e:
|
102
|
+
raise EmbeddingServiceConfigError(f"Failed to construct URL: {e}") from e
|
54
103
|
|
55
104
|
def _format_error_response(self, error: str, lang: Optional[str] = None, text: Optional[str] = None) -> Dict[str, Any]:
|
56
105
|
"""
|
@@ -69,14 +118,158 @@ class EmbeddingServiceAsyncClient:
|
|
69
118
|
response["text"] = text
|
70
119
|
return response
|
71
120
|
|
121
|
+
def extract_embeddings(self, result: Dict[str, Any]) -> List[List[float]]:
|
122
|
+
"""
|
123
|
+
Extract embeddings from API response, supporting both old and new formats.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
result: API response dictionary
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
List of embedding vectors (list of lists of floats)
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
ValueError: If embeddings cannot be extracted from the response
|
133
|
+
"""
|
134
|
+
# Handle direct embeddings field (old format compatibility)
|
135
|
+
if "embeddings" in result:
|
136
|
+
return result["embeddings"]
|
137
|
+
|
138
|
+
# Handle result wrapper
|
139
|
+
if "result" in result:
|
140
|
+
res = result["result"]
|
141
|
+
|
142
|
+
# Handle direct list in result (old format)
|
143
|
+
if isinstance(res, list):
|
144
|
+
return res
|
145
|
+
|
146
|
+
if isinstance(res, dict):
|
147
|
+
# Handle old format: result.embeddings
|
148
|
+
if "embeddings" in res:
|
149
|
+
return res["embeddings"]
|
150
|
+
|
151
|
+
# Handle old format: result.data.embeddings
|
152
|
+
if "data" in res and isinstance(res["data"], dict) and "embeddings" in res["data"]:
|
153
|
+
return res["data"]["embeddings"]
|
154
|
+
|
155
|
+
# Handle new format: result.data[].embedding
|
156
|
+
if "data" in res and isinstance(res["data"], list):
|
157
|
+
embeddings = []
|
158
|
+
for item in res["data"]:
|
159
|
+
if isinstance(item, dict) and "embedding" in item:
|
160
|
+
embeddings.append(item["embedding"])
|
161
|
+
else:
|
162
|
+
raise ValueError(f"Invalid item format in new API response: {item}")
|
163
|
+
return embeddings
|
164
|
+
|
165
|
+
raise ValueError(f"Cannot extract embeddings from response: {result}")
|
166
|
+
|
167
|
+
def extract_embedding_data(self, result: Dict[str, Any]) -> List[Dict[str, Any]]:
|
168
|
+
"""
|
169
|
+
Extract full embedding data from API response (new format only).
|
170
|
+
|
171
|
+
Args:
|
172
|
+
result: API response dictionary
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
List of dictionaries with 'body', 'embedding', and 'chunks' fields
|
176
|
+
|
177
|
+
Raises:
|
178
|
+
ValueError: If data cannot be extracted or is in old format
|
179
|
+
"""
|
180
|
+
if "result" in result and isinstance(result["result"], dict):
|
181
|
+
res = result["result"]
|
182
|
+
if "data" in res and isinstance(res["data"], list):
|
183
|
+
# Validate that all items have required fields
|
184
|
+
for i, item in enumerate(res["data"]):
|
185
|
+
if not isinstance(item, dict):
|
186
|
+
raise ValueError(f"Item {i} is not a dictionary: {item}")
|
187
|
+
if "body" not in item:
|
188
|
+
raise ValueError(f"Item {i} missing 'body' field: {item}")
|
189
|
+
if "embedding" not in item:
|
190
|
+
raise ValueError(f"Item {i} missing 'embedding' field: {item}")
|
191
|
+
if "chunks" not in item:
|
192
|
+
raise ValueError(f"Item {i} missing 'chunks' field: {item}")
|
193
|
+
|
194
|
+
return res["data"]
|
195
|
+
|
196
|
+
raise ValueError(f"Cannot extract embedding data from response (new format required): {result}")
|
197
|
+
|
198
|
+
def extract_texts(self, result: Dict[str, Any]) -> List[str]:
|
199
|
+
"""
|
200
|
+
Extract original texts from API response (new format only).
|
201
|
+
|
202
|
+
Args:
|
203
|
+
result: API response dictionary
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
List of original text strings
|
207
|
+
|
208
|
+
Raises:
|
209
|
+
ValueError: If texts cannot be extracted or is in old format
|
210
|
+
"""
|
211
|
+
data = self.extract_embedding_data(result)
|
212
|
+
return [item["body"] for item in data]
|
213
|
+
|
214
|
+
def extract_chunks(self, result: Dict[str, Any]) -> List[List[str]]:
|
215
|
+
"""
|
216
|
+
Extract text chunks from API response (new format only).
|
217
|
+
|
218
|
+
Args:
|
219
|
+
result: API response dictionary
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
List of chunk lists for each text
|
223
|
+
|
224
|
+
Raises:
|
225
|
+
ValueError: If chunks cannot be extracted or is in old format
|
226
|
+
"""
|
227
|
+
data = self.extract_embedding_data(result)
|
228
|
+
return [item["chunks"] for item in data]
|
229
|
+
|
72
230
|
async def __aenter__(self):
|
73
|
-
|
74
|
-
|
231
|
+
try:
|
232
|
+
# Create session with timeout configuration
|
233
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
234
|
+
self._session = aiohttp.ClientSession(timeout=timeout)
|
235
|
+
return self
|
236
|
+
except Exception as e:
|
237
|
+
raise EmbeddingServiceError(f"Failed to create HTTP session: {e}") from e
|
75
238
|
|
76
239
|
async def __aexit__(self, exc_type, exc, tb):
|
77
240
|
if self._session:
|
78
|
-
|
79
|
-
|
241
|
+
try:
|
242
|
+
await self._session.close()
|
243
|
+
except Exception as e:
|
244
|
+
raise EmbeddingServiceError(f"Failed to close HTTP session: {e}") from e
|
245
|
+
finally:
|
246
|
+
self._session = None
|
247
|
+
|
248
|
+
async def _parse_json_response(self, resp: aiohttp.ClientResponse) -> Dict[str, Any]:
|
249
|
+
"""
|
250
|
+
Parse JSON response with proper error handling.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
resp: aiohttp response object
|
254
|
+
|
255
|
+
Returns:
|
256
|
+
dict: Parsed JSON data
|
257
|
+
|
258
|
+
Raises:
|
259
|
+
EmbeddingServiceJSONError: If JSON parsing fails
|
260
|
+
"""
|
261
|
+
try:
|
262
|
+
return await resp.json()
|
263
|
+
except json.JSONDecodeError as e:
|
264
|
+
try:
|
265
|
+
text = await resp.text()
|
266
|
+
raise EmbeddingServiceJSONError(f"Invalid JSON response: {e}. Response text: {text[:500]}...") from e
|
267
|
+
except Exception as text_error:
|
268
|
+
raise EmbeddingServiceJSONError(f"Invalid JSON response: {e}. Failed to get response text: {text_error}") from e
|
269
|
+
except UnicodeDecodeError as e:
|
270
|
+
raise EmbeddingServiceJSONError(f"Unicode decode error in response: {e}") from e
|
271
|
+
except Exception as e:
|
272
|
+
raise EmbeddingServiceJSONError(f"Unexpected error parsing JSON: {e}") from e
|
80
273
|
|
81
274
|
async def health(self, base_url: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]:
|
82
275
|
"""
|
@@ -89,17 +282,35 @@ class EmbeddingServiceAsyncClient:
|
|
89
282
|
"""
|
90
283
|
url = self._make_url("/health", base_url, port)
|
91
284
|
try:
|
92
|
-
async with self._session.get(url) as resp:
|
285
|
+
async with self._session.get(url, timeout=self.timeout) as resp:
|
93
286
|
await self._raise_for_status(resp)
|
94
|
-
|
287
|
+
try:
|
288
|
+
data = await resp.json()
|
289
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
290
|
+
raise EmbeddingServiceJSONError(f"Invalid JSON response: {e}") from e
|
291
|
+
if "error" in data:
|
292
|
+
raise EmbeddingServiceAPIError(data["error"])
|
293
|
+
return data
|
95
294
|
except EmbeddingServiceHTTPError:
|
96
295
|
raise
|
97
296
|
except EmbeddingServiceConnectionError:
|
98
297
|
raise
|
298
|
+
except EmbeddingServiceJSONError:
|
299
|
+
raise
|
300
|
+
except EmbeddingServiceTimeoutError:
|
301
|
+
raise
|
99
302
|
except aiohttp.ClientConnectionError as e:
|
100
303
|
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
101
304
|
except aiohttp.ClientResponseError as e:
|
102
305
|
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
306
|
+
except asyncio.TimeoutError as e:
|
307
|
+
raise EmbeddingServiceTimeoutError(f"Request timeout: {e}") from e
|
308
|
+
except aiohttp.ServerTimeoutError as e:
|
309
|
+
raise EmbeddingServiceTimeoutError(f"Server timeout: {e}") from e
|
310
|
+
except aiohttp.ClientSSLError as e:
|
311
|
+
raise EmbeddingServiceConnectionError(f"SSL error: {e}") from e
|
312
|
+
except aiohttp.ClientOSError as e:
|
313
|
+
raise EmbeddingServiceConnectionError(f"OS error: {e}") from e
|
103
314
|
except Exception as e:
|
104
315
|
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
105
316
|
|
@@ -114,17 +325,35 @@ class EmbeddingServiceAsyncClient:
|
|
114
325
|
"""
|
115
326
|
url = self._make_url("/openapi.json", base_url, port)
|
116
327
|
try:
|
117
|
-
async with self._session.get(url) as resp:
|
328
|
+
async with self._session.get(url, timeout=self.timeout) as resp:
|
118
329
|
await self._raise_for_status(resp)
|
119
|
-
|
330
|
+
try:
|
331
|
+
data = await resp.json()
|
332
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
333
|
+
raise EmbeddingServiceJSONError(f"Invalid JSON response: {e}") from e
|
334
|
+
if "error" in data:
|
335
|
+
raise EmbeddingServiceAPIError(data["error"])
|
336
|
+
return data
|
120
337
|
except EmbeddingServiceHTTPError:
|
121
338
|
raise
|
122
339
|
except EmbeddingServiceConnectionError:
|
123
340
|
raise
|
341
|
+
except EmbeddingServiceJSONError:
|
342
|
+
raise
|
343
|
+
except EmbeddingServiceTimeoutError:
|
344
|
+
raise
|
124
345
|
except aiohttp.ClientConnectionError as e:
|
125
346
|
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
126
347
|
except aiohttp.ClientResponseError as e:
|
127
348
|
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
349
|
+
except asyncio.TimeoutError as e:
|
350
|
+
raise EmbeddingServiceTimeoutError(f"Request timeout: {e}") from e
|
351
|
+
except aiohttp.ServerTimeoutError as e:
|
352
|
+
raise EmbeddingServiceTimeoutError(f"Server timeout: {e}") from e
|
353
|
+
except aiohttp.ClientSSLError as e:
|
354
|
+
raise EmbeddingServiceConnectionError(f"SSL error: {e}") from e
|
355
|
+
except aiohttp.ClientOSError as e:
|
356
|
+
raise EmbeddingServiceConnectionError(f"OS error: {e}") from e
|
128
357
|
except Exception as e:
|
129
358
|
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
130
359
|
|
@@ -139,17 +368,35 @@ class EmbeddingServiceAsyncClient:
|
|
139
368
|
"""
|
140
369
|
url = self._make_url("/api/commands", base_url, port)
|
141
370
|
try:
|
142
|
-
async with self._session.get(url) as resp:
|
371
|
+
async with self._session.get(url, timeout=self.timeout) as resp:
|
143
372
|
await self._raise_for_status(resp)
|
144
|
-
|
373
|
+
try:
|
374
|
+
data = await resp.json()
|
375
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
376
|
+
raise EmbeddingServiceJSONError(f"Invalid JSON response: {e}") from e
|
377
|
+
if "error" in data:
|
378
|
+
raise EmbeddingServiceAPIError(data["error"])
|
379
|
+
return data
|
145
380
|
except EmbeddingServiceHTTPError:
|
146
381
|
raise
|
147
382
|
except EmbeddingServiceConnectionError:
|
148
383
|
raise
|
384
|
+
except EmbeddingServiceJSONError:
|
385
|
+
raise
|
386
|
+
except EmbeddingServiceTimeoutError:
|
387
|
+
raise
|
149
388
|
except aiohttp.ClientConnectionError as e:
|
150
389
|
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
151
390
|
except aiohttp.ClientResponseError as e:
|
152
391
|
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
392
|
+
except asyncio.TimeoutError as e:
|
393
|
+
raise EmbeddingServiceTimeoutError(f"Request timeout: {e}") from e
|
394
|
+
except aiohttp.ServerTimeoutError as e:
|
395
|
+
raise EmbeddingServiceTimeoutError(f"Server timeout: {e}") from e
|
396
|
+
except aiohttp.ClientSSLError as e:
|
397
|
+
raise EmbeddingServiceConnectionError(f"SSL error: {e}") from e
|
398
|
+
except aiohttp.ClientOSError as e:
|
399
|
+
raise EmbeddingServiceConnectionError(f"OS error: {e}") from e
|
153
400
|
except Exception as e:
|
154
401
|
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
155
402
|
|
@@ -221,41 +468,54 @@ class EmbeddingServiceAsyncClient:
|
|
221
468
|
if command == "embed" and params and "texts" in params:
|
222
469
|
self._validate_texts(params["texts"])
|
223
470
|
|
471
|
+
logger = logging.getLogger('EmbeddingServiceAsyncClient.cmd')
|
224
472
|
url = self._make_url("/cmd", base_url, port)
|
225
473
|
payload = {"command": command}
|
226
474
|
if params is not None:
|
227
475
|
payload["params"] = params
|
228
|
-
|
476
|
+
logger.info(f"Sending embedding command: url={url}, payload={payload}")
|
229
477
|
try:
|
230
|
-
async with self._session.post(url, json=payload) as resp:
|
478
|
+
async with self._session.post(url, json=payload, timeout=self.timeout) as resp:
|
479
|
+
logger.info(f"Embedding service HTTP status: {resp.status}")
|
231
480
|
await self._raise_for_status(resp)
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
raise
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
481
|
+
try:
|
482
|
+
resp_json = await resp.json()
|
483
|
+
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
484
|
+
raise EmbeddingServiceJSONError(f"Invalid JSON response: {e}") from e
|
485
|
+
logger.info(f"Embedding service response: {str(resp_json)[:300]}")
|
486
|
+
# Обработка ошибок API
|
487
|
+
if "error" in resp_json:
|
488
|
+
raise EmbeddingServiceAPIError(resp_json["error"])
|
489
|
+
if "result" in resp_json:
|
490
|
+
result = resp_json["result"]
|
491
|
+
if isinstance(result, dict) and (result.get("success") is False or "error" in result):
|
492
|
+
raise EmbeddingServiceAPIError(result.get("error", result))
|
493
|
+
return resp_json
|
494
|
+
except EmbeddingServiceAPIError:
|
495
|
+
raise
|
496
|
+
except EmbeddingServiceHTTPError:
|
497
|
+
raise
|
498
|
+
except EmbeddingServiceConnectionError:
|
499
|
+
raise
|
500
|
+
except EmbeddingServiceJSONError:
|
501
|
+
raise
|
502
|
+
except EmbeddingServiceTimeoutError:
|
503
|
+
raise
|
504
|
+
except aiohttp.ServerTimeoutError as e:
|
505
|
+
raise EmbeddingServiceTimeoutError(f"Server timeout: {e}") from e
|
245
506
|
except aiohttp.ClientConnectionError as e:
|
246
|
-
raise
|
247
|
-
"code": -32000,
|
248
|
-
"message": f"Connection error: {e}"
|
249
|
-
}) from e
|
507
|
+
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
250
508
|
except aiohttp.ClientResponseError as e:
|
251
509
|
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
252
|
-
except
|
253
|
-
raise
|
510
|
+
except asyncio.TimeoutError as e:
|
511
|
+
raise EmbeddingServiceTimeoutError(f"Request timeout: {e}") from e
|
512
|
+
except aiohttp.ClientSSLError as e:
|
513
|
+
raise EmbeddingServiceConnectionError(f"SSL error: {e}") from e
|
514
|
+
except aiohttp.ClientOSError as e:
|
515
|
+
raise EmbeddingServiceConnectionError(f"OS error: {e}") from e
|
254
516
|
except Exception as e:
|
255
|
-
|
256
|
-
|
257
|
-
"message": f"Unexpected error: {e}"
|
258
|
-
}) from e
|
517
|
+
logger.error(f"Error in embedding cmd: {e}", exc_info=True)
|
518
|
+
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
259
519
|
|
260
520
|
async def _raise_for_status(self, resp: aiohttp.ClientResponse):
|
261
521
|
try:
|
@@ -263,4 +523,22 @@ class EmbeddingServiceAsyncClient:
|
|
263
523
|
except aiohttp.ClientResponseError as e:
|
264
524
|
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
265
525
|
|
526
|
+
async def close(self) -> None:
|
527
|
+
"""
|
528
|
+
Close the underlying HTTP session explicitly.
|
529
|
+
|
530
|
+
This method allows the user to manually close the aiohttp.ClientSession used by the client.
|
531
|
+
It is safe to call multiple times; if the session is already closed or was never opened, nothing happens.
|
532
|
+
|
533
|
+
Raises:
|
534
|
+
EmbeddingServiceError: If closing the session fails.
|
535
|
+
"""
|
536
|
+
if self._session:
|
537
|
+
try:
|
538
|
+
await self._session.close()
|
539
|
+
except Exception as e:
|
540
|
+
raise EmbeddingServiceError(f"Failed to close HTTP session: {e}") from e
|
541
|
+
finally:
|
542
|
+
self._session = None
|
543
|
+
|
266
544
|
# TODO: Add methods for /cmd, /api/commands, etc.
|
@@ -1,19 +1,51 @@
|
|
1
1
|
"""
|
2
2
|
Example usage of EmbeddingServiceAsyncClient.
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
USAGE:
|
5
|
+
python embed_client/example_async_usage.py --base-url http://localhost --port 8001
|
6
|
+
# или
|
7
|
+
python -m asyncio embed_client/example_async_usage.py --base-url http://localhost --port 8001
|
8
|
+
|
9
|
+
# Можно также использовать переменные окружения:
|
10
|
+
export EMBED_CLIENT_BASE_URL=http://localhost
|
11
|
+
export EMBED_CLIENT_PORT=8001
|
12
|
+
python embed_client/example_async_usage.py
|
13
|
+
|
14
|
+
# ВАЖНО:
|
15
|
+
# --base-url и --port должны быть отдельными аргументами (через пробел),
|
16
|
+
# а не через = (НЕ --base_url=...)
|
17
|
+
# base_url должен содержать http:// или https://
|
6
18
|
|
7
|
-
|
19
|
+
EXAMPLES:
|
20
|
+
python embed_client/example_async_usage.py --base-url http://localhost --port 8001
|
8
21
|
python -m asyncio embed_client/example_async_usage.py --base-url http://localhost --port 8001
|
22
|
+
export EMBED_CLIENT_BASE_URL=http://localhost
|
23
|
+
export EMBED_CLIENT_PORT=8001
|
24
|
+
python embed_client/example_async_usage.py
|
9
25
|
|
10
|
-
|
26
|
+
Explicit session close example:
|
27
|
+
import asyncio
|
28
|
+
from embed_client.async_client import EmbeddingServiceAsyncClient
|
29
|
+
async def main():
|
30
|
+
client = EmbeddingServiceAsyncClient(base_url="http://localhost", port=8001)
|
31
|
+
# ... use client ...
|
32
|
+
await client.close() # Explicitly close session
|
33
|
+
asyncio.run(main())
|
11
34
|
"""
|
12
35
|
|
13
36
|
import asyncio
|
14
37
|
import sys
|
15
38
|
import os
|
16
|
-
from embed_client.async_client import
|
39
|
+
from embed_client.async_client import (
|
40
|
+
EmbeddingServiceAsyncClient,
|
41
|
+
EmbeddingServiceError,
|
42
|
+
EmbeddingServiceAPIError,
|
43
|
+
EmbeddingServiceHTTPError,
|
44
|
+
EmbeddingServiceConnectionError,
|
45
|
+
EmbeddingServiceTimeoutError,
|
46
|
+
EmbeddingServiceJSONError,
|
47
|
+
EmbeddingServiceConfigError
|
48
|
+
)
|
17
49
|
|
18
50
|
def get_params():
|
19
51
|
base_url = None
|
@@ -33,37 +65,101 @@ def get_params():
|
|
33
65
|
return None, None
|
34
66
|
return base_url, int(port)
|
35
67
|
|
68
|
+
def extract_vectors(result):
|
69
|
+
"""Extract embeddings from the API response, supporting both old and new formats."""
|
70
|
+
# Handle direct embeddings field (old format compatibility)
|
71
|
+
if "embeddings" in result:
|
72
|
+
return result["embeddings"]
|
73
|
+
|
74
|
+
# Handle result wrapper
|
75
|
+
if "result" in result:
|
76
|
+
res = result["result"]
|
77
|
+
|
78
|
+
# Handle direct list in result (old format)
|
79
|
+
if isinstance(res, list):
|
80
|
+
return res
|
81
|
+
|
82
|
+
if isinstance(res, dict):
|
83
|
+
# Handle old format: result.embeddings
|
84
|
+
if "embeddings" in res:
|
85
|
+
return res["embeddings"]
|
86
|
+
|
87
|
+
# Handle old format: result.data.embeddings
|
88
|
+
if "data" in res and isinstance(res["data"], dict) and "embeddings" in res["data"]:
|
89
|
+
return res["data"]["embeddings"]
|
90
|
+
|
91
|
+
# Handle new format: result.data[].embedding
|
92
|
+
if "data" in res and isinstance(res["data"], list):
|
93
|
+
embeddings = []
|
94
|
+
for item in res["data"]:
|
95
|
+
if isinstance(item, dict) and "embedding" in item:
|
96
|
+
embeddings.append(item["embedding"])
|
97
|
+
else:
|
98
|
+
raise ValueError(f"Invalid item format in new API response: {item}")
|
99
|
+
return embeddings
|
100
|
+
|
101
|
+
raise ValueError(f"Cannot extract embeddings from response: {result}")
|
102
|
+
|
36
103
|
async def main():
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
|
41
|
-
print("
|
104
|
+
try:
|
105
|
+
base_url, port = get_params()
|
106
|
+
# Explicit open/close example
|
107
|
+
client = EmbeddingServiceAsyncClient(base_url=base_url, port=port)
|
108
|
+
print("Explicit session open/close example:")
|
109
|
+
await client.close()
|
110
|
+
print("Session closed explicitly (manual close example).\n")
|
111
|
+
async with EmbeddingServiceAsyncClient(base_url=base_url, port=port) as client:
|
112
|
+
# Check health
|
113
|
+
try:
|
114
|
+
health = await client.health()
|
115
|
+
print("Service health:", health)
|
116
|
+
except EmbeddingServiceConnectionError as e:
|
117
|
+
print(f"Connection error during health check: {e}")
|
118
|
+
return
|
119
|
+
except EmbeddingServiceTimeoutError as e:
|
120
|
+
print(f"Timeout error during health check: {e}")
|
121
|
+
except EmbeddingServiceError as e:
|
122
|
+
print(f"Error during health check: {e}")
|
42
123
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
print(f"
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
124
|
+
# Request embeddings for a list of texts
|
125
|
+
texts = ["hello world", "test embedding"]
|
126
|
+
try:
|
127
|
+
result = await client.cmd("embed", params={"texts": texts})
|
128
|
+
vectors = extract_vectors(result)
|
129
|
+
print(f"Embeddings for {len(texts)} texts:")
|
130
|
+
for i, vec in enumerate(vectors):
|
131
|
+
print(f" Text: {texts[i]!r}\n Vector: {vec[:5]}... (total {len(vec)} dims)")
|
132
|
+
except EmbeddingServiceAPIError as e:
|
133
|
+
print(f"API error during embedding: {e}")
|
134
|
+
except EmbeddingServiceConnectionError as e:
|
135
|
+
print(f"Connection error during embedding: {e}")
|
136
|
+
except EmbeddingServiceTimeoutError as e:
|
137
|
+
print(f"Timeout error during embedding: {e}")
|
138
|
+
except EmbeddingServiceError as e:
|
139
|
+
print(f"Error during embedding: {e}")
|
140
|
+
|
141
|
+
# Example: health check via cmd
|
142
|
+
try:
|
143
|
+
result = await client.cmd("health")
|
144
|
+
print("Health check result:", result)
|
145
|
+
except EmbeddingServiceError as e:
|
146
|
+
print(f"Error during health command: {e}")
|
58
147
|
|
59
|
-
|
60
|
-
|
61
|
-
|
148
|
+
# Example: error handling for empty command
|
149
|
+
try:
|
150
|
+
result = await client.cmd("")
|
151
|
+
print("Empty command result:", result)
|
152
|
+
except EmbeddingServiceAPIError as e:
|
153
|
+
print(f"Expected error for empty command: {e}")
|
154
|
+
except EmbeddingServiceError as e:
|
155
|
+
print(f"Error for empty command: {e}")
|
62
156
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
157
|
+
except EmbeddingServiceConfigError as e:
|
158
|
+
print(f"Configuration error: {e}")
|
159
|
+
sys.exit(1)
|
160
|
+
except Exception as e:
|
161
|
+
print(f"Unexpected error: {e}")
|
162
|
+
sys.exit(1)
|
67
163
|
|
68
164
|
if __name__ == "__main__":
|
69
165
|
asyncio.run(main())
|
@@ -1,13 +1,27 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Пример использования EmbeddingServiceAsyncClient (асинхронный клиент).
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
Run this script with:
|
4
|
+
USAGE:
|
5
|
+
python embed_client/example_async_usage_ru.py --base-url http://localhost --port 8001
|
6
|
+
# или
|
8
7
|
python -m asyncio embed_client/example_async_usage_ru.py --base-url http://localhost --port 8001
|
9
8
|
|
10
|
-
|
9
|
+
# Можно также использовать переменные окружения:
|
10
|
+
export EMBED_CLIENT_BASE_URL=http://localhost
|
11
|
+
export EMBED_CLIENT_PORT=8001
|
12
|
+
python embed_client/example_async_usage_ru.py
|
13
|
+
|
14
|
+
# ВАЖНО:
|
15
|
+
# --base-url и --port должны быть отдельными аргументами (через пробел),
|
16
|
+
# а не через = (НЕ --base_url=...)
|
17
|
+
# base_url должен содержать http:// или https://
|
18
|
+
|
19
|
+
EXAMPLES:
|
20
|
+
python embed_client/example_async_usage_ru.py --base-url http://localhost --port 8001
|
21
|
+
python -m asyncio embed_client/example_async_usage_ru.py --base-url http://localhost --port 8001
|
22
|
+
export EMBED_CLIENT_BASE_URL=http://localhost
|
23
|
+
export EMBED_CLIENT_PORT=8001
|
24
|
+
python embed_client/example_async_usage_ru.py
|
11
25
|
"""
|
12
26
|
|
13
27
|
import asyncio
|
@@ -34,7 +48,7 @@ def get_params():
|
|
34
48
|
if not port:
|
35
49
|
port = os.environ.get("EMBED_CLIENT_PORT")
|
36
50
|
if not base_url or not port:
|
37
|
-
print("Error: base_url and port must be provided via --base-url
|
51
|
+
print("Error: base_url and port must be provided via [--base-url | --port] arguments or [EMBED_CLIENT_BASE_URL/EMBED_CLIENT_PORT] environment variables.")
|
38
52
|
sys.exit(1)
|
39
53
|
return None, None
|
40
54
|
return base_url, int(port)
|
@@ -62,10 +76,22 @@ async def main():
|
|
62
76
|
texts = ["hello world", "test embedding"]
|
63
77
|
try:
|
64
78
|
result = await client.cmd("embed", params={"texts": texts})
|
65
|
-
|
79
|
+
# Use client's extract method for compatibility with both old and new formats
|
80
|
+
vectors = client.extract_embeddings(result)
|
66
81
|
print(f"Embeddings for {len(texts)} texts:")
|
67
82
|
for i, vec in enumerate(vectors):
|
68
83
|
print(f" Text: {texts[i]!r}\n Vector: {vec[:5]}... (total {len(vec)} dims)")
|
84
|
+
|
85
|
+
# Try to extract additional data if new format is available
|
86
|
+
try:
|
87
|
+
embedding_data = client.extract_embedding_data(result)
|
88
|
+
print("\nAdditional data from new format:")
|
89
|
+
for i, data in enumerate(embedding_data):
|
90
|
+
print(f" Text: {data['body']!r}")
|
91
|
+
print(f" Chunks: {data['chunks']}")
|
92
|
+
except ValueError:
|
93
|
+
print("(Old format detected - no additional data available)")
|
94
|
+
|
69
95
|
except EmbeddingServiceAPIError as e:
|
70
96
|
print("[API error]", e.error)
|
71
97
|
except EmbeddingServiceHTTPError as e:
|
@@ -0,0 +1,8 @@
|
|
1
|
+
embed_client/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
2
|
+
embed_client/async_client.py,sha256=BNGBGtionC6Evcr9yTTZGsMt7r9hH-DRcHguSJMxR8s,23514
|
3
|
+
embed_client/example_async_usage.py,sha256=6oCDALFebTv1o5k7lB7UuiacP9Scvf2r3gVIVtIrsPk,6623
|
4
|
+
embed_client/example_async_usage_ru.py,sha256=0ZFeUCSHoWnKQelK9UQ2Y3hSvFhVvRJ9cosWqxMEF8A,4979
|
5
|
+
embed_client-1.0.1.1.dist-info/METADATA,sha256=BaAFA1F76uxxjtMVGhD5NftdIMvkoWyfwOKahOmuTdk,254
|
6
|
+
embed_client-1.0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
+
embed_client-1.0.1.1.dist-info/top_level.txt,sha256=uG00A4d9o9DFrhiN7goObpeig72Pniby0E7UpDRgyXY,13
|
8
|
+
embed_client-1.0.1.1.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
embed_client/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
2
|
-
embed_client/async_client.py,sha256=sppZ8fPr4XNNLE3M6kLFTL6u5HE3nqo87bxAgt0S0zE,10589
|
3
|
-
embed_client/example_async_usage.py,sha256=df0RRwq2FtqVSL2MHVclfVIJj1wyQUuKZXB-lyVb3Kg,2538
|
4
|
-
embed_client/example_async_usage_ru.py,sha256=kZXQcbEFkx9tWXoCq-AoyvvUY4aCuW1XqPVb1ADWeAM,3558
|
5
|
-
embed_client-1.0.0.1.dist-info/METADATA,sha256=nFDbLecEwcLOuqhJe84hSdboS9vCEhmmX-Ajw9LYark,254
|
6
|
-
embed_client-1.0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
7
|
-
embed_client-1.0.0.1.dist-info/top_level.txt,sha256=uG00A4d9o9DFrhiN7goObpeig72Pniby0E7UpDRgyXY,13
|
8
|
-
embed_client-1.0.0.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|