weco 0.1.3__py3-none-any.whl → 0.1.4__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 CHANGED
@@ -1,9 +1,24 @@
1
1
  import asyncio
2
+ import base64
2
3
  import os
3
4
  import warnings
4
- from typing import Any, Callable, Coroutine, Dict, List, Tuple
5
+ from io import BytesIO
6
+ from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Union
5
7
 
6
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
+ )
7
22
 
8
23
 
9
24
  class WecoAI:
@@ -17,7 +32,7 @@ class WecoAI:
17
32
  The API key used for authentication.
18
33
  """
19
34
 
20
- def __init__(self, api_key: str = None, timeout: float = 30.0) -> None:
35
+ def __init__(self, api_key: str = None, timeout: float = 120.0, http2: bool = True) -> None:
21
36
  """Initializes the WecoAI client with the provided API key and base URL.
22
37
 
23
38
  Parameters
@@ -25,6 +40,12 @@ class WecoAI:
25
40
  api_key : str, optional
26
41
  The API key used for authentication. If not provided, the client will attempt to read it from the environment variable - WECO_API_KEY.
27
42
 
43
+ timeout : float, optional
44
+ The timeout for the HTTP requests in seconds (default is 30.0).
45
+
46
+ http2 : bool, optional
47
+ Whether to use HTTP/2 protocol for the HTTP requests (default is True).
48
+
28
49
  Raises
29
50
  ------
30
51
  ValueError
@@ -37,30 +58,17 @@ class WecoAI:
37
58
  except KeyError:
38
59
  raise ValueError("WECO_API_KEY must be passed to client or set as an environment variable")
39
60
  self.api_key = api_key
40
-
41
- self.base_url = "https://function-builder.vercel.app"
42
-
61
+ self.http2 = http2
62
+ self.timeout = timeout
63
+ self.base_url = "https://function.api.weco.ai"
43
64
  # Setup clients
44
- self.client = httpx.Client(http2=False, timeout=timeout)
45
- self.async_client = httpx.AsyncClient(http2=False, timeout=timeout)
46
-
47
- def __del__(self):
48
- """Closes the HTTP clients when the WecoAI instance is deleted."""
49
- try:
50
- self.client.close()
51
- if not self.async_client.is_closed:
52
- try:
53
- loop = asyncio.get_event_loop()
54
- if loop.is_running():
55
- loop.create_task(self.async_client.aclose())
56
- else:
57
- loop.run_until_complete(self.async_client.aclose())
58
- except RuntimeError:
59
- # If the event loop is closed, we can't do anything about it
60
- pass
61
- except AttributeError:
62
- # If the client is not initialized, we can't do anything about it
63
- pass
65
+ self.client = httpx.Client(http2=http2, timeout=timeout)
66
+ self.async_client = httpx.AsyncClient(http2=http2, timeout=timeout)
67
+
68
+ def close(self):
69
+ """Close both synchronous and asynchronous clients."""
70
+ self.client.close()
71
+ asyncio.run(self.async_client.aclose())
64
72
 
65
73
  def _headers(self) -> Dict[str, str]:
66
74
  """Constructs the headers for the API requests."""
@@ -92,22 +100,38 @@ class WecoAI:
92
100
  if is_async:
93
101
 
94
102
  async def _request():
95
- response = await self.async_client.post(url, json=data, headers=headers)
96
- response.raise_for_status()
97
- return response.json()
103
+ try:
104
+ response = await self.async_client.post(url, json=data, headers=headers)
105
+ response.raise_for_status()
106
+ return response.json()
107
+ except HTTPStatusError as e:
108
+ # Handle HTTP errors (4xx and 5xx status codes)
109
+ error_message = f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
110
+ raise ValueError(error_message) from e
111
+ except Exception as e:
112
+ # Handle other exceptions
113
+ raise ValueError(f"An error occurred: {str(e)}") from e
98
114
 
99
115
  return _request()
100
116
  else:
101
117
 
102
118
  def _request():
103
- response = self.client.post(url, json=data, headers=headers)
104
- response.raise_for_status()
105
- return response.json()
119
+ try:
120
+ response = self.client.post(url, json=data, headers=headers)
121
+ response.raise_for_status()
122
+ return response.json()
123
+ except HTTPStatusError as e:
124
+ # Handle HTTP errors (4xx and 5xx status codes)
125
+ error_message = f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
126
+ raise ValueError(error_message) from e
127
+ except Exception as e:
128
+ # Handle other exceptions
129
+ raise ValueError(f"An error occurred: {str(e)}") from e
106
130
 
107
131
  return _request()
