weco 0.1.10__py3-none-any.whl → 0.2.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.
weco/client.py DELETED
@@ -1,586 +0,0 @@
1
- import asyncio
2
- import base64
3
- import os
4
- import warnings
5
- from io import BytesIO
6
- from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Union
7
-
8
- import httpx
9
- import requests
10
- from httpx import HTTPStatusError
11
- from PIL import Image
12
-
13
- from .constants import MAX_IMAGE_SIZE_MB, MAX_IMAGE_UPLOADS, MAX_TEXT_LENGTH, SUPPORTED_IMAGE_EXTENSIONS
14
- from .utils import (
15
- generate_random_base16_code,
16
- get_image_size,
17
- is_base64_image,
18
- is_local_image,
19
- is_public_url_image,
20
- preprocess_image,
21
- )
22
-
23
-
24
- class WecoAI:
25
- """A client for the WecoAI function builder API that allows users to build and query specialized functions built by LLMs.
26
- The user must simply provide a task description to build a function, and then query the function with an input to get the result they need.
27
- Our client supports both synchronous and asynchronous request paradigms and uses HTTP/2 for faster communication with the API.
28
- Support for multimodality is included.
29
-
30
- Attributes
31
- ----------
32
- api_key : str
33
- The API key used for authentication.
34
-
35
- timeout : float
36
- The timeout for the HTTP requests in seconds. Default is 120.0.
37
-
38
- http2 : bool
39
- Whether to use HTTP/2 protocol for the HTTP requests. Default is True.
40
- """
41
-
42
- def __init__(self, api_key: Union[str, None] = None, timeout: float = 120.0, http2: bool = True) -> None:
43
- """Initializes the WecoAI client with the provided API key and base URL.
44
-
45
- Parameters
46
- ----------
47
- api_key : str, optional
48
- The API key used for authentication. If not provided, the client will attempt to read it from the environment variable - WECO_API_KEY.
49
-
50
- timeout : float, optional
51
- The timeout for the HTTP requests in seconds (default is 120.0).
52
-
53
- http2 : bool, optional
54
- Whether to use HTTP/2 protocol for the HTTP requests (default is True).
55
-
56
- Raises
57
- ------
58
- ValueError
59
- If the API key is not provided to the client, is not set as an environment variable or is not a string.
60
- """
61
- # Manage the API key
62
- if api_key is None or not isinstance(api_key, str):
63
- try:
64
- api_key = os.environ["WECO_API_KEY"]
65
- except KeyError:
66
- raise ValueError("WECO_API_KEY must be passed to client or set as an environment variable")
67
- self.api_key = api_key
68
- self.http2 = http2
69
- self.timeout = timeout
70
- self.base_url = "https://function.api.weco.ai"
71
-
72
- # Setup clients
73
- self.client = httpx.Client(http2=http2, timeout=timeout)
74
- self.async_client = httpx.AsyncClient(http2=http2, timeout=timeout)
75
-
76
- def _headers(self) -> Dict[str, str]:
77
- """Constructs the headers for the API requests."""
78
- return {
79
- "Authorization": f"Bearer {self.api_key}",
80
- "Content-Type": "application/json",
81
- }
82
-
83
- def _make_request(self, endpoint: str, data: Dict[str, Any], is_async: bool = False) -> Callable:
84
- """Creates a callable for making either synchronous or asynchronous requests.
85
-
86
- Parameters
87
- ----------
88
- endpoint : str
89
- The API endpoint to which the request will be made.
90
- data : dict
91
- The data to be sent in the request body.
92
- is_async : bool, optional
93
- Whether to create an asynchronous request (default is False).
94
-
95
- Returns
96
- -------
97
- Callable
98
- A callable that performs the HTTP request.
99
- """
100
- url = f"{self.base_url}/{endpoint}"
101
- headers = self._headers()
102
-
103
- if is_async:
104
-
105
- async def _request():
106
- try:
107
- response = await self.async_client.post(url, json=data, headers=headers)
108
- response.raise_for_status()
109
- return response.json()
110
- except HTTPStatusError as e:
111
- # Handle HTTP errors (4xx and 5xx status codes)
112
- error_message = f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
113
- raise ValueError(error_message) from e
114
- except Exception as e:
115
- # Handle other exceptions
116
- raise ValueError(f"An error occurred: {str(e)}") from e
117
-
118
- return _request()
119
- else:
120
-
121
- def _request():
122
- try:
123
- response = self.client.post(url, json=data, headers=headers)
124
- response.raise_for_status()
125
- return response.json()
126
- except HTTPStatusError as e:
127
- # Handle HTTP errors (4xx and 5xx status codes)
128
- error_message = f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
129
- raise ValueError(error_message) from e
130
- except Exception as e:
131
- # Handle other exceptions
132
- raise ValueError(f"An error occurred: {str(e)}") from e
133
-
134
- return _request()
135
-
136
- def _process_query_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
137
- """Processes the query response and handles warnings.
138
-
139
- Parameters
140
- ----------
141
- response : dict
142
- The raw API response.
143
-
144
- Returns
145
- -------
146
- dict
147
- A processed dictionary containing the output, token counts, and latency.
148
-
149
- Raises
150
- ------
151
- UserWarning
152
- If there are any warnings in the API response.
153
- """
154
- for _warning in response.get("warnings", []):
155
- warnings.warn(_warning)
156
-
157
- returned_response = {
158
- "output": response["response"],
159
- "in_tokens": response["num_input_tokens"],
160
- "out_tokens": response["num_output_tokens"],
161
- "latency_ms": response["latency_ms"],
162
- }
163
- if "reasoning_steps" in response:
164
- returned_response["reasoning_steps"] = response["reasoning_steps"]
165
- return returned_response
166
-
167
- def _build(
168
- self, task_description: str, multimodal: bool, is_async: bool
169
- ) -> Union[Tuple[str, int, str], Coroutine[Any, Any, Tuple[str, int, str]]]:
170
- """Internal method to handle both synchronous and asynchronous build requests.
171
-
172
- Parameters
173
- ----------
174
- task_description : str
175
- A description of the task for which the function is being built.
176
-
177
- multimodal : bool
178
- Whether the function is multimodal or not.
179
-
180
- is_async : bool
181
- Whether to perform an asynchronous request.
182
-
183
- Returns
184
- -------
185
- Union[tuple[str, int, str], Coroutine[Any, Any, tuple[str, int, str]]]
186
- A tuple containing the name, version number and description of the function, or a coroutine that returns such a tuple.
187
-
188
- Raises
189
- ------
190
- ValueError
191
- If the task description is empty or exceeds the maximum length.
192
- """
193
- # Validate the input
194
- if len(task_description) == 0:
195
- raise ValueError("Task description must be provided.")
196
- if len(task_description) > MAX_TEXT_LENGTH:
197
- raise ValueError(f"Task description must be less than {MAX_TEXT_LENGTH} characters.")
198
-
199
- endpoint = "build"
200
- data = {"request": task_description, "multimodal": multimodal}
201
- request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
202
-
203
- if is_async:
204
- # return 0 for the version number
205
- async def _async_build():
206
- response = await request
207
- return response["function_name"], 0, response["description"]
208
-
209
- return _async_build()
210
- else:
211
- response = request # the request has already been made and the response is available
212
- return response["function_name"], 0, response["description"]
213
-
214
- async def abuild(self, task_description: str, multimodal: bool = False) -> Tuple[str, int, str]:
215
- """Asynchronously builds a specialized function given a task description.
216
-
217
- Parameters
218
- ----------
219
- task_description : str
220
- A description of the task for which the function is being built.
221
-
222
- multimodal : bool, optional
223
- Whether the function is multimodal or not (default is False).
224
-
225
- Returns
226
- -------
227
- tuple[str, str]
228
- A tuple containing the name, version number and description of the function.
229
- """
230
- return await self._build(task_description=task_description, multimodal=multimodal, is_async=True)
231
-
232
- def build(self, task_description: str, multimodal: bool = False) -> Tuple[str, int, str]:
233
- """Synchronously builds a specialized function given a task description.
234
-
235
- Parameters
236
- ----------
237
- task_description : str
238
- A description of the task for which the function is being built.
239
-
240
- multimodal : bool, optional
241
- Whether the function is multimodal or not (default is False).
242
-
243
- Returns
244
- -------
245
- tuple[str, str]
246
- A tuple containing the name, version number and description of the function.
247
- """
248
- return self._build(task_description=task_description, multimodal=multimodal, is_async=False)
249
-
250
- def _upload_image(self, image_info: Dict[str, Any]) -> str:
251
- """
252
- Uploads an image to an S3 bucket and returns the URL of the uploaded image.
253
-
254
- Parameters
255
- ----------
256
- image_info : Dict[str, Any]
257
- A dictionary containing the image metadata.
258
-
259
- Returns
260
- -------
261
- str
262
- The URL of the uploaded image.
263
- """
264
-
265
- if image_info["source"] == "base64":
266
- _, base64_info = is_base64_image(maybe_base64=image_info["image"])
267
- img_data = base64.b64decode(base64_info["encoding"])
268
- elif image_info["source"] == "url":
269
- response = requests.get(image_info["image"])
270
- response.raise_for_status()
271
- img_data = response.content
272
- elif image_info["source"] == "local":
273
- with open(image_info["image"], "rb") as f:
274
- img_data = f.read()
275
- else:
276
- raise ValueError("Invalid image input")
277
-
278
- # Preprocess the image
279
- img = Image.open(BytesIO(img_data))
280
- file_type = image_info["file_type"]
281
- processed_img, file_type = preprocess_image(image=img, file_type=file_type)
282
- upload_data = BytesIO()
283
- processed_img.save(upload_data, format=file_type)
284
- upload_data = upload_data.getvalue()
285
-
286
- # Request a presigned URL from the server
287
- endpoint = "upload_link"
288
- request_data = {"file_type": file_type}
289
- # This needs to be a synchronous request since we need the presigned URL to upload the image
290
- response = self._make_request(endpoint=endpoint, data=request_data, is_async=False)
291
-
292
- # Upload the image to the S3 bucket
293
- files = {"file": upload_data}
294
- http_response = requests.post(response["url"], data=response["fields"], files=files)
295
- if http_response.status_code == 204:
296
- pass
297
- else:
298
- raise ValueError("Image upload failed")
299
-
300
- # Return the URL of the uploaded image
301
- upload_link = f"{response['url']}{response['fields']['key']}"
302
- return upload_link
303
-
304
- def _validate_query(self, text_input: str, images_input: List[str]) -> List[Dict[str, Any]]:
305
- """
306
- Validate the input for the query method.
307
-
308
- Parameters
309
- ----------
310
- text_input : str
311
- The text input to the function.
312
- images_input : List[str]
313
- A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
314
-
315
- Returns
316
- -------
317
- List[Dict[str, Any]]
318
- A list of dictionaries containing the image metadata.
319
-
320
- Raises
321
- ------
322
- ValueError
323
- If the input is invalid.
324
- """
325
- if not isinstance(text_input, str) or not isinstance(images_input, list):
326
- raise ValueError("Text input must be a string and images input must be a list of strings.")
327
- for image in images_input:
328
- if not isinstance(image, str):
329
- raise ValueError("Images input must be a list of strings.")
330
-
331
- # Assert that either text or images or both must be provded
332
- if len(text_input) == 0 and len(images_input) == 0:
333
- raise ValueError("Either text or images or both must be provided as input.")
334
-
335
- # Check if the text input is within the limit
336
- if len(text_input) > MAX_TEXT_LENGTH:
337
- raise ValueError(f"Text input must be less than {MAX_TEXT_LENGTH} characters.")
338
-
339
- # Check if the images input is within the limit
340
- if len(images_input) > MAX_IMAGE_UPLOADS:
341
- raise ValueError(f"Number of images must be less than {MAX_IMAGE_UPLOADS}.")
342
-
343
- # Check if input is an valid image
344
- image_info = []
345
- for image in images_input:
346
- is_base64, base64_info = is_base64_image(maybe_base64=image)
347
- if is_base64:
348
- file_type = base64_info["media_type"].split("/")[1]
349
-
350
- is_public_url = is_public_url_image(maybe_url_image=image)
351
- if is_public_url:
352
- response = requests.get(image)
353
- response.raise_for_status()
354
- file_type = response.headers["content-type"].split("/")[1]
355
-
356
- is_local = is_local_image(maybe_local_image=image)
357
- if is_local:
358
- file_type = os.path.splitext(image)[1][1:]
359
-
360
- if not (is_base64 or is_public_url or is_local):
361
- raise ValueError("Images must be local paths, public URLs or base64 encoded strings.")
362
-
363
- # Determine the source of image
364
- if is_base64:
365
- source = "base64"
366
- elif is_public_url:
367
- source = "url"
368
- elif is_local:
369
- source = "local"
370
-
371
- # Check if the image type is supported
372
- file_type = file_type.lower()
373
- if file_type not in SUPPORTED_IMAGE_EXTENSIONS:
374
- raise ValueError(
375
- f"Image file type {file_type} is not supported. Supported types are {SUPPORTED_IMAGE_EXTENSIONS}."
376
- )
377
-
378
- # Check if the image size is within the limit
379
- size = get_image_size(image=image, source=source)
380
- if size > MAX_IMAGE_SIZE_MB:
381
- raise ValueError(f"Individual image sizes must be less than {MAX_IMAGE_SIZE_MB} MB each.")
382
-
383
- image_info.append({"image": image, "file_type": file_type, "size": size, "source": source})
384
-
385
- return image_info
386
-
387
- def _query(
388
- self,
389
- is_async: bool,
390
- fn_name: str,
391
- version: Union[str, int],
392
- version_number: int,
393
- text_input: Optional[str],
394
- images_input: Optional[List[str]],
395
- return_reasoning: Optional[bool],
396
- ) -> Union[Dict[str, Any], Coroutine[Any, Any, Dict[str, Any]]]:
397
- """Internal method to handle both synchronous and asynchronous query requests.
398
-
399
- Parameters
400
- ----------
401
- is_async : bool
402
- Whether to perform an asynchronous request.
403
- fn_name : str
404
- The name of the function to query.
405
- version : Union[str, int]
406
- The version alias or number of the function to query.
407
- version_number : int, optional
408
- The version number of the function to query.
409
- text_input : str, optional
410
- The text input to the function.
411
- images_input : List[str], optional
412
- A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
413
- return_reasoning : bool, optional
414
- Whether to return reasoning for the output.
415
-
416
- Returns
417
- -------
418
- Union[Dict[str, Any], Coroutine[Any, Any, dict]]
419
- A dictionary containing the query results, or a coroutine that returns such a dictionary.
420
-
421
- Raises
422
- ------
423
- ValueError
424
- If the input is invalid.
425
- """
426
- # Validate the input
427
- image_info = self._validate_query(text_input=text_input, images_input=images_input)
428
-
429
- # Create links for all images that are not public URLs and upload images
430
- image_urls = []
431
- for i, info in enumerate(image_info):
432
- if info["source"] == "url" or info["source"] == "base64" or info["source"] == "local":
433
- url = self._upload_image(image_info=info)
434
- else:
435
- raise ValueError(f"Image at index {i} must be a public URL or a path to a local image file.")
436
- image_urls.append(url)
437
-
438
- # Make the request
439
- endpoint = "query"
440
- data = {
441
- "name": fn_name,
442
- "version": version,
443
- "version_number": version_number,
444
- "text": text_input,
445
- "images": image_urls,
446
- "return_reasoning": return_reasoning,
447
- }
448
- request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
449
-
450
- if is_async:
451
-
452
- async def _async_query():
453
- response = await request
454
- return self._process_query_response(response=response)
455
-
456
- return _async_query()
457
- else:
458
- response = request # the request has already been made and the response is available
459
- return self._process_query_response(response=response)
460
-
461
- async def aquery(
462
- self,
463
- fn_name: str,
464
- version: Optional[Union[str, int]] = -1,
465
- version_number: Optional[int] = -1,
466
- text_input: Optional[str] = "",
467
- images_input: Optional[List[str]] = [],
468
- return_reasoning: Optional[bool] = False,
469
- ) -> Dict[str, Any]:
470
- """Asynchronously queries a function with the given function ID and input.
471
-
472
- Parameters
473
- ----------
474
- fn_name : str
475
- The name of the function to query.
476
- version : Union[str, int], optional
477
- The version alias or number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
478
- version_number : int, optional
479
- The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
480
- text_input : str, optional
481
- The text input to the function.
482
- images_input : List[str], optional
483
- A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
484
- return_reasoning : bool, optional
485
- Whether to return reasoning for the output. Default is False.
486
-
487
- Returns
488
- -------
489
- dict
490
- A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
491
- and the latency in milliseconds.
492
- """
493
- return await self._query(
494
- fn_name=fn_name,
495
- version=version,
496
- version_number=version_number,
497
- text_input=text_input,
498
- images_input=images_input,
499
- return_reasoning=return_reasoning,
500
- is_async=True,
501
- )
502
-
503
- def query(
504
- self,
505
- fn_name: str,
506
- version: Optional[Union[str, int]] = -1,
507
- version_number: Optional[int] = -1,
508
- text_input: Optional[str] = "",
509
- images_input: Optional[List[str]] = [],
510
- return_reasoning: Optional[bool] = False,
511
- ) -> Dict[str, Any]:
512
- """Synchronously queries a function with the given function ID and input.
513
-
514
- Parameters
515
- ----------
516
- fn_name : str
517
- The name of the function to query.
518
- version_number : int, optional
519
- The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
520
- text_input : str, optional
521
- The text input to the function.
522
- images_input : List[str], optional
523
- A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
524
- return_reasoning : bool, optional
525
- Whether to return reasoning for the output. Default is False.
526
-
527
- Returns
528
- -------
529
- dict
530
- A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
531
- and the latency in milliseconds.
532
- """
533
- return self._query(
534
- fn_name=fn_name,
535
- version=version,
536
- version_number=version_number,
537
- text_input=text_input,
538
- images_input=images_input,
539
- return_reasoning=return_reasoning,
540
- is_async=False,
541
- )
542
-
543
- def batch_query(
544
- self,
545
- fn_name: str,
546
- batch_inputs: List[Dict[str, Any]],
547
- version: Optional[Union[str, int]] = -1,
548
- version_number: Optional[int] = -1,
549
- return_reasoning: Optional[bool] = False,
550
- ) -> List[Dict[str, Any]]:
551
- """Batch queries a function version with a list of inputs.
552
-
553
- Parameters
554
- ----------
555
- fn_name : str
556
- The name of the function or a list of function names to query.
557
- batch_inputs : List[Dict[str, Any]]
558
- A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
559
- when providing for a text input, the dictionary should be {"text_input": "input text"}, for an image input, the dictionary should be {"images_input": ["url1", "url2", ...]}
560
- and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
561
- version : Union[str, int], optional
562
- The version alias or number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
563
- version_number : int, optional
564
- The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
565
- return_reasoning : bool, optional
566
- Whether to return reasoning for the output. Default is False.
567
-
568
- Returns
569
- -------
570
- List[Dict[str, Any]]
571
- A list of dictionaries, each containing the output of a function query,
572
- in the same order as the input queries.
573
- """
574
-
575
- async def run_queries():
576
- tasks = list(
577
- map(
578
- lambda fn_input: self.aquery(
579
- fn_name=fn_name, version=version, version_number=version_number, return_reasoning=return_reasoning, **fn_input
580
- ),
581
- batch_inputs,
582
- )
583
- )
584
- return await asyncio.gather(*tasks)
585
-
586
- return asyncio.run(run_queries())
weco/constants.py DELETED
@@ -1,4 +0,0 @@
1
- MAX_TEXT_LENGTH = 1e6 # 1 million characters
2
- MAX_IMAGE_UPLOADS = 5
3
- MAX_IMAGE_SIZE_MB = 10 # 10 MB
4
- SUPPORTED_IMAGE_EXTENSIONS = ["png", "jpeg", "jpg", "webp", "gif"]