embed-client 1.0.0__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.
@@ -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
- ValueError: If base_url or port is not provided.
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
- self.base_url = base_url or os.getenv("EMBEDDING_SERVICE_BASE_URL", "http://localhost")
43
- if not self.base_url:
44
- raise ValueError("base_url must be provided.")
45
- self.port = port or int(os.getenv("EMBEDDING_SERVICE_PORT", "8001"))
46
- if self.port is None:
47
- raise ValueError("port must be provided.")
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
- url = (base_url or self.base_url).rstrip("/")
52
- port_val = port if port is not None else self.port
53
- return f"{url}:{port_val}{path}"
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
- self._session = aiohttp.ClientSession()
74
- return self
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
- await self._session.close()
79
- self._session = None
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
- return await resp.json()
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
- return await resp.json()
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,20 +368,69 @@ 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
- return await resp.json()
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
 
403
+ def _validate_texts(self, texts: List[str]) -> None:
404
+ """
405
+ Validate input texts before sending to the API.
406
+ Args:
407
+ texts (List[str]): List of texts to validate
408
+ Raises:
409
+ EmbeddingServiceAPIError: If texts are invalid
410
+ """
411
+ if not texts:
412
+ raise EmbeddingServiceAPIError({
413
+ "code": -32602,
414
+ "message": "Empty texts list provided"
415
+ })
416
+
417
+ invalid_texts = []
418
+ for i, text in enumerate(texts):
419
+ if not isinstance(text, str):
420
+ invalid_texts.append(f"Text at index {i} is not a string")
421
+ continue
422
+ if not text or not text.strip():
423
+ invalid_texts.append(f"Text at index {i} is empty or contains only whitespace")
424
+ elif len(text.strip()) < 2: # Минимальная длина текста
425
+ invalid_texts.append(f"Text at index {i} is too short (minimum 2 characters)")
426
+
427
+ if invalid_texts:
428
+ raise EmbeddingServiceAPIError({
429
+ "code": -32602,
430
+ "message": "Invalid input texts",
431
+ "details": invalid_texts
432
+ })
433
+
156
434
  async def cmd(self, command: str, params: Optional[Dict[str, Any]] = None, base_url: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]:
157
435
  """
158
436
  Execute a command via JSON-RPC protocol.
@@ -166,7 +444,8 @@ class EmbeddingServiceAsyncClient:
166
444
  {
167
445
  "error": {
168
446
  "code": <код ошибки>,
169
- "message": <сообщение об ошибке>
447
+ "message": <сообщение об ошибке>,
448
+ "details": <опциональные детали ошибки>
170
449
  }
171
450
  }
172
451
  или
@@ -180,31 +459,63 @@ class EmbeddingServiceAsyncClient:
180
459
  }
181
460
  """
182
461
  if not command:
183
- raise EmbeddingServiceAPIError("Command is required")
462
+ raise EmbeddingServiceAPIError({
463
+ "code": -32602,
464
+ "message": "Command is required"
465
+ })
466
+
467
+ # Валидация текстов для команды embed
468
+ if command == "embed" and params and "texts" in params:
469
+ self._validate_texts(params["texts"])
470
+
471
+ logger = logging.getLogger('EmbeddingServiceAsyncClient.cmd')
184
472
  url = self._make_url("/cmd", base_url, port)
185
473
  payload = {"command": command}
186
474
  if params is not None:
187
475
  payload["params"] = params
476
+ logger.info(f"Sending embedding command: url={url}, payload={payload}")
188
477
  try:
189
- 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}")
190
480
  await self._raise_for_status(resp)
191
- data = await resp.json()
192
- if "error" in data:
193
- raise EmbeddingServiceAPIError(data["error"])
194
- if "result" in data:
195
- res = data["result"]
196
- if isinstance(res, dict) and "success" in res and res["success"] is False:
197
- if "error" in res:
198
- raise EmbeddingServiceAPIError(res["error"])
199
- return data
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
200
506
  except aiohttp.ClientConnectionError as e:
