Skip to content

Commit f6c6cde

Browse files
committed
Add option for reasoning
1 parent df54ea1 commit f6c6cde

File tree

10 files changed

+154
-32
lines changed

10 files changed

+154
-32
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,12 @@ jobs:
7474
inputs: >-
7575
./dist/*.tar.gz
7676
./dist/*.whl
77-
- name: Debug Print github.ref_name
78-
run: >-
79-
echo "github.ref_name: ${{ github.ref_name }}"
8077
- name: Create GitHub Release
8178
env:
8279
GITHUB_TOKEN: ${{ github.token }}
8380
run: >-
8481
gh release create
85-
'v0.1.7'
82+
'v0.1.8'
8683
--repo '${{ github.repository }}'
8784
--notes ""
8885
- name: Upload artifact signatures to GitHub Release
@@ -93,5 +90,5 @@ jobs:
9390
# sigstore-produced signatures and certificates.
9491
run: >-
9592
gh release upload
96-
'v0.1.7' dist/**
93+
'v0.1.8' dist/**
9794
--repo '${{ github.repository }}'

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ pip install weco
2525
```
2626

2727
## Features
28+
- Synchronous & Asynchronous client.
29+
- Batch API
30+
- Multimodality (Language & Vision)
31+
- Interpretability (view the reasoning behind outputs)
32+
33+
34+
## What We Offer
2835

2936
- 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).
3037
- The **query** function allows you to test and use the newly created function in your own code.
31-
- We offer asynchronous versions of the above clients.
32-
- 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.
33-
- We also offer multimodality capabilities. You can now query our client with both **language** AND **vision** inputs!
3438

3539
We provide both services in two ways:
3640
- `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.

examples/cookbook.ipynb

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
"with open(\"/path/to/home_exterior.jpeg\", \"rb\") as img_file:\n",
145145
" my_home_exterior = base64.b64encode(img_file.read()).decode('utf-8')\n",
146146
"\n",
147-
"response = query(\n",
147+
"query_response = query(\n",
148148
" fn_name=fn_name,\n",
149149
" text_input=request,\n",
150150
" images_input=[\n",
@@ -154,7 +154,10 @@
154154
" ]\n",
155155
")\n",
156156
"\n",
157-
"print(response)"
157+
"for key, value in query_response[\"output\"].items(): print(f\"{key}: {value}\")\n",
158+
"print(f\"Input Tokens: {query_response['in_tokens']}\")\n",
159+
"print(f\"Output Tokens: {query_response['out_tokens']}\")\n",
160+
"print(f\"Latency: {query_response['latency_ms']} ms\")"
158161
]
159162
},
160163
{
@@ -214,7 +217,10 @@
214217
" fn_name=fn_name,\n",
215218
" text_input=\"I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle.\"\n",
216219
")\n",
217-
"for key, value in query_response.items(): print(f\"{key}: {value}\")"
220+
"for key, value in query_response[\"output\"].items(): print(f\"{key}: {value}\")\n",
221+
"print(f\"Input Tokens: {query_response['in_tokens']}\")\n",
222+
"print(f\"Output Tokens: {query_response['out_tokens']}\")\n",
223+
"print(f\"Latency: {query_response['latency_ms']} ms\")"
218224
]
219225
},
220226
{
@@ -274,7 +280,12 @@
274280
"query_responses = batch_query(\n",
275281
" fn_names=fn_name,\n",
276282
" batch_inputs=[input_1, input_2]\n",
277-
")"
283+
")\n",
284+
"for i, query_response in enumerate(query_responses):\n",
285+
" print(\"-\"*50)\n",
286+
" print(f\"For input {i + 1}\")\n",
287+
" for key, value in query_response[\"output\"].items(): print(f\"{key}: {value}\")\n",
288+
" print(\"-\"*50)"
278289
]
279290
},
280291
{
@@ -323,14 +334,49 @@
323334
" fn_name=fn_name,\n",
324335
" text_input=\"I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle.\"\n",
325336
")\n",
326-
"for key, value in query_response.items(): print(f\"{key}: {value}\")"
337+
"for key, value in query_response[\"output\"].items(): print(f\"{key}: {value}\")\n",
338+
"print(f\"Input Tokens: {query_response['in_tokens']}\")\n",
339+
"print(f\"Output Tokens: {query_response['out_tokens']}\")\n",
340+
"print(f\"Latency: {query_response['latency_ms']} ms\")"
327341
]
328342
},
329343
{
330344
"cell_type": "markdown",
331345
"metadata": {},
332346
"source": [
333-
"## A/B Testing with Function Versions"
347+
"## Interpretability"
348+
]
349+
},
350+
{
351+
"cell_type": "markdown",
352+
"metadata": {},
353+
"source": [
354+
"You can now understand why a model generated an output simply by passing `return_reasoning=True` at query time!"
355+
]
356+
},
357+
{
358+
"cell_type": "code",
359+
"execution_count": null,
360+
"metadata": {},
361+
"outputs": [],
362+
"source": [
363+
"from weco import build, query\n",
364+
"\n",
365+
"# Describe the task you want the function to perform\n",
366+
"fn_name, fn_desc = build(task_description=task_description)\n",
367+
"print(f\"AI Function {fn_name} built. This does the following - \\n{fn_desc}.\")\n",
368+
"\n",
369+
"# Query the function with a specific input\n",
370+
"query_response = query(\n",
371+
" fn_name=fn_name,\n",
372+
" text_input=\"I want to train a model to predict house prices using the Boston Housing dataset hosted on Kaggle.\",\n",
373+
" return_reasoning=True\n",
374+
")\n",
375+
"for key, value in query_response[\"output\"].items(): print(f\"{key}: {value}\")\n",
376+
"for i, step in enumerate(query_response[\"reasoning_steps\"]): print(f\"Step {i+1}: {step}\")\n",
377+
"print(f\"Input Tokens: {query_response['in_tokens']}\")\n",
378+
"print(f\"Output Tokens: {query_response['out_tokens']}\")\n",
379+
"print(f\"Latency: {query_response['latency_ms']} ms\")"
334380
]
335381
},
336382
{

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ authors = [
1010
]
1111
description = "A client facing API for interacting with the WeCo AI function builder service."
1212
readme = "README.md"
13-
version = "0.1.7"
13+
version = "0.1.8"
1414
license = {text = "MIT"}
1515
requires-python = ">=3.8"
1616
dependencies = ["asyncio", "httpx[http2]", "pillow"]

tests/test_asynchronous.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async def assert_query_response(query_response):
2020
assert isinstance(query_response["in_tokens"], int)
2121
assert isinstance(query_response["out_tokens"], int)
2222
assert isinstance(query_response["latency_ms"], float)
23+
assert "reasoning_steps" not in query_response
2324

2425

2526
@pytest.fixture

tests/test_batching.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def test_batch_query_image(image_evaluator, image_inputs):
7676
assert isinstance(query_response["in_tokens"], int)
7777
assert isinstance(query_response["out_tokens"], int)
7878
assert isinstance(query_response["latency_ms"], float)
79+
assert "reasoning_steps" not in query_response
7980

8081
output = query_response["output"]
8182
assert set(output.keys()) == {"description", "objects"}

tests/test_reasoning.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
3+
from weco import build, query
4+
5+
6+
def assert_query_response(query_response):
7+
assert isinstance(query_response, dict)
8+
assert isinstance(query_response["output"], dict)
9+
assert isinstance(query_response["reasoning_steps"], list)
10+
for step in query_response["reasoning_steps"]: assert isinstance(step, str)
11+
assert isinstance(query_response["in_tokens"], int)
12+
assert isinstance(query_response["out_tokens"], int)
13+
assert isinstance(query_response["latency_ms"], float)
14+
15+
16+
@pytest.fixture
17+
def text_reasoning_evaluator():
18+
fn_name, version_number, fn_desc = build(
19+
task_description="Evaluate the sentiment of the given text. Provide a json object with 'sentiment' and 'explanation' keys.",
20+
multimodal=False,
21+
)
22+
return fn_name, version_number, fn_desc
23+
24+
25+
def test_text_reasoning_query(text_reasoning_evaluator):
26+
fn_name, version_number, _ = text_reasoning_evaluator
27+
query_response = query(fn_name=fn_name, version_number=version_number, text_input="I love this product!", return_reasoning=True)
28+
29+
assert_query_response(query_response)
30+
assert set(query_response["output"].keys()) == {"sentiment", "explanation"}
31+
32+
@pytest.fixture
33+
def vision_reasoning_evaluator():
34+
fn_name, version_number, fn_desc = build(
35+
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.",
36+
multimodal=True,
37+
)
38+
return fn_name, version_number, fn_desc
39+
40+
41+
def test_vision_reasoning_query(vision_reasoning_evaluator):
42+
fn_name, version_number, _ = vision_reasoning_evaluator
43+
query_response = query(
44+
fn_name=fn_name,
45+
version_number=version_number,
46+
text_input="Find x and y.",
47+
images_input=[
48+
"https://i.ytimg.com/vi/cblHUeq3bkE/hq720.jpg?sqp=-oaymwEhCK4FEIIDSFryq4qpAxMIARUAAAAAGAElAADIQj0AgKJD&rs=AOn4CLAKn3piY91QRCBzRgnzAPf7MPrjDQ"
49+
],
50+
return_reasoning=True,
51+
)
52+
53+
assert_query_response(query_response)
54+
assert set(query_response["output"].keys()) == {"answer", "explanation"}

tests/test_synchronous.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def assert_query_response(query_response):
1919
assert isinstance(query_response["in_tokens"], int)
2020
assert isinstance(query_response["out_tokens"], int)
2121
assert isinstance(query_response["latency_ms"], float)
22+
assert "reasoning_steps" not in query_response
2223

2324

2425
@pytest.fixture

weco/client.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class WecoAI:
3939
Whether to use HTTP/2 protocol for the HTTP requests. Default is True.
4040
"""
4141

42-
def __init__(self, api_key: str = None, timeout: float = 120.0, http2: bool = True) -> None:
42+
def __init__(self, api_key: Union[str, None] = None, timeout: float = 120.0, http2: bool = True) -> None:
4343
"""Initializes the WecoAI client with the provided API key and base URL.
4444
4545
Parameters
@@ -67,7 +67,8 @@ def __init__(self, api_key: str = None, timeout: float = 120.0, http2: bool = Tr
6767
self.api_key = api_key
6868
self.http2 = http2
6969
self.timeout = timeout
70-
self.base_url = "https://function.api.weco.ai"
70+
# self.base_url = "https://function.api.weco.ai"
71+
self.base_url = "https://function-dev.api.weco.ai"
7172
# Setup clients
7273
self.client = httpx.Client(http2=http2, timeout=timeout)
7374
self.async_client = httpx.AsyncClient(http2=http2, timeout=timeout)
@@ -153,12 +154,15 @@ def _process_query_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
153154
for _warning in response.get("warnings", []):
154155
warnings.warn(_warning)
155156

156-
return {
157+
returned_response = {
157158
"output": response["response"],
158159
"in_tokens": response["num_input_tokens"],
159160
"out_tokens": response["num_output_tokens"],
160161
"latency_ms": response["latency_ms"],
161162
}
163+
if "reasoning_steps" in response:
164+
returned_response["reasoning_steps"] = response["reasoning_steps"]
165+
return returned_response
162166

163167
def _build(
164168
self, task_description: str, multimodal: bool, is_async: bool
@@ -393,6 +397,7 @@ def _query(
393397
version_number: Optional[int],
394398
text_input: Optional[str],
395399
images_input: Optional[List[str]],
400+
return_reasoning: Optional[bool]
396401
) -> Union[Dict[str, Any], Coroutine[Any, Any, Dict[str, Any]]]:
397402
"""Internal method to handle both synchronous and asynchronous query requests.
398403
@@ -408,6 +413,8 @@ def _query(
408413
The text input to the function.
409414
images_input : List[str], optional
410415
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
416+
return_reasoning : bool, optional
417+
Whether to return reasoning for the output.
411418
412419
Returns
413420
-------
@@ -434,7 +441,7 @@ def _query(
434441

435442
# Make the request
436443
endpoint = "query"
437-
data = {"name": fn_name, "text": text_input, "images": image_urls, "version_number": version_number}
444+
data = {"name": fn_name, "text": text_input, "images": image_urls, "version_number": version_number, "return_reasoning": return_reasoning}
438445
request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
439446

440447
if is_async:
@@ -454,6 +461,7 @@ async def aquery(
454461
version_number: Optional[int] = -1,
455462
text_input: Optional[str] = "",
456463
images_input: Optional[List[str]] = [],
464+
return_reasoning: Optional[bool] = False
457465
) -> Dict[str, Any]:
458466
"""Asynchronously queries a function with the given function ID and input.
459467
@@ -467,6 +475,8 @@ async def aquery(
467475
The text input to the function.
468476
images_input : List[str], optional
469477
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
478+
return_reasoning : bool, optional
479+
Whether to return reasoning for the output. Default is False.
470480
471481
Returns
472482
-------
@@ -475,7 +485,7 @@ async def aquery(
475485
and the latency in milliseconds.
476486
"""
477487
return await self._query(
478-
fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input, is_async=True
488+
fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input, return_reasoning=return_reasoning, is_async=True
479489
)
480490

481491
def query(
@@ -484,6 +494,7 @@ def query(
484494
version_number: Optional[int] = -1,
485495
text_input: Optional[str] = "",
486496
images_input: Optional[List[str]] = [],
497+
return_reasoning: Optional[bool] = False
487498
) -> Dict[str, Any]:
488499
"""Synchronously queries a function with the given function ID and input.
489500
@@ -497,6 +508,8 @@ def query(
497508
The text input to the function.
498509
images_input : List[str], optional
499510
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
511+
return_reasoning : bool, optional
512+
Whether to return reasoning for the output. Default is False.
500513
501514
Returns
502515
-------
@@ -505,26 +518,26 @@ def query(
505518
and the latency in milliseconds.
506519
"""
507520
return self._query(
508-
fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input, is_async=False
521+
fn_name=fn_name, version_number=version_number, text_input=text_input, images_input=images_input, return_reasoning=return_reasoning, is_async=False
509522
)
510523

511524
def batch_query(
512-
self, fn_name: str, batch_inputs: List[Dict[str, Any]], version_number: Optional[int] = -1
525+
self, fn_name: str, batch_inputs: List[Dict[str, Any]], version_number: Optional[int] = -1, return_reasoning: Optional[bool] = False
513526
) -> List[Dict[str, Any]]:
514527
"""Batch queries a function version with a list of inputs.
515528
516529
Parameters
517530
----------
518531
fn_name : str
519532
The name of the function or a list of function names to query.
520-
521533
batch_inputs : List[Dict[str, Any]]
522534
A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
523535
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", ...]}
524536
and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
525-
526537
version_number : int, optional
527538
The version number of the function to query. If not provided, the latest version will be used. Pass -1 to use the latest version.
539+
return_reasoning : bool, optional
540+
Whether to return reasoning for the output. Default is False.
528541
529542
Returns
530543
-------
@@ -535,7 +548,7 @@ def batch_query(
535548

536549
async def run_queries():
537550
tasks = list(
538-
map(lambda fn_input: self.aquery(fn_name=fn_name, version_number=version_number, **fn_input), batch_inputs)
551+
map(lambda fn_input: self.aquery(fn_name=fn_name, version_number=version_number, return_reasoning=return_reasoning, **fn_input), batch_inputs)
539552
)
540553
return await asyncio.gather(*tasks)
541554

0 commit comments

Comments
 (0)