108
132
 
109
- def _process_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
110
- """Processes the API response and handles warnings.
133
+ def _process_query_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
134
+ """Processes the query response and handles warnings.
111
135
 
112
136
  Parameters
113
137
  ----------
@@ -134,7 +158,7 @@ class WecoAI:
134
158
  "latency_ms": response["latency_ms"],
135
159
  }
136
160
 
137
- def _build(self, task_description: str, is_async: bool) -> Tuple[str, str] | Coroutine[Any, Any, Tuple[str, str]]:
161
+ def _build(self, task_description: str, is_async: bool) -> Union[Tuple[str, str], Coroutine[Any, Any, Tuple[str, str]]]:
138
162
  """Internal method to handle both synchronous and asynchronous build requests.
139
163
 
140
164
  Parameters
@@ -146,9 +170,20 @@ class WecoAI:
146
170
 
147
171
  Returns
148
172
  -------
149
- tuple[str, str] | Coroutine[Any, Any, tuple[str, str]]
173
+ Union[tuple[str, str], Coroutine[Any, Any, tuple[str, str]]]
150
174
  A tuple containing the name and description of the function, or a coroutine that returns such a tuple.
175
+
176
+ Raises
177
+ ------
178
+ ValueError
179
+ If the task description is empty or exceeds the maximum length.
151
180
  """
181
+ # Validate the input
182
+ if len(task_description) == 0:
183
+ raise ValueError("Task description must be provided.")
184
+ if len(task_description) > MAX_TEXT_LENGTH:
185
+ raise ValueError(f"Task description must be less than {MAX_TEXT_LENGTH} characters.")
186
+
152
187
  endpoint = "build"
153
188
  data = {"request": task_description}
154
189
  request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
