weco 0.1.1__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/__init__.py CHANGED
@@ -1,8 +1,4 @@
1
1
  from .client import WecoAI
2
- from .functional import build, query
2
+ from .functional import abuild, aquery, batch_query, build, query
3
3
 
4
- __all__ = [
5
- "WecoAI",
6
- "build",
7
- "query",
8
- ]
4
+ __all__ = ["WecoAI", "build", "abuild", "query", "aquery", "batch_query"]
weco/client.py CHANGED
@@ -1,19 +1,50 @@
1
+ import asyncio
2
+ import base64
1
3
  import os
2
- from typing import Any, Dict
4
+ import warnings
5
+ from io import BytesIO
6
+ from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Union
3
7
 
8
+ import httpx
4
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
+ )
5
22
 
6
23
 
7
24
  class WecoAI:
8
- def __init__(self, api_key: str = None) -> None:
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
+
29
+ Attributes
30
+ ----------
31
+ api_key : str
32
+ The API key used for authentication.
33
+ """
34
+
35
+ def __init__(self, api_key: str = None, timeout: float = 120.0, http2: bool = True) -> None:
9
36
  """Initializes the WecoAI client with the provided API key and base URL.
10
37
 
11
38
  Parameters
12
39
  ----------
13
- api_key : str
40
+ api_key : str, optional
14
41
  The API key used for authentication. If not provided, the client will attempt to read it from the environment variable - WECO_API_KEY.
15
- base_url : str
16
- The base URL of the WecoAI API.
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).
17
48
 
18
49
  Raises
19
50
  ------
@@ -25,29 +56,29 @@ class WecoAI:
25
56
  try:
26
57
  api_key = os.environ["WECO_API_KEY"]
27
58
  except KeyError:
28
- raise ValueError(
29
- "WECO_API_KEY must be passed to client or set as an environment variable"
30
- )
59
+ raise ValueError("WECO_API_KEY must be passed to client or set as an environment variable")
31
60
  self.api_key = api_key
61
+ self.http2 = http2
62
+ self.timeout = timeout
63
+ self.base_url = "https://function.api.weco.ai"
64
+ # Setup clients
65
+ self.client = httpx.Client(http2=http2, timeout=timeout)
66
+ self.async_client = httpx.AsyncClient(http2=http2, timeout=timeout)
32
67
 
33
- # base URL
34
- self.base_url = "https://function-builder.vercel.app"
68
+ def close(self):
69
+ """Close both synchronous and asynchronous clients."""
70
+ self.client.close()
71
+ asyncio.run(self.async_client.aclose())
35
72
 
36
73
  def _headers(self) -> Dict[str, str]:
37
- """Constructs the headers for the API requests.
38
-
39
- Returns
40
- -------
41
- dict
42
- A dictionary containing the headers.
43
- """
74
+ """Constructs the headers for the API requests."""
44
75
  return {
45
76
  "Authorization": f"Bearer {self.api_key}",
46
77
  "Content-Type": "application/json",
47
78
  }
48
79
 
49
- def _post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
50
- """Makes a POST request to the specified endpoint with the provided data.
80
+ def _make_request(self, endpoint: str, data: Dict[str, Any], is_async: bool = False) -> Callable:
81
+ """Creates a callable for making either synchronous or asynchronous requests.
51
82
 
52
83
  Parameters
53
84
  ----------
@@ -55,49 +86,359 @@ class WecoAI:
55
86
  The API endpoint to which the request will be made.
56
87
  data : dict
57
88
  The data to be sent in the request body.
89
+ is_async : bool, optional
90
+ Whether to create an asynchronous request (default is False).
91
+
92
+ Returns
93
+ -------
94
+ Callable
95
+ A callable that performs the HTTP request.
96
+ """
97
+ url = f"{self.base_url}/{endpoint}"
98
+ headers = self._headers()
99
+
100
+ if is_async:
101
+
102
+ async def _request():
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
114
+
115
+ return _request()
116
+ else:
117
+
118
+ def _request():
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
130
+
131
+ return _request()
132
+
133
+ def _process_query_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
134
+ """Processes the query response and handles warnings.
135
+
136
+ Parameters
137
+ ----------
138
+ response : dict
139
+ The raw API response.
58
140
 
