Allow custom base Zapier prompt (#4213)
Currently, all Zapier tools are built using the pre-written base Zapier
prompt. These small changes (that retain default behavior) will allow a
user to create a Zapier tool using the ZapierNLARunTool while providing
their own base prompt.
Their prompt must contain input fields for zapier_description and
params, checked and enforced in the tool's root validator.
An example of when this may be useful: user has several, say 10, Zapier
tools enabled. Currently, the long generic default Zapier base prompt is
attached to every single tool, using an extreme number of tokens for no
real added benefit (repeated). User prompts LLM on how to use Zapier
tools once, then overrides the base prompt.
Or: user has a few specific Zapier tools and wants to maximize their
success rate. So, user writes prompts/descriptions for those tools
specific to their use case, and provides those to the ZapierNLARunTool.
A consideration - this is the simplest way to implement this I could
think of... though ideally custom prompting would be possible at the
Toolkit level as well. For now, this should be sufficient in solving the
concerns outlined above.
2023-05-14 04:08:18 +00:00
|
|
|
"""Test building the Zapier tool, not running it."""
|
2023-06-27 23:35:42 +00:00
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
Allow custom base Zapier prompt (#4213)
Currently, all Zapier tools are built using the pre-written base Zapier
prompt. These small changes (that retain default behavior) will allow a
user to create a Zapier tool using the ZapierNLARunTool while providing
their own base prompt.
Their prompt must contain input fields for zapier_description and
params, checked and enforced in the tool's root validator.
An example of when this may be useful: user has several, say 10, Zapier
tools enabled. Currently, the long generic default Zapier base prompt is
attached to every single tool, using an extreme number of tokens for no
real added benefit (repeated). User prompts LLM on how to use Zapier
tools once, then overrides the base prompt.
Or: user has a few specific Zapier tools and wants to maximize their
success rate. So, user writes prompts/descriptions for those tools
specific to their use case, and provides those to the ZapierNLARunTool.
A consideration - this is the simplest way to implement this I could
think of... though ideally custom prompting would be possible at the
Toolkit level as well. For now, this should be sufficient in solving the
concerns outlined above.
2023-05-14 04:08:18 +00:00
|
|
|
import pytest
|
2023-06-27 23:35:42 +00:00
|
|
|
import requests
|
Allow custom base Zapier prompt (#4213)
Currently, all Zapier tools are built using the pre-written base Zapier
prompt. These small changes (that retain default behavior) will allow a
user to create a Zapier tool using the ZapierNLARunTool while providing
their own base prompt.
Their prompt must contain input fields for zapier_description and
params, checked and enforced in the tool's root validator.
An example of when this may be useful: user has several, say 10, Zapier
tools enabled. Currently, the long generic default Zapier base prompt is
attached to every single tool, using an extreme number of tokens for no
real added benefit (repeated). User prompts LLM on how to use Zapier
tools once, then overrides the base prompt.
Or: user has a few specific Zapier tools and wants to maximize their
success rate. So, user writes prompts/descriptions for those tools
specific to their use case, and provides those to the ZapierNLARunTool.
A consideration - this is the simplest way to implement this I could
think of... though ideally custom prompting would be possible at the
Toolkit level as well. For now, this should be sufficient in solving the
concerns outlined above.
2023-05-14 04:08:18 +00:00
|
|
|
|
|
|
|
from langchain.tools.zapier.prompt import BASE_ZAPIER_TOOL_PROMPT
|
|
|
|
from langchain.tools.zapier.tool import ZapierNLARunAction
|
|
|
|
from langchain.utilities.zapier import ZapierNLAWrapper
|
|
|
|
|
|
|
|
|
|
|
|
def test_default_base_prompt() -> None:
|
|
|
|
"""Test that the default prompt is being inserted."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Test that the base prompt was successfully assigned to the default prompt
|
|
|
|
assert tool.base_prompt == BASE_ZAPIER_TOOL_PROMPT
|
|
|
|
assert tool.description == BASE_ZAPIER_TOOL_PROMPT.format(
|
|
|
|
zapier_description="test",
|
|
|
|
params=str(list({"test": "test"}.keys())),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_custom_base_prompt() -> None:
|
|
|
|
"""Test that a custom prompt is being inserted."""
|
|
|
|
base_prompt = "Test. {zapier_description} and {params}."
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
base_prompt=base_prompt,
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Test that the base prompt was successfully assigned to the default prompt
|
|
|
|
assert tool.base_prompt == base_prompt
|
|
|
|
assert tool.description == "Test. test and ['test']."
|
|
|
|
|
|
|
|
|
|
|
|
def test_custom_base_prompt_fail() -> None:
|
|
|
|
"""Test validating an invalid custom prompt."""
|
|
|
|
base_prompt = "Test. {zapier_description}."
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params={"test": "test"},
|
|
|
|
base_prompt=base_prompt,
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
Zapier update oauth support (#6780)
Description: Update documentation to
1) point to updated documentation links at Zapier.com (we've revamped
our help docs and paths), and
2) To provide clarity how to use the wrapper with an access token for
OAuth support
Demo:
Initializing the Zapier Wrapper with an OAuth Access Token
`ZapierNLAWrapper(zapier_nla_oauth_access_token="<redacted>")`
Using LangChain to resolve the current weather in Vancouver BC
leveraging Zapier NLA to lookup weather by coords.
```
> Entering new chain...
I need to use a tool to get the current weather.
Action: The Weather: Get Current Weather
Action Input: Get the current weather for Vancouver BC
Observation: {"coord__lon": -123.1207, "coord__lat": 49.2827, "weather": [{"id": 802, "main": "Clouds", "description": "scattered clouds", "icon": "03d", "icon_url": "http://openweathermap.org/img/wn/03d@2x.png"}], "weather[]icon_url": ["http://openweathermap.org/img/wn/03d@2x.png"], "weather[]icon": ["03d"], "weather[]id": [802], "weather[]description": ["scattered clouds"], "weather[]main": ["Clouds"], "base": "stations", "main__temp": 71.69, "main__feels_like": 71.56, "main__temp_min": 67.64, "main__temp_max": 76.39, "main__pressure": 1015, "main__humidity": 64, "visibility": 10000, "wind__speed": 3, "wind__deg": 155, "wind__gust": 11.01, "clouds__all": 41, "dt": 1687806607, "sys__type": 2, "sys__id": 2011597, "sys__country": "CA", "sys__sunrise": 1687781297, "sys__sunset": 1687839730, "timezone": -25200, "id": 6173331, "name": "Vancouver", "cod": 200, "summary": "scattered clouds", "_zap_search_was_found_status": true}
Thought: I now know the current weather in Vancouver BC.
Final Answer: The current weather in Vancouver BC is scattered clouds with a temperature of 71.69 and wind speed of 3
```
2023-06-27 18:46:32 +00:00
|
|
|
|
|
|
|
|
2023-06-27 23:53:35 +00:00
|
|
|
def test_format_headers_api_key() -> None:
|
|
|
|
"""Test that the action headers is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
|
|
|
headers = tool.api_wrapper._format_headers()
|
|
|
|
assert headers["Content-Type"] == "application/json"
|
|
|
|
assert headers["Accept"] == "application/json"
|
|
|
|
assert headers["X-API-Key"] == "test"
|
|
|
|
|
|
|
|
|
|
|
|
def test_format_headers_access_token() -> None:
|
|
|
|
"""Test that the action headers is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_oauth_access_token="test"),
|
|
|
|
)
|
|
|
|
headers = tool.api_wrapper._format_headers()
|
|
|
|
assert headers["Content-Type"] == "application/json"
|
|
|
|
assert headers["Accept"] == "application/json"
|
|
|
|
assert headers["Authorization"] == "Bearer test"
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_action_payload() -> None:
|
|
|
|
"""Test that the action payload is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
|
|
|
|
|
|
|
payload = tool.api_wrapper._create_action_payload("some instructions")
|
|
|
|
assert payload["instructions"] == "some instructions"
|
|
|
|
assert payload.get("preview_only") is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_action_payload_preview() -> None:
|
|
|
|
"""Test that the action payload with preview is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
|
|
|
|
|
|
|
payload = tool.api_wrapper._create_action_payload(
|
|
|
|
"some instructions",
|
|
|
|
preview_only=True,
|
|
|
|
)
|
|
|
|
assert payload["instructions"] == "some instructions"
|
|
|
|
assert payload["preview_only"] is True
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_action_payload_with_params() -> None:
|
|
|
|
"""Test that the action payload with params is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(zapier_nla_api_key="test"),
|
|
|
|
)
|
|
|
|
|
|
|
|
payload = tool.api_wrapper._create_action_payload(
|
|
|
|
"some instructions",
|
|
|
|
{"test": "test"},
|
|
|
|
preview_only=True,
|
|
|
|
)
|
|
|
|
assert payload["instructions"] == "some instructions"
|
|
|
|
assert payload["preview_only"] is True
|
|
|
|
assert payload["test"] == "test"
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_apreview(mocker) -> None: # type: ignore[no-untyped-def]
|
|
|
|
"""Test that the action payload with params is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(
|
|
|
|
zapier_nla_api_key="test",
|
|
|
|
zapier_nla_api_base="http://localhost:8080/v1/",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
mockObj = mocker.patch.object(ZapierNLAWrapper, "_arequest")
|
|
|
|
await tool.api_wrapper.apreview(
|
|
|
|
"random_action_id",
|
|
|
|
"some instructions",
|
|
|
|
{"test": "test"},
|
|
|
|
)
|
|
|
|
mockObj.assert_called_once_with(
|
|
|
|
"POST",
|
|
|
|
"http://localhost:8080/v1/exposed/random_action_id/execute/",
|
|
|
|
json={
|
|
|
|
"instructions": "some instructions",
|
|
|
|
"preview_only": True,
|
|
|
|
"test": "test",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_arun(mocker) -> None: # type: ignore[no-untyped-def]
|
|
|
|
"""Test that the action payload with params is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(
|
|
|
|
zapier_nla_api_key="test",
|
|
|
|
zapier_nla_api_base="http://localhost:8080/v1/",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
mockObj = mocker.patch.object(ZapierNLAWrapper, "_arequest")
|
|
|
|
await tool.api_wrapper.arun(
|
|
|
|
"random_action_id",
|
|
|
|
"some instructions",
|
|
|
|
{"test": "test"},
|
|
|
|
)
|
|
|
|
mockObj.assert_called_once_with(
|
|
|
|
"POST",
|
|
|
|
"http://localhost:8080/v1/exposed/random_action_id/execute/",
|
|
|
|
json={"instructions": "some instructions", "test": "test"},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_alist(mocker) -> None: # type: ignore[no-untyped-def]
|
|
|
|
"""Test that the action payload with params is being created correctly."""
|
|
|
|
tool = ZapierNLARunAction(
|
|
|
|
action_id="test",
|
|
|
|
zapier_description="test",
|
|
|
|
params_schema={"test": "test"},
|
|
|
|
api_wrapper=ZapierNLAWrapper(
|
|
|
|
zapier_nla_api_key="test",
|
|
|
|
zapier_nla_api_base="http://localhost:8080/v1/",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
mockObj = mocker.patch.object(ZapierNLAWrapper, "_arequest")
|
|
|
|
await tool.api_wrapper.alist()
|
|
|
|
mockObj.assert_called_once_with(
|
|
|
|
"GET",
|
|
|
|
"http://localhost:8080/v1/exposed/",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
Zapier update oauth support (#6780)
Description: Update documentation to
1) point to updated documentation links at Zapier.com (we've revamped
our help docs and paths), and
2) To provide clarity how to use the wrapper with an access token for
OAuth support
Demo:
Initializing the Zapier Wrapper with an OAuth Access Token
`ZapierNLAWrapper(zapier_nla_oauth_access_token="<redacted>")`
Using LangChain to resolve the current weather in Vancouver BC
leveraging Zapier NLA to lookup weather by coords.
```
> Entering new chain...
I need to use a tool to get the current weather.
Action: The Weather: Get Current Weather
Action Input: Get the current weather for Vancouver BC
Observation: {"coord__lon": -123.1207, "coord__lat": 49.2827, "weather": [{"id": 802, "main": "Clouds", "description": "scattered clouds", "icon": "03d", "icon_url": "http://openweathermap.org/img/wn/03d@2x.png"}], "weather[]icon_url": ["http://openweathermap.org/img/wn/03d@2x.png"], "weather[]icon": ["03d"], "weather[]id": [802], "weather[]description": ["scattered clouds"], "weather[]main": ["Clouds"], "base": "stations", "main__temp": 71.69, "main__feels_like": 71.56, "main__temp_min": 67.64, "main__temp_max": 76.39, "main__pressure": 1015, "main__humidity": 64, "visibility": 10000, "wind__speed": 3, "wind__deg": 155, "wind__gust": 11.01, "clouds__all": 41, "dt": 1687806607, "sys__type": 2, "sys__id": 2011597, "sys__country": "CA", "sys__sunrise": 1687781297, "sys__sunset": 1687839730, "timezone": -25200, "id": 6173331, "name": "Vancouver", "cod": 200, "summary": "scattered clouds", "_zap_search_was_found_status": true}
Thought: I now know the current weather in Vancouver BC.
Final Answer: The current weather in Vancouver BC is scattered clouds with a temperature of 71.69 and wind speed of 3
```
2023-06-27 18:46:32 +00:00
|
|
|
def test_wrapper_fails_no_api_key_or_access_token_initialization() -> None:
|
|
|
|
"""Test Wrapper requires either an API Key or OAuth Access Token."""
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
ZapierNLAWrapper()
|
|
|
|
|
|
|
|
|
|
|
|
def test_wrapper_api_key_initialization() -> None:
|
|
|
|
"""Test Wrapper initializes with an API Key."""
|
|
|
|
ZapierNLAWrapper(zapier_nla_api_key="test")
|
|
|
|
|
|
|
|
|
|
|
|
def test_wrapper_access_token_initialization() -> None:
|
|
|
|
"""Test Wrapper initializes with an API Key."""
|
|
|
|
ZapierNLAWrapper(zapier_nla_oauth_access_token="test")
|
2023-06-27 23:35:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_list_raises_401_invalid_api_key() -> None:
|
|
|
|
"""Test that a valid error is raised when the API Key is invalid."""
|
|
|
|
mock_response = MagicMock()
|
|
|
|
mock_response.status_code = 401
|
|
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError(
|
|
|
|
"401 Client Error: Unauthorized for url: https://nla.zapier.com/api/v1/exposed/"
|
|
|
|
)
|
|
|
|
mock_session = MagicMock()
|
|
|
|
mock_session.get.return_value = mock_response
|
|
|
|
|
|
|
|
with patch("requests.Session", return_value=mock_session):
|
|
|
|
wrapper = ZapierNLAWrapper(zapier_nla_api_key="test")
|
|
|
|
|
|
|
|
with pytest.raises(requests.HTTPError) as err:
|
|
|
|
wrapper.list()
|
|
|
|
|
|
|
|
assert str(err.value).startswith(
|
|
|
|
"An unauthorized response occurred. Check that your api key is correct. "
|
|
|
|
"Err:"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_raises_401_invalid_access_token() -> None:
|
|
|
|
"""Test that a valid error is raised when the API Key is invalid."""
|
|
|
|
mock_response = MagicMock()
|
|
|
|
mock_response.status_code = 401
|
|
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError(
|
|
|
|
"401 Client Error: Unauthorized for url: https://nla.zapier.com/api/v1/exposed/"
|
|
|
|
)
|
|
|
|
mock_session = MagicMock()
|
|
|
|
mock_session.get.return_value = mock_response
|
|
|
|
|
|
|
|
with patch("requests.Session", return_value=mock_session):
|
|
|
|
wrapper = ZapierNLAWrapper(zapier_nla_oauth_access_token="test")
|
|
|
|
|
|
|
|
with pytest.raises(requests.HTTPError) as err:
|
|
|
|
wrapper.list()
|
|
|
|
|
|
|
|
assert str(err.value).startswith(
|
|
|
|
"An unauthorized response occurred. Check that your access token is "
|
|
|
|
"correct and doesn't need to be refreshed. Err:"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_raises_other_error() -> None:
|
|
|
|
"""Test that a valid error is raised when an unknown HTTP Error occurs."""
|
|
|
|
mock_response = MagicMock()
|
|
|
|
mock_response.status_code = 404
|
|
|
|
mock_response.raise_for_status.side_effect = requests.HTTPError(
|
|
|
|
"404 Client Error: Not found for url"
|
|
|
|
)
|
|
|
|
mock_session = MagicMock()
|
|
|
|
mock_session.get.return_value = mock_response
|
|
|
|
|
|
|
|
with patch("requests.Session", return_value=mock_session):
|
|
|
|
wrapper = ZapierNLAWrapper(zapier_nla_oauth_access_token="test")
|
|
|
|
|
|
|
|
with pytest.raises(requests.HTTPError) as err:
|
|
|
|
wrapper.list()
|
|
|
|
|
|
|
|
assert str(err.value) == "404 Client Error: Not found for url"
|