diff --git a/CHANGELOG.md b/CHANGELOG.md index 80fc8c5ef..a944a9b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Removed `azure-core` and `azure-storage-blob` dependencies. +- `GriptapeCloudFileManagerDriver` no longer requires `drivers-file-manager-griptape-cloud` extra. + +## \[0.34.0\] - 2024-10-29 + ### Added - `WebScraperTool.text_chunker` default value for `max_tokens`. @@ -702,6 +709,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Type hint for parameter `azure_ad_token_provider` on Azure OpenAI drivers to `Optional[Callable[[], str]]`. - Missing parameters `azure_ad_token` and `azure_ad_token_provider` on the default client for `AzureOpenAiCompletionPromptDriver`. +- Breaking change in `Chat.handle_output` behavior. ## \[0.24.2\] - 2024-04-04 diff --git a/MIGRATION.md b/MIGRATION.md index 89905cf16..5e2d51f9d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -295,83 +295,6 @@ pip install griptape[drivers-prompt-huggingface-hub] pip install torch ``` -### Removed `MediaArtifact` - -`MediaArtifact` has been removed. Use `ImageArtifact` or `AudioArtifact` instead. - -#### Before - -```python -image_media = MediaArtifact( - b"image_data", - media_type="image", - format="jpeg" -) - -audio_media = MediaArtifact( - b"audio_data", - media_type="audio", - format="wav" -) -``` - -#### After - -```python -image_artifact = ImageArtifact( - b"image_data", - format="jpeg" -) - -audio_artifact = AudioArtifact( - b"audio_data", - format="wav" -) -``` - -### `ImageArtifact.format` is now required - -`ImageArtifact.format` is now a required parameter. Update any code that does not provide a `format` parameter. - -#### Before - -```python -image_artifact = ImageArtifact( - b"image_data" -) -``` - -#### After - -```python -image_artifact = ImageArtifact( - b"image_data", - format="jpeg" -) -``` - -### Removed `CsvRowArtifact` - -`CsvRowArtifact` has been removed. Use `TextArtifact` instead. - -#### Before - -```python -artifact = CsvRowArtifact({"name": "John", "age": 30}) -print(artifact.value) # {"name": "John", "age": 30} -print(type(artifact.value)) # -``` - -#### After - -```python -artifact = TextArtifact("name: John\nage: 30") -print(artifact.value) # name: John\nage: 30 -print(type(artifact.value)) # -``` - -If you require storing a dictionary as an Artifact, you can use `GenericArtifact` instead. - ### `CsvLoader`, `DataframeLoader`, and `SqlLoader` return types `CsvLoader`, `DataframeLoader`, and `SqlLoader` now return a `list[TextArtifact]` instead of `list[CsvRowArtifact]`. @@ -405,35 +328,6 @@ print(dict_results[0]) # {"name": "John", "age": 30} print(type(dict_results[0])) # ``` -### Moved `ImageArtifact.prompt` and `ImageArtifact.model` to `ImageArtifact.meta` - -`ImageArtifact.prompt` and `ImageArtifact.model` have been moved to `ImageArtifact.meta`. - -#### Before - -```python -image_artifact = ImageArtifact( - b"image_data", - format="jpeg", - prompt="Generate an image of a cat", - model="DALL-E" -) - -print(image_artifact.prompt, image_artifact.model) # Generate an image of a cat, DALL-E -``` - -#### After - -```python -image_artifact = ImageArtifact( - b"image_data", - format="jpeg", - meta={"prompt": "Generate an image of a cat", "model": "DALL-E"} -) - -print(image_artifact.meta["prompt"], image_artifact.meta["model"]) # Generate an image of a cat, DALL-E -``` - Renamed `GriptapeCloudKnowledgeBaseVectorStoreDriver` to `GriptapeCloudVectorStoreDriver`. #### Before @@ -474,85 +368,6 @@ driver = OpenAiChatPromptDriver( ## 0.31.X to 0.32.X -### Removed `DataframeLoader` - -`DataframeLoader` has been removed. Use `CsvLoader.parse` or build `TextArtifact`s from the dataframe instead. - -#### Before - -```python -DataframeLoader().load(df) -``` - -#### After - -```python -# Convert the dataframe to csv bytes and parse it -CsvLoader().parse(bytes(df.to_csv(line_terminator='\r\n', index=False), encoding='utf-8')) -# Or build TextArtifacts from the dataframe -[TextArtifact(row) for row in source.to_dict(orient="records")] -``` - -### `TextLoader`, `PdfLoader`, `ImageLoader`, and `AudioLoader` now take a `str | PathLike` instead of `bytes`. - -#### Before - -```python -PdfLoader().load(Path("attention.pdf").read_bytes()) -PdfLoader().load_collection([Path("attention.pdf").read_bytes(), Path("CoT.pdf").read_bytes()]) -``` - -#### After - -```python -PdfLoader().load("attention.pdf") -PdfLoader().load_collection([Path("attention.pdf"), "CoT.pdf"]) -``` - -### Removed `fileutils.load_file` and `fileutils.load_files` - -`griptape.utils.file_utils.load_file` and `griptape.utils.file_utils.load_files` have been removed. -You can now pass the file path directly to the Loader. - -#### Before - -```python -PdfLoader().load(load_file("attention.pdf").read_bytes()) -PdfLoader().load_collection(list(load_files(["attention.pdf", "CoT.pdf"]).values())) -``` - -```python -PdfLoader().load("attention.pdf") -PdfLoader().load_collection(["attention.pdf", "CoT.pdf"]) -``` - -### Loaders no longer chunk data - -Loaders no longer chunk the data after loading it. If you need to chunk the data, use a [Chunker](https://docs.griptape.ai/stable/griptape-framework/data/chunkers/) after loading the data. - -#### Before - -```python -chunks = PdfLoader().load("attention.pdf") -vector_store.upsert_text_artifacts( - { - "griptape": chunks, - } -) -``` - -#### After - -```python -artifact = PdfLoader().load("attention.pdf") -chunks = Chunker().chunk(artifact) -vector_store.upsert_text_artifacts( - { - "griptape": chunks, - } -) -``` - ### Removed `MediaArtifact` `MediaArtifact` has been removed. Use `ImageArtifact` or `AudioArtifact` instead. diff --git a/docs/griptape-framework/drivers/file-manager-drivers.md b/docs/griptape-framework/drivers/file-manager-drivers.md index 37012c29f..adb77ed57 100644 --- a/docs/griptape-framework/drivers/file-manager-drivers.md +++ b/docs/griptape-framework/drivers/file-manager-drivers.md @@ -19,9 +19,6 @@ Or use them independently as shown below for each driver: ### Griptape Cloud -!!! info - This driver requires the `drivers-file-manager-griptape-cloud` [extra](../index.md#extras). - The [GriptapeCloudFileManagerDriver](../../reference/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.md) allows you to load and save files sourced from Griptape Cloud Asset and Bucket resources. ```python diff --git a/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py b/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py index 5138a1fe4..7d917c124 100644 --- a/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py +++ b/griptape/drivers/file_manager/griptape_cloud_file_manager_driver.py @@ -2,20 +2,16 @@ import logging import os -from typing import TYPE_CHECKING, Optional +from typing import Optional from urllib.parse import urljoin import requests from attrs import Attribute, Factory, define, field from griptape.drivers import BaseFileManagerDriver -from griptape.utils import import_optional_dependency logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from azure.storage.blob import BlobClient - @define class GriptapeCloudFileManagerDriver(BaseFileManagerDriver): @@ -79,7 +75,6 @@ def try_list_files(self, path: str, postfix: str = "") -> list[str]: data = {"prefix": full_key} if postfix: data["postfix"] = postfix - # TODO: GTC SDK: Pagination list_assets_response = self._call_api( method="list", path=f"/buckets/{self.bucket_id}/assets", json=data, raise_for_status=False ).json() @@ -93,17 +88,15 @@ def try_load_file(self, path: str) -> bytes: raise IsADirectoryError try: - blob_client = self._get_blob_client(full_key=full_key) + sas_url, headers = self._get_asset_url(full_key) + response = requests.get(sas_url, headers=headers) + response.raise_for_status() + return response.content except requests.exceptions.HTTPError as e: if e.response.status_code == 404: raise FileNotFoundError from e raise e - try: - return blob_client.download_blob().readall() - except import_optional_dependency("azure.core.exceptions").ResourceNotFoundError as e: - raise FileNotFoundError from e - def try_save_file(self, path: str, value: bytes) -> str: full_key = self._to_full_key(path) @@ -114,23 +107,25 @@ def try_save_file(self, path: str, value: bytes) -> str: self._call_api(method="get", path=f"/buckets/{self.bucket_id}/assets/{full_key}", raise_for_status=True) except requests.exceptions.HTTPError as e: if e.response.status_code == 404: - logger.info("Asset '%s' not found, attempting to create", full_key) - data = {"name": full_key} - self._call_api(method="put", path=f"/buckets/{self.bucket_id}/assets", json=data, raise_for_status=True) + self._call_api( + method="put", + path=f"/buckets/{self.bucket_id}/assets", + json={"name": full_key}, + raise_for_status=True, + ) else: raise e + sas_url, headers = self._get_asset_url(full_key) + response = requests.put(sas_url, data=value, headers=headers) + response.raise_for_status() - blob_client = self._get_blob_client(full_key=full_key) - - blob_client.upload_blob(data=value, overwrite=True) return f"buckets/{self.bucket_id}/assets/{full_key}" - def _get_blob_client(self, full_key: str) -> BlobClient: + def _get_asset_url(self, full_key: str) -> tuple[str, dict]: url_response = self._call_api( method="post", path=f"/buckets/{self.bucket_id}/asset-urls/{full_key}", raise_for_status=True ).json() - sas_url = url_response["url"] - return import_optional_dependency("azure.storage.blob").BlobClient.from_blob_url(blob_url=sas_url) + return url_response["url"], url_response.get("headers", {}) def _get_url(self, path: str) -> str: path = path.lstrip("/") diff --git a/poetry.lock b/poetry.lock index b3d49dc6f..b08aaa05a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -265,45 +265,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] -[[package]] -name = "azure-core" -version = "1.31.0" -description = "Microsoft Azure Core Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure_core-1.31.0-py3-none-any.whl", hash = "sha256:22954de3777e0250029360ef31d80448ef1be13b80a459bff80ba7073379e2cd"}, - {file = "azure_core-1.31.0.tar.gz", hash = "sha256:656a0dd61e1869b1506b7c6a3b31d62f15984b1a573d6326f6aa2f3e4123284b"}, -] - -[package.dependencies] -requests = ">=2.21.0" -six = ">=1.11.0" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["aiohttp (>=3.0)"] - -[[package]] -name = "azure-storage-blob" -version = "12.23.1" -description = "Microsoft Azure Blob Storage Client Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure_storage_blob-12.23.1-py3-none-any.whl", hash = "sha256:1c2238aa841d1545f42714a5017c010366137a44a0605da2d45f770174bfc6b4"}, - {file = "azure_storage_blob-12.23.1.tar.gz", hash = "sha256:a587e54d4e39d2a27bd75109db164ffa2058fe194061e5446c5a89bca918272f"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["azure-core[aio] (>=1.30.0)"] - [[package]] name = "babel" version = "2.16.0" @@ -2339,17 +2300,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - [[package]] name = "jaraco-classes" version = "3.4.0" @@ -7192,7 +7142,7 @@ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linke test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] -all = ["anthropic", "astrapy", "azure-core", "azure-storage-blob", "beautifulsoup4", "boto3", "cohere", "diffusers", "duckduckgo-search", "elevenlabs", "exa-py", "google-generativeai", "mail-parser", "markdownify", "marqo", "ollama", "opensearch-py", "opentelemetry-api", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation", "opentelemetry-instrumentation-threading", "opentelemetry-sdk", "pandas", "pgvector", "pillow", "pinecone-client", "playwright", "psycopg2-binary", "pusher", "pymongo", "pypdf", "qdrant-client", "redis", "snowflake-sqlalchemy", "sqlalchemy", "tavily-python", "trafilatura", "transformers", "voyageai"] +all = ["anthropic", "astrapy", "beautifulsoup4", "boto3", "cohere", "diffusers", "duckduckgo-search", "elevenlabs", "exa-py", "google-generativeai", "mail-parser", "markdownify", "marqo", "ollama", "opensearch-py", "opentelemetry-api", "opentelemetry-exporter-otlp-proto-http", "opentelemetry-instrumentation", "opentelemetry-instrumentation-threading", "opentelemetry-sdk", "pandas", "pgvector", "pillow", "pinecone-client", "playwright", "psycopg2-binary", "pusher", "pymongo", "pypdf", "qdrant-client", "redis", "snowflake-sqlalchemy", "sqlalchemy", "tavily-python", "trafilatura", "transformers", "voyageai"] drivers-embedding-amazon-bedrock = ["boto3"] drivers-embedding-amazon-sagemaker = ["boto3"] drivers-embedding-cohere = ["cohere"] @@ -7204,7 +7154,6 @@ drivers-event-listener-amazon-iot = ["boto3"] drivers-event-listener-amazon-sqs = ["boto3"] drivers-event-listener-pusher = ["pusher"] drivers-file-manager-amazon-s3 = ["boto3"] -drivers-file-manager-griptape-cloud = ["azure-core", "azure-storage-blob"] drivers-image-generation-huggingface = ["diffusers", "pillow"] drivers-memory-conversation-amazon-dynamodb = ["boto3"] drivers-memory-conversation-redis = ["redis"] @@ -7246,4 +7195,4 @@ loaders-sql = ["sqlalchemy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "816a925736967c12b42ffddce1e48909348d11e7d341127d5e9e1224b44ba00e" +content-hash = "436fc99379ee14642f24a3e43f2ae2c99396839fd1ea986d6dc1a29a891e6865" diff --git a/pyproject.toml b/pyproject.toml index 7b7b5ce85..53e536a08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "griptape" -version = "0.34.0" +version = "0.34.1" description = "Modular Python framework for LLM workflows, tools, memory, and data." authors = ["Griptape "] license = "Apache 2.0" @@ -64,8 +64,6 @@ opentelemetry-exporter-otlp-proto-http = {version = "^1.25.0", optional = true} diffusers = {version = "^0.31.0", optional = true} tavily-python = {version = "^0.5.0", optional = true} exa-py = {version = "^1.1.4", optional = true} -azure-core = "^1.31.0" -azure-storage-blob = "^12.23.1" # loaders pandas = {version = "^1.3", optional = true} @@ -149,7 +147,6 @@ drivers-observability-datadog = [ drivers-image-generation-huggingface = ["diffusers", "pillow"] drivers-file-manager-amazon-s3 = ["boto3"] -drivers-file-manager-griptape-cloud = ["azure-core", "azure-storage-blob"] loaders-pdf = ["pypdf"] loaders-image = ["pillow"] diff --git a/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py b/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py index 0ce837dc1..4e9f9389a 100644 --- a/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py +++ b/tests/unit/drivers/file_manager/test_griptape_cloud_file_manager_driver.py @@ -2,7 +2,6 @@ import pytest import requests -from azure.core.exceptions import ResourceNotFoundError class TestGriptapeCloudFileManagerDriver: @@ -98,19 +97,18 @@ def test_try_list_files_not_directory(self, mocker, driver): driver.try_list_files("foo") def test_try_load_file(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"url": "https://foo.bar"} - mocker.patch("requests.request", return_value=mock_response) + mock_url_response = mocker.Mock() + mock_url_response.status_code = 200 + mock_url_response.json.return_value = {"url": "https://foo.bar"} + mocker.patch("requests.request", return_value=mock_url_response) - mock_bytes = b"bytes" - mock_blob_client = mocker.Mock() - mock_blob_client.download_blob.return_value.readall.return_value = mock_bytes - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) + mock_file_response = mocker.Mock() + mock_file_response.status_code = 200 + mock_file_response.content = b"bytes" + mocker.patch("requests.get", return_value=mock_file_response) response = driver.try_load_file("foo") - - assert response == mock_bytes + assert response == b"bytes" def test_try_load_file_directory(self, mocker, driver): mock_response = mocker.Mock() @@ -121,42 +119,29 @@ def test_try_load_file_directory(self, mocker, driver): with pytest.raises(IsADirectoryError): driver.try_load_file("foo/") - def test_try_load_file_sas_404(self, mocker, driver): + def test_try_load_file_asset_url_404(self, mocker, driver): mocker.patch("requests.request", side_effect=requests.exceptions.HTTPError(response=mock.Mock(status_code=404))) with pytest.raises(FileNotFoundError): driver.try_load_file("foo") - def test_try_load_file_sas_500(self, mocker, driver): + def test_try_load_file_asset_url_500(self, mocker, driver): mocker.patch("requests.request", side_effect=requests.exceptions.HTTPError(response=mock.Mock(status_code=500))) with pytest.raises(requests.exceptions.HTTPError): driver.try_load_file("foo") - def test_try_load_file_blob_404(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"url": "https://foo.bar"} - mocker.patch("requests.request", return_value=mock_response) - - mock_blob_client = mocker.Mock() - mock_blob_client.download_blob.side_effect = ResourceNotFoundError() - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) - - with pytest.raises(FileNotFoundError): - driver.try_load_file("foo") - - def test_try_save_files(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"url": "https://foo.bar"} - mocker.patch("requests.request", return_value=mock_response) + def test_try_save_file(self, mocker, driver): + mock_url_response = mocker.Mock() + mock_url_response.status_code = 200 + mock_url_response.json.return_value = {"url": "https://foo.bar"} + mocker.patch("requests.request", return_value=mock_url_response) - mock_blob_client = mocker.Mock() - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) + mock_put_response = mocker.Mock() + mock_put_response.status_code = 200 + mocker.patch("requests.put", return_value=mock_put_response) response = driver.try_save_file("foo", b"value") - assert response == "buckets/1/assets/foo" def test_try_save_file_directory(self, mocker, driver): @@ -168,24 +153,17 @@ def test_try_save_file_directory(self, mocker, driver): with pytest.raises(IsADirectoryError): driver.try_save_file("foo/", b"value") - def test_try_save_file_sas_404(self, mocker, driver): - mock_response = mocker.Mock() - mock_response.json.return_value = {"url": "https://foo.bar"} - mock_response.raise_for_status.side_effect = [ - requests.exceptions.HTTPError(response=mock.Mock(status_code=404)), - None, - None, - ] - mocker.patch("requests.request", return_value=mock_response) - - mock_blob_client = mocker.Mock() - mocker.patch("azure.storage.blob.BlobClient.from_blob_url", return_value=mock_blob_client) - - response = driver.try_save_file("foo", b"value") + def test_try_save_file_asset_url_404(self, mocker, driver): + mock_create_response = mocker.Mock() + mock_create_response.raise_for_status.side_effect = requests.exceptions.HTTPError( + response=mock.Mock(status_code=404) + ) + mocker.patch("requests.request", return_value=mock_create_response) - assert response == "buckets/1/assets/foo" + with pytest.raises(requests.exceptions.HTTPError): + driver.try_save_file("foo", b"value") - def test_try_save_file_sas_500(self, mocker, driver): + def test_try_save_file_asset_url_500(self, mocker, driver): mocker.patch("requests.request", side_effect=requests.exceptions.HTTPError(response=mock.Mock(status_code=500))) with pytest.raises(requests.exceptions.HTTPError):