59
141
  Returns
60
142
  -------
61
143
  dict
62
- The response from the server as a dictionary.
144
+ A processed dictionary containing the output, token counts, and latency.
63
145
 
64
146
  Raises
65
147
  ------
66
- requests.HTTPError
67
- If the request fails.
148
+ UserWarning
149
+ If there are any warnings in the API response.
68
150
  """
69
- url = f"{self.base_url}/{endpoint}"
70
- response = requests.post(url, json=data, headers=self._headers())
71
- response.raise_for_status()
72
- return response.json()
151
+ for _warning in response.get("warnings", []):
152
+ warnings.warn(_warning)
73
153
 
74
- def build(self, task_description: str) -> tuple[str, str]:
75
- """Builds a specialized function given a task description.
154
+ return {
155
+ "output": response["response"],
156
+ "in_tokens": response["num_input_tokens"],
157
+ "out_tokens": response["num_output_tokens"],
158
+ "latency_ms": response["latency_ms"],
159
+ }
160
+
161
+ def _build(self, task_description: str, is_async: bool) -> Union[Tuple[str, str], Coroutine[Any, Any, Tuple[str, str]]]:
162
+ """Internal method to handle both synchronous and asynchronous build requests.
76
163
 
77
164
  Parameters
78
165
  ----------
79
166
  task_description : str
80
167
  A description of the task for which the function is being built.
168
+ is_async : bool
169
+ Whether to perform an asynchronous request.
81
170
 
82
171
  Returns
83
172
  -------
84
- tuple[str, str]
85
- A tuple containing the name and description of the function.
173
+ Union[tuple[str, str], Coroutine[Any, Any, tuple[str, str]]]
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.
86
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
+
87
187
  endpoint = "build"
88
188
  data = {"request": task_description}
89
- response = self._post(endpoint, data)
90
- return response["name"], response["description"]
189
+ request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
190
+
191
+ if is_async:
91
192
 
92
- def query(self, fn_name: str, fn_input: str) -> Dict[str, Any]:
93
- """Queries a function with the given function ID and input.
193
+ async def _async_build():
194
+ response = await request
195
+ return response["name"], response["description"]
196
+
197
+ return _async_build()
198
+ else:
199
+ response = request # the request has already been made and the response is available
200
+ return response["name"], response["description"]
201
+
202
+ async def abuild(self, task_description: str) -> Tuple[str, str]:
203
+ """Asynchronously builds a specialized function given a task description.
204
+
205
+ Parameters
206
+ ----------
207
+ task_description : str
208
+ A description of the task for which the function is being built.
209
+
210
+ Returns
211
+ -------
212
+ tuple[str, str]
213
+ A tuple containing the name and description of the function.
214
+ """
215
+ return await self._build(task_description=task_description, is_async=True)
216
+
217
+ def build(self, task_description: str) -> Tuple[str, str]:
218
+ """Synchronously builds a specialized function given a task description.
219
+
220
+ Parameters
221
+ ----------
222
+ task_description : str
223
+ A description of the task for which the function is being built.
224
+
225
+ Returns
226
+ -------
227
+ tuple[str, str]
228
+ A tuple containing the name and description of the function.
229
+ """
230
+ return self._build(task_description=task_description, is_async=False)
231
+
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.
235
+
236
+ Parameters
237
+ ----------
238
+ fn_name : str
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
+ ----------
381
+ is_async : bool
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.
389
+
390
+ Returns
391
+ -------
392
+ Union[Dict[str, Any], Coroutine[Any, Any, dict]]
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.
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
414
+ endpoint = "query"
415
+ data = {"name": fn_name, "text": text_input, "images": image_urls}
416
+ request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
417
+
418
+ if is_async:
419
+
420
+ async def _async_query():
421
+ response = await request
422
+ return self._process_query_response(response=response)
423
+
424
+ return _async_query()
425
+ else:
426
+ response = request # the request has already been made and the response is available
427
+ return self._process_query_response(response=response)
428
+
429
+ async def aquery(
430
+ self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []
431
+ ) -> Dict[str, Any]:
432
+ """Asynchronously queries a function with the given function ID and input.
94
433
 
95
434
  Parameters
96
435
  ----------
97
436
  fn_name : str
98
437
  The name of the function to query.
99
- fn_input : str
100
- 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.
101
442
 
102
443
  Returns
103
444
  -------
@@ -105,17 +446,69 @@ class WecoAI:
105
446
  A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
106
447
  and the latency in milliseconds.
107
448
  """
