From 0c87ea2c2e27f85bd70c69bc4b6da93392b20172 Mon Sep 17 00:00:00 2001 From: Collin Dutter Date: Mon, 6 May 2024 10:13:51 -0700 Subject: [PATCH] Add structure run tool/task (#767) --- .github/workflows/docs-integration-tests.yml | 1 + CHANGELOG.md | 7 +- docs/examples/multi-agent-workflow.md | 191 ++++++++++++++++++ .../drivers/structure-run-drivers.md | 97 +++++++++ docs/griptape-framework/structures/tasks.md | 140 +++++++++++++ .../griptape-cloud-structure-run-client.md | 68 ------- .../official-tools/structure-run-client.md | 64 ++++++ griptape/drivers/__init__.py | 7 + .../structure_run}/__init__.py | 0 .../base_structure_run_driver.py | 15 ++ .../griptape_cloud_structure_run_driver.py} | 70 +++---- .../local_structure_run_driver.py | 23 +++ griptape/tasks/__init__.py | 4 + griptape/tasks/base_multi_text_input_task.py | 53 +++++ griptape/tasks/structure_run_task.py | 22 ++ griptape/tools/__init__.py | 4 +- .../manifest.yml | 5 - .../tools/structure_run_client/__init__.py | 0 .../tools/structure_run_client/manifest.yml | 5 + griptape/tools/structure_run_client/tool.py | 34 ++++ mkdocs.yml | 3 +- tests/mocks/mock_multi_text_input_task.py | 9 + tests/unit/drivers/structure_run/__init__.py | 0 ...est_griptape_cloud_structure_run_driver.py | 35 ++++ .../test_local_structure_run_driver.py | 24 +++ .../tasks/test_base_multi_text_input_task.py | 58 ++++++ tests/unit/tasks/test_structure_run_task.py | 18 ++ ...est_griptape_cloud_structure_run_client.py | 27 --- tests/unit/tools/test_structure_run_client.py | 20 ++ 29 files changed, 852 insertions(+), 152 deletions(-) create mode 100644 docs/examples/multi-agent-workflow.md create mode 100644 docs/griptape-framework/drivers/structure-run-drivers.md delete mode 100644 docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md create mode 100644 docs/griptape-tools/official-tools/structure-run-client.md rename griptape/{tools/griptape_cloud_structure_run_client => drivers/structure_run}/__init__.py (100%) create mode 100644 griptape/drivers/structure_run/base_structure_run_driver.py rename griptape/{tools/griptape_cloud_structure_run_client/tool.py => drivers/structure_run/griptape_cloud_structure_run_driver.py} (50%) create mode 100644 griptape/drivers/structure_run/local_structure_run_driver.py create mode 100644 griptape/tasks/base_multi_text_input_task.py create mode 100644 griptape/tasks/structure_run_task.py delete mode 100644 griptape/tools/griptape_cloud_structure_run_client/manifest.yml create mode 100644 griptape/tools/structure_run_client/__init__.py create mode 100644 griptape/tools/structure_run_client/manifest.yml create mode 100644 griptape/tools/structure_run_client/tool.py create mode 100644 tests/mocks/mock_multi_text_input_task.py create mode 100644 tests/unit/drivers/structure_run/__init__.py create mode 100644 tests/unit/drivers/structure_run/test_griptape_cloud_structure_run_driver.py create mode 100644 tests/unit/drivers/structure_run/test_local_structure_run_driver.py create mode 100644 tests/unit/tasks/test_base_multi_text_input_task.py create mode 100644 tests/unit/tasks/test_structure_run_task.py delete mode 100644 tests/unit/tools/test_griptape_cloud_structure_run_client.py create mode 100644 tests/unit/tools/test_structure_run_client.py diff --git a/.github/workflows/docs-integration-tests.yml b/.github/workflows/docs-integration-tests.yml index 0d6cb76ca..2f1dba285 100644 --- a/.github/workflows/docs-integration-tests.yml +++ b/.github/workflows/docs-integration-tests.yml @@ -65,6 +65,7 @@ jobs: 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 }} + GRIPTAPE_CLOUD_BASE_URL: ${{ secrets.INTEG_GRIPTAPE_CLOUD_BASE_URL }} 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 ff497d2fb..255ec88b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,10 @@ 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. +- `StructureRunClient` for running other Structures via a Tool. +- `StructureRunTask` for running Structures as a Task from within another Structure. +- `GriptapeCloudStructureRunDriver` for running Structures in Griptape Cloud. +- `LocalStructureRunDriver` for running Structures in the same run-time environment as the code that is running the Structure. ### Changed - **BREAKING**: Secret fields (ex: api_key) removed from serialized Drivers. @@ -34,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING**: `FileManager.default_loader` is now `None` by default. - **BREAKING** Bumped `pinecone` from `^2` to `^3`. - **BREAKING**: Removed `workdir`, `loaders`, `default_loader`, and `save_file_encoding` fields from `FileManager` and added `file_manager_driver`. -- **BREADKING**: Removed `mime_type` field from `ImageArtifact`. `mime_type` is now a property constructed using the Artifact type and `format` field. +- **BREAKING**: Removed `mime_type` field from `ImageArtifact`. `mime_type` is now a property constructed using the Artifact type and `format` field. - Improved RAG performance in `VectorQueryEngine`. - Moved [Griptape Docs](https://github.com/griptape-ai/griptape-docs) to this repository. - Updated `EventListener.handler`'s behavior so that the return value will be passed to the `EventListenerDriver.try_publish_event_payload`'s `event_payload` parameter. diff --git a/docs/examples/multi-agent-workflow.md b/docs/examples/multi-agent-workflow.md new file mode 100644 index 000000000..0e85b9bde --- /dev/null +++ b/docs/examples/multi-agent-workflow.md @@ -0,0 +1,191 @@ +In this example we implement a multi-agent Workflow. We have a single "Researcher" Agent that conducts research on a topic, and then fans out to multiple "Writer" Agents to write blog posts based on the research. + +By splitting up our workloads across multiple Structures, we can parallelize the work and leverage the strengths of each Agent. The Researcher can focus on gathering data and insights, while the Writers can focus on crafting engaging narratives. +Additionally, this architecture opens us up to using services such as [Griptape Cloud](https://www.griptape.ai/cloud) to have each Agent run on a separate machine, allowing us to scale our Workflow as needed 🤯. + + +```python +import os + +from griptape.drivers import WebhookEventListenerDriver, LocalStructureRunDriver +from griptape.events import EventListener, FinishStructureRunEvent +from griptape.rules import Rule, Ruleset +from griptape.structures import Agent, Workflow +from griptape.tasks import PromptTask, StructureRunTask +from griptape.tools import ( + TaskMemoryClient, + WebScraper, + WebSearch, +) + +WRITERS = [ + { + "role": "Travel Adventure Blogger", + "goal": "Inspire wanderlust with stories of hidden gems and exotic locales", + "backstory": "With a passport full of stamps, you bring distant cultures and breathtaking scenes to life through vivid storytelling and personal anecdotes.", + }, + { + "role": "Lifestyle Freelance Writer", + "goal": "Share practical advice on living a balanced and stylish life", + "backstory": "From the latest trends in home decor to tips for wellness, your articles help readers create a life that feels both aspirational and attainable.", + }, +] + + +def build_researcher(): + """Builds a Researcher Structure.""" + researcher = Agent( + id="researcher", + tools=[ + WebSearch( + google_api_key=os.environ["GOOGLE_API_KEY"], + google_api_search_id=os.environ["GOOGLE_API_SEARCH_ID"], + off_prompt=False, + ), + WebScraper( + off_prompt=True, + ), + TaskMemoryClient(off_prompt=False), + ], + rulesets=[ + Ruleset( + name="Position", + rules=[ + Rule( + value="Lead Research Analyst", + ) + ], + ), + Ruleset( + name="Objective", + rules=[ + Rule( + value="Discover innovative advancements in artificial intelligence and data analytics", + ) + ], + ), + Ruleset( + name="Background", + rules=[ + Rule( + value="""You are part of a prominent technology research institute. + Your speciality is spotting new trends. + You excel at analyzing intricate data and delivering practical insights.""" + ) + ], + ), + Ruleset( + name="Desired Outcome", + rules=[ + Rule( + value="Comprehensive analysis report in list format", + ) + ], + ), + ], + ) + + return researcher + + +def build_writer(role: str, goal: str, backstory: str): + """Builds a Writer Structure. + + Args: + role: The role of the writer. + goal: The goal of the writer. + backstory: The backstory of the writer. + """ + writer = Agent( + id=role.lower().replace(" ", "_"), + event_listeners=[ + EventListener( + event_types=[FinishStructureRunEvent], + driver=WebhookEventListenerDriver( + webhook_url=os.environ["WEBHOOK_URL"], + ), + ) + ], + rulesets=[ + Ruleset( + name="Position", + rules=[ + Rule( + value=role, + ) + ], + ), + Ruleset( + name="Objective", + rules=[ + Rule( + value=goal, + ) + ], + ), + Ruleset( + name="Backstory", + rules=[Rule(value=backstory)], + ), + Ruleset( + name="Desired Outcome", + rules=[ + Rule( + value="Full blog post of at least 4 paragraphs", + ) + ], + ), + ], + ) + + return writer + + +if __name__ == "__main__": + # Build the team + team = Workflow() + research_task = team.add_task( + StructureRunTask( + ( + """Perform a detailed examination of the newest developments in AI as of 2024. + Pinpoint major trends, breakthroughs, and their implications for various industries.""", + ), + id="research", + driver=LocalStructureRunDriver( + structure_factory_fn=build_researcher, + ), + ), + ) + end_task = team.add_task( + PromptTask( + 'State "All Done!"', + ) + ) + team.insert_tasks( + research_task, + [ + StructureRunTask( + ( + """Using insights provided, develop an engaging blog + post that highlights the most significant AI advancements. + Your post should be informative yet accessible, catering to a tech-savvy audience. + Make it sound cool, avoid complex words so it doesn't sound like AI. + + Insights: + {{ parent_outputs["research"] }}""", + ), + driver=LocalStructureRunDriver( + structure_factory_fn=lambda: build_writer( + role=writer["role"], + goal=writer["goal"], + backstory=writer["backstory"], + ) + ), + ) + for writer in WRITERS + ], + end_task, + ) + + team.run() +``` diff --git a/docs/griptape-framework/drivers/structure-run-drivers.md b/docs/griptape-framework/drivers/structure-run-drivers.md new file mode 100644 index 000000000..9c1441608 --- /dev/null +++ b/docs/griptape-framework/drivers/structure-run-drivers.md @@ -0,0 +1,97 @@ +## Overview +Structure Run Drivers can be used to run Griptape Structures in a variety of runtime environments. +When combined with the [Structure Run Task](../../griptape-framework/structures/tasks.md#structure-run-task) or [Structure Run Client](../../griptape-tools/official-tools/structure-run-client.md) you can create complex, multi-agent pipelines that span multiple runtime environments. + +## Local Structure Run Driver + +The [LocalStructureRunDriver](../../reference/griptape/drivers/structure_run/local_structure_run_driver.md) is used to run Griptape Structures in the same runtime environment as the code that is running the Structure. + +```python +from griptape.drivers import LocalStructureRunDriver +from griptape.rules import Rule +from griptape.structures import Agent, Pipeline +from griptape.tasks import StructureRunTask + +joke_teller = Agent( + rules=[ + Rule( + value="You are very funny.", + ) + ], +) + +joke_rewriter = Agent( + rules=[ + Rule( + value="You are the editor of a joke book. But you only speak in riddles", + ) + ], +) + +joke_coordinator = Pipeline( + tasks=[ + StructureRunTask( + driver=LocalStructureRunDriver( + structure_factory_fn=lambda: joke_teller, + ), + ), + StructureRunTask( + ("Rewrite this joke: {{ parent_output }}",), + driver=LocalStructureRunDriver( + structure_factory_fn=lambda: joke_rewriter, + ), + ), + ] +) + +joke_coordinator.run("Tell me a joke") +``` + +## Griptape Cloud Structure Run Driver + +The [GriptapeCloudStructureRunDriver](../../reference/griptape/drivers/structure_run/griptape_cloud_structure_run_driver.md) is used to run Griptape Structures in the Griptape Cloud. + + +```python +import os + +from griptape.drivers import GriptapeCloudStructureRunDriver, LocalStructureRunDriver +from griptape.structures import Pipeline, Agent +from griptape.rules import Rule +from griptape.tasks import StructureRunTask + +base_url = os.environ["GRIPTAPE_CLOUD_BASE_URL"] +api_key = os.environ["GRIPTAPE_CLOUD_API_KEY"] +structure_id = os.environ["GRIPTAPE_CLOUD_STRUCTURE_ID"] + + +pipeline = Pipeline( + tasks=[ + StructureRunTask( + ("Think of a question related to Retrieval Augmented Generation.",), + driver=LocalStructureRunDriver( + structure_factory_fn=lambda: Agent( + rules=[ + Rule( + value="You are an expert in Retrieval Augmented Generation.", + ), + Rule( + value="Only output your answer, no other information.", + ), + ] + ) + ), + ), + StructureRunTask( + ("{{ parent_output }}",), + driver=GriptapeCloudStructureRunDriver( + base_url=base_url, + api_key=api_key, + structure_id=structure_id, + ), + ), + ] +) + +pipeline.run() +``` diff --git a/docs/griptape-framework/structures/tasks.md b/docs/griptape-framework/structures/tasks.md index a7aae8734..c7f9e78cd 100644 --- a/docs/griptape-framework/structures/tasks.md +++ b/docs/griptape-framework/structures/tasks.md @@ -667,3 +667,143 @@ pipeline.add_task( pipeline.run("Describe the weather in the image") ``` + +## Structure Run Task +The [Structure Run Task](../../reference/griptape/tasks/structure_run_task.md) executes another Structure with a given input. +This Task is useful for orchestrating multiple specialized Structures in a single run. Note that the input to the Task is a tuple of arguments that will be passed to the Structure. + +```python +import os + +from griptape.rules import Rule, Ruleset +from griptape.structures import Agent, Pipeline +from griptape.tasks import StructureRunTask +from griptape.drivers import LocalStructureRunDriver +from griptape.tools import ( + TaskMemoryClient, + WebScraper, + WebSearch, +) + + +def build_researcher(): + researcher = Agent( + tools=[ + WebSearch( + google_api_key=os.environ["GOOGLE_API_KEY"], + google_api_search_id=os.environ["GOOGLE_API_SEARCH_ID"], + off_prompt=False, + ), + WebScraper( + off_prompt=True, + ), + TaskMemoryClient(off_prompt=False), + ], + rulesets=[ + Ruleset( + name="Position", + rules=[ + Rule( + value="Senior Research Analyst", + ) + ], + ), + Ruleset( + name="Objective", + rules=[ + Rule( + value="Uncover cutting-edge developments in AI and data science", + ) + ], + ), + Ruleset( + name="Background", + rules=[ + Rule( + value="""You work at a leading tech think tank., + Your expertise lies in identifying emerging trends. + You have a knack for dissecting complex data and presenting actionable insights.""" + ) + ], + ), + Ruleset( + name="Desired Outcome", + rules=[ + Rule( + value="Full analysis report in bullet points", + ) + ], + ), + ], + ) + + return researcher + + +def build_writer(): + writer = Agent( + input_template="Instructions: {{args[0]}}\nContext: {{args[1]}}", + rulesets=[ + Ruleset( + name="Position", + rules=[ + Rule( + value="Tech Content Strategist", + ) + ], + ), + Ruleset( + name="Objective", + rules=[ + Rule( + value="Craft compelling content on tech advancements", + ) + ], + ), + Ruleset( + name="Backstory", + rules=[ + Rule( + value="""You are a renowned Content Strategist, known for your insightful and engaging articles. + You transform complex concepts into compelling narratives.""" + ) + ], + ), + Ruleset( + name="Desired Outcome", + rules=[ + Rule( + value="Full blog post of at least 4 paragraphs", + ) + ], + ), + ], + ) + + return writer + + +team = Pipeline( + tasks=[ + StructureRunTask( + ( + """Perform a detailed examination of the newest developments in AI as of 2024. + Pinpoint major trends, breakthroughs, and their implications for various industries.""", + ), + driver=LocalStructureRunDriver(structure_factory_fn=build_researcher), + ), + StructureRunTask( + ( + """Utilize the gathered insights to craft a captivating blog + article showcasing the key AI innovations. + Ensure the content is engaging yet straightforward, appealing to a tech-aware readership. + Keep the tone appealing and use simple language to make it less technical.""", + "{{parent_output}}", + ), + driver=LocalStructureRunDriver(structure_factory_fn=build_writer), + ), + ], +) + +team.run() +``` 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 deleted file mode 100644 index 1e98de1c3..000000000 --- a/docs/griptape-tools/official-tools/griptape-cloud-structure-run-client.md +++ /dev/null @@ -1,68 +0,0 @@ -# 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"] -structure_id = os.environ["GRIPTAPE_CLOUD_STRUCTURE_ID"] - -# Create the GriptapeCloudStructureRunClient tool -structure_run_tool = GriptapeCloudStructureRunClient( - description="RAG Expert Agent - Structure to invoke with natural language queries about the topic of Retrieval Augmented Generation", - api_key=api_key, - structure_id=structure_id, - 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 modular RAG -agent.run( - "what is modular RAG?" -) -``` -``` -[05/02/24 11:36:16] INFO ToolkitTask 0d99b986140a42c0828215cbc42156e8 - Input: what is modular RAG? -[05/02/24 11:36:24] INFO Subtask b498fb971198476a83ba1a555ff4fb98 - Thought: To provide an explanation of what modular RAG (Retrieval Augmented Generation) is, I will use the - GriptapeCloudStructureRunClient to execute a Structure Run that can provide information on this topic. - - Actions: [ - { - "name": "GriptapeCloudStructureRunClient", - "path": "execute_structure_run", - "input": { - "values": { - "args": ["What is modular RAG?"] - } - }, - "tag": "modular_rag_info" - } - ] -[05/02/24 11:37:28] INFO Subtask b498fb971198476a83ba1a555ff4fb98 - Response: {'id': 'ea96fc6f92f9417880938ff59273be59', 'name': 'ea96fc6f92f9417880938ff59273be59', 'type': - 'TextArtifact', 'value': "Modular RAG is an advanced architecture that builds upon the foundational principles - of Advanced and Naive RAG paradigms. It offers enhanced adaptability and versatility by incorporating diverse - strategies to improve its components. Modular RAG introduces additional specialized components like the Search - module for direct searches across various data sources, the RAG-Fusion module for expanding user queries into - diverse perspectives, and the Memory module to guide retrieval using the LLM's memory. This approach supports - both sequential processing and integrated end-to-end training across its components, illustrating progression - and refinement within the RAG family."} -[05/02/24 11:37:40] INFO ToolkitTask 0d99b986140a42c0828215cbc42156e8 - Output: Modular RAG is an advanced architecture that builds upon the foundational principles of Advanced and - Naive RAG paradigms. It offers enhanced adaptability and versatility by incorporating diverse strategies to - improve its components. Modular RAG introduces additional specialized components like the Search module for - direct searches across various data sources, the RAG-Fusion module for expanding user queries into diverse - perspectives, and the Memory module to guide retrieval using the LLM's memory. This approach supports both - sequential processing and integrated end-to-end training across its components, illustrating progression and - refinement within the RAG family. -Assistant: Modular RAG is an advanced architecture that builds upon the foundational principles of Advanced and Naive RAG paradigms. It offers enhanced adaptability and versatility by incorporating diverse strategies to improve its components. Modular RAG introduces additional specialized components like the Search module for direct searches across various data sources, the RAG-Fusion module for expanding user queries into diverse perspectives, and the Memory module to guide retrieval using the LLM's memory. This approach supports both sequential processing and integrated end-to-end training across its components, illustrating progression and refinement within the RAG family. -``` \ No newline at end of file diff --git a/docs/griptape-tools/official-tools/structure-run-client.md b/docs/griptape-tools/official-tools/structure-run-client.md new file mode 100644 index 000000000..863e02727 --- /dev/null +++ b/docs/griptape-tools/official-tools/structure-run-client.md @@ -0,0 +1,64 @@ +# StructureRunClient + +The StructureRunClient Tool provides a way to run Structures via a Tool. +It requires you to provide a [Structure Run Driver](../../griptape-framework/drivers/structure-run-drivers.md) to run the Structure in the desired environment. + +```python +import os + +from griptape.drivers import GriptapeCloudStructureRunDriver +from griptape.structures import Agent +from griptape.tools import StructureRunClient + +base_url = os.environ["GRIPTAPE_CLOUD_BASE_URL"] +api_key = os.environ["GRIPTAPE_CLOUD_API_KEY"] +structure_id = os.environ["GRIPTAPE_CLOUD_STRUCTURE_ID"] + +structure_run_tool = StructureRunClient( + description="RAG Expert Agent - Structure to invoke with natural language queries about the topic of Retrieval Augmented Generation", + driver=GriptapeCloudStructureRunDriver( + base_url=base_url, + api_key=api_key, + structure_id=structure_id, + ), + off_prompt=False, +) + +# Set up an agent using the StructureRunClient tool +agent = Agent(tools=[structure_run_tool]) + +# Task: Ask the Griptape Cloud Hosted Structure about modular RAG +agent.run("what is modular RAG?") +``` +``` +[05/02/24 13:50:03] INFO ToolkitTask 4e9458375bda4fbcadb77a94624ed64c + Input: what is modular RAG? +[05/02/24 13:50:10] INFO Subtask 5ef2d72028fc495aa7faf6f46825b004 + Thought: To answer this question, I need to run a search for the term "modular RAG". I will use the StructureRunClient action to execute a + search structure. + Actions: [ + { + "name": "StructureRunClient", + "path": "run_structure", + "input": { + "values": { + "args": "modular RAG" + } + }, + "tag": "search_modular_RAG" + } + ] +[05/02/24 13:50:36] INFO Subtask 5ef2d72028fc495aa7faf6f46825b004 + Response: {'id': '87fa21aded76416e988f8bf39c19760b', 'name': '87fa21aded76416e988f8bf39c19760b', 'type': 'TextArtifact', 'value': 'Modular + Retrieval-Augmented Generation (RAG) is an advanced approach that goes beyond the traditional RAG paradigms, offering enhanced adaptability + and versatility. It involves incorporating diverse strategies to improve its components by adding specialized modules for retrieval and + processing capabilities. The Modular RAG framework allows for module substitution or reconfiguration to address specific challenges, expanding + flexibility by integrating new modules or adjusting interaction flow among existing ones. This approach supports both sequential processing + and integrated end-to-end training across its components, illustrating progression and refinement within the RAG family.'} +[05/02/24 13:50:44] INFO ToolkitTask 4e9458375bda4fbcadb77a94624ed64c + Output: Modular Retrieval-Augmented Generation (RAG) is an advanced approach that goes beyond the traditional RAG paradigms, offering enhanced + adaptability and versatility. It involves incorporating diverse strategies to improve its components by adding specialized modules for + retrieval and processing capabilities. The Modular RAG framework allows for module substitution or reconfiguration to address specific + challenges, expanding flexibility by integrating new modules or adjusting interaction flow among existing ones. This approach supports both + sequential processing and integrated end-to-end training across its components, illustrating progression and refinement within the RAG family. +``` diff --git a/griptape/drivers/__init__.py b/griptape/drivers/__init__.py index c727c154d..999cacfb5 100644 --- a/griptape/drivers/__init__.py +++ b/griptape/drivers/__init__.py @@ -96,6 +96,10 @@ from .file_manager.local_file_manager_driver import LocalFileManagerDriver from .file_manager.amazon_s3_file_manager_driver import AmazonS3FileManagerDriver +from .structure_run.base_structure_run_driver import BaseStructureRunDriver +from .structure_run.griptape_cloud_structure_run_driver import GriptapeCloudStructureRunDriver +from .structure_run.local_structure_run_driver import LocalStructureRunDriver + __all__ = [ "BasePromptDriver", "OpenAiChatPromptDriver", @@ -179,4 +183,7 @@ "BaseFileManagerDriver", "LocalFileManagerDriver", "AmazonS3FileManagerDriver", + "BaseStructureRunDriver", + "GriptapeCloudStructureRunDriver", + "LocalStructureRunDriver", ] diff --git a/griptape/tools/griptape_cloud_structure_run_client/__init__.py b/griptape/drivers/structure_run/__init__.py similarity index 100% rename from griptape/tools/griptape_cloud_structure_run_client/__init__.py rename to griptape/drivers/structure_run/__init__.py diff --git a/griptape/drivers/structure_run/base_structure_run_driver.py b/griptape/drivers/structure_run/base_structure_run_driver.py new file mode 100644 index 000000000..8e10fb231 --- /dev/null +++ b/griptape/drivers/structure_run/base_structure_run_driver.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +from attrs import define + +from griptape.artifacts import BaseArtifact + + +@define +class BaseStructureRunDriver(ABC): + def run(self, *args: BaseArtifact) -> BaseArtifact: + return self.try_run(*args) + + @abstractmethod + def try_run(self, *args: BaseArtifact) -> BaseArtifact: + ... diff --git a/griptape/tools/griptape_cloud_structure_run_client/tool.py b/griptape/drivers/structure_run/griptape_cloud_structure_run_driver.py similarity index 50% rename from griptape/tools/griptape_cloud_structure_run_client/tool.py rename to griptape/drivers/structure_run/griptape_cloud_structure_run_driver.py index 7d4edbd72..9ed036995 100644 --- a/griptape/tools/griptape_cloud_structure_run_client/tool.py +++ b/griptape/drivers/structure_run/griptape_cloud_structure_run_driver.py @@ -1,66 +1,41 @@ from __future__ import annotations + import time -from typing import Any, Optional +from typing import Any 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 InfoArtifact, TextArtifact, ErrorArtifact + +from attrs import Factory, define, field + +from griptape.artifacts import BaseArtifact, ErrorArtifact, InfoArtifact, TextArtifact +from griptape.drivers.structure_run.base_structure_run_driver import BaseStructureRunDriver @define -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) +class GriptapeCloudStructureRunDriver(BaseStructureRunDriver): + 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 + ) 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) + async_run: bool = field(default=False, 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"]}') + def try_run(self, *args: BaseArtifact) -> BaseArtifact: + from requests import HTTPError, Response, exceptions, post - 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 }}", - "schema": Schema( - {Literal("args", description="A list of string arguments to submit to the Structure Run"): list} - ), - } - ) - def execute_structure_run(self, params: dict) -> InfoArtifact | 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: Response = post(url, json={"args": args}, headers=self.headers) + response: Response = post(url, json={"args": [arg.value for arg in args]}, headers=self.headers) response.raise_for_status() response_json = response.json() - return self._get_structure_run_result(response_json["structure_run_id"]) + if self.async_run: + return InfoArtifact("Run started successfully") + else: + return self._get_structure_run_result(response_json["structure_run_id"]) except (exceptions.RequestException, HTTPError) as err: return ErrorArtifact(str(err)) @@ -87,7 +62,7 @@ def _get_structure_run_result(self, structure_run_id: str) -> InfoArtifact | Tex return ErrorArtifact(result) if "output" in result: - return TextArtifact(result["output"]) + return TextArtifact.from_dict(result["output"]) else: return InfoArtifact("No output found in response") @@ -96,4 +71,5 @@ 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() diff --git a/griptape/drivers/structure_run/local_structure_run_driver.py b/griptape/drivers/structure_run/local_structure_run_driver.py new file mode 100644 index 000000000..255f91445 --- /dev/null +++ b/griptape/drivers/structure_run/local_structure_run_driver.py @@ -0,0 +1,23 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, Callable + +from attrs import define, field + +from griptape.artifacts import BaseArtifact, InfoArtifact +from griptape.drivers.structure_run.base_structure_run_driver import BaseStructureRunDriver + +if TYPE_CHECKING: + from griptape.structures import Structure + + +@define +class LocalStructureRunDriver(BaseStructureRunDriver): + structure_factory_fn: Callable[[], Structure] = field(kw_only=True) + + def try_run(self, *args: BaseArtifact) -> BaseArtifact: + structure_factory_fn = self.structure_factory_fn().run(*[arg.value for arg in args]) + + if structure_factory_fn.output_task.output is not None: + return structure_factory_fn.output_task.output + else: + return InfoArtifact("No output found in response") diff --git a/griptape/tasks/__init__.py b/griptape/tasks/__init__.py index 7751a0cc9..5d8dac58e 100644 --- a/griptape/tasks/__init__.py +++ b/griptape/tasks/__init__.py @@ -1,5 +1,6 @@ from .base_task import BaseTask from .base_text_input_task import BaseTextInputTask +from .base_multi_text_input_task import BaseMultiTextInputTask from .prompt_task import PromptTask from .actions_subtask import ActionsSubtask from .toolkit_task import ToolkitTask @@ -16,10 +17,12 @@ from .outpainting_image_generation_task import OutpaintingImageGenerationTask from .variation_image_generation_task import VariationImageGenerationTask from .image_query_task import ImageQueryTask +from .structure_run_task import StructureRunTask __all__ = [ "BaseTask", "BaseTextInputTask", + "BaseMultiTextInputTask", "PromptTask", "ActionsSubtask", "ToolkitTask", @@ -36,4 +39,5 @@ "InpaintingImageGenerationTask", "OutpaintingImageGenerationTask", "ImageQueryTask", + "StructureRunTask", ] diff --git a/griptape/tasks/base_multi_text_input_task.py b/griptape/tasks/base_multi_text_input_task.py new file mode 100644 index 000000000..eb00af6ca --- /dev/null +++ b/griptape/tasks/base_multi_text_input_task.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from abc import ABC +from typing import Callable + +from attr import define, field, Factory + +from griptape.artifacts import TextArtifact +from griptape.mixins.rule_mixin import RuleMixin +from griptape.tasks import BaseTask +from griptape.utils import J2 + + +@define +class BaseMultiTextInputTask(RuleMixin, BaseTask, ABC): + DEFAULT_INPUT_TEMPLATE = "{{ args[0] }}" + + _input: tuple[str, ...] | tuple[TextArtifact, ...] | tuple[Callable[[BaseTask], TextArtifact], ...] = field( + default=Factory(lambda self: (self.DEFAULT_INPUT_TEMPLATE,), takes_self=True), alias="input" + ) + + @property + def input(self) -> tuple[TextArtifact, ...]: + if all(isinstance(elem, TextArtifact) for elem in self._input): + return self._input # pyright: ignore + elif all(isinstance(elem, Callable) for elem in self._input): + return tuple([elem(self) for elem in self._input]) # pyright: ignore + elif isinstance(self._input, tuple): + return tuple( + [ + TextArtifact(J2().render_from_string(input_template, **self.full_context)) # pyright: ignore + for input_template in self._input + ] + ) + else: + return tuple([TextArtifact(J2().render_from_string(self._input, **self.full_context))]) + + @input.setter + def input( + self, value: tuple[str, ...] | tuple[TextArtifact, ...] | tuple[Callable[[BaseTask], TextArtifact], ...] + ) -> None: + self._input = value + + def before_run(self) -> None: + super().before_run() + + joined_input = "\n".join([input.to_text() for input in self.input]) + self.structure.logger.info(f"{self.__class__.__name__} {self.id}\nInput: {joined_input}") + + def after_run(self) -> None: + super().after_run() + + self.structure.logger.info(f"{self.__class__.__name__} {self.id}\nOutput: {self.output.to_text()}") diff --git a/griptape/tasks/structure_run_task.py b/griptape/tasks/structure_run_task.py new file mode 100644 index 000000000..b7a8f9ea9 --- /dev/null +++ b/griptape/tasks/structure_run_task.py @@ -0,0 +1,22 @@ +from __future__ import annotations + + +from attr import define, field + +from griptape.artifacts import BaseArtifact +from griptape.drivers.structure_run.base_structure_run_driver import BaseStructureRunDriver +from griptape.tasks import BaseMultiTextInputTask + + +@define +class StructureRunTask(BaseMultiTextInputTask): + """Task to run a Structure. + + Attributes: + driver: Driver to run the Structure. + """ + + driver: BaseStructureRunDriver = field(kw_only=True) + + def run(self) -> BaseArtifact: + return self.driver.run(*self.input) diff --git a/griptape/tools/__init__.py b/griptape/tools/__init__.py index 3e4dbb5bc..0c9b6e01d 100644 --- a/griptape/tools/__init__.py +++ b/griptape/tools/__init__.py @@ -24,7 +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 .structure_run_client.tool import StructureRunClient from .image_query_client.tool import ImageQueryClient __all__ = [ @@ -54,6 +54,6 @@ "InpaintingImageGenerationClient", "OutpaintingImageGenerationClient", "GriptapeCloudKnowledgeBaseClient", - "GriptapeCloudStructureRunClient", + "StructureRunClient", "ImageQueryClient", ] diff --git a/griptape/tools/griptape_cloud_structure_run_client/manifest.yml b/griptape/tools/griptape_cloud_structure_run_client/manifest.yml deleted file mode 100644 index 5741fa336..000000000 --- a/griptape/tools/griptape_cloud_structure_run_client/manifest.yml +++ /dev/null @@ -1,5 +0,0 @@ -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/structure_run_client/__init__.py b/griptape/tools/structure_run_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/griptape/tools/structure_run_client/manifest.yml b/griptape/tools/structure_run_client/manifest.yml new file mode 100644 index 000000000..5f53158d8 --- /dev/null +++ b/griptape/tools/structure_run_client/manifest.yml @@ -0,0 +1,5 @@ +version: "v1" +name: Structure Run Client +description: Tool for running a Structure. +contact_email: hello@griptape.ai +legal_info_url: https://www.griptape.ai/legal diff --git a/griptape/tools/structure_run_client/tool.py b/griptape/tools/structure_run_client/tool.py new file mode 100644 index 000000000..c62b53e97 --- /dev/null +++ b/griptape/tools/structure_run_client/tool.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from attr import define, field +from schema import Literal, Schema + +from griptape.artifacts import BaseArtifact, TextArtifact +from griptape.drivers import BaseStructureRunDriver +from griptape.tools.base_tool import BaseTool +from griptape.utils.decorators import activity + + +@define +class StructureRunClient(BaseTool): + """ + Attributes: + description: A description of what the Structure does. + driver: Driver to run the Structure. + """ + + description: str = field(kw_only=True) + driver: BaseStructureRunDriver = field(kw_only=True) + + @activity( + config={ + "description": "Can be used to run a Griptape 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 run_structure(self, params: dict) -> BaseArtifact: + args: list[str] = params["values"]["args"] + + return self.driver.run(*[TextArtifact(arg) for arg in args]) diff --git a/mkdocs.yml b/mkdocs.yml index bfedcaac4..f5f0dc954 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -129,7 +129,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" + - StructureRunClient: "griptape-tools/official-tools/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" @@ -150,6 +150,7 @@ nav: - Talk to Redshift: "examples/talk-to-redshift.md" - Talk to a Webpage: "examples/talk-to-a-webpage.md" - Talk to a PDF: "examples/talk-to-a-pdf.md" + - Multi Agent Workflows: "examples/multi-agent-workflow.md" - Shared Memory Between Agents: "examples/multiple-agent-shared-memory.md" - Chat Sessions with Amazon DynamoDB: "examples/amazon-dynamodb-sessions.md" - Data: diff --git a/tests/mocks/mock_multi_text_input_task.py b/tests/mocks/mock_multi_text_input_task.py new file mode 100644 index 000000000..1da645ee4 --- /dev/null +++ b/tests/mocks/mock_multi_text_input_task.py @@ -0,0 +1,9 @@ +from attr import define +from griptape.artifacts import TextArtifact +from griptape.tasks import BaseMultiTextInputTask + + +@define +class MockMultiTextInputTask(BaseMultiTextInputTask): + def run(self) -> TextArtifact: + return TextArtifact(self.input[0].to_text()) diff --git a/tests/unit/drivers/structure_run/__init__.py b/tests/unit/drivers/structure_run/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/drivers/structure_run/test_griptape_cloud_structure_run_driver.py b/tests/unit/drivers/structure_run/test_griptape_cloud_structure_run_driver.py new file mode 100644 index 000000000..8318553b1 --- /dev/null +++ b/tests/unit/drivers/structure_run/test_griptape_cloud_structure_run_driver.py @@ -0,0 +1,35 @@ +import pytest +from griptape.artifacts import TextArtifact, InfoArtifact + + +class TestGriptapeCloudStructureRunDriver: + @pytest.fixture + def driver(self, mocker): + from griptape.drivers import GriptapeCloudStructureRunDriver + + mock_response = mocker.Mock() + mock_response.json.return_value = {"structure_run_id": 1} + mocker.patch("requests.post", return_value=mock_response) + + mock_response = mocker.Mock() + mock_response.json.return_value = { + "description": "fizz buzz", + "output": TextArtifact("foo bar").to_dict(), + "status": "SUCCEEDED", + } + mocker.patch("requests.get", return_value=mock_response) + + return GriptapeCloudStructureRunDriver( + base_url="https://cloud-foo.griptape.ai", api_key="foo bar", structure_id="1" + ) + + def test_run(self, driver): + result = driver.run(TextArtifact("foo bar")) + assert isinstance(result, TextArtifact) + assert result.value == "foo bar" + + def test_async_run(self, driver): + driver.async_run = True + result = driver.run(TextArtifact("foo bar")) + assert isinstance(result, InfoArtifact) + assert result.value == "Run started successfully" diff --git a/tests/unit/drivers/structure_run/test_local_structure_run_driver.py b/tests/unit/drivers/structure_run/test_local_structure_run_driver.py new file mode 100644 index 000000000..04da4f2cf --- /dev/null +++ b/tests/unit/drivers/structure_run/test_local_structure_run_driver.py @@ -0,0 +1,24 @@ +import pytest +from griptape.tasks import StructureRunTask +from griptape.structures import Agent +from tests.mocks.mock_prompt_driver import MockPromptDriver +from griptape.drivers import LocalStructureRunDriver +from griptape.structures import Pipeline + + +class TestLocalStructureRunDriver: + @pytest.fixture + def driver(self): + agent = Agent(prompt_driver=MockPromptDriver(mock_output="agent mock output")) + driver = LocalStructureRunDriver(structure_factory_fn=lambda: agent) + + return driver + + def test_run(self, driver): + pipeline = Pipeline(prompt_driver=MockPromptDriver(mock_output="pipeline mock output")) + + task = StructureRunTask(driver=driver) + + pipeline.add_task(task) + + assert task.run().to_text() == "agent mock output" diff --git a/tests/unit/tasks/test_base_multi_text_input_task.py b/tests/unit/tasks/test_base_multi_text_input_task.py new file mode 100644 index 000000000..542162757 --- /dev/null +++ b/tests/unit/tasks/test_base_multi_text_input_task.py @@ -0,0 +1,58 @@ +from tests.mocks.mock_prompt_driver import MockPromptDriver +from griptape.structures import Pipeline +from griptape.artifacts import TextArtifact +from griptape.rules import Ruleset, Rule +from tests.mocks.mock_multi_text_input_task import MockMultiTextInputTask + + +class TestBaseMultiTextInputTask: + def test_string_input(self): + assert MockMultiTextInputTask(("foobar", "bazbar")).input[0].value == "foobar" + assert MockMultiTextInputTask(("foobar", "bazbar")).input[1].value == "bazbar" + + task = MockMultiTextInputTask() + task.input = ("foobar", "bazbar") + assert task.input[0].value == "foobar" + assert task.input[1].value == "bazbar" + + def test_artifact_input(self): + assert MockMultiTextInputTask((TextArtifact("foobar"), TextArtifact("bazbar"))).input[0].value == "foobar" + assert MockMultiTextInputTask((TextArtifact("foobar"), TextArtifact("bazbar"))).input[1].value == "bazbar" + + task = MockMultiTextInputTask() + task.input = (TextArtifact("foobar"), TextArtifact("bazbar")) + assert task.input[0].value == "foobar" + assert task.input[1].value == "bazbar" + + def test_callable_input(self): + assert ( + MockMultiTextInputTask((lambda _: TextArtifact("foobar"), lambda _: TextArtifact("bazbar"))).input[0].value + == "foobar" + ) + assert ( + MockMultiTextInputTask((lambda _: TextArtifact("foobar"), lambda _: TextArtifact("bazbar"))).input[1].value + == "bazbar" + ) + + task = MockMultiTextInputTask() + task.input = (lambda _: TextArtifact("foobar"), lambda _: TextArtifact("bazbar")) + assert task.input[0].value == "foobar" + assert task.input[1].value == "bazbar" + + def test_full_context(self): + parent = MockMultiTextInputTask(("parent1", "parent2")) + subtask = MockMultiTextInputTask(("test1", "test2"), context={"foo": "bar"}) + child = MockMultiTextInputTask(("child2", "child2")) + pipeline = Pipeline(prompt_driver=MockPromptDriver()) + + pipeline.add_tasks(parent, subtask, child) + + pipeline.run() + + context = subtask.full_context + + assert context["foo"] == "bar" + assert context["parent_output"] == parent.output.to_text() + assert context["structure"] == pipeline + assert context["parent"] == parent + assert context["child"] == child diff --git a/tests/unit/tasks/test_structure_run_task.py b/tests/unit/tasks/test_structure_run_task.py new file mode 100644 index 000000000..d89e98c91 --- /dev/null +++ b/tests/unit/tasks/test_structure_run_task.py @@ -0,0 +1,18 @@ +from griptape.tasks import StructureRunTask +from griptape.structures import Agent +from tests.mocks.mock_prompt_driver import MockPromptDriver +from griptape.drivers import LocalStructureRunDriver +from griptape.structures import Pipeline + + +class TestStructureRunTask: + def test_run(self): + agent = Agent(prompt_driver=MockPromptDriver(mock_output="agent mock output")) + pipeline = Pipeline(prompt_driver=MockPromptDriver(mock_output="pipeline mock output")) + driver = LocalStructureRunDriver(structure_factory_fn=lambda: agent) + + task = StructureRunTask(driver=driver) + + pipeline.add_task(task) + + assert task.run().to_text() == "agent mock output" diff --git a/tests/unit/tools/test_griptape_cloud_structure_run_client.py b/tests/unit/tools/test_griptape_cloud_structure_run_client.py deleted file mode 100644 index 8656a8f77..000000000 --- a/tests/unit/tools/test_griptape_cloud_structure_run_client.py +++ /dev/null @@ -1,27 +0,0 @@ -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.json.return_value = {"structure_run_id": 1} - 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_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.description == "fizz buzz" - - client.description = "foo bar" - assert client.description == "foo bar" diff --git a/tests/unit/tools/test_structure_run_client.py b/tests/unit/tools/test_structure_run_client.py new file mode 100644 index 000000000..b57bfb28f --- /dev/null +++ b/tests/unit/tools/test_structure_run_client.py @@ -0,0 +1,20 @@ +import pytest +from griptape.drivers.structure_run.local_structure_run_driver import LocalStructureRunDriver +from griptape.tools import StructureRunClient +from griptape.structures import Agent +from tests.mocks.mock_prompt_driver import MockPromptDriver + + +class TestStructureRunClient: + @pytest.fixture + def client(self): + driver = MockPromptDriver() + agent = Agent(prompt_driver=driver) + + return StructureRunClient( + description="foo bar", driver=LocalStructureRunDriver(structure_factory_fn=lambda: agent) + ) + + def test_run_structure(self, client): + assert client.run_structure({"values": {"args": "foo bar"}}).value == "mock output" + assert client.description == "foo bar"