From 806b7e6ad1e1e2bcf237cb0bcbba182ed9464cc4 Mon Sep 17 00:00:00 2001 From: Zach Giordano Date: Mon, 29 Apr 2024 15:04:22 -0400 Subject: [PATCH 1/5] Add structure run tool --- griptape/tools/__init__.py | 1 + griptape/tools/base_griptape_cloud_client.py | 20 ++++ .../tool.py | 14 +-- .../__init__.py | 0 .../manifest.yml | 5 + .../tool.py | 101 ++++++++++++++++++ ...est_griptape_cloud_structure_run_client.py | 30 ++++++ 7 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 griptape/tools/base_griptape_cloud_client.py create mode 100644 griptape/tools/griptape_cloud_structure_run_client/__init__.py create mode 100644 griptape/tools/griptape_cloud_structure_run_client/manifest.yml create mode 100644 griptape/tools/griptape_cloud_structure_run_client/tool.py create mode 100644 tests/unit/tools/test_griptape_cloud_structure_run_client.py diff --git a/griptape/tools/__init__.py b/griptape/tools/__init__.py index 5e68ca3fc..13bcf41b3 100644 --- a/griptape/tools/__init__.py +++ b/griptape/tools/__init__.py @@ -24,6 +24,7 @@ from .inpainting_image_generation_client.tool import InpaintingImageGenerationClient from .outpainting_image_generation_client.tool import OutpaintingImageGenerationClient from .griptape_cloud_knowledge_base_client.tool import GriptapeCloudKnowledgeBaseClient +from .griptape_cloud_structure_run_client.tool import GriptapeCloudStructureRunClient from .image_query_client.tool import ImageQueryClient __all__ = [ diff --git a/griptape/tools/base_griptape_cloud_client.py b/griptape/tools/base_griptape_cloud_client.py new file mode 100644 index 000000000..cafe01cd1 --- /dev/null +++ b/griptape/tools/base_griptape_cloud_client.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from abc import ABC +from attr import Factory, define, field +from griptape.tools import BaseTool + + +@define +class BaseGriptapeCloudClient(BaseTool, ABC): + """ + Attributes: + base_url: Base URL for the Griptape Cloud Knowledge Base API. + api_key: API key for Griptape Cloud. + headers: Headers for the Griptape Cloud Knowledge Base API. + """ + + base_url: str = field(default="https://cloud.griptape.ai", kw_only=True) + api_key: str = field(kw_only=True) + headers: dict = field( + default=Factory(lambda self: {"Authorization": f"Bearer {self.api_key}"}, takes_self=True), kw_only=True + ) diff --git a/griptape/tools/griptape_cloud_knowledge_base_client/tool.py b/griptape/tools/griptape_cloud_knowledge_base_client/tool.py index da988284f..89a284c98 100644 --- a/griptape/tools/griptape_cloud_knowledge_base_client/tool.py +++ b/griptape/tools/griptape_cloud_knowledge_base_client/tool.py @@ -2,29 +2,21 @@ from typing import Optional from urllib.parse import urljoin from schema import Schema, Literal -from attr import define, field, Factory -from griptape.tools import BaseTool +from attr import define, field +from griptape.tools.base_griptape_cloud_client import BaseGriptapeCloudClient from griptape.utils.decorators import activity from griptape.artifacts import TextArtifact, ErrorArtifact @define -class GriptapeCloudKnowledgeBaseClient(BaseTool): +class GriptapeCloudKnowledgeBaseClient(BaseGriptapeCloudClient): """ Attributes: description: LLM-friendly knowledge base description. - base_url: Base URL for the Griptape Cloud Knowledge Base API. - api_key: API key for Griptape Cloud. - headers: Headers for the Griptape Cloud Knowledge Base API. knowledge_base_id: ID of the Griptape Cloud Knowledge Base. """ description: Optional[str] = field(default=None, kw_only=True) - base_url: str = field(default="https://cloud.griptape.ai", kw_only=True) - api_key: str = field(kw_only=True) - headers: dict = field( - default=Factory(lambda self: {"Authorization": f"Bearer {self.api_key}"}, takes_self=True), kw_only=True - ) knowledge_base_id: str = field(kw_only=True) @activity( diff --git a/griptape/tools/griptape_cloud_structure_run_client/__init__.py b/griptape/tools/griptape_cloud_structure_run_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/griptape/tools/griptape_cloud_structure_run_client/manifest.yml b/griptape/tools/griptape_cloud_structure_run_client/manifest.yml new file mode 100644 index 000000000..5741fa336 --- /dev/null +++ b/griptape/tools/griptape_cloud_structure_run_client/manifest.yml @@ -0,0 +1,5 @@ +version: "v1" +name: Griptape Cloud Structure Run Client +description: Tool for using the Griptape Cloud Structure Run API. +contact_email: hello@griptape.ai +legal_info_url: https://www.griptape.ai/legal diff --git a/griptape/tools/griptape_cloud_structure_run_client/tool.py b/griptape/tools/griptape_cloud_structure_run_client/tool.py new file mode 100644 index 000000000..b20dfbc64 --- /dev/null +++ b/griptape/tools/griptape_cloud_structure_run_client/tool.py @@ -0,0 +1,101 @@ +from __future__ import annotations +import time +from typing import List, Optional +from urllib.parse import urljoin +from schema import Schema, Literal +from attr import define, field +from griptape.tools.base_griptape_cloud_client import BaseGriptapeCloudClient +from griptape.utils.decorators import activity +from griptape.artifacts import TextArtifact, ErrorArtifact + + +@define +class GriptapeCloudStructureRunClient(BaseGriptapeCloudClient): + """ + Attributes: + description: LLM-friendly structure description. + structure_id: ID of the Griptape Cloud Structure. + structure_run_wait_time_attempts: Number of attempts to wait for the structure run to complete. + structure_run_wait_time_multiplier: Multiplier for the wait time between attempts. + """ + + description: Optional[str] = field(default=None, kw_only=True) + structure_id: str = field(kw_only=True) + structure_run_wait_time_attempts: int = field(default=20, kw_only=True) + structure_run_wait_time_multiplier: float = field(default=1.5, kw_only=True) + + @activity( + config={ + "description": "Can be used to submit a run to a structure with the following description: {{ _self._get_structure_description() }}", + "schema": Schema( + {Literal("args", description="A list of arguments to submit to the structure run"): list[str]} + ), + } + ) + def submit_structure_run(self, params: dict) -> TextArtifact | ErrorArtifact: + from requests import post, exceptions + + args: list[str] = params["values"]["args"] + url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/runs") + + try: + response = post(url, json={"args": args}, headers=self.headers) + + return TextArtifact(response.json()) + except exceptions.RequestException as err: + return ErrorArtifact(str(err)) + + @activity( + config={ + "description": "Can be used to get the result of a structure run", + "schema": Schema({Literal("structure_run_id", description="The ID of the structure run"): str}), + } + ) + def get_structure_run_result(self, params: dict) -> TextArtifact | ErrorArtifact: + from requests import get, exceptions + + structure_run_id = params["values"]["structure_run_id"] + url = urljoin(self.base_url.strip("/"), f"/api/structure-runs/{structure_run_id}") + + try: + response = get(url, headers=self.headers) + content = response.json() + + status = content.get("status") + + wait_time = 1 + wait_attempts = 0 + while status in ["QUEUED", "RUNNING"] and wait_attempts < self.structure_run_wait_time_attempts: + # wait + time.sleep(wait_time) + wait_time = wait_time * self.structure_run_wait_time_multiplier + wait_attempts += 1 + + response = get(url, headers=self.headers) + content = response.json() + status = content.get("status") + + if status != "SUCCEEDED": + return ErrorArtifact(content) + + if "output" in content: + return TextArtifact(content["output"]) + else: + return ErrorArtifact("No output found in response") + + except exceptions.RequestException as err: + return ErrorArtifact(str(err)) + + def _get_structure_description(self) -> str: + from requests import get + + if self.description: + return self.description + else: + url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/") + + response = get(url, headers=self.headers).json() + if "description" in response: + return response["description"] + else: + raise ValueError(f'Error getting Structure description: {response["message"]}') diff --git a/tests/unit/tools/test_griptape_cloud_structure_run_client.py b/tests/unit/tools/test_griptape_cloud_structure_run_client.py new file mode 100644 index 000000000..0fa77208a --- /dev/null +++ b/tests/unit/tools/test_griptape_cloud_structure_run_client.py @@ -0,0 +1,30 @@ +import pytest +from griptape.artifacts import TextArtifact + + +class TestGriptapeCloudStructureRunClient: + @pytest.fixture + def client(self, mocker): + from griptape.tools import GriptapeCloudStructureRunClient + + mock_response = mocker.Mock() + mock_response.text.return_value = "foo bar" + mocker.patch("requests.post", return_value=mock_response) + + mock_response = mocker.Mock() + mock_response.json.return_value = {"description": "fizz buzz", "output": "fooey booey", "status": "SUCCEEDED"} + mocker.patch("requests.get", return_value=mock_response) + + return GriptapeCloudStructureRunClient(base_url="https://api.griptape.ai", api_key="foo bar", structure_id="1") + + def test_submit_structure_run(self, client): + assert isinstance(client.submit_structure_run({"values": {"args": ["foo bar"]}}), TextArtifact) + + def test_get_structure_run_result(self, client): + assert isinstance(client.get_structure_run_result({"values": {"structure_run_id": "1"}}), TextArtifact) + + def test_get_structure_description(self, client): + assert client._get_structure_description() == "fizz buzz" + + client.description = "foo bar" + assert client._get_structure_description() == "foo bar" From ff4d92594a1d7a5c38ffd85e862f0d3a2e310aa8 Mon Sep 17 00:00:00 2001 From: Zach Giordano Date: Mon, 29 Apr 2024 17:35:06 -0400 Subject: [PATCH 2/5] Address PR comments --- .../tool.py | 82 ++++++++----------- ...est_griptape_cloud_structure_run_client.py | 9 +- 2 files changed, 37 insertions(+), 54 deletions(-) diff --git a/griptape/tools/griptape_cloud_structure_run_client/tool.py b/griptape/tools/griptape_cloud_structure_run_client/tool.py index b20dfbc64..cb1967af6 100644 --- a/griptape/tools/griptape_cloud_structure_run_client/tool.py +++ b/griptape/tools/griptape_cloud_structure_run_client/tool.py @@ -1,97 +1,83 @@ from __future__ import annotations -import time -from typing import List, Optional +from typing import Optional from urllib.parse import urljoin from schema import Schema, Literal from attr import define, field +from griptape.mixins.exponential_backoff_mixin import ExponentialBackoffMixin from griptape.tools.base_griptape_cloud_client import BaseGriptapeCloudClient from griptape.utils.decorators import activity from griptape.artifacts import TextArtifact, ErrorArtifact @define -class GriptapeCloudStructureRunClient(BaseGriptapeCloudClient): +class GriptapeCloudStructureRunClient(BaseGriptapeCloudClient, ExponentialBackoffMixin): """ Attributes: description: LLM-friendly structure description. structure_id: ID of the Griptape Cloud Structure. - structure_run_wait_time_attempts: Number of attempts to wait for the structure run to complete. - structure_run_wait_time_multiplier: Multiplier for the wait time between attempts. """ description: Optional[str] = field(default=None, kw_only=True) structure_id: str = field(kw_only=True) - structure_run_wait_time_attempts: int = field(default=20, kw_only=True) - structure_run_wait_time_multiplier: float = field(default=1.5, kw_only=True) @activity( config={ - "description": "Can be used to submit a run to a structure with the following description: {{ _self._get_structure_description() }}", + "description": "Can be used to execute a Run of a Structure with the following description: {{ _self._get_structure_description() }}", "schema": Schema( - {Literal("args", description="A list of arguments to submit to the structure run"): list[str]} + {Literal("args", description="A list of string arguments to submit to the Structure Run"): list} ), } ) - def submit_structure_run(self, params: dict) -> TextArtifact | ErrorArtifact: - from requests import post, exceptions + def execute_structure_run(self, params: dict) -> TextArtifact | ErrorArtifact: + from requests import post, exceptions, HTTPError, Response args: list[str] = params["values"]["args"] url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/runs") try: - response = post(url, json={"args": args}, headers=self.headers) + response: Response = post(url, json={"args": args}, headers=self.headers) + response.raise_for_status() + response_json = response.json() + return self._get_structure_run_result(response_json["structure_run_id"]) - return TextArtifact(response.json()) - except exceptions.RequestException as err: + except (exceptions.RequestException, HTTPError) as err: return ErrorArtifact(str(err)) - @activity( - config={ - "description": "Can be used to get the result of a structure run", - "schema": Schema({Literal("structure_run_id", description="The ID of the structure run"): str}), - } - ) - def get_structure_run_result(self, params: dict) -> TextArtifact | ErrorArtifact: - from requests import get, exceptions - - structure_run_id = params["values"]["structure_run_id"] + def _get_structure_run_result(self, structure_run_id: str) -> TextArtifact | ErrorArtifact: url = urljoin(self.base_url.strip("/"), f"/api/structure-runs/{structure_run_id}") - try: - response = get(url, headers=self.headers) - content = response.json() + for attempt in self.retrying(): + with attempt: + return self._get_structure_run_result_attempt(url) + else: + return ErrorArtifact("Failed to get Run result.") - status = content.get("status") + def _get_structure_run_result_attempt(self, structure_run_url: str) -> TextArtifact | ErrorArtifact: + from requests import get, Response - wait_time = 1 - wait_attempts = 0 - while status in ["QUEUED", "RUNNING"] and wait_attempts < self.structure_run_wait_time_attempts: - # wait - time.sleep(wait_time) - wait_time = wait_time * self.structure_run_wait_time_multiplier - wait_attempts += 1 + response: Response = get(structure_run_url, headers=self.headers) + response.raise_for_status() + content = response.json() - response = get(url, headers=self.headers) - content = response.json() - status = content.get("status") + status = content.get("status") - if status != "SUCCEEDED": - return ErrorArtifact(content) + if status in ("QUEUED", "RUNNING"): + raise Exception("Structure Run is still in progress") - if "output" in content: - return TextArtifact(content["output"]) - else: - return ErrorArtifact("No output found in response") + if status != "SUCCEEDED": + return ErrorArtifact(content) - except exceptions.RequestException as err: - return ErrorArtifact(str(err)) + if "output" in content: + return TextArtifact(content["output"]) + else: + return ErrorArtifact("No output found in response") def _get_structure_description(self) -> str: - from requests import get - if self.description: return self.description else: + from requests import get + url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/") response = get(url, headers=self.headers).json() diff --git a/tests/unit/tools/test_griptape_cloud_structure_run_client.py b/tests/unit/tools/test_griptape_cloud_structure_run_client.py index 0fa77208a..2085b85ac 100644 --- a/tests/unit/tools/test_griptape_cloud_structure_run_client.py +++ b/tests/unit/tools/test_griptape_cloud_structure_run_client.py @@ -8,7 +8,7 @@ def client(self, mocker): from griptape.tools import GriptapeCloudStructureRunClient mock_response = mocker.Mock() - mock_response.text.return_value = "foo bar" + mock_response.json.return_value = {"structure_run_id": 1} mocker.patch("requests.post", return_value=mock_response) mock_response = mocker.Mock() @@ -17,11 +17,8 @@ def client(self, mocker): return GriptapeCloudStructureRunClient(base_url="https://api.griptape.ai", api_key="foo bar", structure_id="1") - def test_submit_structure_run(self, client): - assert isinstance(client.submit_structure_run({"values": {"args": ["foo bar"]}}), TextArtifact) - - def test_get_structure_run_result(self, client): - assert isinstance(client.get_structure_run_result({"values": {"structure_run_id": "1"}}), TextArtifact) + def test_execute_structure_run(self, client): + assert isinstance(client.execute_structure_run({"values": {"args": ["foo bar"]}}), TextArtifact) def test_get_structure_description(self, client): assert client._get_structure_description() == "fizz buzz" From 099d483d0a76e0749842bf5e4bd19c32f0362fcf Mon Sep 17 00:00:00 2001 From: Zach Giordano Date: Mon, 29 Apr 2024 21:19:17 -0400 Subject: [PATCH 3/5] Refactor tool, revert to basic polling logic, add docs page --- .../griptape-cloud-structure-run-client.md | 63 +++++++++++++++ .../tool.py | 76 +++++++++++-------- mkdocs.yml | 1 + ...est_griptape_cloud_structure_run_client.py | 4 +- 4 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md diff --git a/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md b/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md new file mode 100644 index 000000000..4b24548cf --- /dev/null +++ b/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md @@ -0,0 +1,63 @@ +# GriptapeCloudStructureRunClient + +The GriptapeCloudStructureRunClient tool provides a way to interact with the Griptape Cloud Structure Run API. It can be used to execute a Structure Run and retrieve the results. + +```python +from griptape.tools import GriptapeCloudStructureRunClient +from griptape.structures import Agent +import os + +api_key = os.environ["GRIPTAPE_CLOUD_API_KEY"] + +# Create the GriptapeCloudStructureRunClient tool +structure_run_tool = GriptapeCloudStructureRunClient( + description="Danish Baker Agent - Structure to invoke with natural language queries about Danish pastries", + api_key=api_key, + structure_id="15071570-94a6-493b-8011-62547ebe7fd2", + off_prompt=False, +) + +# Set up an agent using the GriptapeCloudStructureRunClient tool +agent = Agent( + tools=[structure_run_tool] +) + +# Task: Ask the Griptape Cloud Hosted Structure about new Danish pastries +agent.run( + "What are the new pastries?" +) +``` +``` +[04/29/24 20:46:14] INFO ToolkitTask 3b3f31a123584f05be9bcb02a58dddb6 + Input: what are the new pastries? +[04/29/24 20:46:23] INFO Subtask 2740dcd92bdf4b159dc7a7fb132c98f3 + Thought: To find out about new pastries, I need to use the Danish Baker Agent Structure. I will execute a run of + this Structure with the query "what are the new pastries". + + Actions: [ + { + "name": "GriptapeCloudStructureRunClient", + "path": "execute_structure_run", + "input": { + "values": { + "args": ["what are the new pastries"] + } + }, + "tag": "query_new_pastries" + } + ] +[04/29/24 20:47:01] INFO Subtask 2740dcd92bdf4b159dc7a7fb132c98f3 + Response: {'id': '4a329cbd09ad42e0bd265e9ba4690400', 'name': '4a329cbd09ad42e0bd265e9ba4690400', 'type': + 'TextArtifact', 'value': 'Ah, my friend, I am glad you asked! We have been busy in the bakery, kneading dough + and sprinkling sugar. Our new pastries include the "Copenhagen Cream Puff", a delightful puff pastry filled with + sweet cream and dusted with powdered sugar. We also have the "Danish Delight", a buttery croissant filled with + raspberry jam and topped with a drizzle of white chocolate. And let\'s not forget the "Nordic Nutella Twist", a + flaky pastry twisted with Nutella and sprinkled with chopped hazelnuts. I promise, each bite will transport you + to a cozy Danish bakery!'} +[04/29/24 20:47:07] INFO ToolkitTask 3b3f31a123584f05be9bcb02a58dddb6 + Output: The new pastries include the "Copenhagen Cream Puff," which is a puff pastry filled with sweet cream and + dusted with powdered sugar; the "Danish Delight," a buttery croissant filled with raspberry jam and topped with + white chocolate; and the "Nordic Nutella Twist," a flaky pastry twisted with Nutella and sprinkled with chopped + hazelnuts. +Assistant: The new pastries include the "Copenhagen Cream Puff," which is a puff pastry filled with sweet cream and dusted with powdered sugar; the "Danish Delight," a buttery croissant filled with raspberry jam and topped with white chocolate; and the "Nordic Nutella Twist," a flaky pastry twisted with Nutella and sprinkled with chopped hazelnuts. +``` \ No newline at end of file diff --git a/griptape/tools/griptape_cloud_structure_run_client/tool.py b/griptape/tools/griptape_cloud_structure_run_client/tool.py index cb1967af6..4644547cd 100644 --- a/griptape/tools/griptape_cloud_structure_run_client/tool.py +++ b/griptape/tools/griptape_cloud_structure_run_client/tool.py @@ -1,34 +1,36 @@ from __future__ import annotations -from typing import Optional +import time +from typing import Any, Optional from urllib.parse import urljoin from schema import Schema, Literal from attr import define, field -from griptape.mixins.exponential_backoff_mixin import ExponentialBackoffMixin from griptape.tools.base_griptape_cloud_client import BaseGriptapeCloudClient from griptape.utils.decorators import activity -from griptape.artifacts import TextArtifact, ErrorArtifact +from griptape.artifacts import InfoArtifact, TextArtifact, ErrorArtifact @define -class GriptapeCloudStructureRunClient(BaseGriptapeCloudClient, ExponentialBackoffMixin): +class GriptapeCloudStructureRunClient(BaseGriptapeCloudClient): """ Attributes: description: LLM-friendly structure description. structure_id: ID of the Griptape Cloud Structure. """ - description: Optional[str] = field(default=None, kw_only=True) + _description: Optional[str] = field(default=None, kw_only=True) structure_id: str = field(kw_only=True) + structure_run_wait_time_interval: int = field(default=2, kw_only=True) + structure_run_max_wait_time_attempts: int = field(default=20, kw_only=True) @activity( config={ - "description": "Can be used to execute a Run of a Structure with the following description: {{ _self._get_structure_description() }}", + "description": "Can be used to execute a Run of a Structure with the following description: {{ _self.description }}", "schema": Schema( {Literal("args", description="A list of string arguments to submit to the Structure Run"): list} ), } ) - def execute_structure_run(self, params: dict) -> TextArtifact | ErrorArtifact: + def execute_structure_run(self, params: dict) -> InfoArtifact | TextArtifact | ErrorArtifact: from requests import post, exceptions, HTTPError, Response args: list[str] = params["values"]["args"] @@ -43,45 +45,55 @@ def execute_structure_run(self, params: dict) -> TextArtifact | ErrorArtifact: except (exceptions.RequestException, HTTPError) as err: return ErrorArtifact(str(err)) - def _get_structure_run_result(self, structure_run_id: str) -> TextArtifact | ErrorArtifact: + def _get_structure_run_result(self, structure_run_id: str) -> InfoArtifact | TextArtifact | ErrorArtifact: url = urljoin(self.base_url.strip("/"), f"/api/structure-runs/{structure_run_id}") - for attempt in self.retrying(): - with attempt: - return self._get_structure_run_result_attempt(url) - else: - return ErrorArtifact("Failed to get Run result.") + result = self._get_structure_run_result_attempt(url) + status = result.get("status") - def _get_structure_run_result_attempt(self, structure_run_url: str) -> TextArtifact | ErrorArtifact: - from requests import get, Response + wait_attempts = 0 + while status in ["QUEUED", "RUNNING"] and wait_attempts < self.structure_run_max_wait_time_attempts: + # wait + time.sleep(self.structure_run_wait_time_interval) + wait_attempts += 1 + result = self._get_structure_run_result_attempt(url) + status = result.get("status") - response: Response = get(structure_run_url, headers=self.headers) - response.raise_for_status() - content = response.json() - - status = content.get("status") - - if status in ("QUEUED", "RUNNING"): - raise Exception("Structure Run is still in progress") + if wait_attempts >= self.structure_run_max_wait_time_attempts: + return ErrorArtifact( + f"Failed to get Run result after {self.structure_run_max_wait_time_attempts} attempts." + ) if status != "SUCCEEDED": - return ErrorArtifact(content) + return ErrorArtifact(result) - if "output" in content: - return TextArtifact(content["output"]) + if "output" in result: + return TextArtifact(result["output"]) else: - return ErrorArtifact("No output found in response") + return InfoArtifact("No output found in response") - def _get_structure_description(self) -> str: - if self.description: - return self.description - else: + def _get_structure_run_result_attempt(self, structure_run_url: str) -> Any: + from requests import get, Response + + response: Response = get(structure_run_url, headers=self.headers) + response.raise_for_status() + return response.json() + + @property + def description(self) -> str: + if self._description is None: from requests import get url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/") response = get(url, headers=self.headers).json() if "description" in response: - return response["description"] + self._description = response["description"] else: raise ValueError(f'Error getting Structure description: {response["message"]}') + + return self._description + + @description.setter + def description(self, value: str) -> None: + self._description = value diff --git a/mkdocs.yml b/mkdocs.yml index a083f2b8b..9359bbce8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -128,6 +128,7 @@ nav: - GoogleGmailClient: "griptape-tools/official-tools/google-gmail-client.md" - GoogleDriveClient: "griptape-tools/official-tools/google-drive-client.md" - GoogleDocsClient: "griptape-tools/official-tools/google-docs-client.md" + - GriptapeCloudStructureRunClient: "griptape-tools/official-tools/griptape-cloud-structure-run-client.md" - OpenWeatherClient: "griptape-tools/official-tools/openweather-client.md" - RestApiClient: "griptape-tools/official-tools/rest-api-client.md" - SqlClient: "griptape-tools/official-tools/sql-client.md" diff --git a/tests/unit/tools/test_griptape_cloud_structure_run_client.py b/tests/unit/tools/test_griptape_cloud_structure_run_client.py index 2085b85ac..8656a8f77 100644 --- a/tests/unit/tools/test_griptape_cloud_structure_run_client.py +++ b/tests/unit/tools/test_griptape_cloud_structure_run_client.py @@ -21,7 +21,7 @@ def test_execute_structure_run(self, client): assert isinstance(client.execute_structure_run({"values": {"args": ["foo bar"]}}), TextArtifact) def test_get_structure_description(self, client): - assert client._get_structure_description() == "fizz buzz" + assert client.description == "fizz buzz" client.description = "foo bar" - assert client._get_structure_description() == "foo bar" + assert client.description == "foo bar" From 5205e22dc6415b83416af908c5dc5efd22b2e7a3 Mon Sep 17 00:00:00 2001 From: Zach Giordano Date: Tue, 30 Apr 2024 11:52:33 -0400 Subject: [PATCH 4/5] Update docs example, integ tests config, and changelog --- .github/workflows/docs-integration-tests.yml | 2 ++ CHANGELOG.md | 1 + .../official-tools/griptape-cloud-structure-run-client.md | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs-integration-tests.yml b/.github/workflows/docs-integration-tests.yml index ad4457a20..005b8dd5f 100644 --- a/.github/workflows/docs-integration-tests.yml +++ b/.github/workflows/docs-integration-tests.yml @@ -63,6 +63,8 @@ jobs: GOOGLE_AUTH_URI: ${{ secrets.INTEG_GOOGLE_AUTH_URI }} GOOGLE_TOKEN_URI: ${{ secrets.INTEG_GOOGLE_TOKEN_URI }} GOOGLE_AUTH_PROVIDER_X509_CERT_URL: ${{ secrets.INTEG_GOOGLE_AUTH_PROVIDER_X509_CERT_URL }} + GRIPTAPE_CLOUD_API_KEY: ${{ secrets.INTEG_GRIPTAPE_CLOUD_API_KEY }} + GRIPTAPE_CLOUD_STRUCTURE_ID: ${{ secrets.INTEG_GRIPTAPE_CLOUD_STRUCTURE_ID }} OPENWEATHER_API_KEY: ${{ secrets.INTEG_OPENWEATHER_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.INTEG_ANTHROPIC_API_KEY }} SAGEMAKER_LLAMA_ENDPOINT_NAME: ${{ secrets.INTEG_LLAMA_ENDPOINT_NAME }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ec366ab..d445aa96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AmazonS3FileManagerDriver` for managing files on Amazon S3. - `MediaArtifact` as a base class for `ImageArtifact` and future media Artifacts. - Optional `exception` field to `ErrorArtifact`. +- `GriptapeCloudStructureRunClient` tool for invoking Griptape Cloud Structure Run APIs. ### Changed - **BREAKING**: Secret fields (ex: api_key) removed from serialized Drivers. diff --git a/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md b/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md index 4b24548cf..032a84b97 100644 --- a/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md +++ b/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md @@ -8,12 +8,13 @@ from griptape.structures import Agent import os api_key = os.environ["GRIPTAPE_CLOUD_API_KEY"] +structure_id = os.environ["GRIPTAPE_CLOUD_STRUCTURE_ID"] # Create the GriptapeCloudStructureRunClient tool structure_run_tool = GriptapeCloudStructureRunClient( description="Danish Baker Agent - Structure to invoke with natural language queries about Danish pastries", api_key=api_key, - structure_id="15071570-94a6-493b-8011-62547ebe7fd2", + structure_id=structure_id, off_prompt=False, ) From c66005575a71734b0c66df6ec138629913f66114 Mon Sep 17 00:00:00 2001 From: Zach Giordano Date: Tue, 30 Apr 2024 13:55:45 -0400 Subject: [PATCH 5/5] Small edits --- .../tool.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/griptape/tools/griptape_cloud_structure_run_client/tool.py b/griptape/tools/griptape_cloud_structure_run_client/tool.py index 4644547cd..7d4edbd72 100644 --- a/griptape/tools/griptape_cloud_structure_run_client/tool.py +++ b/griptape/tools/griptape_cloud_structure_run_client/tool.py @@ -22,6 +22,25 @@ class GriptapeCloudStructureRunClient(BaseGriptapeCloudClient): structure_run_wait_time_interval: int = field(default=2, kw_only=True) structure_run_max_wait_time_attempts: int = field(default=20, kw_only=True) + @property + def description(self) -> str: + if self._description is None: + from requests import get + + url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/") + + response = get(url, headers=self.headers).json() + if "description" in response: + self._description = response["description"] + else: + raise ValueError(f'Error getting Structure description: {response["message"]}') + + return self._description + + @description.setter + def description(self, value: str) -> None: + self._description = value + @activity( config={ "description": "Can be used to execute a Run of a Structure with the following description: {{ _self.description }}", @@ -49,15 +68,15 @@ def _get_structure_run_result(self, structure_run_id: str) -> InfoArtifact | Tex url = urljoin(self.base_url.strip("/"), f"/api/structure-runs/{structure_run_id}") result = self._get_structure_run_result_attempt(url) - status = result.get("status") + status = result["status"] wait_attempts = 0 - while status in ["QUEUED", "RUNNING"] and wait_attempts < self.structure_run_max_wait_time_attempts: + while status in ("QUEUED", "RUNNING") and wait_attempts < self.structure_run_max_wait_time_attempts: # wait time.sleep(self.structure_run_wait_time_interval) wait_attempts += 1 result = self._get_structure_run_result_attempt(url) - status = result.get("status") + status = result["status"] if wait_attempts >= self.structure_run_max_wait_time_attempts: return ErrorArtifact( @@ -78,22 +97,3 @@ def _get_structure_run_result_attempt(self, structure_run_url: str) -> Any: response: Response = get(structure_run_url, headers=self.headers) response.raise_for_status() return response.json() - - @property - def description(self) -> str: - if self._description is None: - from requests import get - - url = urljoin(self.base_url.strip("/"), f"/api/structures/{self.structure_id}/") - - response = get(url, headers=self.headers).json() - if "description" in response: - self._description = response["description"] - else: - raise ValueError(f'Error getting Structure description: {response["message"]}') - - return self._description - - @description.setter - def description(self, value: str) -> None: - self._description = value