449
+ return await self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=True)
108
450
 
109
- endpoint = "query"
110
- data = {
111
- "name": fn_name,
112
- "user_message": fn_input,
113
- }
114
- response = self._post(endpoint, data)
115
- result = {
116
- "output": response["response"],
117
- "in_tokens": response["num_input_tokens"],
118
- "out_tokens": response["num_output_tokens"],
119
- "latency_ms": response["latency_ms"],
120
- }
121
- return result
451
+ def query(self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []) -> Dict[str, Any]:
452
+ """Synchronously queries a function with the given function ID and input.
453
+
454
+ Parameters
455
+ ----------
456
+ fn_name : str
457
+ The name of the function to query.
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.
462
+
463
+ Returns
464
+ -------
465
+ dict
466
+ A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
467
+ and the latency in milliseconds.
468
+ """
469
+ return self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=False)
470
+
471
+ def batch_query(self, fn_names: Union[str, List[str]], batch_inputs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
472
+ """Synchronously queries multiple functions using asynchronous calls internally.
473
+
474
+ This method uses the asynchronous queries to submit all queries concurrently
475
+ and waits for all responses to be received before returning the results.
476
+
477
+ Parameters
478
+ ----------
479
+ fn_name : Union[str, List[str]]
480
+ The name of the function or a list of function names to query.
481
+ Note that if a single function name is provided, it will be used for all queries.
482
+ If a list of function names is provided, the length must match the number of queries.
483
+
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.
489
+
490
+ Returns
491
+ -------
492
+ List[Dict[str, Any]]
493
+ A list of dictionaries, each containing the output of a function query,
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.
501
+ """
502
+ if isinstance(fn_names, str):
503
+ fn_names = [fn_names] * len(batch_inputs)
504
+ elif len(fn_names) != len(batch_inputs):
505
+ raise ValueError("The number of function names must match the number of inputs.")
506
+
507
+ async def run_queries():
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
+ ]
512
+ return await asyncio.gather(*tasks)
513
+
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
@@ -1,10 +1,11 @@
1
- from typing import Any, Dict, Optional
1
+ from typing import Any, Dict, List, Optional
2
2
 
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
- """Builds a specialized function given a task description.
8
+ """Builds a specialized function synchronously given a task description.
8
9
 
9
10
  Parameters
10
11
  ----------
@@ -23,15 +24,39 @@ def build(task_description: str, api_key: str = None) -> tuple[str, str]:
23
24
  return response
24
25
 
25
26
 
26
- def query(fn_name: str, fn_input: str, api_key: Optional[str] = None) -> Dict[str, Any]:
27
- """Queries a function with the given function ID and input.
27
+ async def abuild(task_description: str, api_key: str = None) -> tuple[str, str]:
28
+ """Builds a specialized function asynchronously given a task description.
29
+
30
+ Parameters
31
+ ----------
32
+ task_description : str
33
+ A description of the task for which the function is being built.
34
+ api_key : str
35
+ The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
36
+
37
+ Returns
38
+ -------
39
+ tuple[str, str]
40
+ A tuple containing the name and description of the function.
41
+ """
42
+ client = WecoAI(api_key=api_key)
43
+ response = await client.abuild(task_description=task_description)
44
+ return response
45
+
46
+
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]:
50
+ """Queries a function synchronously with the given function ID and input.
28
51
 
29
52
  Parameters
30
53
  ----------
31
54
  fn_name : str
32
55
  The name of the function to query.
33
- fn_input : str
34
- 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.
35
60
  api_key : str
36
61
  The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
37
62
 
