weco 0.1.5__tar.gz → 0.1.7__tar.gz
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-0.1.5 → weco-0.1.7}/.github/workflows/release.yml +5 -2
- {weco-0.1.5 → weco-0.1.7}/PKG-INFO +1 -1
- {weco-0.1.5 → weco-0.1.7}/examples/cookbook.ipynb +7 -0
- {weco-0.1.5 → weco-0.1.7}/pyproject.toml +1 -1
- {weco-0.1.5 → weco-0.1.7}/tests/test_asynchronous.py +20 -14
- weco-0.1.7/tests/test_batching.py +75 -0
- {weco-0.1.5 → weco-0.1.7}/tests/test_synchronous.py +20 -14
- {weco-0.1.5 → weco-0.1.7}/weco/client.py +60 -55
- {weco-0.1.5 → weco-0.1.7}/weco/functional.py +22 -13
- {weco-0.1.5 → weco-0.1.7}/weco.egg-info/PKG-INFO +1 -1
- weco-0.1.5/tests/test_batching.py +0 -66
- {weco-0.1.5 → weco-0.1.7}/.github/workflows/lint.yml +0 -0
- {weco-0.1.5 → weco-0.1.7}/.gitignore +0 -0
- {weco-0.1.5 → weco-0.1.7}/LICENSE +0 -0
- {weco-0.1.5 → weco-0.1.7}/README.md +0 -0
- {weco-0.1.5 → weco-0.1.7}/assets/weco.svg +0 -0
- {weco-0.1.5 → weco-0.1.7}/setup.cfg +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco/__init__.py +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco/constants.py +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco/utils.py +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco.egg-info/SOURCES.txt +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco.egg-info/dependency_links.txt +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco.egg-info/requires.txt +0 -0
- {weco-0.1.5 → weco-0.1.7}/weco.egg-info/top_level.txt +0 -0
|
@@ -74,12 +74,15 @@ jobs:
|
|
|
74
74
|
inputs: >-
|
|
75
75
|
./dist/*.tar.gz
|
|
76
76
|
./dist/*.whl
|
|
77
|
+
- name: Debug Print github.ref_name
|
|
78
|
+
run: >-
|
|
79
|
+
echo "github.ref_name: ${{ github.ref_name }}"
|
|
77
80
|
- name: Create GitHub Release
|
|
78
81
|
env:
|
|
79
82
|
GITHUB_TOKEN: ${{ github.token }}
|
|
80
83
|
run: >-
|
|
81
84
|
gh release create
|
|
82
|
-
'
|
|
85
|
+
'v0.1.7'
|
|
83
86
|
--repo '${{ github.repository }}'
|
|
84
87
|
--notes ""
|
|
85
88
|
- name: Upload artifact signatures to GitHub Release
|
|
@@ -90,5 +93,5 @@ jobs:
|
|
|
90
93
|
# sigstore-produced signatures and certificates.
|
|
91
94
|
run: >-
|
|
92
95
|
gh release upload
|
|
93
|
-
'
|
|
96
|
+
'v0.1.7' dist/**
|
|
94
97
|
--repo '${{ github.repository }}'
|
|
@@ -326,6 +326,13 @@
|
|
|
326
326
|
"for key, value in query_response.items(): print(f\"{key}: {value}\")"
|
|
327
327
|
]
|
|
328
328
|
},
|
|
329
|
+
{
|
|
330
|
+
"cell_type": "markdown",
|
|
331
|
+
"metadata": {},
|
|
332
|
+
"source": [
|
|
333
|
+
"## A/B Testing with Function Versions"
|
|
334
|
+
]
|
|
335
|
+
},
|
|
329
336
|
{
|
|
330
337
|
"cell_type": "markdown",
|
|
331
338
|
"metadata": {},
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
description = "A client facing API for interacting with the WeCo AI function builder service."
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
version = "0.1.
|
|
13
|
+
version = "0.1.7"
|
|
14
14
|
license = {text = "MIT"}
|
|
15
15
|
requires-python = ">=3.8"
|
|
16
16
|
dependencies = ["asyncio", "httpx[http2]", "pillow"]
|
|
@@ -8,8 +8,9 @@ from weco import abuild, aquery
|
|
|
8
8
|
@pytest.mark.asyncio
|
|
9
9
|
async def test_abuild(text_evaluator, image_evaluator, text_and_image_evaluator):
|
|
10
10
|
for evaluator in [text_evaluator, image_evaluator, text_and_image_evaluator]:
|
|
11
|
-
fn_name, fn_desc = await evaluator
|
|
11
|
+
fn_name, version_number, fn_desc = await evaluator
|
|
12
12
|
assert isinstance(fn_name, str)
|
|
13
|
+
assert isinstance(version_number, int)
|
|
13
14
|
assert isinstance(fn_desc, str)
|
|
14
15
|
|
|
15
16
|
|
|
@@ -23,16 +24,17 @@ async def assert_query_response(query_response):
|
|
|
23
24
|
|
|
24
25
|
@pytest.fixture
|
|
25
26
|
async def text_evaluator():
|
|
26
|
-
fn_name, fn_desc = await abuild(
|
|
27
|
-
task_description="Evaluate the sentiment of the given text. Provide a json object with 'sentiment' and 'explanation' keys."
|
|
27
|
+
fn_name, version_number, fn_desc = await abuild(
|
|
28
|
+
task_description="Evaluate the sentiment of the given text. Provide a json object with 'sentiment' and 'explanation' keys.",
|
|
29
|
+
multimodal=False
|
|
28
30
|
)
|
|
29
|
-
return fn_name, fn_desc
|
|
31
|
+
return fn_name, version_number, fn_desc
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
@pytest.mark.asyncio
|
|
33
35
|
async def test_text_aquery(text_evaluator):
|
|
34
|
-
fn_name, _ = await text_evaluator
|
|
35
|
-
query_response = await aquery(fn_name=fn_name, text_input="I love this product!")
|
|
36
|
+
fn_name, version_number, _ = await text_evaluator
|
|
37
|
+
query_response = await aquery(fn_name=fn_name, version_number=version_number, text_input="I love this product!")
|
|
36
38
|
|
|
37
39
|
await assert_query_response(query_response)
|
|
38
40
|
assert set(query_response["output"].keys()) == {"sentiment", "explanation"}
|
|
@@ -40,17 +42,19 @@ async def test_text_aquery(text_evaluator):
|
|
|
40
42
|
|
|
41
43
|
@pytest.fixture
|
|
42
44
|
async def image_evaluator():
|
|
43
|
-
fn_name, fn_desc = await abuild(
|
|
44
|
-
task_description="Describe the contents of the given images. Provide a json object with 'description' and 'objects' keys."
|
|
45
|
+
fn_name, version_number, fn_desc = await abuild(
|
|
46
|
+
task_description="Describe the contents of the given images. Provide a json object with 'description' and 'objects' keys.",
|
|
47
|
+
multimodal=True
|
|
45
48
|
)
|
|
46
|
-
return fn_name, fn_desc
|
|
49
|
+
return fn_name, version_number, fn_desc
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
@pytest.mark.asyncio
|
|
50
53
|
async def test_image_aquery(image_evaluator):
|
|
51
|
-
fn_name, _ = await image_evaluator
|
|
54
|
+
fn_name, version_number, _ = await image_evaluator
|
|
52
55
|
query_response = await aquery(
|
|
53
56
|
fn_name=fn_name,
|
|
57
|
+
version_number=version_number,
|
|
54
58
|
images_input=[
|
|
55
59
|
"https://www.integratedtreatmentservices.co.uk/wp-content/uploads/2013/12/Objects-of-Reference.jpg",
|
|
56
60
|
"https://t4.ftcdn.net/jpg/05/70/90/23/360_F_570902339_kNj1reH40GFXakTy98EmfiZHci2xvUCS.jpg",
|
|
@@ -63,17 +67,19 @@ async def test_image_aquery(image_evaluator):
|
|
|
63
67
|
|
|
64
68
|
@pytest.fixture
|
|
65
69
|
async def text_and_image_evaluator():
|
|
66
|
-
fn_name, fn_desc = await abuild(
|
|
67
|
-
task_description="Evaluate, solve and arrive at a numerical answer for the image provided. Provide a json object with 'answer' and 'explanation' keys."
|
|
70
|
+
fn_name, version_number, fn_desc = await abuild(
|
|
71
|
+
task_description="Evaluate, solve and arrive at a numerical answer for the image provided. Provide a json object with 'answer' and 'explanation' keys.",
|
|
72
|
+
multimodal=True
|
|
68
73
|
)
|
|
69
|
-
return fn_name, fn_desc
|
|
74
|
+
return fn_name, version_number, fn_desc
|
|
70
75
|
|
|
71
76
|
|
|
72
77
|
@pytest.mark.asyncio
|
|
73
78
|
async def test_text_and_image_aquery(text_and_image_evaluator):
|
|
74
|
-
fn_name, _ = await text_and_image_evaluator
|
|
79
|
+
fn_name, version_number, _ = await text_and_image_evaluator
|
|
75
80
|
query_response = await aquery(
|
|
76
81
|
fn_name=fn_name,
|
|
82
|
+
version_number=version_number,
|
|
77
83
|
text_input="Find x and y.",
|
|
78
84
|
images_input=[
|
|
79
85
|
"https://i.ytimg.com/vi/cblHUeq3bkE/hq720.jpg?sqp=-oaymwEhCK4FEIIDSFryq4qpAxMIARUAAAAAGAElAADIQj0AgKJD&rs=AOn4CLAKn3piY91QRCBzRgnzAPf7MPrjDQ"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from weco import batch_query, build
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Internally, these functions use the WecoAI client
|
|
7
|
+
# therefore, we can test both the client and functional forms here
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def ml_task_evaluator():
|
|
10
|
+
fn_name, version_number, _ = build(
|
|
11
|
+
task_description="I want to evaluate the feasibility of a machine learning task. Give me a json object with three keys - 'feasibility', 'justification', and 'suggestions'.",
|
|
12
|
+
multimodal=False
|
|
13
|
+
)
|
|
14
|
+
return fn_name, version_number
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def ml_task_inputs():
|
|
19
|
+
return [
|
|
20
|
+
{"text_input": "I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle."},
|
|
21
|
+
{"text_input": "I want to train a model to classify digits using the MNIST dataset hosted on Kaggle using a Google Colab notebook."},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def image_evaluator():
|
|
27
|
+
fn_name, version_number, _ = build(
|
|
28
|
+
task_description="Describe the contents of the given images. Provide a json object with 'description' and 'objects' keys.",
|
|
29
|
+
multimodal=True
|
|
30
|
+
)
|
|
31
|
+
return fn_name, version_number
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def image_inputs():
|
|
36
|
+
return [
|
|
37
|
+
{"images_input": ["https://www.integratedtreatmentservices.co.uk/wp-content/uploads/2013/12/Objects-of-Reference.jpg"]},
|
|
38
|
+
{"images_input": ["https://t4.ftcdn.net/jpg/05/70/90/23/360_F_570902339_kNj1reH40GFXakTy98EmfiZHci2xvUCS.jpg"]},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_batch_query_text(ml_task_evaluator, ml_task_inputs):
|
|
43
|
+
fn_name, version_number = ml_task_evaluator
|
|
44
|
+
batch_inputs = ml_task_inputs
|
|
45
|
+
|
|
46
|
+
query_responses = batch_query(fn_name=fn_name, version_number=version_number, batch_inputs=batch_inputs)
|
|
47
|
+
|
|
48
|
+
assert len(query_responses) == len(batch_inputs)
|
|
49
|
+
|
|
50
|
+
for query_response in query_responses:
|
|
51
|
+
assert isinstance(query_response["output"], dict)
|
|
52
|
+
assert isinstance(query_response["in_tokens"], int)
|
|
53
|
+
assert isinstance(query_response["out_tokens"], int)
|
|
54
|
+
assert isinstance(query_response["latency_ms"], float)
|
|
55
|
+
|
|
56
|
+
output = query_response["output"]
|
|
57
|
+
assert set(output.keys()) == {"feasibility", "justification", "suggestions"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_batch_query_image(image_evaluator, image_inputs):
|
|
61
|
+
fn_name, version_number = image_evaluator
|
|
62
|
+
batch_inputs = image_inputs
|
|
63
|
+
|
|
64
|
+
query_responses = batch_query(fn_name=fn_name, version_number=version_number, batch_inputs=batch_inputs)
|
|
65
|
+
|
|
66
|
+
assert len(query_responses) == len(batch_inputs)
|
|
67
|
+
|
|
68
|
+
for query_response in query_responses:
|
|
69
|
+
assert isinstance(query_response["output"], dict)
|
|
70
|
+
assert isinstance(query_response["in_tokens"], int)
|
|
71
|
+
assert isinstance(query_response["out_tokens"], int)
|
|
72
|
+
assert isinstance(query_response["latency_ms"], float)
|
|
73
|
+
|
|
74
|
+
output = query_response["output"]
|
|
75
|
+
assert set(output.keys()) == {"description", "objects"}
|
|
@@ -7,8 +7,9 @@ from weco import build, query
|
|
|
7
7
|
# therefore, we can test both the client and functional forms here
|
|
8
8
|
def test_build(text_evaluator, image_evaluator, text_and_image_evaluator):
|
|
9
9
|
for evaluator in [text_evaluator, image_evaluator, text_and_image_evaluator]:
|
|
10
|
-
fn_name, fn_desc = evaluator
|
|
10
|
+
fn_name, version_number, fn_desc = evaluator
|
|
11
11
|
assert isinstance(fn_name, str)
|
|
12
|
+
assert isinstance(version_number, int)
|
|
12
13
|
assert isinstance(fn_desc, str)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -22,15 +23,16 @@ def assert_query_response(query_response):
|
|
|
22
23
|
|
|
23
24
|
@pytest.fixture
|
|
24
25
|
def text_evaluator():
|
|
25
|
-
fn_name, fn_desc = build(
|
|
26
|
-
task_description="Evaluate the sentiment of the given text. Provide a json object with 'sentiment' and 'explanation' keys."
|
|
26
|
+
fn_name, version_number, fn_desc = build(
|
|
27
|
+
task_description="Evaluate the sentiment of the given text. Provide a json object with 'sentiment' and 'explanation' keys.",
|
|
28
|
+
multimodal=False
|
|
27
29
|
)
|
|
28
|
-
return fn_name, fn_desc
|
|
30
|
+
return fn_name, version_number, fn_desc
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def test_text_query(text_evaluator):
|
|
32
|
-
fn_name, _ = text_evaluator
|
|
33
|
-
query_response = query(fn_name=fn_name, text_input="I love this product!")
|
|
34
|
+
fn_name, version_number, _ = text_evaluator
|
|
35
|
+
query_response = query(fn_name=fn_name, version_number=version_number, text_input="I love this product!")
|
|
34
36
|
|
|
35
37
|
assert_query_response(query_response)
|
|
36
38
|
assert set(query_response["output"].keys()) == {"sentiment", "explanation"}
|
|
@@ -38,16 +40,18 @@ def test_text_query(text_evaluator):
|
|
|
38
40
|
|
|
39
41
|
@pytest.fixture
|
|
40
42
|
def image_evaluator():
|
|
41
|
-
fn_name, fn_desc = build(
|
|
42
|
-
task_description="Describe the contents of the given images. Provide a json object with 'description' and 'objects' keys."
|
|
43
|
+
fn_name, version_number, fn_desc = build(
|
|
44
|
+
task_description="Describe the contents of the given images. Provide a json object with 'description' and 'objects' keys.",
|
|
45
|
+
multimodal=True
|
|
43
46
|
)
|
|
44
|
-
return fn_name, fn_desc
|
|
47
|
+
return fn_name, version_number, fn_desc
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
def test_image_query(image_evaluator):
|
|
48
|
-
fn_name, _ = image_evaluator
|
|
51
|
+
fn_name, version_number, _ = image_evaluator
|
|
49
52
|
query_response = query(
|
|
50
53
|
fn_name=fn_name,
|
|
54
|
+
version_number=version_number,
|
|
51
55
|
images_input=[
|
|
52
56
|
"https://www.integratedtreatmentservices.co.uk/wp-content/uploads/2013/12/Objects-of-Reference.jpg",
|
|
53
57
|
"https://t4.ftcdn.net/jpg/05/70/90/23/360_F_570902339_kNj1reH40GFXakTy98EmfiZHci2xvUCS.jpg",
|
|
@@ -60,16 +64,18 @@ def test_image_query(image_evaluator):
|
|
|
60
64
|
|
|
61
65
|
@pytest.fixture
|
|
62
66
|
def text_and_image_evaluator():
|
|
63
|
-
fn_name, fn_desc = build(
|
|
64
|
-
task_description="Evaluate, solve and arrive at a numerical answer for the image provided. Perform any additional things if instructed. Provide a json object with 'answer' and 'explanation' keys."
|
|
67
|
+
fn_name, version_number, fn_desc = build(
|
|
68
|
+
task_description="Evaluate, solve and arrive at a numerical answer for the image provided. Perform any additional things if instructed. Provide a json object with 'answer' and 'explanation' keys.",
|
|
69
|
+
multimodal=True
|
|
65
70
|
)
|
|
66
|
-
return fn_name, fn_desc
|
|
71
|
+
return fn_name, version_number, fn_desc
|
|
67
72
|
|
|
68
73
|
|
|
69
74
|
def test_text_and_image_query(text_and_image_evaluator):
|
|
70
|
-
fn_name, _ = text_and_image_evaluator
|
|
75
|
+
fn_name, version_number, _ = text_and_image_evaluator
|
|
71
76
|
query_response = query(
|
|
72
77
|
fn_name=fn_name,
|
|
78
|
+
version_number=version_number,
|
|
73
79
|
text_input="Find x and y.",
|
|
74
80
|
images_input=[
|
|
75
81
|
"https://i.ytimg.com/vi/cblHUeq3bkE/hq720.jpg?sqp=-oaymwEhCK4FEIIDSFryq4qpAxMIARUAAAAAGAElAADIQj0AgKJD&rs=AOn4CLAKn3piY91QRCBzRgnzAPf7MPrjDQ"
|
|
@@ -25,11 +25,18 @@ class WecoAI:
|
|
|
25
25
|
"""A client for the WecoAI function builder API that allows users to build and query specialized functions built by LLMs.
|
|
26
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
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.
|
|
28
29
|
|
|
29
30
|
Attributes
|
|
30
31
|
----------
|
|
31
32
|
api_key : str
|
|
32
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.
|
|
33
40
|
"""
|
|
34
41
|
|
|
35
42
|
def __init__(self, api_key: str = None, timeout: float = 120.0, http2: bool = True) -> None:
|
|
@@ -41,7 +48,7 @@ class WecoAI:
|
|
|
41
48
|
The API key used for authentication. If not provided, the client will attempt to read it from the environment variable - WECO_API_KEY.
|
|
42
49
|
|
|
43
50
|
timeout : float, optional
|
|
44
|
-
The timeout for the HTTP requests in seconds (default is
|
|
51
|
+
The timeout for the HTTP requests in seconds (default is 120.0).
|
|
45
52
|
|
|
46
53
|
http2 : bool, optional
|
|
47
54
|
Whether to use HTTP/2 protocol for the HTTP requests (default is True).
|
|
@@ -65,11 +72,6 @@ class WecoAI:
|
|
|
65
72
|
self.client = httpx.Client(http2=http2, timeout=timeout)
|
|
66
73
|
self.async_client = httpx.AsyncClient(http2=http2, timeout=timeout)
|
|
67
74
|
|
|
68
|
-
def close(self):
|
|
69
|
-
"""Close both synchronous and asynchronous clients."""
|
|
70
|
-
self.client.close()
|
|
71
|
-
asyncio.run(self.async_client.aclose())
|
|
72
|
-
|
|
73
75
|
def _headers(self) -> Dict[str, str]:
|
|
74
76
|
"""Constructs the headers for the API requests."""
|
|
75
77
|
return {
|
|
@@ -158,20 +160,24 @@ class WecoAI:
|
|
|
158
160
|
"latency_ms": response["latency_ms"],
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
def _build(self, task_description: str, is_async: bool) -> Union[Tuple[str, str], Coroutine[Any, Any, Tuple[str, str]]]:
|
|
163
|
+
def _build(self, task_description: str, multimodal: bool, is_async: bool) -> Union[Tuple[str, int, str], Coroutine[Any, Any, Tuple[str, int, str]]]:
|
|
162
164
|
"""Internal method to handle both synchronous and asynchronous build requests.
|
|
163
165
|
|
|
164
166
|
Parameters
|
|
165
167
|
----------
|
|
166
168
|
task_description : str
|
|
167
169
|
A description of the task for which the function is being built.
|
|
170
|
+
|
|
171
|
+
multimodal : bool
|
|
172
|
+
Whether the function is multimodal or not.
|
|
173
|
+
|
|
168
174
|
is_async : bool
|
|
169
175
|
Whether to perform an asynchronous request.
|
|
170
176
|
|
|
171
177
|
Returns
|
|
172
178
|
-------
|
|
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.
|
|
179
|
+
Union[tuple[str, int, str], Coroutine[Any, Any, tuple[str, int, str]]]
|
|
180
|
+
A tuple containing the name, version number and description of the function, or a coroutine that returns such a tuple.
|
|
175
181
|
|
|
176
182
|
Raises
|
|
177
183
|
------
|
|
@@ -185,51 +191,57 @@ class WecoAI:
|
|
|
185
191
|
raise ValueError(f"Task description must be less than {MAX_TEXT_LENGTH} characters.")
|
|
186
192
|
|
|
187
193
|
endpoint = "build"
|
|
188
|
-
data = {"request": task_description}
|
|
194
|
+
data = {"request": task_description, "multimodal": multimodal}
|
|
189
195
|
request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
|
|
190
196
|
|
|
191
197
|
if is_async:
|
|
192
|
-
|
|
198
|
+
# return 0 for the version number
|
|
193
199
|
async def _async_build():
|
|
194
200
|
response = await request
|
|
195
|
-
return response["
|
|
201
|
+
return response["function_name"], 0, response["description"]
|
|
196
202
|
|
|
197
203
|
return _async_build()
|
|
198
204
|
else:
|
|
199
205
|
response = request # the request has already been made and the response is available
|
|
200
|
-
return response["
|
|
206
|
+
return response["function_name"], 0, response["description"]
|
|
201
207
|
|
|
202
|
-
async def abuild(self, task_description: str) -> Tuple[str, str]:
|
|
208
|
+
async def abuild(self, task_description: str, multimodal: bool = False) -> Tuple[str, int, str]:
|
|
203
209
|
"""Asynchronously builds a specialized function given a task description.
|
|
204
210
|
|
|
205
211
|
Parameters
|
|
206
212
|
----------
|
|
207
213
|
task_description : str
|
|
208
214
|
A description of the task for which the function is being built.
|
|
215
|
+
|
|
216
|
+
multimodal : bool, optional
|
|
217
|
+
Whether the function is multimodal or not (default is False).
|
|
209
218
|
|
|
210
219
|
Returns
|
|
211
220
|
-------
|
|
212
221
|
tuple[str, str]
|
|
213
|
-
A tuple containing the name and description of the function.
|
|
222
|
+
A tuple containing the name, version number and description of the function.
|
|
214
223
|
"""
|
|
215
|
-
return await self._build(task_description=task_description, is_async=True)
|
|
224
|
+
return await self._build(task_description=task_description, multimodal=multimodal, is_async=True)
|
|
216
225
|
|
|
217
|
-
def build(self, task_description: str) -> Tuple[str, str]:
|
|
226
|
+
def build(self, task_description: str, multimodal: bool = False) -> Tuple[str, int, str]:
|
|
218
227
|
"""Synchronously builds a specialized function given a task description.
|
|
219
228
|
|
|
220
229
|
Parameters
|
|
221
230
|
----------
|
|
222
231
|
task_description : str
|
|
223
232
|
A description of the task for which the function is being built.
|
|
233
|
+
|
|
234
|
+
multimodal : bool, optional
|
|
235
|
+
Whether the function is multimodal or not (default is False).
|
|
224
236
|
|
|
225
237
|
Returns
|
|
226
238
|
-------
|
|
227
239
|
tuple[str, str]
|
|
228
|
-
A tuple containing the name and description of the function.
|
|
240
|
+
A tuple containing the name, version number and description of the function.
|
|
229
241
|
"""
|
|
230
|
-
return self._build(task_description=task_description, is_async=False)
|
|
242
|
+
return self._build(task_description=task_description, multimodal=multimodal, is_async=False)
|
|
231
243
|
|
|
232
|
-
def _upload_image(self, fn_name: str, upload_id: str, image_info: Dict[str, Any]) -> str:
|
|
244
|
+
def _upload_image(self, fn_name: str, version_number: int, upload_id: str, image_info: Dict[str, Any]) -> str:
|
|
233
245
|
"""
|
|
234
246
|
Uploads an image to an S3 bucket and returns the URL of the uploaded image.
|
|
235
247
|
|
|
@@ -237,6 +249,8 @@ class WecoAI:
|
|
|
237
249
|
----------
|
|
238
250
|
fn_name : str
|
|
239
251
|
The name of the function for which the image is being uploaded.
|
|
252
|
+
version_number : int
|
|
253
|
+
The version number of the function for which the image is being uploaded.
|
|
240
254
|
upload_id: str
|
|
241
255
|
A unique identifier for the image upload.
|
|
242
256
|
image_info : Dict[str, Any]
|
|
@@ -271,13 +285,12 @@ class WecoAI:
|
|
|
271
285
|
|
|
272
286
|
# Request a presigned URL from the server
|
|
273
287
|
endpoint = "upload_link"
|
|
274
|
-
request_data = {"fn_name": fn_name, "upload_id": upload_id, "file_type": file_type}
|
|
288
|
+
request_data = {"fn_name": fn_name, "version_number": version_number, "upload_id": upload_id, "file_type": file_type}
|
|
275
289
|
# This needs to be a synchronous request since we need the presigned URL to upload the image
|
|
276
290
|
response = self._make_request(endpoint=endpoint, data=request_data, is_async=False)
|
|
277
291
|
|
|
278
292
|
# Upload the image to the S3 bucket
|
|
279
|
-
|
|
280
|
-
files = {"file": (f"{image_name}.{file_type}", upload_data)}
|
|
293
|
+
files = {"file": upload_data}
|
|
281
294
|
http_response = requests.post(response["url"], data=response["fields"], files=files)
|
|
282
295
|
if http_response.status_code == 204:
|
|
283
296
|
pass
|
|
@@ -372,7 +385,7 @@ class WecoAI:
|
|
|
372
385
|
return image_info
|
|
373
386
|
|
|
374
387
|
def _query(
|
|
375
|
-
self, is_async: bool, fn_name: str, text_input: Optional[str], images_input: Optional[List[str]]
|
|
388
|
+
self, is_async: bool, fn_name: str, version_number: Optional[int], text_input: Optional[str], images_input: Optional[List[str]]
|
|
376
389
|
) -> Union[Dict[str, Any], Coroutine[Any, Any, Dict[str, Any]]]:
|
|
377
390
|
"""Internal method to handle both synchronous and asynchronous query requests.
|
|
378
391
|
|
|
@@ -382,6 +395,8 @@ class WecoAI:
|
|
|
382
395
|
Whether to perform an asynchronous request.
|
|
383
396
|
fn_name : str
|
|
384
397
|
The name of the function to query.
|
|
398
|
+
version_number : int, optional
|
|
399
|
+
The version number of the function to query.
|
|
385
400
|
text_input : str, optional
|
|
386
401
|
The text input to the function.
|
|
387
402
|
images_input : List[str], optional
|
|
@@ -405,14 +420,14 @@ class WecoAI:
|
|
|
405
420
|
upload_id = generate_random_base16_code()
|
|
406
421
|
for i, info in enumerate(image_info):
|
|
407
422
|
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)
|
|
423
|
+
url = self._upload_image(fn_name=fn_name, version_number=version_number, upload_id=upload_id, image_info=info)
|
|
409
424
|
else:
|
|
410
425
|
raise ValueError(f"Image at index {i} must be a public URL or a path to a local image file.")
|
|
411
426
|
image_urls.append(url)
|
|
412
427
|
|
|
413
428
|
# Make the request
|
|
414
429
|
endpoint = "query"
|
|
415
|
-
data = {"name": fn_name, "text": text_input, "images": image_urls}
|
|
430
|
+
data = {"name": fn_name, "text": text_input, "images": image_urls, "version_number": version_number}
|
|
416
431
|
request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
|
|
417
432
|
|
|
418
433
|
if is_async:
|
|
@@ -427,7 +442,7 @@ class WecoAI:
|
|
|
427
442
|
return self._process_query_response(response=response)
|
|
428
443
|
|
|
429
444
|
async def aquery(
|
|
430
|
-
self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []
|
|
445
|
+
self, fn_name: str, version_number: Optional[int] = -1, text_input: Optional[str] = "", images_input: Optional[List[str]] = []
|
|
431
446
|
) -> Dict[str, Any]:
|
|
432
447
|
"""Asynchronously queries a function with the given function ID and input.
|
|
433
448
|
|
|
@@ -435,6 +450,8 @@ class WecoAI:
|
|
|
435
450
|
----------
|
|
436
451
|
fn_name : str
|
|
437
452
|
The name of the function to query.
|
|
453
|
+
version_number : int, optional
|
|
454
|
+
The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
|
|
438
455
|
text_input : str, optional
|
|
439
456
|
The text input to the function.
|
|
440
457
|
images_input : List[str], optional
|
|
@@ -446,15 +463,17 @@ class WecoAI:
|
|
|
446
463
|
A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
|
|
447
464
|
and the latency in milliseconds.
|
|
448
465
|
"""
|
|
449
|
-
return await self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=True)
|
|
466
|
+
return await self._query(fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input, is_async=True)
|
|
450
467
|
|
|
451
|
-
def query(self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []) -> Dict[str, Any]:
|
|
468
|
+
def query(self, fn_name: str, version_number: Optional[int] = -1, text_input: Optional[str] = "", images_input: Optional[List[str]] = []) -> Dict[str, Any]:
|
|
452
469
|
"""Synchronously queries a function with the given function ID and input.
|
|
453
470
|
|
|
454
471
|
Parameters
|
|
455
472
|
----------
|
|
456
473
|
fn_name : str
|
|
457
474
|
The name of the function to query.
|
|
475
|
+
version_number : int, optional
|
|
476
|
+
The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
|
|
458
477
|
text_input : str, optional
|
|
459
478
|
The text input to the function.
|
|
460
479
|
images_input : List[str], optional
|
|
@@ -463,52 +482,38 @@ class WecoAI:
|
|
|
463
482
|
Returns
|
|
464
483
|
-------
|
|
465
484
|
dict
|
|
466
|
-
|
|
485
|
+
A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
|
|
467
486
|
and the latency in milliseconds.
|
|
468
487
|
"""
|
|
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.
|
|
488
|
+
return self._query(fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input, is_async=False)
|
|
473
489
|
|
|
474
|
-
|
|
475
|
-
|
|
490
|
+
def batch_query(self, fn_name: str, batch_inputs: List[Dict[str, Any]], version_number: Optional[int] = -1) -> List[Dict[str, Any]]:
|
|
491
|
+
"""Batch queries a function version with a list of inputs.
|
|
476
492
|
|
|
477
493
|
Parameters
|
|
478
494
|
----------
|
|
479
|
-
fn_name :
|
|
495
|
+
fn_name : str
|
|
480
496
|
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
497
|
|
|
484
498
|
batch_inputs : List[Dict[str, Any]]
|
|
485
499
|
A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
|
|
486
500
|
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
501
|
and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
|
|
488
|
-
|
|
502
|
+
|
|
503
|
+
version_number : int, optional
|
|
504
|
+
The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
|
|
489
505
|
|
|
490
506
|
Returns
|
|
491
507
|
-------
|
|
492
508
|
List[Dict[str, Any]]
|
|
493
509
|
A list of dictionaries, each containing the output of a function query,
|
|
494
510
|
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
511
|
"""
|
|
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
512
|
async def run_queries():
|
|
508
|
-
tasks =
|
|
509
|
-
self.aquery(fn_name=fn_name, **fn_input)
|
|
510
|
-
|
|
511
|
-
|
|
513
|
+
tasks = list(map(
|
|
514
|
+
lambda fn_input: self.aquery(fn_name=fn_name, version_number=version_number, **fn_input),
|
|
515
|
+
batch_inputs
|
|
516
|
+
))
|
|
512
517
|
return await asyncio.gather(*tasks)
|
|
513
518
|
|
|
514
519
|
return asyncio.run(run_queries())
|
|
@@ -3,14 +3,15 @@ from typing import Any, Dict, List, Optional
|
|
|
3
3
|
from .client import WecoAI
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
def build(task_description: str, api_key: str = None) -> tuple[str, str]:
|
|
6
|
+
def build(task_description: str, multimodal: bool = False, api_key: str = None) -> tuple[str, int, str]:
|
|
8
7
|
"""Builds a specialized function synchronously given a task description.
|
|
9
8
|
|
|
10
9
|
Parameters
|
|
11
10
|
----------
|
|
12
11
|
task_description : str
|
|
13
12
|
A description of the task for which the function is being built.
|
|
13
|
+
multimodal : bool, optional
|
|
14
|
+
A flag to indicate if the function should be multimodal. Default is False.
|
|
14
15
|
api_key : str
|
|
15
16
|
The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
|
|
16
17
|
|
|
@@ -20,32 +21,34 @@ def build(task_description: str, api_key: str = None) -> tuple[str, str]:
|
|
|
20
21
|
A tuple containing the name and description of the function.
|
|
21
22
|
"""
|
|
22
23
|
client = WecoAI(api_key=api_key)
|
|
23
|
-
response = client.build(task_description=task_description)
|
|
24
|
+
response = client.build(task_description=task_description, multimodal=multimodal)
|
|
24
25
|
return response
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
async def abuild(task_description: str, api_key: str = None) -> tuple[str, str]:
|
|
28
|
+
async def abuild(task_description: str, multimodal: bool = False, api_key: str = None) -> tuple[str, int, str]:
|
|
28
29
|
"""Builds a specialized function asynchronously given a task description.
|
|
29
30
|
|
|
30
31
|
Parameters
|
|
31
32
|
----------
|
|
32
33
|
task_description : str
|
|
33
34
|
A description of the task for which the function is being built.
|
|
35
|
+
multimodal : bool, optional
|
|
36
|
+
A flag to indicate if the function should be multimodal. Default is False.
|
|
34
37
|
api_key : str
|
|
35
38
|
The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
|
|
36
39
|
|
|
37
40
|
Returns
|
|
38
41
|
-------
|
|
39
42
|
tuple[str, str]
|
|
40
|
-
A tuple containing the name and description of the function.
|
|
43
|
+
A tuple containing the name, version number and description of the function.
|
|
41
44
|
"""
|
|
42
45
|
client = WecoAI(api_key=api_key)
|
|
43
|
-
response = await client.abuild(task_description=task_description)
|
|
46
|
+
response = await client.abuild(task_description=task_description, multimodal=multimodal)
|
|
44
47
|
return response
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
def query(
|
|
48
|
-
fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
|
|
51
|
+
fn_name: str, version_number: Optional[int] = -1, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
|
|
49
52
|
) -> Dict[str, Any]:
|
|
50
53
|
"""Queries a function synchronously with the given function ID and input.
|
|
51
54
|
|
|
@@ -53,6 +56,8 @@ def query(
|
|
|
53
56
|
----------
|
|
54
57
|
fn_name : str
|
|
55
58
|
The name of the function to query.
|
|
59
|
+
version_number : int, optional
|
|
60
|
+
The version number of the function to query. If not provided, the latest version is used. Default is -1 for the same behavior.
|
|
56
61
|
text_input : str, optional
|
|
57
62
|
The text input to the function.
|
|
58
63
|
images_input : List[str], optional
|
|
@@ -67,12 +72,12 @@ def query(
|
|
|
67
72
|
and the latency in milliseconds.
|
|
68
73
|
"""
|
|
69
74
|
client = WecoAI(api_key=api_key)
|
|
70
|
-
response = client.query(fn_name=fn_name, text_input=text_input, images_input=images_input)
|
|
75
|
+
response = client.query(fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input)
|
|
71
76
|
return response
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
async def aquery(
|
|
75
|
-
fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
|
|
80
|
+
fn_name: str, version_number: Optional[int] = -1, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
|
|
76
81
|
) -> Dict[str, Any]:
|
|
77
82
|
"""Queries a function asynchronously with the given function ID and input.
|
|
78
83
|
|
|
@@ -80,6 +85,8 @@ async def aquery(
|
|
|
80
85
|
----------
|
|
81
86
|
fn_name : str
|
|
82
87
|
The name of the function to query.
|
|
88
|
+
version_number : int, optional
|
|
89
|
+
The version number of the function to query. If not provided, the latest version is used. Default is -1 for the same behavior.
|
|
83
90
|
text_input : str, optional
|
|
84
91
|
The text input to the function.
|
|
85
92
|
images_input : List[str], optional
|
|
@@ -94,12 +101,12 @@ async def aquery(
|
|
|
94
101
|
and the latency in milliseconds.
|
|
95
102
|
"""
|
|
96
103
|
client = WecoAI(api_key=api_key)
|
|
97
|
-
response = await client.aquery(fn_name=fn_name, text_input=text_input, images_input=images_input)
|
|
104
|
+
response = await client.aquery(fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input)
|
|
98
105
|
return response
|
|
99
106
|
|
|
100
107
|
|
|
101
108
|
def batch_query(
|
|
102
|
-
|
|
109
|
+
fn_name: str, batch_inputs: List[Dict[str, Any]], version_number: Optional[int] = -1, api_key: Optional[str] = None
|
|
103
110
|
) -> List[Dict[str, Any]]:
|
|
104
111
|
"""Synchronously queries multiple functions using asynchronous calls internally.
|
|
105
112
|
|
|
@@ -117,7 +124,9 @@ def batch_query(
|
|
|
117
124
|
A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
|
|
118
125
|
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
126
|
and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
|
|
120
|
-
|
|
127
|
+
|
|
128
|
+
version_number : int, optional
|
|
129
|
+
The version number of the function to query. If not provided, the latest version is used. Default is -1 for the same behavior.
|
|
121
130
|
|
|
122
131
|
api_key : str, optional
|
|
123
132
|
The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
|
|
@@ -129,5 +138,5 @@ def batch_query(
|
|
|
129
138
|
in the same order as the input queries.
|
|
130
139
|
"""
|
|
131
140
|
client = WecoAI(api_key=api_key)
|
|
132
|
-
responses = client.batch_query(
|
|
141
|
+
responses = client.batch_query(fn_name=fn_name, version_number=version_number, batch_inputs=batch_inputs)
|
|
133
142
|
return responses
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from weco import batch_query, build
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# Internally, these functions use the WecoAI client
|
|
7
|
-
# therefore, we can test both the client and functional forms here
|
|
8
|
-
# We do NOT need to test for a list of function names here since interally
|
|
9
|
-
# we cast the input to a list of function names to match the fn_input size
|
|
10
|
-
@pytest.fixture
|
|
11
|
-
def ml_task_evaluator():
|
|
12
|
-
fn_name, _ = build(
|
|
13
|
-
task_description="I want to evaluate the feasibility of a machine learning task. Give me a json object with three keys - 'feasibility', 'justification', and 'suggestions'."
|
|
14
|
-
)
|
|
15
|
-
return fn_name
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
def ml_task_inputs():
|
|
20
|
-
return [
|
|
21
|
-
{"text_input": "I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle."},
|
|
22
|
-
{
|
|
23
|
-
"text_input": "I want to train a model to classify digits using the MNIST dataset hosted on Kaggle using a Google Colab notebook."
|
|
24
|
-
},
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@pytest.fixture
|
|
29
|
-
def image_evaluator():
|
|
30
|
-
fn_name, _ = build(
|
|
31
|
-
task_description="Describe the contents of the given images. Provide a json object with 'description' and 'objects' keys."
|
|
32
|
-
)
|
|
33
|
-
return fn_name
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@pytest.fixture
|
|
37
|
-
def image_inputs():
|
|
38
|
-
return [
|
|
39
|
-
{
|
|
40
|
-
"images_input": [
|
|
41
|
-
"https://www.integratedtreatmentservices.co.uk/wp-content/uploads/2013/12/Objects-of-Reference.jpg"
|
|
42
|
-
]
|
|
43
|
-
},
|
|
44
|
-
{"images_input": ["https://t4.ftcdn.net/jpg/05/70/90/23/360_F_570902339_kNj1reH40GFXakTy98EmfiZHci2xvUCS.jpg"]},
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_batch_query(ml_task_evaluator, ml_task_inputs, image_evaluator, image_inputs):
|
|
49
|
-
fn_names = [ml_task_evaluator, ml_task_evaluator, image_evaluator, image_evaluator]
|
|
50
|
-
batch_inputs = ml_task_inputs + image_inputs
|
|
51
|
-
|
|
52
|
-
query_responses = batch_query(fn_names=fn_names, batch_inputs=batch_inputs)
|
|
53
|
-
|
|
54
|
-
assert len(query_responses) == len(batch_inputs)
|
|
55
|
-
|
|
56
|
-
for i, query_response in enumerate(query_responses):
|
|
57
|
-
assert isinstance(query_response["output"], dict)
|
|
58
|
-
assert isinstance(query_response["in_tokens"], int)
|
|
59
|
-
assert isinstance(query_response["out_tokens"], int)
|
|
60
|
-
assert isinstance(query_response["latency_ms"], float)
|
|
61
|
-
|
|
62
|
-
output = query_response["output"]
|
|
63
|
-
if i < len(ml_task_inputs):
|
|
64
|
-
assert set(output.keys()) == {"feasibility", "justification", "suggestions"}
|
|
65
|
-
else:
|
|
66
|
-
assert set(output.keys()) == {"description", "objects"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|