@@ -194,47 +229,216 @@ class WecoAI:
194
229
  """
195
230
  return self._build(task_description=task_description, is_async=False)
196
231
 
197
- def _query(self, fn_name: str, fn_input: str, is_async: bool) -> Dict[str, Any] | Coroutine[Any, Any, Dict[str, Any]]:
198
- """Internal method to handle both synchronous and asynchronous query requests.
232
+ def _upload_image(self, fn_name: str, upload_id: str, image_info: Dict[str, Any]) -> str:
233
+ """
234
+ Uploads an image to an S3 bucket and returns the URL of the uploaded image.
199
235
 
200
236
  Parameters
201
237
  ----------
202
238
  fn_name : str
203
- The name of the function to query.
204
- fn_input : str
205
- The input to the function.
239
+ The name of the function for which the image is being uploaded.
240
+ upload_id: str
241
+ A unique identifier for the image upload.
242
+ image_info : Dict[str, Any]
243
+ A dictionary containing the image metadata.
244
+
245
+ Returns
246
+ -------
247
+ str
248
+ The URL of the uploaded image.
249
+ """
250
+
251
+ if image_info["source"] == "base64":
252
+ _, base64_info = is_base64_image(maybe_base64=image_info["image"])
253
+ img_data = base64.b64decode(base64_info["encoding"])
254
+ elif image_info["source"] == "url":
255
+ response = requests.get(image_info["image"])
256
+ response.raise_for_status()
257
+ img_data = response.content
258
+ elif image_info["source"] == "local":
259
+ with open(image_info["image"], "rb") as f:
260
+ img_data = f.read()
261
+ else:
262
+ raise ValueError("Invalid image input")
263
+
264
+ # Preprocess the image
265
+ img = Image.open(BytesIO(img_data))
266
+ file_type = image_info["file_type"]
267
+ processed_img, file_type = preprocess_image(image=img, file_type=file_type)
268
+ upload_data = BytesIO()
269
+ processed_img.save(upload_data, format=file_type)
270
+ upload_data = upload_data.getvalue()
271
+
272
+ # Request a presigned URL from the server
273
+ endpoint = "upload_link"
274
+ request_data = {"fn_name": fn_name, "upload_id": upload_id, "file_type": file_type}
275
+ # This needs to be a synchronous request since we need the presigned URL to upload the image
276
+ response = self._make_request(endpoint=endpoint, data=request_data, is_async=False)
277
+
278
+ # Upload the image to the S3 bucket
279
+ image_name = generate_random_base16_code()
280
+ files = {"file": (f"{image_name}.{file_type}", upload_data)}
281
+ http_response = requests.post(response["url"], data=response["fields"], files=files)
282
+ if http_response.status_code == 204:
283
+ pass
284
+ else:
285
+ raise ValueError("Image upload failed")
286
+
287
+ # Return the URL of the uploaded image
288
+ upload_link = f"{response['url']}{response['fields']['key']}"
289
+ return upload_link
290
+
291
+ def _validate_query(self, text_input: str, images_input: List[str]) -> List[Dict[str, Any]]:
292
+ """
293
+ Validate the input for the query method.
294
+
295
+ Parameters
296
+ ----------
297
+ text_input : str
298
+ The text input to the function.
299
+ images_input : List[str]
300
+ A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
301
+
302
+ Returns
303
+ -------
304
+ List[Dict[str, Any]]
305
+ A list of dictionaries containing the image metadata.
306
+
307
+ Raises
308
+ ------
309
+ ValueError
310
+ If the input is invalid.
311
+ """
312
+ if not isinstance(text_input, str) or not isinstance(images_input, list):
313
+ raise ValueError("Text input must be a string and images input must be a list of strings.")
314
+ for image in images_input:
315
+ if not isinstance(image, str):
316
+ raise ValueError("Images input must be a list of strings.")
317
+
318
+ # Assert that either text or images or both must be provded
319
+ if len(text_input) == 0 and len(images_input) == 0:
320
+ raise ValueError("Either text or images or both must be provided as input.")
321
+
322
+ # Check if the text input is within the limit
323
+ if len(text_input) > MAX_TEXT_LENGTH:
324
+ raise ValueError(f"Text input must be less than {MAX_TEXT_LENGTH} characters.")
325
+
326
+ # Check if the images input is within the limit
327
+ if len(images_input) > MAX_IMAGE_UPLOADS:
328
+ raise ValueError(f"Number of images must be less than {MAX_IMAGE_UPLOADS}.")
329
+
330
+ # Check if input is an valid image
331
+ image_info = []
332
+ for image in images_input:
333
+ is_base64, base64_info = is_base64_image(maybe_base64=image)
334
+ if is_base64:
335
+ file_type = base64_info["media_type"].split("/")[1]
336
+
337
+ is_public_url = is_public_url_image(maybe_url_image=image)
338
+ if is_public_url:
339
+ response = requests.get(image)
340
+ response.raise_for_status()
341
+ file_type = response.headers["content-type"].split("/")[1]
342
+
343
+ is_local = is_local_image(maybe_local_image=image)
344
+ if is_local:
345
+ file_type = os.path.splitext(image)[1][1:]
346
+
347
+ if not (is_base64 or is_public_url or is_local):
348
+ raise ValueError("Images must be local paths, public URLs or base64 encoded strings.")
349
+
350
+ # Determine the source of image
351
+ if is_base64:
352
+ source = "base64"
353
+ elif is_public_url:
354
+ source = "url"
355
+ elif is_local:
356
+ source = "local"
357
+
358
+ # Check if the image type is supported
359
+ file_type = file_type.lower()
360
+ if file_type not in SUPPORTED_IMAGE_EXTENSIONS:
361
+ raise ValueError(
362
+ f"Image file type {file_type} is not supported. Supported types are {SUPPORTED_IMAGE_EXTENSIONS}."
363
+ )
364
+
365
+ # Check if the image size is within the limit
366
+ size = get_image_size(image=image, source=source)
367
+ if size > MAX_IMAGE_SIZE_MB:
368
+ raise ValueError(f"Individual image sizes must be less than {MAX_IMAGE_SIZE_MB} MB each.")
369
+
370
+ image_info.append({"image": image, "file_type": file_type, "size": size, "source": source})
371
+
372
+ return image_info
373
+
374
+ def _query(
375
+ self, is_async: bool, fn_name: str, text_input: Optional[str], images_input: Optional[List[str]]
376
+ ) -> Union[Dict[str, Any], Coroutine[Any, Any, Dict[str, Any]]]:
377
+ """Internal method to handle both synchronous and asynchronous query requests.
378
+
379
+ Parameters
380
+ ----------
206
381
  is_async : bool
207
382
  Whether to perform an asynchronous request.
383
+ fn_name : str
384
+ The name of the function to query.
385
+ text_input : str, optional
386
+ The text input to the function.
387
+ images_input : List[str], optional
388
+ A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
208
389
 
209
390
  Returns
210
391
  -------
211
- dict | Coroutine[Any, Any, dict]
392
+ Union[Dict[str, Any], Coroutine[Any, Any, dict]]
212
393
  A dictionary containing the query results, or a coroutine that returns such a dictionary.
394
+
395
+ Raises
396
+ ------
397
+ ValueError
398
+ If the input is invalid.
213
399
  """