201
- raise EmbeddingServiceAPIError(f"Connection error: {e}") from e
507
+ raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
202
508
  except aiohttp.ClientResponseError as e:
203
509
  raise EmbeddingServiceHTTPError(e.status, e.message) from e
204
- except EmbeddingServiceHTTPError:
205
- 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
206
516
  except Exception as e:
207
- raise EmbeddingServiceAPIError(f"Unexpected error: {e}") from e
517
+ logger.error(f"Error in embedding cmd: {e}", exc_info=True)
518
+ raise EmbeddingServiceError(f"Unexpected error: {e}") from e
208
519
 
209
520
  async def _raise_for_status(self, resp: aiohttp.ClientResponse):
210
521
  try:
@@ -212,4 +523,22 @@ class EmbeddingServiceAsyncClient:
212
523
  except aiohttp.ClientResponseError as e:
213
524
  raise EmbeddingServiceHTTPError(e.status, e.message) from e
214
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
+
215
544
  # TODO: Add methods for /cmd, /api/commands, etc.
@@ -1,19 +1,51 @@
1
1
  """
2
2
  Example usage of EmbeddingServiceAsyncClient.
3
3
 
4
- This example demonstrates how to use the async client to check the health of the embedding service,
5
- request embeddings, and handle all possible errors.
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
- Run this script with:
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
- You can also set EMBED_CLIENT_BASE_URL and EMBED_CLIENT_PORT environment variables.
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 EmbeddingServiceAsyncClient
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
- base_url, port = get_params()
38
- async with EmbeddingServiceAsyncClient(base_url=base_url, port=port) as client:
39
- # Check health
40
- health = await client.health()
41
- print("Service health:", health)
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
- # Request embeddings for a list of texts
44
- texts = ["hello world", "test embedding"]
45
- result = await client.cmd("embed", params={"texts": texts})
46
-
47
- if "error" in result:
48
- print(f"Error occurred: {result['error']}")
49
- if "lang" in result:
50
- print(f"Language: {result['lang']}")
51
- if "text" in result:
52
- print(f"Text: {result['text']}")
53
- else:
54
- vectors = result["result"]
55
- print(f"Embeddings for {len(texts)} texts:")
56
- for i, vec in enumerate(vectors):
57
- print(f" Text: {texts[i]!r}\n Vector: {vec[:5]}... (total {len(vec)} dims)")
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
- # Example: error handling for invalid command
60
- result = await client.cmd("health")
61
- print("Health check result:", result)
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
- # Example: error handling for empty command
64
- # result = await client.cmd("")
65
- # if "error" in result:
66
- # print(f"Error for empty command: {result['error']}")
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
- Example usage of EmbeddingServiceAsyncClient.
2
+ Пример использования EmbeddingServiceAsyncClient (асинхронный клиент).
3
3
 
4
- This example demonstrates how to use the async client to check the health of the embedding service,
5
- request embeddings, and handle all possible exceptions.
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
- You can also set EMBED_CLIENT_BASE_URL and EMBED_CLIENT_PORT environment variables.
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/--port arguments or EMBED_CLIENT_BASE_URL/EMBED_CLIENT_PORT environment variables.")
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
- vectors = result["result"]
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: embed-client
3
- Version: 1.0.0
3
+ Version: 1.0.1.1
4
4
  Summary: Async client for Embedding Service API
5
5
  Author: Your Name
6
6
  Requires-Dist: aiohttp
@@ -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=twSw-YhWdo1yeQLkfbi6ldCrq647TrEhEbPigpsAX6U,8802
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.dist-info/METADATA,sha256=vsgnbBHhgvu1Mkj-NV-iUFs0w03aqSSe1Ju6kDECHk0,252
6
- embed_client-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- embed_client-1.0.0.dist-info/top_level.txt,sha256=uG00A4d9o9DFrhiN7goObpeig72Pniby0E7UpDRgyXY,13
8
- embed_client-1.0.0.dist-info/RECORD,,