diff --git a/CHANGELOG.md b/CHANGELOG.md index 601ed5598..0a96b7a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [1.0.2] - 2024-12-16 + +### Fixed + +- Exception when calling `Structure.to_json()` after it has run. +- `Agent` unintentionally modifying `stream` for all Prompt Drivers. +- `StructureVisualizer.base_url` for setting the base URL on the url generated by `StructureVisualizer.to_url()`. +- `StructureVisualizer.query_params` for setting query parameters on the url generated by `StructureVisualizer.to_url()`. +- Parsing `ActionCallDeltaMessageContent`s with empty string `partial_input`s. + +## [1.0.1] - 2024-12-11 + +### Fixed +- Prompt Driver image input doc example not rendering ## [1.0.0] - 2024-12-09 diff --git a/docs/griptape-framework/drivers/prompt-drivers.md b/docs/griptape-framework/drivers/prompt-drivers.md index 9df4aae0a..6c51d2d01 100644 --- a/docs/griptape-framework/drivers/prompt-drivers.md +++ b/docs/griptape-framework/drivers/prompt-drivers.md @@ -22,7 +22,7 @@ Or use them independently: You can pass images to the Driver if the model supports it: ```python ---8<-- "docs/griptape-framework/drivers/src/prompt_driver_images.py" +--8<-- "docs/griptape-framework/drivers/src/prompt_drivers_images.py" ``` ## Prompt Drivers diff --git a/docs/griptape-tools/official-tools/src/file_manager_tool_1.py b/docs/griptape-tools/official-tools/src/file_manager_tool_1.py index 0b5596d2b..61552dbaa 100644 --- a/docs/griptape-tools/official-tools/src/file_manager_tool_1.py +++ b/docs/griptape-tools/official-tools/src/file_manager_tool_1.py @@ -13,7 +13,7 @@ filename = "sample1.txt" content = "This is the content of sample1.txt" -Path(filename).write_text(filename) +Path(filename).write_text(content) # Now, read content from the file 'sample1.txt' using the agent's command agent.run("Can you get me the sample1.txt file?") diff --git a/griptape/common/prompt_stack/contents/action_call_message_content.py b/griptape/common/prompt_stack/contents/action_call_message_content.py index 94cc1cd14..208d9a638 100644 --- a/griptape/common/prompt_stack/contents/action_call_message_content.py +++ b/griptape/common/prompt_stack/contents/action_call_message_content.py @@ -37,7 +37,7 @@ def from_deltas(cls, deltas: Sequence[BaseDeltaMessageContent]) -> ActionCallMes if tag is not None and name is not None and path is not None: try: - parsed_input = json.loads(json_input) + parsed_input = json.loads(json_input or "{}") except json.JSONDecodeError as exc: raise ValueError("Invalid JSON input for ToolAction") from exc action = ToolAction(tag=tag, name=name, path=path, input=parsed_input) diff --git a/griptape/structures/agent.py b/griptape/structures/agent.py index be1b73e34..128c02faa 100644 --- a/griptape/structures/agent.py +++ b/griptape/structures/agent.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Callable, Optional, Union -from attrs import Attribute, Factory, define, field +from attrs import Attribute, Factory, define, evolve, field from griptape.artifacts.text_artifact import TextArtifact from griptape.common import observable @@ -24,7 +24,10 @@ class Agent(Structure): ) stream: bool = field(default=Factory(lambda: Defaults.drivers_config.prompt_driver.stream), kw_only=True) prompt_driver: BasePromptDriver = field( - default=Factory(lambda: Defaults.drivers_config.prompt_driver), kw_only=True + default=Factory( + lambda self: evolve(Defaults.drivers_config.prompt_driver, stream=self.stream), takes_self=True + ), + kw_only=True, ) tools: list[BaseTool] = field(factory=list, kw_only=True) max_meta_memory_entries: Optional[int] = field(default=20, kw_only=True) diff --git a/griptape/tasks/base_task.py b/griptape/tasks/base_task.py index 29aaf33d5..e00307adc 100644 --- a/griptape/tasks/base_task.py +++ b/griptape/tasks/base_task.py @@ -3,6 +3,7 @@ import logging import uuid from abc import ABC, abstractmethod +from copy import deepcopy from enum import Enum from typing import TYPE_CHECKING, Any, Optional @@ -194,7 +195,8 @@ def try_run(self) -> BaseArtifact: ... @property def full_context(self) -> dict[str, Any]: - context = self.context + # Need to deep copy so that the serialized context doesn't contain non-serializable data + context = deepcopy(self.context) if self.structure is not None: context.update(self.structure.context(self)) diff --git a/pyproject.toml b/pyproject.toml index 580d8a455..541daf801 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "griptape" -version = "1.0.0" +version = "1.0.2" description = "Modular Python framework for LLM workflows, tools, memory, and data." authors = ["Griptape "] license = "Apache 2.0" diff --git a/tests/unit/common/contents/test_action_call_message_content.py b/tests/unit/common/contents/test_action_call_message_content.py index d6c3f438f..ddd68a645 100644 --- a/tests/unit/common/contents/test_action_call_message_content.py +++ b/tests/unit/common/contents/test_action_call_message_content.py @@ -21,6 +21,7 @@ def test_from_deltas(self): ActionCallDeltaMessageContent(tag="testtag"), ActionCallDeltaMessageContent(name="TestName"), ActionCallDeltaMessageContent(path="test_tag"), + ActionCallDeltaMessageContent(partial_input=""), ActionCallDeltaMessageContent(partial_input='{"foo":'), ActionCallDeltaMessageContent(partial_input='"bar"}'), ] diff --git a/tests/unit/structures/test_agent.py b/tests/unit/structures/test_agent.py index b18d74e09..387910f40 100644 --- a/tests/unit/structures/test_agent.py +++ b/tests/unit/structures/test_agent.py @@ -255,50 +255,6 @@ def test_task_outputs(self): assert len(agent.task_outputs) == 1 assert agent.task_outputs[task.id] == task.output - def test_to_dict(self): - task = PromptTask("test prompt") - agent = Agent(prompt_driver=MockPromptDriver()) - agent.add_task(task) - expected_agent_dict = { - "type": "Agent", - "id": agent.id, - "tasks": [ - { - "type": agent.tasks[0].type, - "id": agent.tasks[0].id, - "state": str(agent.tasks[0].state), - "parent_ids": agent.tasks[0].parent_ids, - "child_ids": agent.tasks[0].child_ids, - "max_meta_memory_entries": agent.tasks[0].max_meta_memory_entries, - "context": agent.tasks[0].context, - } - ], - "conversation_memory": { - "type": agent.conversation_memory.type, - "runs": agent.conversation_memory.runs, - "meta": agent.conversation_memory.meta, - "max_runs": agent.conversation_memory.max_runs, - }, - } - assert agent.to_dict() == expected_agent_dict - - def test_from_dict(self): - task = PromptTask("test prompt") - agent = Agent(prompt_driver=MockPromptDriver()) - agent.add_task(task) - - serialized_agent = agent.to_dict() - assert isinstance(serialized_agent, dict) - - deserialized_agent = Agent.from_dict(serialized_agent) - assert isinstance(deserialized_agent, Agent) - - assert deserialized_agent.task_outputs[task.id] is None - deserialized_agent.run() - - assert len(deserialized_agent.task_outputs) == 1 - assert deserialized_agent.task_outputs[task.id].value == "mock output" - def test_runnable_mixin(self): mock_on_before_run = Mock() mock_after_run = Mock() @@ -320,3 +276,11 @@ def test_is_running(self): task.state = BaseTask.State.RUNNING assert agent.is_running() + + def test_stream_mutation(self): + prompt_driver = MockPromptDriver() + agent = Agent(prompt_driver=MockPromptDriver(), stream=True) + + assert isinstance(agent.tasks[0], PromptTask) + assert agent.tasks[0].prompt_driver.stream is True + assert agent.tasks[0].prompt_driver is not prompt_driver diff --git a/tests/unit/structures/test_structure.py b/tests/unit/structures/test_structure.py index 4c5aa47fc..0458b60bf 100644 --- a/tests/unit/structures/test_structure.py +++ b/tests/unit/structures/test_structure.py @@ -1,6 +1,8 @@ import pytest from griptape.structures import Agent, Pipeline +from griptape.tasks import PromptTask +from tests.mocks.mock_prompt_driver import MockPromptDriver class TestStructure: @@ -16,3 +18,65 @@ def test_output(self): ValueError, match="Structure's output Task has no output. Run the Structure to generate output." ): assert agent.output + + def test_to_dict(self): + task = PromptTask("test prompt") + agent = Agent(prompt_driver=MockPromptDriver()) + agent.add_task(task) + expected_agent_dict = { + "type": "Agent", + "id": agent.id, + "tasks": [ + { + "type": agent.tasks[0].type, + "id": agent.tasks[0].id, + "state": str(agent.tasks[0].state), + "parent_ids": agent.tasks[0].parent_ids, + "child_ids": agent.tasks[0].child_ids, + "max_meta_memory_entries": agent.tasks[0].max_meta_memory_entries, + "context": agent.tasks[0].context, + } + ], + "conversation_memory": { + "type": agent.conversation_memory.type, + "runs": agent.conversation_memory.runs, + "meta": agent.conversation_memory.meta, + "max_runs": agent.conversation_memory.max_runs, + }, + "conversation_memory_strategy": agent.conversation_memory_strategy, + } + assert agent.to_dict() == expected_agent_dict + + agent.run() + + expected_agent_dict = { + **expected_agent_dict, + "tasks": [ + { + **expected_agent_dict["tasks"][0], + "state": str(agent.tasks[0].state), + } + ], + "conversation_memory": { + **expected_agent_dict["conversation_memory"], + "runs": agent.conversation_memory.to_dict()["runs"], + }, + } + assert agent.to_dict() == expected_agent_dict + + def test_from_dict(self): + task = PromptTask("test prompt") + agent = Agent(prompt_driver=MockPromptDriver()) + agent.add_task(task) + + serialized_agent = agent.to_dict() + assert isinstance(serialized_agent, dict) + + deserialized_agent = Agent.from_dict(serialized_agent) + assert isinstance(deserialized_agent, Agent) + + assert deserialized_agent.task_outputs[task.id] is None + deserialized_agent.run() + + assert len(deserialized_agent.task_outputs) == 1 + assert deserialized_agent.task_outputs[task.id].value == "mock output"