@@ -42,5 +67,67 @@ def query(fn_name: str, fn_input: str, api_key: Optional[str] = None) -> Dict[st
42
67
  and the latency in milliseconds.
43
68
  """
44
69
  client = WecoAI(api_key=api_key)
45
- 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)
46
71
  return response
72
+
73
+
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]:
77
+ """Queries a function asynchronously with the given function ID and input.
78
+
79
+ Parameters
80
+ ----------
81
+ fn_name : str
82
+ The name of the function to query.
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.
87
+ api_key : str
88
+ The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
89
+
90
+ Returns
91
+ -------
92
+ dict
93
+ A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
94
+ and the latency in milliseconds.
95
+ """
96
+ client = WecoAI(api_key=api_key)
97
+ response = await client.aquery(fn_name=fn_name, text_input=text_input, images_input=images_input)
98
+ return response
99
+
100
+
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]]:
104
+ """Synchronously queries multiple functions using asynchronous calls internally.
105
+
106
+ This method uses the asynchronous queries to submit all queries concurrently
107
+ and waits for all responses to be received before returning the results.
108
+
109
+ Parameters
110
+ ----------
111
+ fn_name : str | List[str]
112
+ The name of the function or a list of function names to query.
113
+ Note that if a single function name is provided, it will be used for all queries.
114
+ If a list of function names is provided, the length must match the number of queries.
115
+
116
+ batch_inputs : List[str]
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.
121
+
122
+ api_key : str, optional
123
+ The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
124
+
125
+ Returns
126
+ -------
127
+ List[Dict[str, Any]]
128
+ A list of dictionaries, each containing the output of a function query,
129
+ in the same order as the input queries.
130
+ """
131
+ client = WecoAI(api_key=api_key)
132
+ responses = client.batch_query(fn_names=fn_names, batch_inputs=batch_inputs)
133
+ return responses
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))
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.1
2
+ Name: weco
3
+ Version: 0.1.4
4
+ Summary: A client facing API for interacting with the WeCo AI function builder service.
5
+ Author-email: WeCo AI Team <dhruv@weco.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/WecoAI/weco-python
8
+ Keywords: AI,LLM,machine learning,data science,function builder,AI function
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: asyncio
16
+ Requires-Dist: httpx[http2]
17
+ Requires-Dist: pillow
18
+ Provides-Extra: dev
19
+ Requires-Dist: flake8 ; extra == 'dev'
20
+ Requires-Dist: flake8-pyproject ; extra == 'dev'
21
+ Requires-Dist: black ; extra == 'dev'
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'
27
+
28
+ <div align="center" style="display: flex; align-items: center; justify-content: center;">
29
+ <img src="assets/weco.svg" alt="WeCo AI" style="height: 50px; margin-right: 10px;">
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>
31
+ </div>
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 -->
37
+ # $f$(👷‍♂️)
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
+
42
+ A client facing API for interacting with the [WeCo AI](https://www.weco.ai/) function builder [service](https://weco-app.vercel.app/function)!
43
+
44
+
45
+ Use this API to build *complex* systems *fast*. We lower the barrier of entry to software engineer, data science and machine learning by providing an interface to prototype difficult solutions quickly in just a few lines of code.
46
+
47
+ ## Installation
48
+
49
+ Install the `weco` package simply by calling this in your terminal of choice:
50
+ ```bash
51
+ pip install weco
52
+ ```
53
+
54
+ ## Features
55
+
56
+ - The **build** function enables quick and easy prototyping of new functions via LLMs through just natural language. We encourage users to do this through our [web console](https://weco-app.vercel.app/function) for maximum control and ease of use, however, you can also do this through our API as shown in [here](examples/cookbook.ipynb).
57
+ - The **query** function allows you to test and use the newly created function in your own code.
58
+ - We offer asynchronous versions of the above clients.
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!
61
+
62
+ We provide both services in two ways:
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.
64
+ - `weco.query` and `weco.build` to be used when you only require sparse usage.
65
+
66
+ ## Usage
67
+
68
+ When using the WeCo API, you will need to set the API key:
69
+ You can find/setup your API key [here](https://weco-app.vercel.app/account) by navigating to the API key tab. Once you have your API key, you may pass it to the `weco` client using the `api_key` argument input or set it as an environment variable such as:
70
+ ```bash
71
+ export WECO_API_KEY=<YOUR_WECO_API_KEY>
72
+ ```
73
+
74
+ ## Example
75
+
76
+ We create a function on the [web console](https://weco-app.vercel.app/function) for the following task:
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)."
78
+
79
+ Now, you're ready to query this function anywhere in your code!
80
+
81
+ ```python
82
+ from weco import query
83
+ response = query(
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."
86
+ )
87
+ ```
88
+
89
+ For more examples and an advanced user guide, check out our function builder [cookbook](examples/cookbook.ipynb).
90
+
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: bdist_wheel (0.43.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,69 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: weco
3
- Version: 0.1.1
4
- Summary: A client facing API for interacting with the WeCo AI function builder service.
5
- Home-page: https://github.com/WecoAI/weco
6
- Author: ['WeCo AI Team']
7
- Author-email: dhruv@weco.ai
8
- License: MIT
9
- Keywords: artificial intelligence,machine learning,data science,function builder,LLM
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.8
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Requires-Dist: requests
16
-
17
- [![Typing SVG](https://readme-typing-svg.demolab.com?font=Georgia&size=32&duration=4000&pause=400&color=FD4578&vCenter=true&multiline=false&width=750&height=100&lines=WeCo:+Function+Builder+Client;)](https://git.io/typing-svg)
18
-
19
- [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
20
- ![Python](https://img.shields.io/badge/Python-3.10.14-blue)
21
-
22
-
23
- # WeCo $f$(👷‍♂️)
24
-
25
- A client facing API for interacting with the [WeCo AI](https://www.weco.ai/) function builder [service](https://weco-app.vercel.app/function)!
26
-
27
-
28
- Use this API to build *complex* systems *fast*. We lower the barrier of entry to software engineer, data science and machine learning by providing an interface to prototype difficult solutions quickly in just a few lines of code.
29
-
30
- ## Installation
31
-
32
- Install the `weco` package simply by calling this in your terminal of choice:
33
- ```bash
34
- pip install weco
35
- ```
36
-
37
- ## Features
38
-
39
- - The **build** function enables quick and easy prototyping of new functions via LLMs through just natural language. We encourage users to do this through our [web console](https://weco-app.vercel.app/function) for maximum control and ease of use, however, you can also do this through our API as shown in [here](examples/).
40
- - The **query** function allows you to test and use the newly created function in your own code.
41
-
42
- We provide both services in two ways:
43
- - `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. An example is shown [here](examples/example_client.py).
44
- - `weco.query` and `weco.build` to be used when you only require sparse usage. An example is provided [here](examples/example_functional.py).
45
-
46
- ## Usage
47
-
48
- When using the WeCo API, you will need to set the API key:
49
- You can find/setup your API key [here](https://weco-app.vercel.app/account) by navigating to the API key tab. Once you have your API key, you may pass it to the `weco` client using the `api_key` argument input or set it as an environment variable such as:
50
- ```
51
- export WECO_API_KEY=<YOUR_WECO_API_KEY>
52
- ```
53
-
54
- ## Example
55
-
56
- We create a function on the [web console](https://weco-app.vercel.app/function) for the following task:
57
- > "I want to evaluate the feasibility of a machine learning task. Give me a json object with three keys - 'feasibility', 'justification', and 'suggestions'."
58
-
59
- Now, you're ready to query this function anywhere in your code!
60
-
61
- ```python
62
- from weco import query
63
- response = query(
64
- fn_name=fn_name,
65
- fn_input="I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle.",
66
- )
67
- ```
68
-
69
- ## Enjoy $f$(👷‍♂️)!
@@ -1,8 +0,0 @@
1
- weco/__init__.py,sha256=Xl80uFblDNbhIxL3_BQgYc_SninCeOeF0shnzPCrxrw,119
2
- weco/client.py,sha256=YOUgWJwLKlp9GR2u40pmnjaBtmDjLtdP_pUuvDAMCJQ,3744
3
- weco/functional.py,sha256=AnQ0PaZKr2K9a7wxXfVPJTTJLgrn1niZDQctdkj5vFA,1501
4
- weco-0.1.1.dist-info/LICENSE,sha256=NvpxfBuSajszAczWBGKxhHe4gsvil1H63zmu8xXZdL0,1064
5
- weco-0.1.1.dist-info/METADATA,sha256=ieRckEKjQ-shxHf5VPcNCHENd6CUpJz3i43-EeLSnMw,3214
6
- weco-0.1.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
- weco-0.1.1.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
8
- weco-0.1.1.dist-info/RECORD,,
File without changes