400
+ # Validate the input
401
+ image_info = self._validate_query(text_input=text_input, images_input=images_input)
402
+
403
+ # Create links for all images that are not public URLs and upload images
404
+ image_urls = []
405
+ upload_id = generate_random_base16_code()
406
+ for i, info in enumerate(image_info):
407
+ if info["source"] == "url" or info["source"] == "base64" or info["source"] == "local":
408
+ url = self._upload_image(fn_name=fn_name, upload_id=upload_id, image_info=info)
409
+ else:
410
+ raise ValueError(f"Image at index {i} must be a public URL or a path to a local image file.")
411
+ image_urls.append(url)
412
+
413
+ # Make the request
214
414
  endpoint = "query"
215
- data = {"name": fn_name, "user_message": fn_input}
415
+ data = {"name": fn_name, "text": text_input, "images": image_urls}
216
416
  request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
217
417
 
218
418
  if is_async:
219
419
 
220
420
  async def _async_query():
221
421
  response = await request
222
- return self._process_response(response=response)
422
+ return self._process_query_response(response=response)
223
423
 
224
424
  return _async_query()
225
425
  else:
226
426
  response = request # the request has already been made and the response is available
227
- return self._process_response(response=response)
427
+ return self._process_query_response(response=response)
228
428
 
229
- async def aquery(self, fn_name: str, fn_input: str) -> Dict[str, Any]:
429
+ async def aquery(
430
+ self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []
431
+ ) -> Dict[str, Any]:
230
432
  """Asynchronously queries a function with the given function ID and input.
231
433
 
232
434
  Parameters
233
435
  ----------
234
436
  fn_name : str
235
437
  The name of the function to query.
236
- fn_input : str
237
- The input to the function.
438
+ text_input : str, optional
439
+ The text input to the function.
440
+ images_input : List[str], optional
441
+ A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
238
442
 
239
443
  Returns
240
444
  -------
@@ -242,17 +446,19 @@ class WecoAI:
242
446
  A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
243
447
  and the latency in milliseconds.
244
448
  """
245
- return await self._query(fn_name=fn_name, fn_input=fn_input, is_async=True)
449
+ return await self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=True)
246
450
 
247
- def query(self, fn_name: str, fn_input: str) -> Dict[str, Any]:
451
+ def query(self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []) -> Dict[str, Any]:
248
452
  """Synchronously queries a function with the given function ID and input.
249
453
 
250
454
  Parameters
251
455
  ----------
252
456
  fn_name : str
253
457
  The name of the function to query.
254
- fn_input : str
255
- The input to the function.
458
+ text_input : str, optional
459
+ The text input to the function.
460
+ images_input : List[str], optional
461
+ A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
256
462
 
257
463
  Returns
258
464
  -------
@@ -260,9 +466,9 @@ class WecoAI:
260
466
  A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
261
467
  and the latency in milliseconds.
262
468
  """
263
- return self._query(fn_name=fn_name, fn_input=fn_input, is_async=False)
469
+ return self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=False)
264
470
 
