From 38aaecc29c7070247ae0552c47f48e97c79b5312 Mon Sep 17 00:00:00 2001 From: Mo Sriha <22803208+medsriha@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:22:11 -0500 Subject: [PATCH] feat: Snowflake integration (#268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add md * Update integrations/snowflake.md --------- Co-authored-by: Tuana Çelik --- integrations/snowflake.md | 116 ++++++++++++++++++++++++++++++++++++++ logos/snowflake.png | Bin 0 -> 12157 bytes 2 files changed, 116 insertions(+) create mode 100644 integrations/snowflake.md create mode 100644 logos/snowflake.png diff --git a/integrations/snowflake.md b/integrations/snowflake.md new file mode 100644 index 00000000..f3f47896 --- /dev/null +++ b/integrations/snowflake.md @@ -0,0 +1,116 @@ +--- +layout: integration +name: Snowflake +description: A Snowflake integration that allows table retrieval from a Snowflake database. +authors: + - name: Mohamed Sriha + socials: + github: medsriha + - name: deepset + socials: + github: deepset-ai + twitter: deepset_ai + linkedin: deepset-ai +pypi: https://pypi.org/project/snowflake-haystack +repo: https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/snowflake +report_issue: https://github.com/deepset-ai/haystack-core-integrations/issues +type: Data Ingestion +logo: /logos/snowflake.png +version: Haystack 2.0 +--- + +[![PyPI - Version](https://img.shields.io/pypi/v/snowflake-haystack.svg)](https://pypi.org/project/snowflake-haystack) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/snowflake-haystack.svg)](https://pypi.org/project/snowflake-haystack) +----- + +**Table of Contents** + +- [Snowflake table retriever for Haystack](#snowfkale-table-retriever-for-haystack) + - [Installation](#installation) + - [Usage](#usage) + - [Examples](#examples) + - [License](#license) + +## Installation +Use `pip` to install Snowflake: + +```console +pip install snowflake-haystack +``` +## Usage +Once installed, initialize the `SnowflakeTableRetriever` to use it with Haystack 2.0: + +```python +from haystack_integrations.components.retrievers.snowflake import SnowflakeTableRetriever + +# Provide your Snowflake credentials during intialization. +executor = SnowflakeTableRetriever( + user="", + account="", + api_key=Secret.from_env_var("SNOWFLAKE_API_KEY"), + warehouse="", +) +``` + +Ensure you have `select` access to the tables before querying the database. More details [here](https://docs.snowflake.com/en/user-guide/security-access-control-privileges): +```python +response = executor.run(query="""select * from database_name.schema_name.table_name""") +``` +During component initialization, you could provide the schema and database name to avoid needing to provide them in the SQL query: +```python +executor = SnowflakeTableRetriever( + ... + schema_name="", + database ="" +) + +response = executor.run(query="""select * from table_name""") +``` +Snowflake table retriever returns a Pandas dataframe and a Markdown version of the table: +```python + +print(response["dataframe"].head(2)) # Pandas dataframe +# Column 1 Column 2 +# 0 Value1 Value2 +# 1 Value1 Value2 + +print(response["table"]) # Markdown +# | Column 1 | Column 2 | +# |:----------|:----------| +# | Value1 | Value2 | +# | Value1 | Value2 | +``` + +Using `SnowflakeTableRetriever` within a pipeline: + +```python +from haystack import Pipeline +from haystack.utils import Secret +from haystack.components.builders import PromptBuilder +from haystack.components.generators import OpenAIGenerator +from haystack_integrations.components.retrievers.snowflake import SnowflakeTableRetriever + +executor = SnowflakeTableRetriever( + user="", + account="", + api_key=Secret.from_env_var("SNOWFLAKE_API_KEY"), + warehouse="", +) + +pipeline = Pipeline() +pipeline.add_component("builder", PromptBuilder(template="Describe this table: {{ table }}")) +pipeline.add_component("snowflake", executor) +pipeline.add_component("llm", OpenAIGenerator(model="gpt-4o")) + +pipeline.connect("snowflake.table", "builder.table") +pipeline.connect("builder", "llm") + +pipeline.run(data={"query": "select employee, salary from table limit 10;"}) +``` + +## Examples +You can find a code example showing how to use the Snowflake Retriever under the `example/` folder of [this repo](https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/snowflake). + +## License + +`snowflake-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license. \ No newline at end of file diff --git a/logos/snowflake.png b/logos/snowflake.png new file mode 100644 index 0000000000000000000000000000000000000000..86d42c43a3c96d47ab645728d09080187eb6097a GIT binary patch literal 12157 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 BMwS2o literal 0 HcmV?d00001