265
- def batch_query(self, fn_names: str | List[str], batch_inputs: List[str]) -> List[Dict[str, Any]]:
471
+ def batch_query(self, fn_names: Union[str, List[str]], batch_inputs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
266
472
  """Synchronously queries multiple functions using asynchronous calls internally.
267
473
 
268
474
  This method uses the asynchronous queries to submit all queries concurrently
@@ -270,20 +476,28 @@ class WecoAI:
270
476
 
271
477
  Parameters
272
478
  ----------
273
- fn_name : str | List[str]
479
+ fn_name : Union[str, List[str]]
274
480
  The name of the function or a list of function names to query.
275
481
  Note that if a single function name is provided, it will be used for all queries.
276
482
  If a list of function names is provided, the length must match the number of queries.
277
483
 
278
- batch_inputs : List[str]
279
- A list of inputs for the functions to query.
280
- Note that the index of each input must correspond to the index of the function name.
484
+ batch_inputs : List[Dict[str, Any]]
485
+ A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
486
+ 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", ...]}
487
+ and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
488
+ Note that the index of each input must correspond to the index of the function name when both inputs are lists.
281
489
 
282
490
  Returns
283
491
  -------
284
492
  List[Dict[str, Any]]
285
493
  A list of dictionaries, each containing the output of a function query,
286
494
  in the same order as the input queries.
495
+
496
+
497
+ Raises
498
+ ------
499
+ ValueError
500
+ If the number of function names (when provided as a list) does not match the number of inputs.
287
501
  """
288
502
  if isinstance(fn_names, str):
289
503
  fn_names = [fn_names] * len(batch_inputs)
@@ -291,7 +505,10 @@ class WecoAI:
291
505
  raise ValueError("The number of function names must match the number of inputs.")
292
506
 
293
507
  async def run_queries():
294
- tasks = [self.aquery(fn_name=fn_name, fn_input=fn_input) for fn_name, fn_input in zip(fn_names, batch_inputs)]
508
+ tasks = [
509
+ self.aquery(fn_name=fn_name, **fn_input) # unpack the input kwargs
510
+ for fn_name, fn_input in zip(fn_names, batch_inputs)
511
+ ]
295
512
  return await asyncio.gather(*tasks)
296
513
 
297
514
  return asyncio.run(run_queries())
weco/constants.py ADDED
@@ -0,0 +1,4 @@
1
+ MAX_TEXT_LENGTH = 10000
2
+ MAX_IMAGE_UPLOADS = 5
3
+ MAX_IMAGE_SIZE_MB = 10 # 10 MB
4
+ SUPPORTED_IMAGE_EXTENSIONS = ["png", "jpeg", "jpg", "webp", "gif"]
weco/functional.py CHANGED
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional
3
3
  from .client import WecoAI
4
4
 
5
5
 
6
+ # TODO: Implement the closing stuff for the client
6
7
  def build(task_description: str, api_key: str = None) -> tuple[str, str]:
7
8
  """Builds a specialized function synchronously given a task description.
8
9
 
@@ -43,15 +44,19 @@ async def abuild(task_description: str, api_key: str = None) -> tuple[str, str]:
43
44
  return response
44
45
 
45
46
 
46
- def query(fn_name: str, fn_input: str, api_key: Optional[str] = None) -> Dict[str, Any]:
47
+ def query(
48
+ fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
49
+ ) -> Dict[str, Any]:
47
50
  """Queries a function synchronously with the given function ID and input.
48
51
 
49
52
  Parameters
50
53
  ----------
51
54
  fn_name : str
52
55
  The name of the function to query.
53
- fn_input : str
54
- The input to the function.
56
+ text_input : str, optional
57
+ The text input to the function.
58
+ images_input : List[str], optional
59
+ A list of image URLs or base64 encoded images to be used as input to the function.
55
60
  api_key : str
56
61
  The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
57
62
 
@@ -62,19 +67,23 @@ def query(fn_name: str, fn_input: str, api_key: Optional[str] = None) -> Dict[st
62
67
  and the latency in milliseconds.
63
68
  """
64
69
  client = WecoAI(api_key=api_key)
65
- response = client.query(fn_name=fn_name, fn_input=fn_input)
70
+ response = client.query(fn_name=fn_name, text_input=text_input, images_input=images_input)
66
71
  return response
67
72
 
68
73
 
69
- async def aquery(fn_name: str, fn_input: str, api_key: Optional[str] = None) -> Dict[str, Any]:
74
+ async def aquery(
75
+ fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
76
+ ) -> Dict[str, Any]:
70
77
  """Queries a function asynchronously with the given function ID and input.
71
78
 
72
79
  Parameters
73
80
  ----------
74
81
  fn_name : str
75
82
  The name of the function to query.
76
- fn_input : str
77
- The input to the function.
83
+ text_input : str, optional
84
+ The text input to the function.
85
+ images_input : List[str], optional
86
+ A list of image URLs to be used as input to the function.
78
87
  api_key : str
79
88
  The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
80
89
 
@@ -85,11 +94,13 @@ async def aquery(fn_name: str, fn_input: str, api_key: Optional[str] = None) ->
85
94
  and the latency in milliseconds.
86
95
  """
87
96
  client = WecoAI(api_key=api_key)
88
- response = await client.aquery(fn_name=fn_name, fn_input=fn_input)
97
+ response = await client.aquery(fn_name=fn_name, text_input=text_input, images_input=images_input)
89
98
  return response
90
99
 
91
100
 
92
- def batch_query(fn_names: str | List[str], batch_inputs: List[str], api_key: Optional[str] = None) -> List[Dict[str, Any]]:
101
+ def batch_query(
102
+ fn_names: str | List[str], batch_inputs: List[Dict[str, Any]], api_key: Optional[str] = None
103
+ ) -> List[Dict[str, Any]]:
93
104
  """Synchronously queries multiple functions using asynchronous calls internally.
94
105
 
95
106
  This method uses the asynchronous queries to submit all queries concurrently
@@ -103,8 +114,10 @@ def batch_query(fn_names: str | List[str], batch_inputs: List[str], api_key: Opt
103
114
  If a list of function names is provided, the length must match the number of queries.
104
115
 
105
116
  batch_inputs : List[str]
106
- A list of inputs for the functions to query.
107
- Note that the index of each input must correspond to the index of the function name.
117
+ A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
118
+ 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", ...]}
119
+ and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
120
+ Note that the index of each input must correspond to the index of the function name when both inputs are lists.
108
121
 
109
122
  api_key : str, optional
110
123
  The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
weco/utils.py ADDED
@@ -0,0 +1,180 @@
1
+ import base64
2
+ import os
3
+ import random
4
+ import re
5
+ import string
6
+ from io import BytesIO
7
+ from typing import Dict, Optional, Tuple
8
+ from urllib.parse import urlparse
9
+
10
+ import requests
11
+ from PIL import Image
12
+
13
+
14
+ def is_local_image(maybe_local_image: str) -> bool:
15
+ """
16
+ Check if the file is a local image.
17
+
18
+ Parameters
19
+ ----------
20
+ maybe_local_image : str
21
+ The file path.
22
+
23
+ Returns
24
+ -------
25
+ bool
26
+ True if the file is a local image, False otherwise.
27
+ """
28
+ if not os.path.exists(maybe_local_image): # Check if the file exists
29
+ return False
30
+
31
+ try: # Check if the file is an image
32
+ Image.open(maybe_local_image)
33
+ except IOError:
34
+ return False
35
+
36
+ return True
37
+
38
+
39
+ def is_base64_image(maybe_base64: str) -> Tuple[bool, Optional[Dict[str, str]]]:
40
+ """
41
+ Check if the image is a base64 encoded image and if so, extract the image information from the encoded data or URL provided.
42
+
43
+ Parameters
44
+ ----------
45
+ data : str
46
+ The image data or URL.
47
+
48
+ Returns
49
+ -------
50
+ Tuple[bool, Optional[Dict[str, str]]]
51
+ """
52
+ pattern = r"data:(?P<media_type>[\w/]+);(?P<source_type>\w+),(?P<encoding>.*)"
53
+ match = re.match(pattern, maybe_base64)
54
+ if match:
55
+ return True, match.groupdict()
56
+
57
+ return False, None
58
+
59
+
60
+ def is_public_url_image(maybe_url_image: str) -> bool:
61
+ """
62
+ Check if the string is a publicly accessible URL
63
+
64
+ Parameters
65
+ ----------
66
+ maybe_url_image : str
67
+ The URL to check.
68
+
69
+ Returns
70
+ -------
71
+ bool
72
+ True if the URL is publicly accessible, False otherwise.
73
+ """
74
+ try:
75
+ # Check if it is a valid URL
76
+ if not urlparse(maybe_url_image).scheme:
77
+ return False
78
+
79
+ # Check if the URL is publicly accessible
80
+ response = requests.head(maybe_url_image)
81
+ if response.status_code != 200:
82
+ return False
83
+
84
+ # Check if the URL is an image
85
+ content_type = response.headers.get("content-type")
86
+ if not content_type:
87
+ return False
88
+ if not content_type.startswith("image"):
89
+ return False
90
+ except Exception:
91
+ return False
92
+
93
+ return True
94
+
95
+
96
+ def get_image_size(image: str, source: str) -> float:
97
+ """
98
+ Get the size of the image in MB.
99
+
100
+ Parameters
101
+ ----------
102
+ image : str
103
+ The image data or URL.
104
+
105
+ source : str
106
+ The source of the image. It can be 'base64', 'url', or 'local'.
107
+
108
+ Returns
109
+ -------
110
+ float
111
+ The size of the image in MB.
112
+
113
+ Raises
114
+ ------
115
+ ValueError
116
+ If the image is not a valid image.
117
+ """
118
+ if source == "base64":
119
+ _, base64_info = is_base64_image(maybe_base64=image)
120
+ img_data = base64.b64decode(base64_info["encoding"])
121
+ elif source == "url":
122
+ response = requests.get(image)
123
+ response.raise_for_status()
124
+ img_data = response.content
125
+ elif source == "local":
126
+ with open(image, "rb") as f:
127
+ img_data = f.read()
128
+ else:
129
+ raise ValueError("Invalid image input")
130
+
131
+ img = Image.open(BytesIO(img_data))
132
+ img_byte_arr = BytesIO()
133
+ img.save(img_byte_arr, format=img.format)
134
+ return img_byte_arr.tell() / 1000000 # MB
135
+
136
+
137
+ def preprocess_image(image: Image, file_type: str) -> Tuple:
138
+ """
139
+ Preprocess the image by converting it to RGB if it has an alpha channel.
140
+
141
+ Parameters
142
+ ----------
143
+ image : Image
144
+ The image to preprocess.
145
+ file_type : str
146
+ The file type of the image.
147
+
148
+ Returns
149
+ -------
150
+ Image
151
+ The preprocessed image.
152
+ file_type : str
153
+ The file type of the image.
154
+ """
155
+ # Do not rescale or resize. Only do this if latency becomes an issue.
156
+ # Remove the alpha channel for PNG and WEBP images if it exists.
157
+ if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
158
+ image = image.convert("RGB")
159
+
160
+ # If the image file type is JPG, convert to JPEG for PIL compatibility.
161
+ if file_type == "jpg":
162
+ file_type = "jpeg"
163
+ return image, file_type
164
+
165
+
166
+ def generate_random_base16_code(length: int = 5):
167
+ """
168
+ Generate a random base16 code.
169
+
170
+ Parameters
171
+ ----------
172
+ length : int
173
+ The length of the code.
174
+
175
+ Returns
176
+ -------
177
+ str
178
+ The random base16 code.
179
+ """
180
+ return "".join(random.choices(string.hexdigits, k=length))
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: weco
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A client facing API for interacting with the WeCo AI function builder service.
5
5
  Author-email: WeCo AI Team <dhruv@weco.ai>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/WecoAI/weco-python
8
- Keywords: AI,LLM,machine learning,data science,function builder
8
+ Keywords: AI,LLM,machine learning,data science,function builder,AI function
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: License :: OSI Approved :: MIT License
@@ -14,21 +14,31 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: asyncio
16
16
  Requires-Dist: httpx[http2]
17
+ Requires-Dist: pillow
17
18
  Provides-Extra: dev
18
- Requires-Dist: build ; extra == 'dev'
19
- Requires-Dist: setuptools-scm ; extra == 'dev'
20
19
  Requires-Dist: flake8 ; extra == 'dev'
21
20
  Requires-Dist: flake8-pyproject ; extra == 'dev'
22
21
  Requires-Dist: black ; extra == 'dev'
23
22
  Requires-Dist: isort ; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio ; extra == 'dev'
24
+ Requires-Dist: pytest-xdist ; extra == 'dev'
25
+ Requires-Dist: build ; extra == 'dev'
26
+ Requires-Dist: setuptools-scm ; extra == 'dev'
24
27
 
25
28
  <div align="center" style="display: flex; align-items: center; justify-content: center;">
26
29
  <img src="assets/weco.svg" alt="WeCo AI" style="height: 50px; margin-right: 10px;">
27
30
  <a href="https://git.io/typing-svg"><img src="https://readme-typing-svg.demolab.com?font=Georgia&size=32&duration=4000&pause=400&color=FD4578&vCenter=true&multiline=false&width=200&height=50&lines=WeCo+Client" alt="Typing SVG" /></a>
28
31
  </div>
29
32
 
33
+ ![Python](https://img.shields.io/badge/Python-3.10.14-blue)
34
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
35
+
36
+ <!-- TODO: Update examples -->
30
37
  # $f$(👷‍♂️)
31
38
 
39
+ <a href="https://colab.research.google.com/github/WecoAI/weco-python/blob/main/examples/cookbook.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab" width=110 height=20/></a>
40
+ <a target="_blank" href="https://lightning.ai/new?repo_url=https%3A%2F%2Fgithub.com%2FWecoAI%2Fweco-python%2Fblob%2Fmain%2Fexamples%2Fcookbook.ipynb"><img src="https://pl-bolts-doc-images.s3.us-east-2.amazonaws.com/app-2/studio-badge.svg" alt="Open in Studio" width=100 height=20/></a>
41
+
32
42
  A client facing API for interacting with the [WeCo AI](https://www.weco.ai/) function builder [service](https://weco-app.vercel.app/function)!
33
43
 
34
44
 
@@ -47,6 +57,7 @@ pip install weco
47
57
  - The **query** function allows you to test and use the newly created function in your own code.
48
58
  - We offer asynchronous versions of the above clients.
49
59
  - We provide a **batch_query** functions that allows users to batch functions for various inputs as well as multiple inputs for the same function in a query. This is helpful to make a large number of queries more efficiently.
60
+ - We also offer multimodality capabilities. You can now query our client with both **language** AND **vision** inputs!
50
61
 
51
62
  We provide both services in two ways:
52
63
  - `weco.WecoAI` client to be used when you want to maintain the same client service across a portion of code. This is better for dense service usage.
@@ -63,18 +74,48 @@ export WECO_API_KEY=<YOUR_WECO_API_KEY>
63
74
  ## Example
64
75
 
65
76
  We create a function on the [web console](https://weco-app.vercel.app/function) for the following task:
66
- > "I want to evaluate the feasibility of a machine learning task. Give me a json object with three keys - 'feasibility', 'justification', and 'suggestions'."
77
+ > "Analyze a business idea and provide a structured evaluation. Output a JSON with 'viability_score' (0-100), 'strengths' (list), 'weaknesses' (list), and 'next_steps' (list)."
67
78
 
68
79
  Now, you're ready to query this function anywhere in your code!
69
80
 
70
81
  ```python
71
82
  from weco import query
72
83
  response = query(
73
- fn_name=fn_name,
74
- fn_input="I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle.",
84
+ fn_name="BusinessIdeaAnalyzer-XYZ123", # Replace with your actual function name
85
+ text_input="A subscription service for personalized, AI-generated bedtime stories for children."
75
86
  )
76
87
  ```
77
88
 
78
89
  For more examples and an advanced user guide, check out our function builder [cookbook](examples/cookbook.ipynb).
79
90
 
80
91
  ## Happy building $f$(👷‍♂️)!
92
+
93
+ ## Contributing
94
+
95
+ We value your contributions! If you believe you can help to improve our package enabling people to build AI with AI, please contribute!
96
+
97
+ Use the following steps as a guideline to help you make contributions:
98
+
99
+ 1. Download and install package from source:
100
+ ```bash
101
+ git clone https://github.com/WecoAI/weco-python.git
102
+ cd weco-python
103
+ pip install -e ".[dev]"
104
+ ```
105
+
106
+ 2. Create a new branch for your feature or bugfix:
107
+ ```bash
108
+ git checkout -b feature/your-feature-name
109
+ ```
110
+
111
+ 3. Make your changes and run tests to ensure everything is working:
112
+
113
+ > **Tests can be expensive to run as they make LLM requests with the API key being used so it is the developers best interests to write small and simple tests that adds coverage for a large portion of the package.**
114
+
115
+ ```bash
116
+ pytest -n auto tests
117
+ ```
118
+
119
+ 4. Commit and push your changes, then open a PR for us to view 😁
120
+
121
+ Please ensure your code follows our style guidelines (Numpy docstrings) and includes appropriate tests. We appreciate your contributions!
@@ -0,0 +1,10 @@
1
+ weco/__init__.py,sha256=qiKpnrm6t0n0bpAtXEKJO1Yz2xYXnJJRZBWt-cH7DdU,168
2
+ weco/client.py,sha256=MGwLxV0pgZ_dighM5tvE32VbM1lspppTq8EmdZnXCfE,20608
3
+ weco/constants.py,sha256=zIVqyFh6JQVWkPpeh7ABUvTmJ9CjQk_4LPlz4pEO0Uk,145
4
+ weco/functional.py,sha256=MpyFaREjoPBWqY0iLgsN0JovL6cyCyNf5SzYI0i1b5w,5328
5
+ weco/utils.py,sha256=UUSw6ocqWdlSmIXVcH66DAL4NuLU2rFOyviD8aTWsv0,4371
6
+ weco-0.1.4.dist-info/LICENSE,sha256=NvpxfBuSajszAczWBGKxhHe4gsvil1H63zmu8xXZdL0,1064
7
+ weco-0.1.4.dist-info/METADATA,sha256=YonF54xTkK71fPqBSuRmRcyqQDRh059LKxUpVvrZ94w,5957
8
+ weco-0.1.4.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
9
+ weco-0.1.4.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
10
+ weco-0.1.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.1)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- weco/__init__.py,sha256=qiKpnrm6t0n0bpAtXEKJO1Yz2xYXnJJRZBWt-cH7DdU,168
2
- weco/client.py,sha256=RZ6wsFohWhcJSz8HIGTIfVpXozUE_bYK7K037GSlZdA,10991
3
- weco/functional.py,sha256=oZljY8enxw8nPKP1TGBc8AxERt3Y9ukpV1_40vIJ8oE,4375
4
- weco-0.1.3.dist-info/LICENSE,sha256=NvpxfBuSajszAczWBGKxhHe4gsvil1H63zmu8xXZdL0,1064
5
- weco-0.1.3.dist-info/METADATA,sha256=vqzoh9iOLs7ZbSVjQwZz1jiLmSdEITITT0A7FWAdz0c,3876
6
- weco-0.1.3.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
7
- weco-0.1.3.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
8
- weco-0.1.3.dist-info/RECORD,,
File without changes