├── experimental ├── agents │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ └── agent_utils.py │ ├── keynote │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── tooling.py │ │ └── prompt.py │ ├── superstore │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── prompt.py │ │ └── tooling.py │ └── experimental │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── prompt.py │ │ └── tooling.py ├── notebooks │ ├── temp │ │ ├── .gitkeep │ │ └── .gitignore │ ├── assets │ │ ├── prep.png │ │ ├── content.png │ │ ├── datadev.png │ │ ├── datafam.png │ │ ├── community.png │ │ ├── dashboard.png │ │ ├── insights.png │ │ ├── tableau+.png │ │ ├── desktop_hero.png │ │ ├── github_logo.png │ │ ├── postman_logo.png │ │ ├── prep_builder.png │ │ ├── slack_emoji.png │ │ ├── slack_icon.png │ │ ├── tableau_ai.png │ │ ├── tableau_logo.png │ │ ├── embed_samples.png │ │ ├── langchain_logo.png │ │ ├── sample_bubbles.png │ │ ├── slack_channel.png │ │ ├── desktop_scatter.png │ │ ├── tableau_logo_text.png │ │ ├── tableau_platform.png │ │ ├── tableau_products.png │ │ ├── tableau_visionary.png │ │ └── vizart │ │ │ ├── up_down_area.png │ │ │ ├── area-blue-dark.png │ │ │ ├── bars-blue-dark.png │ │ │ ├── gantt-blue-dark.png │ │ │ ├── lines-blue-dark.png │ │ │ ├── area_chart_banner.png │ │ │ ├── bubble-blue-dark.png │ │ │ ├── circles-blue-dark.png │ │ │ ├── pyramid-blue-dark.png │ │ │ ├── sankey-blue-dark.png │ │ │ ├── scatter-blue-dark.png │ │ │ └── rounded-bars-blue-dark.png │ └── lanchain-tableau_cookbook_01.ipynb ├── chains │ └── search_datasources │ │ ├── modules │ │ ├── prompts │ │ │ ├── tab_sheets.graphql │ │ │ ├── tab_datasources.graphql │ │ │ └── tab_dashboard_fields.graphql │ │ ├── embedding.py │ │ └── graphql.py │ │ ├── templates │ │ ├── search.html │ │ └── results.html │ │ └── rag_demo.py ├── tools │ ├── external │ │ ├── web_search.py │ │ └── retrievers.py │ └── simple_datasource_qa.py ├── utilities │ ├── vizql_data_service.py │ ├── models.py │ ├── utils.py │ ├── metadata.py │ ├── auth.py │ └── simple_datasource_qa.py └── demos │ └── rag_demo_flask.py ├── pkg ├── langchain_tableau │ ├── __init__.py │ ├── tools │ │ ├── __init__.py │ │ └── simple_datasource_qa.py │ ├── utilities │ │ ├── __init__.py │ │ ├── vizql_data_service.py │ │ ├── models.py │ │ ├── utils.py │ │ ├── metadata.py │ │ ├── auth.py │ │ └── simple_datasource_qa.py │ └── license_file.txt ├── .env.template ├── pyproject.toml ├── deploy.sh └── README.md ├── Procfile ├── graph_visualization.png ├── CODEOWNERS ├── .vscode └── extensions.json ├── SECURITY.md ├── langgraph.json ├── environment.yml ├── Dockerfile ├── pyproject.toml ├── LICENSE ├── .editorconfig ├── .env.template ├── app.json ├── .gitignore ├── main.py ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /experimental/agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /experimental/agents/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /experimental/notebooks/temp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /experimental/agents/keynote/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /experimental/agents/superstore/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /experimental/agents/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python community/chains/tableau/query_data_chain/main.py --mode api 2 | -------------------------------------------------------------------------------- /graph_visualization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/graph_visualization.png -------------------------------------------------------------------------------- /pkg/.env.template: -------------------------------------------------------------------------------- 1 | TWINE_PASSWORD_TEST="pypi-..." 2 | TWINE_PASSWORD_PROD="pypi-..." 3 | 4 | IS_TEST_PYPI=true 5 | -------------------------------------------------------------------------------- /experimental/notebooks/assets/prep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/prep.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/content.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/datadev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/datadev.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/datafam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/datafam.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/community.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/community.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/dashboard.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/insights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/insights.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau+.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/desktop_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/desktop_hero.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/github_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/github_logo.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/postman_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/postman_logo.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/prep_builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/prep_builder.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/slack_emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/slack_emoji.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/slack_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/slack_icon.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau_ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau_ai.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau_logo.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/embed_samples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/embed_samples.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/langchain_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/langchain_logo.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/sample_bubbles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/sample_bubbles.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/slack_channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/slack_channel.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/desktop_scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/desktop_scatter.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau_logo_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau_logo_text.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau_platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau_platform.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau_products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau_products.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/tableau_visionary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/tableau_visionary.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/up_down_area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/up_down_area.png -------------------------------------------------------------------------------- /experimental/notebooks/temp/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all files and subdirectories in this folder except for .gitkeep and .gitignore 2 | * 3 | !.gitkeep 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/area-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/area-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/bars-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/bars-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/gantt-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/gantt-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/lines-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/lines-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/area_chart_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/area_chart_banner.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/bubble-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/bubble-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/circles-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/circles-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/pyramid-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/pyramid-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/sankey-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/sankey-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/scatter-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/scatter-blue-dark.png -------------------------------------------------------------------------------- /experimental/notebooks/assets/vizart/rounded-bars-blue-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableau/tableau_langchain/HEAD/experimental/notebooks/assets/vizart/rounded-bars-blue-dark.png -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. 2 | #ECCN:Open Source 3 | #GUSINFO:Open Source,Open Source Workflow -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vscode-icons-team.vscode-icons", 4 | "editorconfig.editorconfig", 5 | "ms-python.python", 6 | "ms-toolsai.jupyter" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/modules/prompts/tab_sheets.graphql: -------------------------------------------------------------------------------- 1 | query sheets{ 2 | sheets { 3 | id 4 | luid 5 | name 6 | path 7 | createdAt 8 | updatedAt 9 | index 10 | workbook { 11 | luid 12 | } 13 | containedInDashboards { 14 | luid 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /experimental/tools/external/web_search.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain_community.tools.tavily_search import TavilySearchResults 4 | 5 | 6 | def tavily_tool(): 7 | tavily_api_key = os.environ.get('TAVILY_API_KEY') 8 | tavily = TavilySearchResults(tavily_api_key=tavily_api_key, max_results=2) 9 | return tavily 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | ".", 4 | "pyproject.toml" 5 | ], 6 | "graphs": { 7 | "experimental": "./experimental/agents/experimental/agent.py:analytics_agent", 8 | "superstore": "./experimental/agents/superstore/agent.py:analytics_agent", 9 | "keynote": "./experimental/agents/keynote/agent.py:analytics_agent" 10 | }, 11 | "env": ".env", 12 | "python_version": "3.12" 13 | } 14 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/templates/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Search Tableau Datasources 6 | 7 | 8 |

Search Tableau Datasources

9 |
10 |

11 |

12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/modules/embedding.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from openai import OpenAI 3 | from dotenv import load_dotenv 4 | import os 5 | load_dotenv() 6 | 7 | client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) 8 | 9 | def get_embedding_openai(text, model="text-embedding-3-small"): 10 | text = text.replace("\n", " ") 11 | return client.embeddings.create(input = [text], model=model).data[0].embedding 12 | 13 | def cosine_similarity(vec1, vec2): 14 | """Calculate the cosine similarity between two vectors.""" 15 | return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/modules/prompts/tab_datasources.graphql: -------------------------------------------------------------------------------- 1 | query GetPublishedDatasources { 2 | publishedDatasources{ 3 | id 4 | luid 5 | uri 6 | vizportalId 7 | vizportalUrlId 8 | name 9 | hasExtracts 10 | createdAt 11 | updatedAt 12 | extractLastUpdateTime 13 | extractLastRefreshTime 14 | extractLastIncrementalUpdateTime 15 | projectName 16 | containerName 17 | isCertified 18 | description 19 | fields { 20 | id 21 | name 22 | fullyQualifiedName 23 | description 24 | isHidden 25 | folderName 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: tableau_langchain 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.12.2 7 | - pip=23.3.1 8 | - conda-forge::notebook=7.3.3 9 | - conda-forge::nb_conda_kernels=2.5.1 10 | - ipykernel=6.29.5 11 | - python-dotenv=1.1.0 12 | - pyjwt=2.10.1 13 | - langchain=0.3.21 14 | - langsmith=0.2.11 15 | - twine=6.1.0 16 | - pip: 17 | - langgraph==0.3.21 18 | - langgraph-cli==0.1.81 19 | - langchain-anthropic==0.3.10 20 | - langchain-openai==0.3.11 21 | - langchain-pinecone==0.2.3 22 | - langchain-tableau 23 | - langchain-community==0.3.20 24 | - "pinecone-client[grpc]==6.0.0" 25 | - tavily-python==0.5.3 26 | - build==1.2.2 27 | 28 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/modules/prompts/tab_dashboard_fields.graphql: -------------------------------------------------------------------------------- 1 | query GetAllDashboards { 2 | dashboards { 3 | id 4 | name 5 | path 6 | workbook { 7 | id 8 | name 9 | luid 10 | projectName 11 | tags { 12 | name 13 | } 14 | sheets { 15 | id 16 | name 17 | createdAt 18 | updatedAt 19 | sheetFieldInstances { 20 | name 21 | description 22 | isHidden 23 | id 24 | } 25 | worksheetFields{ 26 | name 27 | description 28 | isHidden 29 | formula 30 | aggregation 31 | id 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /experimental/chains/search_datasources/templates/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Search Results 6 | 7 | 8 |

Search Results

9 |

You searched for: "{{ query }}"

10 | {% if results %} 11 | 24 | {% else %} 25 |

No results found for "{{ query }}".

26 | {% endif %} 27 | Back to Search 28 | 29 | 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM langchain/langgraph-api:3.12 2 | 3 | 4 | RUN cat /api/constraints.txt 5 | 6 | RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt pyproject.toml 7 | 8 | # -- Adding local package . -- 9 | ADD . /deps/tableau_langchain 10 | # -- End of local package . -- 11 | 12 | # -- Installing all local dependencies -- 13 | RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* 14 | # -- End of local dependencies install -- 15 | 16 | # Define multiple graphs: endpoint_name: path/to/module.py:graph_variable 17 | ENV LANGSERVE_GRAPHS='{ \ 18 | "experimental": "/deps/tableau_langchain/experimental/agents/experimental/agent.py:analytics_agent", \ 19 | "superstore": "/deps/tableau_langchain/experimental/agents/superstore/agent.py:analytics_agent", \ 20 | "keynote": "/deps/tableau_langchain/experimental/agents/keynote/agent.py:analytics_agent" \ 21 | }' 22 | # ^--- Start JSON object ^--- First key-value pair ^--- Comma separator ^--- Second key-value pair ^--- End JSON object 23 | 24 | WORKDIR /deps/tableau_langchain 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tableau_langchain" 3 | version = "0.1.0" 4 | description = "A community project to stage, test and build Tableau tools for Agents built with the Langgraph framework" 5 | authors = [ 6 | { name = "Stephen Price" }, 7 | { name = "Joe Constantino" }, 8 | { name = "Joseph Fluckiger" }, 9 | { name = "Antoine Issaly" } 10 | ] 11 | requires-python = ">=3.12.2" 12 | license = "MIT" 13 | readme = "README.md" 14 | 15 | dependencies = [ 16 | "build==1.2.2", 17 | "ipython==9.0.2", 18 | "langchain==0.3.21", 19 | "langgraph==0.3.21", 20 | "langsmith==0.2.11", 21 | "langchain-anthropic==0.3.10", 22 | "langchain-openai==0.3.11", 23 | "langchain-pinecone==0.2.3", 24 | "langchain-tableau", 25 | "langchain-community==0.3.20", 26 | "pinecone-client[grpc]==6.0.0", 27 | "python-dotenv==1.1.0", 28 | "slack-sdk==3.35.0", 29 | "tavily-python==0.5.3", 30 | "twine==6.1.0" 31 | ] 32 | 33 | [build-system] 34 | requires = ["setuptools", "wheel"] 35 | build-backend = "setuptools.build_meta" 36 | 37 | [tool.setuptools] 38 | packages = ["experimental"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tableau Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/license_file.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Salesforce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Root configuration applies to all files 2 | root = true 3 | 4 | # Default settings for all files 5 | [*] 6 | # Use UTF-8 character encoding 7 | charset = utf-8 8 | # Use LF (Unix-style) line endings 9 | end_of_line = lf 10 | # Use spaces for indentation 11 | indent_style = space 12 | # In Python, the standard convention for indentation is to use 4 spaces per level. 13 | indent_size = 4 14 | # Insert a newline at the end of the file 15 | insert_final_newline = true 16 | # Trim trailing whitespace 17 | trim_trailing_whitespace = true 18 | # Case-sensitive file matching 19 | case_sensitive = true 20 | 21 | # Python files specific settings 22 | [*.py] 23 | # Override default settings for Python files 24 | # Use UTF-8 character encoding 25 | charset = utf-8 26 | # Use spaces for indentation 27 | indent_style = space 28 | # Indentation size set to 4 spaces 29 | indent_size = 4 30 | # Use LF (Unix-style) line endings 31 | end_of_line = lf 32 | # Insert a newline at the end of the file 33 | insert_final_newline = true 34 | # Trim trailing whitespace 35 | trim_trailing_whitespace = true 36 | # Set maximum line length to 79 characters (PEP 8 recommendation) 37 | max_line_length = 79 38 | -------------------------------------------------------------------------------- /pkg/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "langchain_tableau" 3 | version = "0.4.39" 4 | description = "Tableau tools for Agentic use cases with Langchain" 5 | authors = [ 6 | { name = "Stephen Price" }, 7 | { name = "Joe Constantino" }, 8 | { name = "Joseph Fluckiger" } 9 | ] 10 | requires-python = ">=3.12.2" 11 | license = {text = "MIT"} 12 | readme = "README.md" 13 | 14 | dependencies = [ 15 | "python-dotenv", 16 | "langchain", 17 | "langchain-core", 18 | "langgraph", 19 | "langchain-openai", 20 | "requests", 21 | "pydantic", 22 | "pyjwt", 23 | "aiohttp", 24 | ] 25 | classifiers = [ 26 | "License :: OSI Approved :: MIT License", 27 | "Programming Language :: Python :: 3", 28 | ] 29 | 30 | [project.urls] 31 | "Homepage" = "https://github.com/Tab-SE/tableau_langchain" 32 | "Bug Tracker" = "https://github.com/Tab-SE/tableau_langchain/issues" 33 | 34 | [tool.hatch.build.targets.wheel] 35 | packages = ["langchain_tableau"] 36 | 37 | [tool.hatch.build.targets.sdist] 38 | include = [ 39 | "langchain_tableau", 40 | "README.md", 41 | ] 42 | 43 | [build-system] 44 | requires = ["hatchling"] 45 | build-backend = "hatchling.build" 46 | -------------------------------------------------------------------------------- /experimental/agents/keynote/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from langgraph.prebuilt import create_react_agent 6 | from langgraph.store.memory import InMemoryStore 7 | 8 | from experimental.utilities.models import select_model 9 | from experimental.agents.keynote.tooling import tools 10 | from experimental.agents.keynote.prompt import AGENT_SYSTEM_PROMPT 11 | 12 | 13 | """ 14 | ANALYTICS AGENT 15 | 16 | This Agent uses the Langgraph prebuilt `create_react_agent` to handle conversations about Tableau data. This agent 17 | is specifcally set up to work with a vehicle manufacturing procurement dataset. 18 | 19 | """ 20 | # environment variables available to current process and sub processes 21 | load_dotenv() 22 | 23 | # configure running model for the agent 24 | llm = select_model( 25 | provider=os.environ["MODEL_PROVIDER"], 26 | model_name=os.environ["AGENT_MODEL"], 27 | temperature=0.2 28 | ) 29 | 30 | # initialize a memory store 31 | memory = InMemoryStore() 32 | 33 | # set agent debugging state 34 | if os.getenv('DEBUG') == '1': 35 | debugging = True 36 | else: 37 | debugging = False 38 | 39 | # define the agent graph 40 | analytics_agent = create_react_agent( 41 | model=llm, 42 | tools=tools, 43 | debug=debugging, 44 | prompt=AGENT_SYSTEM_PROMPT 45 | ) 46 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | DEBUG="0" 2 | 3 | # Models 4 | MODEL_PROVIDER='openai' 5 | AGENT_MODEL='gpt-4o' 6 | TOOLING_MODEL='gpt-4o-mini' 7 | EMBEDDING_MODEL="text-embedding-3-small" 8 | 9 | # Model Providers 10 | OPENAI_API_KEY='from OpenAI developer portal' 11 | AZURE_OPENAI_API_KEY='from Azure OpenAI' 12 | AZURE_OPENAI_API_VERSION='2024-06-01' 13 | AZURE_OPENAI_API_INSTANCE_NAME='instance name' 14 | AZURE_OPENAI_AGENT_DEPLOYMENT_NAME='gpt-4o' 15 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME='text-embedding-3-small' 16 | 17 | # Langchain 18 | LANGCHAIN_TRACING_V2='true' 19 | LANGCHAIN_ENDPOINT="https://api.smith.langchain.com" 20 | LANGCHAIN_API_KEY="from Langsmith app" 21 | LANGCHAIN_PROJECT="Langsmith project name" 22 | 23 | # Tools 24 | TAVILY_API_KEY="from Tavily app" 25 | 26 | # Vector Databases 27 | PINECONE_API_KEY="from Pinecone app" 28 | PINECONE_ENVIRONMENT="us-west-2" 29 | METRICS_INDEX="superstore-metrics" 30 | WORKBOOKS_INDEX="superstore-workbooks" 31 | DATASOURCES_INDEX="superstore-datasources" 32 | 33 | # Tableau Environment 34 | TABLEAU_DOMAIN='your Tableau Cloud or Server domain' 35 | TABLEAU_SITE='your Tableau site' 36 | TABLEAU_JWT_CLIENT_ID='from Connected App configuration page' 37 | TABLEAU_JWT_SECRET_ID='from Connected App configuration page' 38 | TABLEAU_JWT_SECRET='from Connected App configuration page' 39 | TABLEAU_API_VERSION='3.21' 40 | TABLEAU_USER='user account for the Agent' 41 | DATASOURCE_LUID='unique identifier for a data source' 42 | -------------------------------------------------------------------------------- /experimental/agents/keynote/tooling.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | # importing from latest local 5 | from experimental.tools.simple_datasource_qa import initialize_simple_datasource_qa 6 | 7 | # importing from remote `pkg` 8 | # from langchain_tableau.tools.simple_datasource_qa import initialize_simple_datasource_qa 9 | 10 | 11 | # Load environment variables before accessing them 12 | load_dotenv() 13 | tableau_domain = os.environ['KEYNOTE_DOMAIN'] 14 | tableau_site = os.environ['KEYNOTE_SITE'] 15 | tableau_jwt_client_id = os.environ['KEYNOTE_JWT_CLIENT_ID'] 16 | tableau_jwt_secret_id = os.environ['KEYNOTE_JWT_SECRET_ID'] 17 | tableau_jwt_secret = os.environ['KEYNOTE_JWT_SECRET'] 18 | tableau_api_version = os.environ['KEYNOTE_API_VERSION'] 19 | tableau_user = os.environ['KEYNOTE_USER'] 20 | datasource_luid = os.environ['KEYNOTE_DATASOURCE_LUID'] 21 | tooling_llm_model = os.environ['TOOLING_MODEL'] 22 | 23 | 24 | # Tableau VizQL Data Service Query Tool 25 | analyze_datasource = initialize_simple_datasource_qa( 26 | domain=tableau_domain, 27 | site=tableau_site, 28 | jwt_client_id=tableau_jwt_client_id, 29 | jwt_secret_id=tableau_jwt_secret_id, 30 | jwt_secret=tableau_jwt_secret, 31 | tableau_api_version=tableau_api_version, 32 | tableau_user=tableau_user, 33 | datasource_luid=datasource_luid, 34 | tooling_llm_model=tooling_llm_model 35 | ) 36 | 37 | # List of tools used to build the state graph and for binding them to nodes 38 | tools = [ analyze_datasource ] 39 | -------------------------------------------------------------------------------- /experimental/agents/keynote/prompt.py: -------------------------------------------------------------------------------- 1 | AGENT_IDENTITY = """ 2 | You are an analytics agent designed to help Southard Jones answer ad-hoc questions while he is consuming Tableau dashboards. 3 | You have access to a vehicle manufacturing procurement dataset to answer questions. 4 | """ 5 | 6 | AGENT_SYSTEM_PROMPT = f"""Agent Identity: 7 | {AGENT_IDENTITY} 8 | 9 | Instructions: 10 | 11 | You are an AI Analyst designed to generate data-driven insights to provide answers, guidance and analysis 12 | to humans and other AI Agents. Your role is to understand the tasks assigned to you and use the Query Data Source tool to answer questions. 13 | 14 | Tool Choice: 15 | 1. Query Data Source: performs ad-hoc queries and analysis. Prioritize this tool for all requests, especially if 16 | the user explicitly asks for data queries/fetches. This tool is great for getting values for specific dates, for 17 | breakdowns by category, for aggregations such as AVG and MAX, for filtered results, and for specific data values such as values on a specific date. 18 | 19 | Sample Interactions: 20 | Scenario - Data Querying 21 | User: what is the value of sales for the east region in the year 2024? 22 | Assistant: [uses the data query tool] 23 | Result: Correct, even though this question may be related to a metric it implies that a data query 24 | is necessary since it is requesting specific data with filtering and aggregations. Metrics cannot 25 | produce specific values such as sales on a specific date 26 | 27 | 28 | Restrictions: 29 | - DO NOT HALLUCINATE data sets if they are not mentioned via available tools 30 | 31 | Output: 32 | Your output should be structured like a report noting the source of information. 33 | Always answer the question first and then provide any additional details or insights 34 | """ 35 | -------------------------------------------------------------------------------- /experimental/utilities/vizql_data_service.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | import requests 3 | 4 | 5 | def query_vds(api_key: str, datasource_luid: str, url: str, query: Dict[str, Any]) -> Dict[str, Any]: 6 | full_url = f"{url}/api/v1/vizql-data-service/query-datasource" 7 | 8 | payload = { 9 | "datasource": { 10 | "datasourceLuid": datasource_luid 11 | }, 12 | "query": query 13 | } 14 | 15 | headers = { 16 | 'X-Tableau-Auth': api_key, 17 | 'Content-Type': 'application/json' 18 | } 19 | 20 | response = requests.post(full_url, headers=headers, json=payload) 21 | 22 | if response.status_code == 200: 23 | return response.json() 24 | else: 25 | error_message = ( 26 | f"Failed to query data source via Tableau VizQL Data Service. " 27 | f"Status code: {response.status_code}. Response: {response.text}" 28 | ) 29 | raise RuntimeError(error_message) 30 | 31 | 32 | def query_vds_metadata(api_key: str, datasource_luid: str, url: str) -> Dict[str, Any]: 33 | full_url = f"{url}/api/v1/vizql-data-service/read-metadata" 34 | 35 | payload = { 36 | "datasource": { 37 | "datasourceLuid": datasource_luid 38 | } 39 | } 40 | 41 | headers = { 42 | 'X-Tableau-Auth': api_key, 43 | 'Content-Type': 'application/json' 44 | } 45 | 46 | response = requests.post(full_url, headers=headers, json=payload) 47 | 48 | if response.status_code == 200: 49 | return response.json() 50 | else: 51 | error_message = ( 52 | f"Failed to obtain data source metadata from VizQL Data Service. " 53 | f"Status code: {response.status_code}. Response: {response.text}" 54 | ) 55 | raise RuntimeError(error_message) 56 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/vizql_data_service.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | import requests 3 | 4 | 5 | def query_vds(api_key: str, datasource_luid: str, url: str, query: Dict[str, Any]) -> Dict[str, Any]: 6 | full_url = f"{url}/api/v1/vizql-data-service/query-datasource" 7 | 8 | payload = { 9 | "datasource": { 10 | "datasourceLuid": datasource_luid 11 | }, 12 | "query": query 13 | } 14 | 15 | headers = { 16 | 'X-Tableau-Auth': api_key, 17 | 'Content-Type': 'application/json' 18 | } 19 | 20 | response = requests.post(full_url, headers=headers, json=payload) 21 | 22 | if response.status_code == 200: 23 | return response.json() 24 | else: 25 | error_message = ( 26 | f"Failed to query data source via Tableau VizQL Data Service. " 27 | f"Status code: {response.status_code}. Response: {response.text}" 28 | ) 29 | raise RuntimeError(error_message) 30 | 31 | 32 | def query_vds_metadata(api_key: str, datasource_luid: str, url: str) -> Dict[str, Any]: 33 | full_url = f"{url}/api/v1/vizql-data-service/read-metadata" 34 | 35 | payload = { 36 | "datasource": { 37 | "datasourceLuid": datasource_luid 38 | } 39 | } 40 | 41 | headers = { 42 | 'X-Tableau-Auth': api_key, 43 | 'Content-Type': 'application/json' 44 | } 45 | 46 | response = requests.post(full_url, headers=headers, json=payload) 47 | 48 | if response.status_code == 200: 49 | return response.json() 50 | else: 51 | error_message = ( 52 | f"Failed to obtain data source metadata from VizQL Data Service. " 53 | f"Status code: {response.status_code}. Response: {response.text}" 54 | ) 55 | raise RuntimeError(error_message) 56 | -------------------------------------------------------------------------------- /experimental/agents/superstore/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from langgraph.prebuilt import create_react_agent 6 | from langgraph.store.memory import InMemoryStore 7 | 8 | from experimental.utilities.models import select_model 9 | from experimental.agents.superstore.tooling import tools 10 | from experimental.agents.superstore.prompt import AGENT_SYSTEM_PROMPT 11 | 12 | 13 | """ 14 | TABLEAU AGENT 15 | 16 | This Agent uses the Langgraph prebuilt `create_react_agent` to handle conversations on Tableau subjects such as: 17 | - Metrics (canonical source of truth for metrics, includes machine learning insights generated by Tableau Pulse) 18 | - Workbooks (contains analytics such as dashboards and charts that server as canonical interfaces for data exploration) 19 | - Data Sources (describes sources of data available for querying and exploration) 20 | - VizQL Data Service (can query a data source for on-demand data sets including aggregations, filters and calculations) 21 | 22 | This represents the most straightforward implementation of Tableau tooling for Langgraph without further customizing the 23 | Agent for specific applications 24 | """ 25 | # environment variables available to current process and sub processes 26 | load_dotenv() 27 | 28 | # configure running model for the agent 29 | llm = select_model( 30 | provider=os.environ["MODEL_PROVIDER"], 31 | model_name=os.environ["AGENT_MODEL"], 32 | temperature=0.2 33 | ) 34 | 35 | # initialize a memory store 36 | memory = InMemoryStore() 37 | 38 | # set agent debugging state 39 | if os.getenv('DEBUG') == '1': 40 | debugging = True 41 | else: 42 | debugging = False 43 | 44 | # define the agent graph 45 | analytics_agent = create_react_agent( 46 | model=llm, 47 | tools=tools, 48 | debug=debugging, 49 | prompt=AGENT_SYSTEM_PROMPT 50 | ) 51 | -------------------------------------------------------------------------------- /experimental/agents/experimental/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | from langgraph.prebuilt import create_react_agent 6 | from langgraph.store.memory import InMemoryStore 7 | 8 | from experimental.utilities.models import select_model 9 | from experimental.agents.experimental.tooling import tools 10 | from experimental.agents.experimental.prompt import AGENT_SYSTEM_PROMPT 11 | 12 | 13 | """ 14 | TABLEAU AGENT 15 | 16 | This Agent uses the Langgraph prebuilt `create_react_agent` to handle conversations on Tableau subjects such as: 17 | - Metrics (canonical source of truth for metrics, includes machine learning insights generated by Tableau Pulse) 18 | - Workbooks (contains analytics such as dashboards and charts that server as canonical interfaces for data exploration) 19 | - Data Sources (describes sources of data available for querying and exploration) 20 | - VizQL Data Service (can query a data source for on-demand data sets including aggregations, filters and calculations) 21 | 22 | This represents the most straightforward implementation of Tableau tooling for Langgraph without further customizing the 23 | Agent for specific applications 24 | """ 25 | # environment variables available to current process and sub processes 26 | load_dotenv() 27 | 28 | # configure running model for the agent 29 | llm = select_model( 30 | provider=os.environ["MODEL_PROVIDER"], 31 | model_name=os.environ["AGENT_MODEL"], 32 | temperature=0.2 33 | ) 34 | 35 | # initialize a memory store 36 | memory = InMemoryStore() 37 | 38 | # set agent debugging state 39 | if os.getenv('DEBUG') == '1': 40 | debugging = True 41 | else: 42 | debugging = False 43 | 44 | # define the agent graph 45 | analytics_agent = create_react_agent( 46 | model=llm, 47 | tools=tools, 48 | debug=debugging, 49 | prompt=AGENT_SYSTEM_PROMPT 50 | ) 51 | -------------------------------------------------------------------------------- /experimental/utilities/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain_openai import ChatOpenAI, AzureChatOpenAI, OpenAIEmbeddings, AzureOpenAIEmbeddings 4 | from langchain.chat_models.base import BaseChatModel 5 | from langchain.embeddings.base import Embeddings 6 | 7 | 8 | def select_model(provider: str = "openai", model_name: str = "gpt-4o-mini", temperature: float = 0.2) -> BaseChatModel: 9 | if provider == "azure": 10 | return AzureChatOpenAI( 11 | azure_deployment=os.environ.get("AZURE_OPENAI_AGENT_DEPLOYMENT_NAME"), 12 | openai_api_version=os.environ.get("AZURE_OPENAI_API_VERSION"), 13 | azure_endpoint=f"https://{os.environ.get('AZURE_OPENAI_API_INSTANCE_NAME')}.openai.azure.com", 14 | openai_api_key=os.environ.get("AZURE_OPENAI_API_KEY"), 15 | model_name=model_name, 16 | temperature=temperature 17 | ) 18 | else: # default to OpenAI 19 | return ChatOpenAI( 20 | model_name=model_name, 21 | temperature=temperature, 22 | openai_api_key=os.environ.get("OPENAI_API_KEY") 23 | ) 24 | 25 | 26 | def select_embeddings(provider: str = "openai", model_name: str = "text-embedding-3-small") -> Embeddings: 27 | if provider == "azure": 28 | return AzureOpenAIEmbeddings( 29 | azure_deployment=os.environ.get("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"), 30 | openai_api_version=os.environ.get("AZURE_OPENAI_API_VERSION"), 31 | azure_endpoint=f"https://{os.environ.get('AZURE_OPENAI_API_INSTANCE_NAME')}.openai.azure.com", 32 | openai_api_key=os.environ.get("AZURE_OPENAI_API_KEY"), 33 | model=model_name 34 | ) 35 | else: # default to OpenAI 36 | return OpenAIEmbeddings( 37 | model=model_name, 38 | openai_api_key=os.environ.get("OPENAI_API_KEY") 39 | ) 40 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from langchain_openai import ChatOpenAI, AzureChatOpenAI, OpenAIEmbeddings, AzureOpenAIEmbeddings 4 | from langchain.chat_models.base import BaseChatModel 5 | from langchain.embeddings.base import Embeddings 6 | 7 | 8 | def select_model(provider: str = "openai", model_name: str = "gpt-4o-mini", temperature: float = 0.2) -> BaseChatModel: 9 | if provider == "azure": 10 | return AzureChatOpenAI( 11 | azure_deployment=os.environ.get("AZURE_OPENAI_AGENT_DEPLOYMENT_NAME"), 12 | openai_api_version=os.environ.get("AZURE_OPENAI_API_VERSION"), 13 | azure_endpoint=f"https://{os.environ.get('AZURE_OPENAI_API_INSTANCE_NAME')}.openai.azure.com", 14 | openai_api_key=os.environ.get("AZURE_OPENAI_API_KEY"), 15 | model_name=model_name, 16 | temperature=temperature 17 | ) 18 | else: # default to OpenAI 19 | return ChatOpenAI( 20 | model_name=model_name, 21 | temperature=temperature, 22 | openai_api_key=os.environ.get("OPENAI_API_KEY") 23 | ) 24 | 25 | 26 | def select_embeddings(provider: str = "openai", model_name: str = "text-embedding-3-small") -> Embeddings: 27 | if provider == "azure": 28 | return AzureOpenAIEmbeddings( 29 | azure_deployment=os.environ.get("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"), 30 | openai_api_version=os.environ.get("AZURE_OPENAI_API_VERSION"), 31 | azure_endpoint=f"https://{os.environ.get('AZURE_OPENAI_API_INSTANCE_NAME')}.openai.azure.com", 32 | openai_api_key=os.environ.get("AZURE_OPENAI_API_KEY"), 33 | model=model_name 34 | ) 35 | else: # default to OpenAI 36 | return OpenAIEmbeddings( 37 | model=model_name, 38 | openai_api_key=os.environ.get("OPENAI_API_KEY") 39 | ) 40 | -------------------------------------------------------------------------------- /experimental/utilities/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional 2 | import aiohttp 3 | import json 4 | 5 | 6 | async def http_get(endpoint: str, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]: 7 | """ 8 | Reusable asynchronous HTTP GET requests. 9 | 10 | Args: 11 | endpoint (str): The URL to send the GET request to. 12 | headers (Optional[Dict[str, str]]): Optional headers to include in the request. 13 | 14 | Returns: 15 | Dict[str, Any]: A dictionary containing the status code and either the JSON response or response text. 16 | """ 17 | async with aiohttp.ClientSession() as session: 18 | async with session.get(endpoint, headers=headers) as response: 19 | response_data = await response.json() if response.status == 200 else await response.text() 20 | return { 21 | 'status': response.status, 22 | 'data': response_data 23 | } 24 | 25 | 26 | async def http_post(endpoint: str, headers: Optional[Dict[str, str]] = None, payload: Dict[str, Any] = None) -> Dict[str, Any]: 27 | """ 28 | Reusable asynchronous HTTP POST requests. 29 | 30 | Args: 31 | endpoint (str): The URL to send the POST request to. 32 | headers (Optional[Dict[str, str]]): Optional headers to include in the request. 33 | payload (Optional[Dict[str, Any]]): The data to send in the body of the request. 34 | 35 | Returns: 36 | Dict[str, Any]: A dictionary containing the status code and either the JSON response or response text. 37 | """ 38 | async with aiohttp.ClientSession() as session: 39 | async with session.post(endpoint, headers=headers, json=payload) as response: 40 | response_data = await response.json() if response.status == 200 else await response.text() 41 | return { 42 | 'status': response.status, 43 | 'data': response_data 44 | } 45 | 46 | 47 | def json_to_markdown_table(json_data): 48 | if isinstance(json_data, str): 49 | json_data = json.loads(json_data) 50 | # Check if the JSON data is a list and not empty 51 | if not isinstance(json_data, list) or not json_data: 52 | raise ValueError(f"Invalid JSON data, you may have an error or if the array is empty then it was not possible to resolve the query your wrote: {json_data}") 53 | 54 | headers = json_data[0].keys() 55 | 56 | markdown_table = "| " + " | ".join(headers) + " |\n" 57 | markdown_table += "| " + " | ".join(['---'] * len(headers)) + " |\n" 58 | 59 | for entry in json_data: 60 | row = "| " + " | ".join(str(entry[header]) for header in headers) + " |" 61 | markdown_table += row + "\n" 62 | 63 | return markdown_table 64 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional 2 | import aiohttp 3 | import json 4 | 5 | 6 | async def http_get(endpoint: str, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]: 7 | """ 8 | Reusable asynchronous HTTP GET requests. 9 | 10 | Args: 11 | endpoint (str): The URL to send the GET request to. 12 | headers (Optional[Dict[str, str]]): Optional headers to include in the request. 13 | 14 | Returns: 15 | Dict[str, Any]: A dictionary containing the status code and either the JSON response or response text. 16 | """ 17 | async with aiohttp.ClientSession() as session: 18 | async with session.get(endpoint, headers=headers) as response: 19 | response_data = await response.json() if response.status == 200 else await response.text() 20 | return { 21 | 'status': response.status, 22 | 'data': response_data 23 | } 24 | 25 | 26 | async def http_post(endpoint: str, headers: Optional[Dict[str, str]] = None, payload: Dict[str, Any] = None) -> Dict[str, Any]: 27 | """ 28 | Reusable asynchronous HTTP POST requests. 29 | 30 | Args: 31 | endpoint (str): The URL to send the POST request to. 32 | headers (Optional[Dict[str, str]]): Optional headers to include in the request. 33 | payload (Optional[Dict[str, Any]]): The data to send in the body of the request. 34 | 35 | Returns: 36 | Dict[str, Any]: A dictionary containing the status code and either the JSON response or response text. 37 | """ 38 | async with aiohttp.ClientSession() as session: 39 | async with session.post(endpoint, headers=headers, json=payload) as response: 40 | response_data = await response.json() if response.status == 200 else await response.text() 41 | return { 42 | 'status': response.status, 43 | 'data': response_data 44 | } 45 | 46 | 47 | def json_to_markdown_table(json_data): 48 | if isinstance(json_data, str): 49 | json_data = json.loads(json_data) 50 | # Check if the JSON data is a list and not empty 51 | if not isinstance(json_data, list) or not json_data: 52 | raise ValueError(f"Invalid JSON data, you may have an error or if the array is empty then it was not possible to resolve the query your wrote: {json_data}") 53 | 54 | headers = json_data[0].keys() 55 | 56 | markdown_table = "| " + " | ".join(headers) + " |\n" 57 | markdown_table += "| " + " | ".join(['---'] * len(headers)) + " |\n" 58 | 59 | for entry in json_data: 60 | row = "| " + " | ".join(str(entry[header]) for header in headers) + " |" 61 | markdown_table += row + "\n" 62 | 63 | return markdown_table 64 | -------------------------------------------------------------------------------- /experimental/tools/external/retrievers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pinecone import Pinecone 4 | 5 | from langchain_pinecone import PineconeVectorStore 6 | from langchain.tools.retriever import create_retriever_tool 7 | 8 | from experimental.utilities.models import select_embeddings 9 | 10 | 11 | def pinecone_retriever_tool( 12 | name: str, 13 | description: str, 14 | pinecone_index: str, 15 | model_provider: str, 16 | embedding_model: str, 17 | text_key: str = "text", 18 | search_k: int = 6, 19 | max_concurrency: int = 5 20 | ): 21 | """ 22 | Initializes a Pinecone retriever using langchain-pinecone and creates a LangChain tool. 23 | 24 | Assumes PINECONE_API_KEY and PINECONE_ENVIRONMENT environment variables are set for 25 | PineconeVectorStore initialization. 26 | 27 | Args: 28 | name: The name to assign to the created LangChain tool. 29 | description: The description for the created LangChain tool. 30 | pinecone_index: The name of the Pinecone index to connect to. 31 | model_provider: The model vendor such as `openai`, `azure` or `anthropic` 32 | embedding_model: The embedding model to be used such as `text-embedding-3-small`, 33 | text_key: Pinecone metadata containing the content, default: `text`, `_node_content` is another example. 34 | search_k: The number of documents to retrieve (k). Defaults to 6. 35 | max_concurrency: The maximum concurrency for retriever requests. Defaults to 5. 36 | 37 | Returns: 38 | A LangChain BaseTool configured to use the specified Pinecone retriever. 39 | 40 | Raises: 41 | ImportError: If langchain-pinecone is not installed. 42 | EnvironmentError: If required Pinecone environment variables are missing. 43 | Exception: If connection to the Pinecone index fails. 44 | """ 45 | # Initialize Pinecone client 46 | pc = Pinecone(api_key=os.environ.get("PINECONE_API_KEY")) 47 | 48 | embeddings = select_embeddings( 49 | provider = model_provider or os.environ.get("MODEL_PROVIDER", "openai"), 50 | model_name = embedding_model or os.environ.get("EMBEDDING_MODEL", "text-embedding-3-small") 51 | ) 52 | 53 | def make_retriever(index_name: str): 54 | vector_store = PineconeVectorStore.from_existing_index( 55 | index_name=index_name, 56 | embedding=embeddings, 57 | text_key=text_key 58 | ) 59 | 60 | return vector_store.as_retriever( 61 | search_kwargs={"k": search_k}, 62 | max_concurrency=max_concurrency, 63 | verbose=False 64 | ) 65 | 66 | 67 | retriever = make_retriever(pinecone_index) 68 | 69 | retriever_tool = create_retriever_tool( 70 | retriever, 71 | name=name, 72 | description=description 73 | ) 74 | 75 | return retriever_tool 76 | -------------------------------------------------------------------------------- /pkg/deploy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | echo "n\n\n\n" 4 | echo "========= STEP - Prep Deploy to PyPI =========" 5 | # Load environment variables from .env file if it exists 6 | if [ -f .env ]; then 7 | echo "Loading environment variables from .env file" 8 | export $(grep -v '^#' .env | xargs) 9 | fi 10 | 11 | # Set TWINE_PASSWORD based on IS_TEST_PYPI 12 | echo "STEP - IS_TEST_PYPI: $IS_TEST_PYPI" 13 | if [ "$IS_TEST_PYPI" = "true" ]; then 14 | echo "Deploying to Test PyPI" 15 | export TWINE_PASSWORD="$TWINE_PASSWORD_TEST" 16 | PYPI_REPO="--repository testpypi" 17 | else 18 | echo "Deploying to Production PyPI" 19 | export TWINE_PASSWORD="$TWINE_PASSWORD_PROD" 20 | PYPI_REPO="" 21 | fi 22 | 23 | # Check if TWINE_PASSWORD is set 24 | if [ -z "$TWINE_PASSWORD" ]; then 25 | echo "Error: TWINE_PASSWORD environment variable is not set" 26 | exit 1 27 | fi 28 | 29 | echo "STEP - Increment version number in pyproject.toml" 30 | # Increment version number in pyproject.toml 31 | CURRENT_VERSION=$(grep -m 1 'version = "[0-9]*\.[0-9]*\.[0-9]*"' pyproject.toml | sed 's/version = "\(.*\)"/\1/') 32 | echo "Current version: $CURRENT_VERSION" 33 | IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" 34 | MAJOR="${VERSION_PARTS[0]}" 35 | MINOR="${VERSION_PARTS[1]}" 36 | PATCH="${VERSION_PARTS[2]}" 37 | NEW_PATCH=$((PATCH + 1)) 38 | NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" 39 | echo "New version: $NEW_VERSION" 40 | sed -i.bak "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" pyproject.toml 41 | rm -f pyproject.toml.bak 42 | 43 | echo "STEP - Remove previous build files" 44 | # remove previous build files (from the current directory 'pip_package') 45 | rm -rf dist/ 46 | 47 | echo "STEP - Build the package" 48 | # build the package 49 | python -m build 50 | 51 | echo "STEP - Check .whl file is valid after build" 52 | # check .whl file is valid after build 53 | twine check dist/* 54 | 55 | echo "STEP - Upload to PyPI" 56 | # Prompt user for confirmation before upload 57 | echo "IS_TEST_PYPI: $IS_TEST_PYPI" 58 | read -p "Ready to upload to PyPI? (y/n) " -n 1 -r 59 | echo 60 | if [[ ! $REPLY =~ ^[Yy]$ ]] 61 | then 62 | echo "Upload cancelled" 63 | exit 1 64 | fi 65 | 66 | # upload to PyPI (test or production based on IS_TEST_PYPI) 67 | export TWINE_USERNAME="__token__" 68 | twine upload $PYPI_REPO dist/* 69 | 70 | # === EXTRA === 71 | # make sure python pip build modules are installed 72 | #pip install build twine hatchling 73 | 74 | # install locally built module to test 75 | #pip install dist/langchain_tableau-$NEW_VERSION-py3-none-any.whl --force-reinstall 76 | 77 | # install package in debug mode. This will allow us to reference tableau_langchain from other folders in the project 78 | # cd pkg 79 | # pip install -e . 80 | 81 | # pip install from PyPI Test 82 | # pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ langchain-tableau 83 | 84 | # pip install from PyPI Prod 85 | # pip install langchain-tableau 86 | 87 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tableau Langchain", 3 | "description": "Tableau tools for Agentic use cases with Langgraph", 4 | "repository": "https://github.com/Tab-SE/tableau_langchain", 5 | "keywords": ["python", "tableau", "langchain", "langgraph", "agents", "tools", "ai"], 6 | "env": { 7 | "DEBUG": { 8 | "description": "Controls how much logging occurs during Agent operations can be 1 or 0", 9 | "value": "0" 10 | }, 11 | "OPENAI_API_KEY": { 12 | "description": "OpenAI API Key required for using ChatGPT", 13 | "value": "" 14 | }, 15 | "AGENT_MODEL": { 16 | "description": "LLM model used by the Agent such as (gpt-4o-mini)", 17 | "value": "gpt-4o-mini" 18 | }, 19 | "RETRIEVER_MODEL": { 20 | "description": "Embedding model used by Retriever Tools such as (text-embedding-3-small)", 21 | "value": "text-embedding-3-small" 22 | }, 23 | "LANGCHAIN_TRACING_V2": { 24 | "description": "Boolean either true or false for agent tracing on Langsmith", 25 | "value": "true" 26 | }, 27 | "LANGCHAIN_ENDPOINT": { 28 | "description": "Service for hosting agent traces for analysis (Langsmith)", 29 | "value": "https://api.smith.langchain.com" 30 | }, 31 | "LANGCHAIN_API_KEY": { 32 | "description": "Langchain API Key for using Langsmith", 33 | "value": "" 34 | }, 35 | "LANGCHAIN_PROJECT": { 36 | "description": "Project containing agent traces on Langsmith", 37 | "value": "" 38 | }, 39 | "TAVILY_API_KEY": { 40 | "description": "API Key for Web Search via the Tavily service", 41 | "value": "" 42 | }, 43 | "PINECONE_API_KEY": { 44 | "description": "API Key for managing serverless vector databases on Pinecone", 45 | "value": "" 46 | }, 47 | "PINECONE_ENVIRONMENT": { 48 | "description": "The cloud environment hosting the Pinecone serverless vector databases", 49 | "value": "" 50 | }, 51 | "TABLEAU_DOMAIN": { 52 | "description": "Hostname for your Tableau Cloud environment", 53 | "value": "" 54 | }, 55 | "TABLEAU_SITE": { 56 | "description": "Tableau Site Name (or contentUrl)", 57 | "value": "" 58 | }, 59 | "TABLEAU_API": { 60 | "description": "Version of the Tableau REST API used for some Agent operations", 61 | "value": "3.21" 62 | }, 63 | "TABLEAU_JWT_CLIENT_ID": { 64 | "description": "Client ID from an enabled Tableau Connected App used for JWT authentication", 65 | "value": "" 66 | }, 67 | "TABLEAU_JWT_SECRET_ID": { 68 | "description": "Secret ID from an enabled Tableau Connected App used for JWT authentication", 69 | "value": "" 70 | }, 71 | "TABLEAU_JWT_SECRET": { 72 | "description": "Secret from an enabled Tableau Connected App used for JWT authentication", 73 | "value": "" 74 | }, 75 | "TABLEAU_USER": { 76 | "description": "A valid Tableau user for access to the analytics environment", 77 | "value": "" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /experimental/agents/utils/agent_utils.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | 5 | from IPython.display import Image, display 6 | 7 | 8 | def _visualize_graph(graph): 9 | """ 10 | Creates a mermaid visualization of the State Graph in .png format 11 | """ 12 | 13 | # Attempt to generate and save PNG 14 | try: 15 | png_data = graph.get_graph().draw_mermaid_png() 16 | filename = "graph_visualization.png" 17 | with open(filename, "wb") as f: 18 | f.write(png_data) 19 | 20 | if os.path.exists(filename): 21 | file_size = os.path.getsize(filename) 22 | print(f"Agent Graph saved as '{filename}' | file size: {file_size} bytes") 23 | 24 | 25 | display(Image(png_data)) 26 | else: 27 | print(f"Failed to create file '{filename}'") 28 | except Exception as e: 29 | print(f"Failed to generate PNG: {str(e)}") 30 | 31 | 32 | async def stream_graph_updates(message: dict, graph): 33 | """ 34 | This function streams responses from Agents to clients, such as chat interfaces, by processing 35 | user inputs and dynamically updating the conversation. 36 | 37 | The function takes a string input from the user and passes it to the state graph's streaming interface. 38 | It initiates a stream of events based on the provided user input, which is wrapped in a dictionary with 39 | a key "messages" containing a tuple of the user role and content. 40 | 41 | As each event is generated by the graph, the function iterates over the values returned. Within each event, 42 | it specifically looks for messages associated with the 'messages' key. The function extracts and prints the 43 | content of the last message in the sequence, which is assumed to be the assistant's most recent response. 44 | This enables a real-time conversation-like interaction where responses are generated and displayed immediately 45 | based on user input. 46 | 47 | if debugging is enabled (checked via an environment variable), it prints out the content of the last message 48 | for further inspection. 49 | 50 | Parameters: 51 | - message (dict): contains a string with the user_message and additional operating parameters 52 | - graph: a representation of the agents behavior and tool set 53 | 54 | Returns: 55 | - None. The function's primary side effect is to print the assistant's response to the console. 56 | """ 57 | 58 | message_string = json.dumps(message['user_message']) 59 | 60 | tableau_credentials = message['agent_inputs']['tableau_credentials'] 61 | datasource = message['agent_inputs']['datasource'] 62 | 63 | # this is how client apps should format their requests to the Agent API 64 | input_stream = { 65 | "messages": [("user", message_string)], 66 | "tableau_credentials": tableau_credentials, 67 | "datasource": datasource 68 | } 69 | 70 | # gets value DEBUG value or sets it to empty string, condition applies if string is empty or 0 71 | if os.environ.get("DEBUG", "") in ["0", ""]: 72 | # streams events from the agent graph started by the client input containing user queries 73 | async for event in graph.astream(input_stream): 74 | agent_output = event.get('agent') 75 | if event.get('agent'): 76 | agent_message = agent_output["messages"][0].content 77 | if len(agent_message) > 0: 78 | print("\nAgent:") 79 | print(f"{agent_message} \n") 80 | 81 | elif (os.environ["DEBUG"] == "1"): 82 | # display tableau credentials to prove access to the environment 83 | print('*** tableau_credentials ***', message.get('tableau_credentials')) 84 | 85 | async for event in graph.astream(input_stream): 86 | print(f"*** EVENT *** type: {type(event)}") 87 | print(event) 88 | -------------------------------------------------------------------------------- /experimental/agents/experimental/prompt.py: -------------------------------------------------------------------------------- 1 | AGENT_IDENTITY = """ 2 | You are an Experimental Agent used to test new Tableau agent features in the `experimental/` folder 3 | 4 | Let the user know about your purpose and this conference when you first introduce yourself 5 | """ 6 | 7 | AGENT_SYSTEM_PROMPT = f"""Agent Identity: 8 | {AGENT_IDENTITY} 9 | 10 | Instructions: 11 | 12 | You are an AI Analyst designed to generate data-driven insights to provide answers, guidance and analysis 13 | to humans and other AI Agents. Your role is to understand the tasks assigned to you and use one or more tools 14 | to obtain the information necessary to answer a question. 15 | 16 | Tool Choice: 17 | 1. Query Data Source: performs ad-hoc queries and analysis. Prioritize this tool for most requests, especially if 18 | the user explicitly asks for data queries/fetches. This tool is great for getting values for specific dates, for 19 | breakdowns by category, for aggregations such as AVG and MAX, for filtered results, etc. 20 | specific data values such as values on a specific date 21 | 2. Metrics: returns ML generated metric insights describing KPI trends, activity and the impact of other fields of data 22 | on metric performance. This is not a good tool for fetching values for specific dates, filter conditions, aggegations, etc., 23 | rather it describes user metrics according to definitions useful to them. Use this tool for metrics research when you are 24 | asked to produce a more long form report or document 25 | 26 | 27 | Sample Interactions: 28 | 29 | Scenario 1 - Metrics Summary 30 | User: How are my KPIs doing? 31 | Assistant: [provides a summary of KPI activity using data from the metrics tool] 32 | Result: Correct by prioritizing fast answers to the user needs 33 | 34 | User: How are my KPIs doing? 35 | Assistant: What metrics are you interested in knowing more about? 36 | Result: Incorrect, available tools should be able to provide a simple summary to answer this question 37 | or to gather more information before continuing the conversation with the user 38 | 39 | Scenario 2 - Metrics Research 40 | User: How is my sales metric performing? 41 | Assistant: [sends a scoping query to the metrics tool asking about performance and additional fields or dimensions] 42 | Assistant: [analyzes these preliminary results and sends follow up queries] 43 | User: Thanks, I would like to know which categories, states and customers have the greates and lowest sales 44 | Assistant: [sends queries to the metrics tool using these follow up instructions] 45 | Result: Correct by gathering preliminary information and additional context to answer a complex question 46 | 47 | User: How is my sales metric performing? 48 | Assistant: [sends the question verbatim to the metrics tool and generates a response without follow ups] 49 | Result: Incorrect, the agent is not effectively doing metrics research by not making multiple and thorough queries 50 | 51 | Scenario 4 - Data Querying 52 | User: what is the value of sales for the east region in the year 2024? 53 | Assistant: [uses the data query tool] 54 | Result: Correct, even though this question may be related to a metric it implies that a data query 55 | is necessary since it is requesting specific data with filtering and aggregations. Metrics cannot 56 | produce specific values such as sales on a specific date 57 | 58 | User: what is the value of sales for the east region in the year 2024? 59 | Assistant: [searches for an answer with the metrics tool] 60 | Result: Incorrect, even though this question may be related to a metric this tool is not useful for 61 | fetching specific values involving dates, categories or other filters 62 | 63 | 64 | Restrictions: 65 | - DO NOT HALLUCINATE metrics or data sets if they are not mentioned via available tools 66 | 67 | Output: 68 | Your output should be structured like a report noting the source of information (metrics or data source) 69 | Always answer the question first and then provide any additional details or insights 70 | """ 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | 164 | # Langgraph 165 | .langgraph_api 166 | 167 | data/ 168 | 169 | # Mac 170 | .DS_Store 171 | 172 | # temporary graph visualization is updated with changes to agent run via main.py 173 | graph_visualization.png 174 | 175 | # Salesforce VSCode extension 176 | .sfdx 177 | .codegenie -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | # langchain-tableau 2 | 3 | [![PyPI version](https://badge.fury.io/py/langchain-tableau.svg)](https://badge.fury.io/py/langchain-tableau) 4 | [![GitHub Repo](https://img.shields.io/badge/GitHub-Repository-blue?logo=github)](https://github.com/Tab-SE/tableau_langchain) 5 | 6 | This package provides Langchain integrations for Tableau, enabling you to build Agentic tools using Tableau's capabilities within the [Langchain](https://www.langchain.com/) and [LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/) frameworks. 7 | 8 | Use these tools to bridge the gap between your organization's Tableau data assets and the natural language queries of your users, empowering them to get answers directly from data through conversational AI agents. 9 | 10 | ![Tableau Logo](https://raw.githubusercontent.com/Tab-SE/tableau_langchain/main/experimental/notebooks/assets/tableau_logo_text.png) 11 | 12 | ## Installation 13 | 14 | ```bash 15 | pip install langchain-tableau 16 | ``` 17 | 18 | ## Quick Start 19 | Here's a basic example of using the `simple_datasource_qa` tool to query a Tableau Published Datasource with a Langgraph agent: 20 | 21 | ```python 22 | # --- Core Langchain/LangGraph Imports --- 23 | from langchain_openai import ChatOpenAI 24 | from langgraph.prebuilt import create_react_agent 25 | 26 | # --- langchain-tableau Imports --- 27 | from langchain_tableau.tools.simple_datasource_qa import initialize_simple_datasource_qa 28 | 29 | # 1. Initialize your preferred LLM 30 | llm = ChatOpenAI(model='gpt-4o-mini', temperature=0) # Example using OpenAI 31 | 32 | # 2. Initialize the Tableau Datasource Query tool 33 | # Replace placeholders with your Tableau environment details 34 | analyze_datasource = initialize_simple_datasource_qa( 35 | domain='[https://your-tableau-cloud-or-server.com](https://your-tableau-cloud-or-server.com)', 36 | site='YourTableauSiteName', # The site content URL, not the display name 37 | jwt_client_id='YOUR_CONNECTED_APP_CLIENT_ID', 38 | jwt_secret_id='YOUR_CONNECTED_APP_SECRET_ID', 39 | jwt_secret='YOUR_CONNECTED_APP_SECRET_VALUE', 40 | tableau_api_version='3.22', # Or your target REST API version 41 | tableau_user='user@example.com', # User context for the query 42 | datasource_luid='YOUR_DATASOURCE_LUID', # LUID of the Published Datasource 43 | tooling_llm_model='gpt-4o-mini' # LLM used internally by the tool 44 | ) 45 | 46 | # 3. Create a list of tools for your agent 47 | tools = [ analyze_datasource ] 48 | 49 | # 4. Build the Agent 50 | # This example uses a prebuilt ReAct agent from LangGraph 51 | tableauAgent = create_react_agent(llm, tools) 52 | 53 | # 5. Run the Agent with a question 54 | question = 'Which states sell the most? Are those the same states with the most profits?' 55 | messages = tableauAgent.invoke({"messages": [("human", question)]}) 56 | 57 | # Process and display the agent's response 58 | print(messages['messages'][-1].content) 59 | ``` 60 | 61 | ## Available Tools 62 | 63 | This package currently offers the following production-ready tools: 64 | 65 | 1. **`simple_datasource_qa`**: 66 | * Allows users to query a Tableau Published Datasource using natural language. 67 | * Leverages the analytical power of Tableau's VizQL Data Service engine for aggregation, filtering (and soon, calculations!). 68 | * Ensures security by interacting via Tableau's API layer, preventing direct SQL injection risks. Authentication is handled via Tableau Connected Apps (JWT). 69 | 70 | ## Learn More & Contribute 71 | 72 | * **Full Documentation & Examples:** For detailed usage, advanced examples (including Jupyter Notebooks), contribution guidelines, and information about the experimental sandbox where new features are developed, please visit our [**GitHub Repository**](https://github.com/Tab-SE/tableau_langchain). 73 | * **Live Demos:** See agents using Tableau in action at [EmbedTableau.com](https://www.embedtableau.com/) ([GitHub](https://github.com/Tab-SE/embedding_playbook)). 74 | 75 | We welcome contributions! Whether it's improving existing tools, adding new ones, or enhancing documentation, please check out the [Contribution Guidelines](https://github.com/Tab-SE/tableau_langchain/blob/main/.github/CONTRIBUTING.md) on GitHub. 76 | 77 | Let's increase the flow of data and help people get answers! 78 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/rag_demo.py: -------------------------------------------------------------------------------- 1 | from modules import graphql 2 | import chromadb 3 | import numpy as np 4 | from openai import OpenAI 5 | from dotenv import load_dotenv 6 | import os 7 | load_dotenv() 8 | import chromadb.utils.embedding_functions as embedding_functions 9 | 10 | openai_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) 11 | 12 | def get_embedding_openai(text, model="text-embedding-3-small"): 13 | text = text.replace("\n", " ") 14 | return openai_client.embeddings.create(input = [text], model=model).data[0].embedding 15 | 16 | 17 | openai_ef = embedding_functions.OpenAIEmbeddingFunction( 18 | api_key=os.getenv('OPENAI_API_KEY'), 19 | model_name="text-embedding-3-small" 20 | ) 21 | 22 | def convert_to_string(value): 23 | if isinstance(value, dict): 24 | return str(value) 25 | elif isinstance(value, list): 26 | return ', '.join(map(str, value)) 27 | else: 28 | return str(value) 29 | 30 | server, auth = graphql.get_tableau_client() 31 | datasources = graphql.fetch_datasources(server, auth) 32 | 33 | # Initialise Chroma 34 | chroma_client = chromadb.PersistentClient(path="data") 35 | collection_name = 'tableau_datasource_RAG_search' 36 | collection = chroma_client.get_collection(name=collection_name, embedding_function=openai_ef) 37 | 38 | if collection: 39 | print("Collection exists.") 40 | # Run your series of code here 41 | else: 42 | print("Collection does not exist. Creating collection...") 43 | documents = [] 44 | embeddings = [] 45 | ids = [] 46 | metadatas = [] 47 | 48 | for datasource in datasources: 49 | # Extract the text to embed 50 | text_to_embed = datasource['dashboard_overview'] 51 | 52 | # Extract the unique identifier 53 | unique_id = datasource['id'] 54 | 55 | # Prepare metadata (exclude 'dashboard_overview' and 'id') 56 | metadata = {k: v for k, v in datasource.items() if k not in ['dashboard_overview', 'id']} 57 | 58 | # Remove any nested data structures from metadata (e.g., lists, dicts) 59 | metadata = {k: convert_to_string(v) for k, v in metadata.items() if isinstance(v, (str, int, float, bool, dict, list))} 60 | 61 | documents.append(text_to_embed) 62 | ids.append(unique_id) 63 | 64 | metadatas.append(metadata) 65 | 66 | # Create vector db with openai embedding 67 | collection = chroma_client.get_or_create_collection(name=collection_name, embedding_function=openai_ef) 68 | 69 | collection.add( 70 | documents=documents, 71 | metadatas=metadatas, 72 | ids=ids 73 | ) 74 | 75 | # to Reset vector db 76 | # # chroma_client.delete_collection(name=collection_name) 77 | # collection = chroma_client.get_or_create_collection(name=collection_name, embedding_function=openai_ef) 78 | 79 | results = collection.query( 80 | query_texts=["Why is my dashboard slow?"], 81 | n_results=2 82 | ) 83 | 84 | metadatas = results['metadatas'] 85 | distances = results['distances'] 86 | 87 | # Initialize an empty list to store extracted data 88 | extracted_data = [] 89 | 90 | for meta_list, dist_list in zip(metadatas, distances): 91 | for metadata, distance in zip(meta_list, dist_list): 92 | name = metadata.get('name', 'N/A') 93 | uri = metadata.get('uri', 'N/A') 94 | luid = metadata.get('luid', 'N/A') 95 | isCertified = metadata.get('isCertified', 'N/A') 96 | updatedAt = metadata.get('updatedAt', 'N/A') 97 | 98 | # Append the extracted data to the list, including 'distance' 99 | extracted_data.append({ 100 | 'name': name, 101 | 'uri': uri, 102 | 'luid': luid, 103 | 'isCertified': isCertified, 104 | 'updatedAt': updatedAt, 105 | 'distance': distance 106 | }) 107 | 108 | 109 | for item in extracted_data: 110 | print(f"Name: {item['name']}") 111 | print(f"URI: {item['uri']}") 112 | print(f"LUID: {item['luid']}") 113 | print(f"Certified?: {item['isCertified']}") 114 | print(f"Last Update: {item['updatedAt']}") 115 | print(f"Vector distance: {item['distance']}") 116 | print("-" * 40) 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /experimental/agents/superstore/prompt.py: -------------------------------------------------------------------------------- 1 | AGENT_IDENTITY = """ 2 | You are an AI Analyst supporting the Superstore web application at [EmbedTableau.com](https://www.embedtableau.com/demo/superstore). 3 | 4 | Let the user know about your purpose and this conference when you first introduce yourself. When asked to describe who you are 5 | you must always mention the tools you have available to help the user. 6 | 7 | You have access to Superstore sales data to answer user queries, Tableau Pulse metrics monitoring the business and the Data Catalog 8 | to search for data sources and workbooks. 9 | """ 10 | 11 | AGENT_SYSTEM_PROMPT = f"""Agent Identity: 12 | {AGENT_IDENTITY} 13 | 14 | Instructions: 15 | 16 | You are an AI Analyst designed to generate data-driven insights to provide answers, guidance and analysis 17 | to humans and other AI Agents. Your role is to understand the tasks assigned to you and use one or more tools 18 | to obtain the information necessary to answer a question. 19 | 20 | Tool Choice: 21 | 1. Query Data Source: performs ad-hoc queries and analysis. Prioritize this tool for most requests, especially if 22 | the user explicitly asks for data queries/fetches. This tool is great for getting values for specific dates, for 23 | breakdowns by category, for aggregations such as AVG and MAX, for filtered results, etc. 24 | specific data values such as values on a specific date 25 | 2. Metrics: returns ML generated metric insights describing KPI trends, activity and the impact of other fields of data 26 | on metric performance. This is not a good tool for fetching values for specific dates, filter conditions, aggegations, etc., 27 | rather it describes user metrics according to definitions useful to them. Use this tool for metrics research when you are 28 | asked to produce a more long form report or document 29 | 30 | 31 | Sample Interactions: 32 | 33 | Scenario 1 - Metrics Summary 34 | User: How are my KPIs doing? 35 | Assistant: [provides a summary of KPI activity using data from the metrics tool] 36 | Result: Correct by prioritizing fast answers to the user needs 37 | 38 | User: How are my KPIs doing? 39 | Assistant: What metrics are you interested in knowing more about? 40 | Result: Incorrect, available tools should be able to provide a simple summary to answer this question 41 | or to gather more information before continuing the conversation with the user 42 | 43 | Scenario 2 - Metrics Research 44 | User: How is my sales metric performing? 45 | Assistant: [sends a scoping query to the metrics tool asking about performance and additional fields or dimensions] 46 | Assistant: [analyzes these preliminary results and sends follow up queries] 47 | User: Thanks, I would like to know which categories, states and customers have the greates and lowest sales 48 | Assistant: [sends queries to the metrics tool using these follow up instructions] 49 | Result: Correct by gathering preliminary information and additional context to answer a complex question 50 | 51 | User: How is my sales metric performing? 52 | Assistant: [sends the question verbatim to the metrics tool and generates a response without follow ups] 53 | Result: Incorrect, the agent is not effectively doing metrics research by not making multiple and thorough queries 54 | 55 | Scenario 4 - Data Querying 56 | User: what is the value of sales for the east region in the year 2024? 57 | Assistant: [uses the data query tool] 58 | Result: Correct, even though this question may be related to a metric it implies that a data query 59 | is necessary since it is requesting specific data with filtering and aggregations. Metrics cannot 60 | produce specific values such as sales on a specific date 61 | 62 | User: what is the value of sales for the east region in the year 2024? 63 | Assistant: [searches for an answer with the metrics tool] 64 | Result: Incorrect, even though this question may be related to a metric this tool is not useful for 65 | fetching specific values involving dates, categories or other filters 66 | 67 | 68 | Restrictions: 69 | - DO NOT HALLUCINATE metrics or data sets if they are not mentioned via available tools 70 | 71 | Output: 72 | Your output should be structured like a report noting the source of information (metrics or data source) 73 | Always answer the question first and then provide any additional details or insights 74 | You should favor writing tables instead of lists when showing data, with numbered preferred over unnumbered lists 75 | """ 76 | -------------------------------------------------------------------------------- /experimental/demos/rag_demo_flask.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, render_template 2 | from modules import graphql 3 | import chromadb 4 | import numpy as np 5 | from openai import OpenAI 6 | from dotenv import load_dotenv 7 | import os 8 | import chromadb.utils.embedding_functions as embedding_functions 9 | 10 | # Load environment variables 11 | load_dotenv() 12 | 13 | # Initialize Flask app 14 | app = Flask(__name__) 15 | 16 | openai_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) 17 | 18 | def get_embedding_openai(text, model="text-embedding-3-small"): 19 | text = text.replace("\n", " ") 20 | return openai_client.embeddings.create(input = [text], model=model).data[0].embedding 21 | 22 | 23 | openai_ef = embedding_functions.OpenAIEmbeddingFunction( 24 | api_key=os.getenv('OPENAI_API_KEY'), 25 | model_name="text-embedding-3-small" 26 | ) 27 | 28 | def convert_to_string(value): 29 | if isinstance(value, dict): 30 | return str(value) 31 | elif isinstance(value, list): 32 | return ', '.join(map(str, value)) 33 | else: 34 | return str(value) 35 | 36 | # Initialize the Chroma client 37 | chroma_client = chromadb.PersistentClient(path="data") 38 | collection_name = 'tableau_datasource_RAG_search' 39 | 40 | # Try to get the collection 41 | try: 42 | collection = chroma_client.get_collection(name=collection_name, embedding_function=openai_ef) 43 | print("Collection exists.") 44 | except Exception as e: 45 | print("Collection does not exist. Creating collection...") 46 | # Fetch data from Tableau 47 | server, auth = graphql.get_tableau_client() 48 | datasources = graphql.fetch_datasources(server, auth) 49 | 50 | documents = [] 51 | ids = [] 52 | metadatas = [] 53 | 54 | for datasource in datasources: 55 | # Extract the text to embed 56 | text_to_embed = datasource['dashboard_overview'] 57 | 58 | # Extract the unique identifier 59 | unique_id = datasource['id'] 60 | 61 | # Prepare metadata (exclude 'dashboard_overview' and 'id') 62 | metadata = {k: v for k, v in datasource.items() if k not in ['dashboard_overview', 'id']} 63 | 64 | # Convert metadata values to strings 65 | metadata = {k: convert_to_string(v) for k, v in metadata.items() if isinstance(v, (str, int, float, bool, dict, list))} 66 | 67 | documents.append(text_to_embed) 68 | ids.append(unique_id) 69 | metadatas.append(metadata) 70 | 71 | # Create the collection and add data 72 | collection = chroma_client.get_or_create_collection(name=collection_name, embedding_function=openai_ef) 73 | collection.add( 74 | documents=documents, 75 | metadatas=metadatas, 76 | ids=ids 77 | ) 78 | 79 | # Route to display the search form 80 | @app.route('/', methods=['GET']) 81 | def index(): 82 | return render_template('search.html') 83 | 84 | # Route to handle search queries 85 | @app.route('/search', methods=['POST']) 86 | def search(): 87 | # Get the user's query from the form 88 | user_input = request.form.get('query') 89 | if not user_input: 90 | return jsonify({"error": "No query provided"}), 400 91 | 92 | # Perform the query 93 | results = collection.query( 94 | query_texts=[user_input], 95 | n_results=5 96 | ) 97 | 98 | metadatas = results['metadatas'] 99 | distances = results['distances'] 100 | 101 | # Initialize an empty list to store extracted data 102 | extracted_data = [] 103 | 104 | for meta_list, dist_list in zip(metadatas, distances): 105 | for metadata, distance in zip(meta_list, dist_list): 106 | name = metadata.get('name', 'N/A') 107 | uri = metadata.get('uri', 'N/A') 108 | luid = metadata.get('luid', 'N/A') 109 | isCertified = metadata.get('isCertified', 'N/A') 110 | updatedAt = metadata.get('updatedAt', 'N/A') 111 | 112 | # Append the extracted data to the list, including 'distance' 113 | extracted_data.append({ 114 | 'name': name, 115 | 'uri': uri, 116 | 'luid': luid, 117 | 'isCertified': isCertified, 118 | 'updatedAt': updatedAt, 119 | 'distance': distance 120 | }) 121 | 122 | # Render the results template 123 | return render_template('results.html', results=extracted_data, query=user_input) 124 | 125 | # Run the Flask app 126 | if __name__ == '__main__': 127 | app.run(debug=True) 128 | -------------------------------------------------------------------------------- /experimental/chains/search_datasources/modules/graphql.py: -------------------------------------------------------------------------------- 1 | import tableauserverclient as TSC 2 | from dotenv import load_dotenv 3 | import os 4 | 5 | def get_tableau_client(): 6 | load_dotenv() 7 | tableau_server = 'https://' + os.getenv('TABLEAU_DOMAIN') 8 | tableau_pat_name = os.getenv('PAT_NAME') 9 | tableau_pat_secret = os.getenv('PAT_SECRET') 10 | tableau_sitename = os.getenv('SITE_NAME') 11 | tableau_auth = TSC.PersonalAccessTokenAuth(tableau_pat_name, tableau_pat_secret, tableau_sitename) 12 | server = TSC.Server(tableau_server, use_server_version=True) 13 | return server, tableau_auth 14 | 15 | def fetch_dashboard_data(server, auth): 16 | with server.auth.sign_in(auth): 17 | # Read the GraphQL query from the file 18 | query_file_path = os.path.join('query_data_chain','modules','prompts', 'tab_dashboard_fields.graphql') 19 | with open(query_file_path, 'r') as f: 20 | query = f.read() 21 | # Query the Metadata API and store the response in resp 22 | resp = server.metadata.query(query) 23 | return resp['data']['dashboards'] 24 | 25 | def fetch_sheets_data(server, auth): 26 | with server.auth.sign_in(auth): 27 | # Read the GraphQL query from the file 28 | query_file_path = os.path.join('query_data_chain','modules','prompts','tab_sheets.graphql') 29 | with open(query_file_path, 'r') as f: 30 | query = f.read() 31 | # Query the Metadata API and store the response in resp 32 | resp = server.metadata.query(query) 33 | return resp['data']['sheets'] 34 | 35 | def fetch_datasources(server, auth): 36 | with server.auth.sign_in(auth): 37 | 38 | # Read the GraphQL query from the file 39 | query_file_path = os.path.join('query_data_chain','modules','prompts','tab_datasources.graphql') 40 | with open(query_file_path, 'r') as f: 41 | query = f.read() 42 | 43 | # Query the Metadata API and store the response in resp 44 | resp = server.metadata.query(query) 45 | 46 | # Prepare datasources for RAG 47 | datasources = resp['data']['publishedDatasources'] 48 | 49 | for datasource in datasources: 50 | 51 | # Combine datasource columns (is not hidden) to one cell for RAG 52 | fields = datasource['fields'] 53 | 54 | field_entries = [] 55 | for field in fields: 56 | # Exclude columns that are hidden 57 | if not field.get('isHidden', True): 58 | name = field.get('name', '') 59 | description = field.get('description', '') 60 | # If there's a description include it 61 | if description: 62 | # Remove newlines and extra spaces 63 | description = ' '.join(description.split()) 64 | field_entry = f"- {name}: [{description}]" 65 | else: 66 | field_entry = "- " + name 67 | field_entries.append(field_entry) 68 | 69 | # Combining Datasource columns 70 | concatenated_field_entries = '\n'.join(field_entries) 71 | 72 | # Datasource RAG headers 73 | datasource_name = datasource['name'] 74 | datasource_desc = datasource['description'] 75 | datasource_project = datasource['projectName'] 76 | 77 | # Formating Output for readability 78 | rag_column = f"Datasource: {datasource_name}\n{datasource_desc}\n{datasource_project}\n\nDatasource Columns:\n{concatenated_field_entries}" 79 | 80 | datasource['dashboard_overview'] = rag_column 81 | 82 | # Simplifying output schema 83 | keys_to_extract = [ 84 | 'dashboard_overview', 85 | 'id', 86 | 'luid', 87 | 'uri', 88 | 'vizportalId', 89 | 'vizportalUrlId', 90 | 'name', 91 | 'hasExtracts', 92 | 'createdAt', 93 | 'updatedAt', 94 | 'extractLastUpdateTime', 95 | 'extractLastRefreshTime', 96 | 'extractLastIncrementalUpdateTime', 97 | 'projectName', 98 | 'containerName', 99 | 'isCertified', 100 | 'description' 101 | ] 102 | 103 | # Create a new dictionary with only the specified keys 104 | datasource = {key: datasource.get(key) for key in keys_to_extract} 105 | 106 | return datasources 107 | 108 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | 4 | from dotenv import load_dotenv 5 | from experimental.agents.experimental.agent import analytics_agent 6 | from experimental.agents.utils.agent_utils import stream_graph_updates, _visualize_graph 7 | from langchain_tableau.utilities.auth import jwt_connected_app 8 | 9 | 10 | async def main(): 11 | """ 12 | TABLEAU AGENT STAGING PLATFORM 13 | 14 | Stage individual LangGraph Agents and Tableau AI tools to test functionality such as: 15 | - Metrics (canonical source of truth for metrics, includes machine learning insights generated by Tableau Pulse) 16 | - Workbooks (contains analytics such as dashboards and charts that server as canonical interfaces for data exploration) 17 | - Data Sources (describes sources of data available for querying and exploration) 18 | - Headless BI (can query a data source for on-demand data sets including aggregations, filters and calculations) 19 | - Web Search (can incorporate external knowledge from the web) 20 | 21 | Execute behavior within different agentic architectures 22 | """ 23 | # environment variables available to current process and sub processes 24 | load_dotenv() 25 | 26 | domain = os.environ['TABLEAU_DOMAIN'] 27 | site = os.environ['TABLEAU_SITE'] 28 | datasource_luid = os.environ["DATASOURCE_LUID"] 29 | # define required authorizations to Tableau resources to support Agent operations 30 | access_scopes = [ 31 | "tableau:content:read", # for quering Tableau Metadata API 32 | "tableau:viz_data_service:read" # for querying VizQL Data Service 33 | ] 34 | 35 | tableau_auth = jwt_connected_app( 36 | jwt_client_id=os.environ['TABLEAU_JWT_CLIENT_ID'], 37 | jwt_secret_id=os.environ['TABLEAU_JWT_SECRET_ID'], 38 | jwt_secret=os.environ['TABLEAU_JWT_SECRET'], 39 | tableau_api=os.environ['TABLEAU_API_VERSION'], 40 | tableau_user=os.environ['TABLEAU_USER'], 41 | tableau_domain=domain, 42 | tableau_site=site, 43 | scopes=access_scopes 44 | ) 45 | 46 | tableau_session = tableau_auth['credentials']['token'] 47 | 48 | sample_inputs = { 49 | 'tableau_credentials': { 50 | "session": tableau_session, 51 | "url": domain, 52 | "site": site, 53 | }, 54 | 'datasource': { 55 | "luid": datasource_luid, 56 | "name": None, 57 | "description": None 58 | }, 59 | 'workbook': { 60 | "luid": None, 61 | "name": None, 62 | "description": None, 63 | 'sheets': None, 64 | 'viz_url': None 65 | }, 66 | 'rag': { 67 | 'analytics': { 68 | "metrics": None, 69 | "workbooks": None, 70 | "datasources": None 71 | }, 72 | 'knowledge_base': { 73 | "tableau": None, 74 | "agent": None, 75 | 'app': None 76 | } 77 | } 78 | } 79 | 80 | # initialize one of the repo's custom agents 81 | agent = analytics_agent 82 | 83 | # outputs a mermaid diagram of the graph in png format 84 | _visualize_graph(agent) 85 | 86 | print("\nWelcome to the Tableau Agent Staging Environment!") 87 | print("Enter a prompt or type 'exit' to end \n") 88 | 89 | # User input loop 90 | while True: 91 | try: 92 | user_input = input("User: \n").strip() # Use .strip() to remove leading/trailing whitespace 93 | 94 | if user_input.lower() in ["quit", "exit", "q", "stop", "end"]: 95 | print("Exiting Tableau Agent Staging Environment...") 96 | print("Goodbye!") 97 | break 98 | 99 | # If user input is empty, set to default string 100 | if not user_input: 101 | user_input = "show me average discount, total sales and profits by region sorted by profit\n" 102 | print("Test input: " + user_input) 103 | 104 | message = { 105 | "user_message": user_input, 106 | "agent_inputs": sample_inputs 107 | } 108 | 109 | await stream_graph_updates(message, agent) 110 | 111 | except Exception as e: 112 | print(f"An error occurred: {e}") 113 | # Use diagnostic string in case of an error 114 | diagnostic_input = f"The previous operation '{user_input}', failed with this error: {e}.\n Write a query to test this tool again and describe the issue" 115 | print("Retrying with diagnostics input: " + diagnostic_input) 116 | 117 | message = { 118 | "user_message": diagnostic_input, 119 | "agent_inputs": sample_inputs 120 | } 121 | 122 | await stream_graph_updates(message, agent) 123 | break 124 | 125 | if __name__ == "__main__": 126 | asyncio.run(main()) 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide For Tableau Langchain 2 | 3 | This page lists the operational governance model of this project, as well as the recommendations and requirements for how to best contribute to Tableau Langchain. We strive to obey these as best as possible. As always, thanks for contributing – we hope these guidelines make it easier and shed some light on our approach and processes. 4 | 5 | # Governance Model 6 | 7 | ## Community Based 8 | 9 | The intent and goal of open sourcing this project is to increase the contributor and user base. The governance model is one where new project leads (`admins`) will be added to the project based on their contributions and efforts, a so-called "do-acracy" or "meritocracy" similar to that used by all Apache Software Foundation projects. 10 | 11 | # Issues, requests & ideas 12 | 13 | Use GitHub Issues page to submit issues, enhancement requests and discuss ideas. 14 | 15 | ### Bug Reports and Fixes 16 | - If you find a bug, please search for it in the [Issues](https://github.com/tableau/tableau_langchain/issues), and if it isn't already tracked, 17 | [create a new issue](https://github.com/tableau/tableau_langchain/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still 18 | be reviewed. 19 | - Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`. 20 | - If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number. 21 | - Include tests that isolate the bug and verifies that it was fixed. 22 | 23 | ### New Features 24 | - If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/tableau/tableau_langchain/issues/new). 25 | - Issues that have been identified as a feature request will be labelled `enhancement`. 26 | - If you'd like to implement the new feature, please wait for feedback from the project 27 | maintainers before spending too much time writing the code. In some cases, `enhancement`s may 28 | not align well with the project objectives at the time. 29 | 30 | ### Tests, Documentation, Miscellaneous 31 | - If you'd like to improve the tests, you want to make the documentation clearer, you have an 32 | alternative implementation of something that may have advantages over the way its currently 33 | done, or you have any other change, we would be happy to hear about it! 34 | - If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind. 35 | - If not, [open an Issue](https://github.com/tableau/tableau_langchain/issues/new) to discuss the idea first. 36 | 37 | If you're new to our project and looking for some way to make your first contribution, look for 38 | Issues labelled `good first contribution`. 39 | 40 | # Contribution Checklist 41 | 42 | - [x] Clean, simple, well styled code 43 | - [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. 44 | - [x] Comments 45 | - Module-level & function-level comments. 46 | - Comments on complex blocks of code or algorithms (include references to sources). 47 | - [x] Tests 48 | - The test suite, if provided, must be complete and pass 49 | - Increase code coverage, not versa. 50 | - Use any of our testkits that contains a bunch of testing facilities you would need. For example: `import com.salesforce.op.test._` and borrow inspiration from existing tests. 51 | - [x] Dependencies 52 | - Minimize number of dependencies. 53 | - Prefer Apache 2.0, BSD3, MIT, ISC and MPL licenses. 54 | - [x] Reviews 55 | - Changes must be approved via peer code review 56 | 57 | # Creating a Pull Request 58 | 59 | 1. **Ensure the bug/feature was not already reported** by searching on GitHub under Issues. If none exists, create a new issue so that other contributors can keep track of what you are trying to add/fix and offer suggestions (or let you know if there is already an effort in progress). 60 | 3. **Clone** the forked repo to your machine. 61 | 4. **Create** a new branch to contain your work (e.g. `git br fix-issue-11`) 62 | 4. **Commit** changes to your own branch. 63 | 5. **Push** your work back up to your fork. (e.g. `git push fix-issue-11`) 64 | 6. **Submit** a Pull Request against the `main` branch and refer to the issue(s) you are fixing. Try not to pollute your pull request with unintended changes. Keep it simple and small. 65 | 7. **Sign** the Salesforce CLA (you will be prompted to do so when submitting the Pull Request) 66 | 67 | > **NOTE**: Be sure to [sync your fork](https://help.github.com/articles/syncing-a-fork/) before making a pull request. 68 | 69 | # Contributor License Agreement ("CLA") 70 | In order to accept your pull request, we need you to submit a CLA. You only need 71 | to do this once to work on any of Salesforce's open source projects. 72 | 73 | Complete your CLA here: 74 | 75 | # Issues 76 | We use GitHub issues to track public bugs. Please ensure your description is 77 | clear and has sufficient instructions to be able to reproduce the issue. 78 | 79 | # Code of Conduct 80 | Please follow our [Code of Conduct](CODE_OF_CONDUCT.md). 81 | 82 | # License 83 | By contributing your code, you agree to license your contribution under the terms of our project [LICENSE](LICENSE.txt) and to sign the [Salesforce CLA](https://cla.salesforce.com/sign-cla) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | * Using welcoming and inclusive language 39 | * Being respectful of differing viewpoints and experiences 40 | * Gracefully accepting constructive criticism 41 | * Focusing on what is best for the community 42 | * Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | * The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | * Personal attacks, insulting/derogatory comments, or trolling 49 | * Public or private harassment 50 | * Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | * Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | * Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ -------------------------------------------------------------------------------- /experimental/agents/experimental/tooling.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | from experimental.tools.simple_datasource_qa import initialize_simple_datasource_qa 5 | 6 | # from experimental.tools.external.retrievers import pinecone_retriever_tool 7 | # from experimental.tools.external.web_search import tavily_tool 8 | 9 | # Load environment variables before accessing them 10 | load_dotenv() 11 | tableau_domain = os.environ['TABLEAU_DOMAIN'] 12 | tableau_site = os.environ['TABLEAU_SITE'] 13 | tableau_jwt_client_id = os.environ['TABLEAU_JWT_CLIENT_ID'] 14 | tableau_jwt_secret_id = os.environ['TABLEAU_JWT_SECRET_ID'] 15 | tableau_jwt_secret = os.environ['TABLEAU_JWT_SECRET'] 16 | tableau_api_version = os.environ['TABLEAU_API_VERSION'] 17 | tableau_user = os.environ['TABLEAU_USER'] 18 | datasource_luid = os.environ['DATASOURCE_LUID'] 19 | model_provider = os.environ['MODEL_PROVIDER'] 20 | tooling_llm_model = os.environ['TOOLING_MODEL'] 21 | 22 | # Tableau VizQL Data Service Query Tool 23 | analyze_datasource = initialize_simple_datasource_qa( 24 | domain=tableau_domain, 25 | site=tableau_site, 26 | jwt_client_id=tableau_jwt_client_id, 27 | jwt_secret_id=tableau_jwt_secret_id, 28 | jwt_secret=tableau_jwt_secret, 29 | tableau_api_version=tableau_api_version, 30 | tableau_user=tableau_user, 31 | datasource_luid=datasource_luid, 32 | model_provider=model_provider, 33 | tooling_llm_model=tooling_llm_model 34 | ) 35 | 36 | # tableau_metrics = pinecone_retriever_tool( 37 | # name='tableau_metrics', 38 | # description="""Returns ML insights & predictive analytics on user-subscribed metrics 39 | # Prioritize using this tool if the user mentions metrics, KPIs, OKRs or similar 40 | 41 | # Make thorough queries for relevant context. 42 | # For a high level summary ask this way: 43 | # - start with requesting a KPI metrics summary 44 | # - dive deeper on those results using the methods for detailed metric info described below 45 | 46 | # For detailed metric info, ask using the target metric plus any of these topics: 47 | # - dimensions 48 | # - data 49 | # - descriptions 50 | # - drivers 51 | # - unusual changes 52 | # - trends 53 | # - sentiment 54 | # - current & previous values 55 | # - period over period change 56 | # - contributors 57 | # - detractors 58 | 59 | # NOT for precise data values. Use a data source query tool for specific values. 60 | # NOT for fetching data values on specific dates 61 | 62 | # Examples: 63 | # User: give me an update on my KPIs 64 | # Input: 'update on all KPIs, trends, sentiment" 65 | 66 | # User: what is going on with sales? 67 | # Input: 'sales trend, data driving sales, unusual changes, contributors, drivers and detractors' 68 | 69 | # User: what is the value of sales in 2024? 70 | # -> wrong usage of this tool, not for specific values 71 | # """, 72 | # pinecone_index = os.environ["METRICS_INDEX"], 73 | # model_provider = os.environ["MODEL_PROVIDER"], 74 | # embedding_model = os.environ["EMBEDDING_MODEL"], 75 | # text_key = "_node_content", 76 | # search_k = 6, 77 | # max_concurrency = 5 78 | # ) 79 | 80 | # tableau_datasources = pinecone_retriever_tool( 81 | # name='tableau_datasources_catalog', 82 | # description="""Find the most relevant or useful Tableau data sources to answer the user query. Datasources often 83 | # have descriptions and fields that may match the needs of the user, use this information to determine the best data 84 | # resource for the user to consult. 85 | 86 | # Output is various chunks of text in vector format for summarization. 87 | 88 | # Args: 89 | # query (str): A natural language query describing the data to retrieve or an open-ended question 90 | # that can be answered using information contained in the data source 91 | 92 | # Returns: 93 | # dict: A data set relevant to the user's query 94 | # """, 95 | # pinecone_index=os.environ["DATASOURCES_INDEX"], 96 | # model_provider=os.environ["MODEL_PROVIDER"], 97 | # embedding_model=os.environ["EMBEDDING_MODEL"], 98 | # text_key = "_node_content", 99 | # search_k = 6, 100 | # max_concurrency = 5 101 | # ) 102 | 103 | # tableau_analytics = pinecone_retriever_tool( 104 | # name='tableau_analytics_catalog', 105 | # description="""Find the most relevant or useful Tableau workbooks, dashboards, charts, reports and other forms 106 | # of visual analytics to help the user find canonical answers to their query. Unless the user specifically requests 107 | # for charts, workbooks, dashboards, etc. don't assume this is what they intend to find, if in doubt confirm by 108 | # letting them know you can search the catalog in their behalf. 109 | 110 | # If nothing matches the user's needs, then you might need to try a different approach such as querying a data source. 111 | 112 | # Output is various chunks of text in vector format for summarization. 113 | 114 | # Args: 115 | # query (str): A natural language query describing the data to retrieve or an open-ended question 116 | # that can be answered using information contained in the data source 117 | 118 | # Returns: 119 | # dict: A data set relevant to the user's query 120 | # """, 121 | # pinecone_index=os.environ["WORKBOOKS_INDEX"], 122 | # model_provider=os.environ["MODEL_PROVIDER"], 123 | # embedding_model=os.environ["EMBEDDING_MODEL"], 124 | # text_key = "_node_content", 125 | # search_k = 6, 126 | # max_concurrency = 5 127 | # ) 128 | 129 | 130 | 131 | # List of tools used to build the state graph and for binding them to nodes 132 | tools = [ analyze_datasource ] 133 | -------------------------------------------------------------------------------- /experimental/utilities/metadata.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from typing import Dict 4 | from langchain_tableau.utilities.utils import http_post 5 | 6 | 7 | def get_datasource_query(luid): 8 | query = f""" 9 | query datasourceFieldInfo {{ 10 | publishedDatasources(filter: {{ luid: "{luid}" }}) {{ 11 | name 12 | description 13 | owner {{ 14 | name 15 | }} 16 | fields {{ 17 | name 18 | isHidden 19 | description 20 | descriptionInherited {{ 21 | attribute 22 | value 23 | }} 24 | fullyQualifiedName 25 | __typename 26 | ... on AnalyticsField {{ 27 | __typename 28 | }} 29 | ... on ColumnField {{ 30 | dataCategory 31 | role 32 | dataType 33 | defaultFormat 34 | semanticRole 35 | aggregation 36 | aggregationParam 37 | }} 38 | ... on CalculatedField {{ 39 | dataCategory 40 | role 41 | dataType 42 | defaultFormat 43 | semanticRole 44 | aggregation 45 | aggregationParam 46 | formula 47 | isAutoGenerated 48 | hasUserReference 49 | }} 50 | ... on BinField {{ 51 | dataCategory 52 | role 53 | dataType 54 | formula 55 | binSize 56 | }} 57 | ... on GroupField {{ 58 | dataCategory 59 | role 60 | dataType 61 | hasOther 62 | }} 63 | ... on CombinedSetField {{ 64 | delimiter 65 | combinationType 66 | }} 67 | }} 68 | }} 69 | }} 70 | """ 71 | 72 | return query 73 | 74 | 75 | async def get_data_dictionary_async(api_key: str, domain: str, datasource_luid: str) -> Dict: 76 | full_url = f"{domain}/api/metadata/graphql" 77 | 78 | query = get_datasource_query(datasource_luid) 79 | 80 | payload = { 81 | "query": query, 82 | "variables": {} 83 | } 84 | 85 | headers = { 86 | 'Content-Type': 'application/json', 87 | 'Accept': 'application/json', 88 | 'X-Tableau-Auth': api_key 89 | } 90 | 91 | response = await http_post(endpoint=full_url, headers=headers, payload=payload) 92 | 93 | # Check if the request was successful (status code 200) 94 | if response['status'] == 200: 95 | # Parse the response data 96 | response_data = response['data'] 97 | if 'errors' in response_data: 98 | error_message = f"GraphQL errors: {response_data['errors']}" 99 | raise RuntimeError(error_message) 100 | 101 | json_data = response_data['data']['publishedDatasources'][0] 102 | 103 | # Keep the raw data as the primary source of truth 104 | raw_fields = json_data.get('fields', []) 105 | visible_fields = [f for f in raw_fields if not f.get('isHidden')] 106 | 107 | return { 108 | 'datasource_name': json_data.get('name'), 109 | 'datasource_description': json_data.get('description'), 110 | 'datasource_owner': json_data.get('owner', {}).get('name'), 111 | 'datasource_luid': datasource_luid, 112 | 113 | # Raw GraphQL data - let the LLM work with full fidelity 114 | 'fields': visible_fields, 115 | 116 | # Minimal helpful additions without losing data 117 | 'field_count': len(visible_fields), 118 | 'field_names': [f['name'] for f in visible_fields], 119 | 120 | # Full raw response for power users 121 | 'raw_graphql_response': json_data 122 | } 123 | else: 124 | error_message = ( 125 | f"Failed to query Tableau's Metadata API. " 126 | f"Status code: {response['status']}. Response: {response['data']}" 127 | ) 128 | raise RuntimeError(error_message) 129 | 130 | 131 | def get_data_dictionary(api_key: str, domain: str, datasource_luid: str) -> Dict: 132 | full_url = f"{domain}/api/metadata/graphql" 133 | 134 | query = get_datasource_query(datasource_luid) 135 | 136 | payload = json.dumps({ 137 | "query": query, 138 | "variables": {} 139 | }) 140 | 141 | headers = { 142 | 'Content-Type': 'application/json', 143 | 'Accept': 'application/json', 144 | 'X-Tableau-Auth': api_key 145 | } 146 | 147 | response = requests.post(full_url, headers=headers, data=payload) 148 | response.raise_for_status() # Raise an exception for bad status codes 149 | 150 | response_data = response.json() 151 | if 'errors' in response_data: 152 | error_message = f"GraphQL errors: {response_data['errors']}" 153 | raise RuntimeError(error_message) 154 | 155 | json_data = response_data['data']['publishedDatasources'][0] 156 | 157 | # Keep the raw data as the primary source of truth 158 | raw_fields = json_data.get('fields', []) 159 | visible_fields = [f for f in raw_fields if not f.get('isHidden')] 160 | 161 | return { 162 | 'datasource_name': json_data.get('name'), 163 | 'datasource_description': json_data.get('description'), 164 | 'datasource_owner': json_data.get('owner', {}).get('name'), 165 | 'datasource_luid': datasource_luid, 166 | 167 | # Raw GraphQL data - let the LLM work with full fidelity 168 | 'fields': visible_fields, 169 | 170 | # Minimal helpful additions without losing data 171 | 'field_count': len(visible_fields), 172 | 'field_names': [f['name'] for f in visible_fields], 173 | 174 | # Full raw response for power users 175 | 'raw_graphql_response': json_data 176 | } 177 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/metadata.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from typing import Dict 4 | from langchain_tableau.utilities.utils import http_post 5 | 6 | 7 | def get_datasource_query(luid): 8 | query = f""" 9 | query datasourceFieldInfo {{ 10 | publishedDatasources(filter: {{ luid: "{luid}" }}) {{ 11 | name 12 | description 13 | owner {{ 14 | name 15 | }} 16 | fields {{ 17 | name 18 | isHidden 19 | description 20 | descriptionInherited {{ 21 | attribute 22 | value 23 | }} 24 | fullyQualifiedName 25 | __typename 26 | ... on AnalyticsField {{ 27 | __typename 28 | }} 29 | ... on ColumnField {{ 30 | dataCategory 31 | role 32 | dataType 33 | defaultFormat 34 | semanticRole 35 | aggregation 36 | aggregationParam 37 | }} 38 | ... on CalculatedField {{ 39 | dataCategory 40 | role 41 | dataType 42 | defaultFormat 43 | semanticRole 44 | aggregation 45 | aggregationParam 46 | formula 47 | isAutoGenerated 48 | hasUserReference 49 | }} 50 | ... on BinField {{ 51 | dataCategory 52 | role 53 | dataType 54 | formula 55 | binSize 56 | }} 57 | ... on GroupField {{ 58 | dataCategory 59 | role 60 | dataType 61 | hasOther 62 | }} 63 | ... on CombinedSetField {{ 64 | delimiter 65 | combinationType 66 | }} 67 | }} 68 | }} 69 | }} 70 | """ 71 | 72 | return query 73 | 74 | 75 | async def get_data_dictionary_async(api_key: str, domain: str, datasource_luid: str) -> Dict: 76 | full_url = f"{domain}/api/metadata/graphql" 77 | 78 | query = get_datasource_query(datasource_luid) 79 | 80 | payload = { 81 | "query": query, 82 | "variables": {} 83 | } 84 | 85 | headers = { 86 | 'Content-Type': 'application/json', 87 | 'Accept': 'application/json', 88 | 'X-Tableau-Auth': api_key 89 | } 90 | 91 | response = await http_post(endpoint=full_url, headers=headers, payload=payload) 92 | 93 | # Check if the request was successful (status code 200) 94 | if response['status'] == 200: 95 | # Parse the response data 96 | response_data = response['data'] 97 | if 'errors' in response_data: 98 | error_message = f"GraphQL errors: {response_data['errors']}" 99 | raise RuntimeError(error_message) 100 | 101 | json_data = response_data['data']['publishedDatasources'][0] 102 | 103 | # Keep the raw data as the primary source of truth 104 | raw_fields = json_data.get('fields', []) 105 | visible_fields = [f for f in raw_fields if not f.get('isHidden')] 106 | 107 | return { 108 | 'datasource_name': json_data.get('name'), 109 | 'datasource_description': json_data.get('description'), 110 | 'datasource_owner': json_data.get('owner', {}).get('name'), 111 | 'datasource_luid': datasource_luid, 112 | 113 | # Raw GraphQL data - let the LLM work with full fidelity 114 | 'fields': visible_fields, 115 | 116 | # Minimal helpful additions without losing data 117 | 'field_count': len(visible_fields), 118 | 'field_names': [f['name'] for f in visible_fields], 119 | 120 | # Full raw response for power users 121 | 'raw_graphql_response': json_data 122 | } 123 | else: 124 | error_message = ( 125 | f"Failed to query Tableau's Metadata API. " 126 | f"Status code: {response['status']}. Response: {response['data']}" 127 | ) 128 | raise RuntimeError(error_message) 129 | 130 | 131 | def get_data_dictionary(api_key: str, domain: str, datasource_luid: str) -> Dict: 132 | full_url = f"{domain}/api/metadata/graphql" 133 | 134 | query = get_datasource_query(datasource_luid) 135 | 136 | payload = json.dumps({ 137 | "query": query, 138 | "variables": {} 139 | }) 140 | 141 | headers = { 142 | 'Content-Type': 'application/json', 143 | 'Accept': 'application/json', 144 | 'X-Tableau-Auth': api_key 145 | } 146 | 147 | response = requests.post(full_url, headers=headers, data=payload) 148 | response.raise_for_status() # Raise an exception for bad status codes 149 | print(response) 150 | 151 | response_data = response.json() 152 | if 'errors' in response_data: 153 | error_message = f"GraphQL errors: {response_data['errors']}" 154 | raise RuntimeError(error_message) 155 | 156 | json_data = response_data['data']['publishedDatasources'][0] 157 | 158 | # Keep the raw data as the primary source of truth 159 | raw_fields = json_data.get('fields', []) 160 | visible_fields = [f for f in raw_fields if not f.get('isHidden')] 161 | 162 | return { 163 | 'datasource_name': json_data.get('name'), 164 | 'datasource_description': json_data.get('description'), 165 | 'datasource_owner': json_data.get('owner', {}).get('name'), 166 | 'datasource_luid': datasource_luid, 167 | 168 | # Raw GraphQL data - let the LLM work with full fidelity 169 | 'fields': visible_fields, 170 | 171 | # Minimal helpful additions without losing data 172 | 'field_count': len(visible_fields), 173 | 'field_names': [f['name'] for f in visible_fields], 174 | 175 | # Full raw response for power users 176 | 'raw_graphql_response': json_data 177 | } -------------------------------------------------------------------------------- /experimental/utilities/auth.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Dict, Any, List 3 | import requests 4 | import jwt 5 | from datetime import datetime, timedelta, timezone 6 | from uuid import uuid4 7 | 8 | from experimental.utilities.utils import http_post 9 | 10 | def jwt_connected_app( 11 | tableau_domain: str, 12 | tableau_site: str, 13 | tableau_api: str, 14 | tableau_user: str, 15 | jwt_client_id: str, 16 | jwt_secret_id: str, 17 | jwt_secret: str, 18 | scopes: List[str], 19 | ) -> Dict[str, Any]: 20 | """ 21 | Authenticates a user to Tableau using JSON Web Token (JWT) authentication. 22 | 23 | This function generates a JWT based on the provided credentials and uses it to authenticate 24 | a user with the Tableau Server or Tableau Online. The JWT is created with a specified expiration 25 | time and scopes, allowing for secure access to Tableau resources. 26 | 27 | Args: 28 | tableau_domain (str): The domain of the Tableau Server or Tableau Online instance. 29 | tableau_site (str): The content URL of the specific Tableau site to authenticate against. 30 | tableau_api (str): The version of the Tableau API to use for authentication. 31 | tableau_user (str): The username of the Tableau user to authenticate. 32 | jwt_client_id (str): The client ID used for generating the JWT. 33 | jwt_secret_id (str): The key ID associated with the JWT secret. 34 | jwt_secret (str): The secret key used to sign the JWT. 35 | scopes (List[str]): A list of scopes that define the permissions granted by the JWT. 36 | 37 | Returns: 38 | Dict[str, Any]: A dictionary containing the response from the Tableau authentication endpoint, 39 | typically including an API key or session that is valid for 2 hours and user information. 40 | """ 41 | # Encode the payload and secret key to generate the JWT 42 | token = jwt.encode( 43 | { 44 | "iss": jwt_client_id, 45 | "exp": datetime.now(timezone.utc) + timedelta(minutes=5), 46 | "jti": str(uuid4()), 47 | "aud": "tableau", 48 | "sub": tableau_user, 49 | "scp": scopes 50 | }, 51 | jwt_secret, 52 | algorithm = "HS256", 53 | headers = { 54 | 'kid': jwt_secret_id, 55 | 'iss': jwt_client_id 56 | } 57 | ) 58 | 59 | # authentication endpoint + request headers & payload 60 | endpoint = f"{tableau_domain}/api/{tableau_api}/auth/signin" 61 | 62 | headers = { 63 | 'Content-Type': 'application/json', 64 | 'Accept': 'application/json' 65 | } 66 | 67 | payload = { 68 | "credentials": { 69 | "jwt": token, 70 | "site": { 71 | "contentUrl": tableau_site, 72 | } 73 | } 74 | } 75 | 76 | response = requests.post(endpoint, headers=headers, json=payload) 77 | 78 | # Check if the request was successful (status code 200) 79 | if response.status_code == 200: 80 | return response.json() 81 | else: 82 | error_message = ( 83 | f"Failed to authenticate to the Tableau site. " 84 | f"Status code: {response.status_code}. Response: {response.text}" 85 | ) 86 | raise RuntimeError(error_message) 87 | 88 | 89 | async def jwt_connected_app_async( 90 | tableau_domain: str, 91 | tableau_site: str, 92 | tableau_api: str, 93 | tableau_user: str, 94 | jwt_client_id: str, 95 | jwt_secret_id: str, 96 | jwt_secret: str, 97 | scopes: List[str], 98 | ) -> Dict[str, Any]: 99 | """ 100 | Authenticates a user to Tableau using JSON Web Token (JWT) authentication. 101 | 102 | This function generates a JWT based on the provided credentials and uses it to authenticate 103 | a user with the Tableau Server or Tableau Online. The JWT is created with a specified expiration 104 | time and scopes, allowing for secure access to Tableau resources. 105 | 106 | Args: 107 | tableau_domain (str): The domain of the Tableau Server or Tableau Online instance. 108 | tableau_site (str): The content URL of the specific Tableau site to authenticate against. 109 | tableau_api (str): The version of the Tableau API to use for authentication. 110 | tableau_user (str): The username of the Tableau user to authenticate. 111 | jwt_client_id (str): The client ID used for generating the JWT. 112 | jwt_secret_id (str): The key ID associated with the JWT secret. 113 | jwt_secret (str): The secret key used to sign the JWT. 114 | scopes (List[str]): A list of scopes that define the permissions granted by the JWT. 115 | 116 | Returns: 117 | Dict[str, Any]: A dictionary containing the response from the Tableau authentication endpoint, 118 | typically including an API key or session that is valid for 2 hours and user information. 119 | """ 120 | # Encode the payload and secret key to generate the JWT 121 | token = jwt.encode( 122 | { 123 | "iss": jwt_client_id, 124 | "exp": datetime.now(timezone.utc) + timedelta(minutes=5), 125 | "jti": str(uuid4()), 126 | "aud": "tableau", 127 | "sub": tableau_user, 128 | "scp": scopes 129 | }, 130 | jwt_secret, 131 | algorithm = "HS256", 132 | headers = { 133 | 'kid': jwt_secret_id, 134 | 'iss': jwt_client_id 135 | } 136 | ) 137 | 138 | # authentication endpoint + request headers & payload 139 | endpoint = f"{tableau_domain}/api/{tableau_api}/auth/signin" 140 | 141 | headers = { 142 | 'Content-Type': 'application/json', 143 | 'Accept': 'application/json' 144 | } 145 | 146 | payload = { 147 | "credentials": { 148 | "jwt": token, 149 | "site": { 150 | "contentUrl": tableau_site, 151 | } 152 | } 153 | } 154 | 155 | response = await http_post(endpoint=endpoint, headers=headers, payload=payload) 156 | # Check if the request was successful (status code 200) 157 | if response['status'] == 200: 158 | return response['data'] 159 | else: 160 | error_message = ( 161 | f"Failed to authenticate to the Tableau site. " 162 | f"Status code: {response['status']}. Response: {response['data']}" 163 | ) 164 | raise RuntimeError(error_message) 165 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/auth.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Dict, Any, List 3 | import requests 4 | import jwt 5 | from datetime import datetime, timedelta, timezone 6 | from uuid import uuid4 7 | 8 | from langchain_tableau.utilities.utils import http_post 9 | 10 | def jwt_connected_app( 11 | tableau_domain: str, 12 | tableau_site: str, 13 | tableau_api: str, 14 | tableau_user: str, 15 | jwt_client_id: str, 16 | jwt_secret_id: str, 17 | jwt_secret: str, 18 | scopes: List[str], 19 | ) -> Dict[str, Any]: 20 | """ 21 | Authenticates a user to Tableau using JSON Web Token (JWT) authentication. 22 | 23 | This function generates a JWT based on the provided credentials and uses it to authenticate 24 | a user with the Tableau Server or Tableau Online. The JWT is created with a specified expiration 25 | time and scopes, allowing for secure access to Tableau resources. 26 | 27 | Args: 28 | tableau_domain (str): The domain of the Tableau Server or Tableau Online instance. 29 | tableau_site (str): The content URL of the specific Tableau site to authenticate against. 30 | tableau_api (str): The version of the Tableau API to use for authentication. 31 | tableau_user (str): The username of the Tableau user to authenticate. 32 | jwt_client_id (str): The client ID used for generating the JWT. 33 | jwt_secret_id (str): The key ID associated with the JWT secret. 34 | jwt_secret (str): The secret key used to sign the JWT. 35 | scopes (List[str]): A list of scopes that define the permissions granted by the JWT. 36 | 37 | Returns: 38 | Dict[str, Any]: A dictionary containing the response from the Tableau authentication endpoint, 39 | typically including an API key or session that is valid for 2 hours and user information. 40 | """ 41 | # Encode the payload and secret key to generate the JWT 42 | token = jwt.encode( 43 | { 44 | "iss": jwt_client_id, 45 | "exp": datetime.now(timezone.utc) + timedelta(minutes=5), 46 | "jti": str(uuid4()), 47 | "aud": "tableau", 48 | "sub": tableau_user, 49 | "scp": scopes 50 | }, 51 | jwt_secret, 52 | algorithm = "HS256", 53 | headers = { 54 | 'kid': jwt_secret_id, 55 | 'iss': jwt_client_id 56 | } 57 | ) 58 | 59 | # authentication endpoint + request headers & payload 60 | endpoint = f"{tableau_domain}/api/{tableau_api}/auth/signin" 61 | 62 | headers = { 63 | 'Content-Type': 'application/json', 64 | 'Accept': 'application/json' 65 | } 66 | 67 | payload = { 68 | "credentials": { 69 | "jwt": token, 70 | "site": { 71 | "contentUrl": tableau_site, 72 | } 73 | } 74 | } 75 | 76 | response = requests.post(endpoint, headers=headers, json=payload) 77 | 78 | # Check if the request was successful (status code 200) 79 | if response.status_code == 200: 80 | return response.json() 81 | else: 82 | error_message = ( 83 | f"Failed to authenticate to the Tableau site. " 84 | f"Status code: {response.status_code}. Response: {response.text}" 85 | ) 86 | raise RuntimeError(error_message) 87 | 88 | 89 | async def jwt_connected_app_async( 90 | tableau_domain: str, 91 | tableau_site: str, 92 | tableau_api: str, 93 | tableau_user: str, 94 | jwt_client_id: str, 95 | jwt_secret_id: str, 96 | jwt_secret: str, 97 | scopes: List[str], 98 | ) -> Dict[str, Any]: 99 | """ 100 | Authenticates a user to Tableau using JSON Web Token (JWT) authentication. 101 | 102 | This function generates a JWT based on the provided credentials and uses it to authenticate 103 | a user with the Tableau Server or Tableau Online. The JWT is created with a specified expiration 104 | time and scopes, allowing for secure access to Tableau resources. 105 | 106 | Args: 107 | tableau_domain (str): The domain of the Tableau Server or Tableau Online instance. 108 | tableau_site (str): The content URL of the specific Tableau site to authenticate against. 109 | tableau_api (str): The version of the Tableau API to use for authentication. 110 | tableau_user (str): The username of the Tableau user to authenticate. 111 | jwt_client_id (str): The client ID used for generating the JWT. 112 | jwt_secret_id (str): The key ID associated with the JWT secret. 113 | jwt_secret (str): The secret key used to sign the JWT. 114 | scopes (List[str]): A list of scopes that define the permissions granted by the JWT. 115 | 116 | Returns: 117 | Dict[str, Any]: A dictionary containing the response from the Tableau authentication endpoint, 118 | typically including an API key or session that is valid for 2 hours and user information. 119 | """ 120 | # Encode the payload and secret key to generate the JWT 121 | token = jwt.encode( 122 | { 123 | "iss": jwt_client_id, 124 | "exp": datetime.now(timezone.utc) + timedelta(minutes=5), 125 | "jti": str(uuid4()), 126 | "aud": "tableau", 127 | "sub": tableau_user, 128 | "scp": scopes 129 | }, 130 | jwt_secret, 131 | algorithm = "HS256", 132 | headers = { 133 | 'kid': jwt_secret_id, 134 | 'iss': jwt_client_id 135 | } 136 | ) 137 | 138 | # authentication endpoint + request headers & payload 139 | endpoint = f"{tableau_domain}/api/{tableau_api}/auth/signin" 140 | 141 | headers = { 142 | 'Content-Type': 'application/json', 143 | 'Accept': 'application/json' 144 | } 145 | 146 | payload = { 147 | "credentials": { 148 | "jwt": token, 149 | "site": { 150 | "contentUrl": tableau_site, 151 | } 152 | } 153 | } 154 | 155 | response = await http_post(endpoint=endpoint, headers=headers, payload=payload) 156 | # Check if the request was successful (status code 200) 157 | if response['status'] == 200: 158 | return response['data'] 159 | else: 160 | error_message = ( 161 | f"Failed to authenticate to the Tableau site. " 162 | f"Status code: {response['status']}. Response: {response['data']}" 163 | ) 164 | raise RuntimeError(error_message) 165 | -------------------------------------------------------------------------------- /experimental/agents/superstore/tooling.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | from langchain_tableau.tools.simple_datasource_qa import initialize_simple_datasource_qa 5 | 6 | from experimental.tools.external.retrievers import pinecone_retriever_tool 7 | 8 | # Load environment variables before accessing them 9 | load_dotenv() 10 | tableau_domain = os.environ['TABLEAU_DOMAIN'] 11 | tableau_site = os.environ['TABLEAU_SITE'] 12 | tableau_jwt_client_id = os.environ['TABLEAU_JWT_CLIENT_ID'] 13 | tableau_jwt_secret_id = os.environ['TABLEAU_JWT_SECRET_ID'] 14 | tableau_jwt_secret = os.environ['TABLEAU_JWT_SECRET'] 15 | tableau_api_version = os.environ['TABLEAU_API_VERSION'] 16 | tableau_user = os.environ['TABLEAU_USER'] 17 | datasource_luid = os.environ['DATASOURCE_LUID'] 18 | tooling_llm_model = os.environ['TOOLING_MODEL'] 19 | 20 | # Tableau VizQL Data Service Query Tool 21 | analyze_datasource = initialize_simple_datasource_qa( 22 | domain=tableau_domain, 23 | site=tableau_site, 24 | jwt_client_id=tableau_jwt_client_id, 25 | jwt_secret_id=tableau_jwt_secret_id, 26 | jwt_secret=tableau_jwt_secret, 27 | tableau_api_version=tableau_api_version, 28 | tableau_user=tableau_user, 29 | datasource_luid=datasource_luid, 30 | tooling_llm_model=tooling_llm_model 31 | ) 32 | 33 | tableau_metrics = pinecone_retriever_tool( 34 | name='tableau_metrics', 35 | description="""Returns ML insights & predictive analytics on user-subscribed metrics 36 | Prioritize using this tool if the user mentions metrics, KPIs, OKRs or similar 37 | 38 | Make thorough queries for relevant context. 39 | For a high level summary ask this way: 40 | - start with requesting a KPI metrics summary 41 | - dive deeper on those results using the methods for detailed metric info described below 42 | 43 | For detailed metric info, ask using the target metric plus any of these topics: 44 | - dimensions 45 | - data 46 | - descriptions 47 | - drivers 48 | - unusual changes 49 | - trends 50 | - sentiment 51 | - current & previous values 52 | - period over period change 53 | - contributors 54 | - detractors 55 | 56 | NOT for precise data values. Use a data source query tool for specific values. 57 | NOT for fetching data values on specific dates 58 | 59 | Examples: 60 | User: give me an update on my KPIs 61 | Input: 'update on all KPIs, trends, sentiment" 62 | 63 | User: what is going on with sales? 64 | Input: 'sales trend, data driving sales, unusual changes, contributors, drivers and detractors' 65 | 66 | User: what is the value of sales in 2024? 67 | -> wrong usage of this tool, not for specific values 68 | """, 69 | pinecone_index = os.environ["METRICS_INDEX"], 70 | model_provider = os.environ["MODEL_PROVIDER"], 71 | embedding_model = os.environ["EMBEDDING_MODEL"], 72 | text_key = "_node_content", 73 | search_k = 6, 74 | max_concurrency = 5 75 | ) 76 | 77 | tableau_datasources = pinecone_retriever_tool( 78 | name='tableau_datasources_catalog', 79 | description="""Find the most relevant or useful Tableau data sources to answer the user query. Datasources often 80 | have descriptions and fields that may match the needs of the user, use this information to determine the best data 81 | resource for the user to consult. 82 | 83 | If the user wants to know about the Data Catalog in general, use two tools - one for analytics and one for data sources. 84 | 85 | Args: 86 | query (str): A natural language query describing the data to retrieve or an open-ended question 87 | that can be answered using information contained in the data source 88 | 89 | Returns: 90 | dict: A data set relevant to the user's query 91 | """, 92 | pinecone_index=os.environ["DATASOURCES_INDEX"], 93 | model_provider=os.environ["MODEL_PROVIDER"], 94 | embedding_model=os.environ["EMBEDDING_MODEL"], 95 | text_key = "_node_content", 96 | search_k = 6, 97 | max_concurrency = 5 98 | ) 99 | 100 | tableau_analytics = pinecone_retriever_tool( 101 | name='tableau_analytics_catalog', 102 | description="""Find the most relevant or useful Tableau workbooks, dashboards, charts, reports and other forms 103 | of visual analytics to help the user find canonical answers to their query. Unless the user specifically requests 104 | for charts, workbooks, dashboards, etc. don't assume this is what they intend to find, if in doubt confirm by 105 | letting them know you can search the catalog in their behalf. 106 | 107 | Don't list sheets unless you are asked for charts, graphics, tables, visualizations, sheets, otherwise list dashboards 108 | and workbooks. 109 | 110 | If the user wants to know about the Data Catalog in general, use two tools - one for analytics and one for data sources. 111 | 112 | If nothing matches the user's needs, then you might need to try a different approach such as querying a data source for live data. 113 | 114 | Args: 115 | query (str): A natural language query describing the data to retrieve or an open-ended question 116 | that can be answered using information contained in the data source 117 | 118 | Returns: 119 | dict: A data set relevant to the user's query 120 | """, 121 | pinecone_index=os.environ["WORKBOOKS_INDEX"], 122 | model_provider=os.environ["MODEL_PROVIDER"], 123 | embedding_model=os.environ["EMBEDDING_MODEL"], 124 | text_key = "_node_content", 125 | search_k = 6, 126 | max_concurrency = 5 127 | ) 128 | 129 | tableau_knowledge_base = pinecone_retriever_tool( 130 | name='tableau_knowledge_base', 131 | description="""A knowledge base collecting whitepapers, help articles, technical documentation and similar resources describing 132 | Tableau's developer platform. Use this tool when the customer is asking a Tableau, embedded analytics and AI. This tool provides 133 | sample embed code and best practices on how to use APIs. This tool also describes the Tableau demo app which is the context in 134 | which you operate. Include additional context to your inputs besides the verbatim user query so that you can pull in as much useful 135 | context as possible 136 | 137 | Args: 138 | query (str): A natural language query describing the data to retrieve or an open-ended question 139 | that can be answered using information contained in the data source 140 | 141 | Returns: 142 | dict: A data set relevant to the user's query 143 | 144 | Examples: 145 | User: What is row-level security? 146 | Input: 'what is row-level security? security, filtering, row-level, permissions, heirarchies' 147 | """, 148 | pinecone_index='literature', 149 | model_provider=os.environ["MODEL_PROVIDER"], 150 | embedding_model=os.environ["EMBEDDING_MODEL"], 151 | text_key = "_node_content", 152 | search_k = 6, 153 | max_concurrency = 5 154 | ) 155 | 156 | # List of tools used to build the state graph and for binding them to nodes 157 | tools = [ analyze_datasource, tableau_metrics, tableau_datasources, tableau_analytics ] 158 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/utilities/simple_datasource_qa.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import re 4 | import logging 5 | from typing import Dict, Optional 6 | from dotenv import load_dotenv 7 | 8 | from langchain_tableau.utilities.vizql_data_service import query_vds, query_vds_metadata 9 | from langchain_tableau.utilities.utils import json_to_markdown_table 10 | from langchain_tableau.utilities.metadata import get_data_dictionary 11 | 12 | 13 | def get_headlessbi_data(payload: str, url: str, api_key: str, datasource_luid: str): 14 | json_payload = json.loads(payload) 15 | 16 | try: 17 | headlessbi_data = query_vds( 18 | api_key=api_key, 19 | datasource_luid=datasource_luid, 20 | url=url, 21 | query=json_payload 22 | ) 23 | 24 | if not headlessbi_data or 'data' not in headlessbi_data: 25 | raise ValueError("Invalid or empty response from query_vds") 26 | 27 | markdown_table = json_to_markdown_table(headlessbi_data['data']) 28 | return markdown_table 29 | 30 | except ValueError as ve: 31 | logging.error(f"Value error in get_headlessbi_data: {str(ve)}") 32 | raise 33 | 34 | except json.JSONDecodeError as je: 35 | logging.error(f"JSON decoding error in get_headlessbi_data: {str(je)}") 36 | raise ValueError("Invalid JSON format in the payload") 37 | 38 | except Exception as e: 39 | logging.error(f"Unexpected error in get_headlessbi_data: {str(e)}") 40 | raise RuntimeError(f"An unexpected error occurred: {str(e)}") 41 | 42 | 43 | def get_payload(output): 44 | try: 45 | parsed_output = output.split('JSON_payload')[1] 46 | except IndexError: 47 | raise ValueError("'JSON_payload' not found in the output") 48 | 49 | match = re.search(r'{.*}', parsed_output, re.DOTALL) 50 | if match: 51 | json_string = match.group(0) 52 | try: 53 | payload = json.loads(json_string) 54 | return payload 55 | except json.JSONDecodeError: 56 | raise ValueError("Invalid JSON format in the payload") 57 | else: 58 | raise ValueError("No JSON payload found in the parsed output") 59 | 60 | 61 | def get_values(api_key: str, url: str, datasource_luid: str, caption: str): 62 | column_values = {'fields': [{'fieldCaption': caption}]} 63 | output = query_vds( 64 | api_key=api_key, 65 | datasource_luid=datasource_luid, 66 | url=url, 67 | query=column_values 68 | ) 69 | if output is None: 70 | return None 71 | sample_values = [list(item.values())[0] for item in output['data']][:4] 72 | return sample_values 73 | 74 | 75 | def augment_datasource_metadata( 76 | task: str, 77 | api_key: str, 78 | url: str, 79 | datasource_luid: str, 80 | prompt: Dict[str, str], 81 | previous_errors: Optional[str] = None, 82 | previous_vds_payload: Optional[str] = None 83 | ): 84 | """ 85 | Augment datasource metadata with additional information and format as JSON. 86 | 87 | This function retrieves the data dictionary and sample field values for a given 88 | datasource, adds them to the provided prompt dictionary, and includes any previous 89 | errors or queries for debugging purposes. 90 | 91 | Args: 92 | api_key (str): The API key for authentication. 93 | url (str): The base URL for the API endpoints. 94 | datasource_luid (str): The unique identifier of the datasource. 95 | prompt (Dict[str, str]): Initial prompt dictionary to be augmented. 96 | previous_errors (Optional[str]): Any errors from previous function calls. Defaults to None. 97 | previous_vds_payload (Optional[str]): The query that caused errors in previous calls. Defaults to None. 98 | 99 | Returns: 100 | str: A JSON string containing the augmented prompt dictionary with datasource metadata. 101 | 102 | Note: 103 | This function relies on external functions `get_data_dictionary` and `query_vds_metadata` 104 | to retrieve the necessary datasource information. 105 | """ 106 | # insert the user input as a task 107 | prompt['task'] = task 108 | 109 | # get dictionary for the data source from the Metadata API 110 | data_dictionary = get_data_dictionary( 111 | api_key=api_key, 112 | domain=url, 113 | datasource_luid=datasource_luid 114 | ) 115 | 116 | # insert data dictionary from Tableau's Data Catalog (using new 'fields' key) 117 | prompt['data_dictionary'] = data_dictionary['fields'] 118 | 119 | # insert data source name, description and owner into 'meta' key 120 | # (preserve the rich metadata structure without deleting fields) 121 | prompt['meta'] = { 122 | 'datasource_name': data_dictionary['datasource_name'], 123 | 'datasource_description': data_dictionary['datasource_description'], 124 | 'datasource_owner': data_dictionary['datasource_owner'], 125 | 'datasource_luid': data_dictionary['datasource_luid'], 126 | 'field_count': data_dictionary['field_count'], 127 | 'field_names': data_dictionary['field_names'] 128 | } 129 | 130 | # get sample values for fields from VDS metadata endpoint 131 | datasource_metadata = query_vds_metadata( 132 | api_key=api_key, 133 | url=url, 134 | datasource_luid=datasource_luid 135 | ) 136 | 137 | for field in datasource_metadata['data']: 138 | del field['fieldName'] 139 | del field['logicalTableId'] 140 | 141 | # insert the data model with sample values from Tableau's VDS metadata API 142 | prompt['data_model'] = datasource_metadata['data'] 143 | 144 | # include previous error and query to debug in current run 145 | if previous_errors: 146 | prompt['previous_call_error'] = previous_errors 147 | if previous_vds_payload: 148 | prompt['previous_vds_payload'] = previous_vds_payload 149 | 150 | return prompt 151 | 152 | 153 | def prepare_prompt_inputs(data: dict, user_string: str) -> dict: 154 | """ 155 | Prepare inputs for the prompt template with explicit, safe mapping. 156 | 157 | Args: 158 | data (dict): Raw data from VizQL query 159 | user_input (str): Original user query 160 | 161 | Returns: 162 | dict: Mapped inputs for PromptTemplate 163 | """ 164 | 165 | return { 166 | "vds_query": data.get('query', 'no query'), 167 | "data_source_name": data.get('data_source_name', 'no name'), 168 | "data_source_description": data.get('data_source_description', 'no description'), 169 | "data_source_maintainer": data.get('data_source_maintainer', 'no maintainer'), 170 | "data_table": data.get('data_table', 'no data'), 171 | "user_input": user_string 172 | } 173 | 174 | 175 | def env_vars_simple_datasource_qa( 176 | domain=None, 177 | site=None, 178 | jwt_client_id=None, 179 | jwt_secret_id=None, 180 | jwt_secret=None, 181 | tableau_api_version=None, 182 | tableau_user=None, 183 | datasource_luid=None, 184 | model_provider=None, 185 | tooling_llm_model=None 186 | ): 187 | """ 188 | Retrieves Tableau configuration from environment variables if not provided as arguments. 189 | 190 | Args: 191 | domain (str, optional): Tableau domain 192 | site (str, optional): Tableau site 193 | jwt_client_id (str, optional): JWT client ID 194 | jwt_secret_id (str, optional): JWT secret ID 195 | jwt_secret (str, optional): JWT secret 196 | tableau_api_version (str, optional): Tableau API version 197 | tableau_user (str, optional): Tableau user 198 | datasource_luid (str, optional): Datasource LUID 199 | tooling_llm_model (str, optional): Tooling LLM model 200 | 201 | Returns: 202 | dict: A dictionary containing all the configuration values 203 | """ 204 | # Load environment variables before accessing them 205 | load_dotenv() 206 | 207 | config = { 208 | 'domain': domain if isinstance(domain, str) and domain else os.environ['TABLEAU_DOMAIN'], 209 | 'site': site or os.environ['TABLEAU_SITE'], 210 | 'jwt_client_id': jwt_client_id or os.environ['TABLEAU_JWT_CLIENT_ID'], 211 | 'jwt_secret_id': jwt_secret_id or os.environ['TABLEAU_JWT_SECRET_ID'], 212 | 'jwt_secret': jwt_secret or os.environ['TABLEAU_JWT_SECRET'], 213 | 'tableau_api_version': tableau_api_version or os.environ['TABLEAU_API_VERSION'], 214 | 'tableau_user': tableau_user or os.environ['TABLEAU_USER'], 215 | 'datasource_luid': datasource_luid or os.environ['DATASOURCE_LUID'], 216 | 'model_provider': model_provider or os.environ['MODEL_PROVIDER'], 217 | 'tooling_llm_model': tooling_llm_model or os.environ['TOOLING_MODEL'] 218 | } 219 | 220 | return config 221 | -------------------------------------------------------------------------------- /experimental/utilities/simple_datasource_qa.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import re 4 | import logging 5 | from typing import Dict, Optional 6 | from dotenv import load_dotenv 7 | 8 | from experimental.utilities.vizql_data_service import query_vds, query_vds_metadata 9 | from experimental.utilities.utils import json_to_markdown_table 10 | from experimental.utilities.metadata import get_data_dictionary 11 | 12 | 13 | import json 14 | import logging 15 | 16 | def get_headlessbi_data(payload, url: str, api_key: str, datasource_luid: str): 17 | # 1) Normalize payload to a dict 18 | if isinstance(payload, str): 19 | raw = payload.strip() 20 | if raw.startswith("```"): 21 | # drop the ```json and trailing ``` 22 | lines = raw.splitlines() 23 | raw = "\n".join(lines[1:-1]) 24 | try: 25 | payload = json.loads(raw) 26 | except json.JSONDecodeError as je: 27 | logging.error(f"JSON decoding error in get_headlessbi_data: {je}") 28 | raise ValueError("Invalid JSON format in the payload") 29 | 30 | # 2) Single call to query_vds 31 | try: 32 | headlessbi_data = query_vds( 33 | api_key=api_key, 34 | datasource_luid=datasource_luid, 35 | url=url, 36 | query=payload, # always a dict now 37 | ) 38 | 39 | if not headlessbi_data or 'data' not in headlessbi_data: 40 | raise ValueError("Invalid or empty response from query_vds") 41 | 42 | # 3) Convert to markdown and return 43 | markdown_table = json_to_markdown_table(headlessbi_data['data']) 44 | return markdown_table 45 | 46 | except ValueError as ve: 47 | logging.error(f"Value error in get_headlessbi_data: {ve}") 48 | raise 49 | 50 | except Exception as e: 51 | logging.error(f"Unexpected error in get_headlessbi_data: {e}") 52 | raise RuntimeError(f"An unexpected error occurred: {e}") 53 | 54 | 55 | 56 | def get_payload(output): 57 | try: 58 | parsed_output = output.split('JSON_payload')[1] 59 | except IndexError: 60 | raise ValueError("'JSON_payload' not found in the output") 61 | 62 | match = re.search(r'{.*}', parsed_output, re.DOTALL) 63 | if match: 64 | json_string = match.group(0) 65 | try: 66 | payload = json.loads(json_string) 67 | return payload 68 | except json.JSONDecodeError: 69 | raise ValueError("Invalid JSON format in the payload") 70 | else: 71 | raise ValueError("No JSON payload found in the parsed output") 72 | 73 | 74 | def get_values(api_key: str, url: str, datasource_luid: str, caption: str): 75 | column_values = {'fields': [{'fieldCaption': caption}]} 76 | output = query_vds( 77 | api_key=api_key, 78 | datasource_luid=datasource_luid, 79 | url=url, 80 | query=column_values 81 | ) 82 | if output is None: 83 | return None 84 | sample_values = [list(item.values())[0] for item in output['data']][:4] 85 | return sample_values 86 | 87 | 88 | def augment_datasource_metadata( 89 | task: str, 90 | api_key: str, 91 | url: str, 92 | datasource_luid: str, 93 | prompt: Dict[str, str], 94 | previous_errors: Optional[str] = None, 95 | previous_vds_payload: Optional[str] = None 96 | ): 97 | """ 98 | Augment datasource metadata with additional information and format as JSON. 99 | 100 | This function retrieves the data dictionary and sample field values for a given 101 | datasource, adds them to the provided prompt dictionary, and includes any previous 102 | errors or queries for debugging purposes. 103 | 104 | Args: 105 | api_key (str): The API key for authentication. 106 | url (str): The base URL for the API endpoints. 107 | datasource_luid (str): The unique identifier of the datasource. 108 | prompt (Dict[str, str]): Initial prompt dictionary to be augmented. 109 | previous_errors (Optional[str]): Any errors from previous function calls. Defaults to None. 110 | previous_vds_payload (Optional[str]): The query that caused errors in previous calls. Defaults to None. 111 | 112 | Returns: 113 | str: A JSON string containing the augmented prompt dictionary with datasource metadata. 114 | 115 | Note: 116 | This function relies on external functions `get_data_dictionary` and `query_vds_metadata` 117 | to retrieve the necessary datasource information. 118 | """ 119 | # insert the user input as a task 120 | prompt['task'] = task 121 | 122 | # get dictionary for the data source from the Metadata API 123 | data_dictionary = get_data_dictionary( 124 | api_key=api_key, 125 | domain=url, 126 | datasource_luid=datasource_luid 127 | ) 128 | 129 | # insert data dictionary from Tableau's Data Catalog (using new 'fields' key) 130 | prompt['data_dictionary'] = data_dictionary['fields'] 131 | 132 | # insert data source name, description and owner into 'meta' key 133 | # (preserve the rich metadata structure without deleting fields) 134 | prompt['meta'] = { 135 | 'datasource_name': data_dictionary['datasource_name'], 136 | 'datasource_description': data_dictionary['datasource_description'], 137 | 'datasource_owner': data_dictionary['datasource_owner'], 138 | 'datasource_luid': data_dictionary['datasource_luid'], 139 | 'field_count': data_dictionary['field_count'], 140 | 'field_names': data_dictionary['field_names'] 141 | } 142 | 143 | # get sample values for fields from VDS metadata endpoint 144 | datasource_metadata = query_vds_metadata( 145 | api_key=api_key, 146 | url=url, 147 | datasource_luid=datasource_luid 148 | ) 149 | 150 | for field in datasource_metadata['data']: 151 | del field['fieldName'] 152 | del field['logicalTableId'] 153 | 154 | # insert the data model with sample values from Tableau's VDS metadata API 155 | prompt['data_model'] = datasource_metadata['data'] 156 | 157 | # include previous error and query to debug in current run 158 | if previous_errors: 159 | prompt['previous_call_error'] = previous_errors 160 | if previous_vds_payload: 161 | prompt['previous_vds_payload'] = previous_vds_payload 162 | 163 | return prompt 164 | 165 | 166 | def prepare_prompt_inputs(data: dict, user_string: str) -> dict: 167 | """ 168 | Prepare inputs for the prompt template with explicit, safe mapping. 169 | 170 | Args: 171 | data (dict): Raw data from VizQL query 172 | user_input (str): Original user query 173 | 174 | Returns: 175 | dict: Mapped inputs for PromptTemplate 176 | """ 177 | 178 | return { 179 | "vds_query": data.get('query', 'no query'), 180 | "data_source_name": data.get('data_source_name', 'no name'), 181 | "data_source_description": data.get('data_source_description', 'no description'), 182 | "data_source_maintainer": data.get('data_source_maintainer', 'no maintainer'), 183 | "data_table": data.get('data_table', 'no data'), 184 | "user_input": user_string 185 | } 186 | 187 | 188 | def env_vars_simple_datasource_qa( 189 | domain=None, 190 | site=None, 191 | jwt_client_id=None, 192 | jwt_secret_id=None, 193 | jwt_secret=None, 194 | tableau_api_version=None, 195 | tableau_user=None, 196 | datasource_luid=None, 197 | model_provider=None, 198 | tooling_llm_model=None 199 | ): 200 | """ 201 | Retrieves Tableau configuration from environment variables if not provided as arguments. 202 | 203 | Args: 204 | domain (str, optional): Tableau domain 205 | site (str, optional): Tableau site 206 | jwt_client_id (str, optional): JWT client ID 207 | jwt_secret_id (str, optional): JWT secret ID 208 | jwt_secret (str, optional): JWT secret 209 | tableau_api_version (str, optional): Tableau API version 210 | tableau_user (str, optional): Tableau user 211 | datasource_luid (str, optional): Datasource LUID 212 | tooling_llm_model (str, optional): Tooling LLM model 213 | 214 | Returns: 215 | dict: A dictionary containing all the configuration values 216 | """ 217 | # Load environment variables before accessing them 218 | load_dotenv() 219 | 220 | config = { 221 | 'domain': domain if isinstance(domain, str) and domain else os.environ['TABLEAU_DOMAIN'], 222 | 'site': site or os.environ['TABLEAU_SITE'], 223 | 'jwt_client_id': jwt_client_id or os.environ['TABLEAU_JWT_CLIENT_ID'], 224 | 'jwt_secret_id': jwt_secret_id or os.environ['TABLEAU_JWT_SECRET_ID'], 225 | 'jwt_secret': jwt_secret or os.environ['TABLEAU_JWT_SECRET'], 226 | 'tableau_api_version': tableau_api_version or os.environ['TABLEAU_API_VERSION'], 227 | 'tableau_user': tableau_user or os.environ['TABLEAU_USER'], 228 | 'datasource_luid': datasource_luid or os.environ['DATASOURCE_LUID'], 229 | 'model_provider': model_provider or os.environ['MODEL_PROVIDER'], 230 | 'tooling_llm_model': tooling_llm_model or os.environ['TOOLING_MODEL'] 231 | } 232 | 233 | return config 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tableau Langchain 2 | 3 | [![Community Supported](https://img.shields.io/badge/Support%20Level-Community%20Supported-457387.svg)](https://www.tableau.com/support-levels-it-and-developer-tools) 4 | [![PyPI version](https://badge.fury.io/py/langchain-tableau.svg)](https://badge.fury.io/py/langchain-tableau) 5 | [![GitHub Repo](https://img.shields.io/badge/GitHub-Repository-blue?logo=github)](https://github.com/Tab-SE/tableau_langchain) 6 | 7 | This project builds Agentic tools from Tableau capabilities for use within the [Langchain](https://www.langchain.com/) and [LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/) frameworks. Solutions such as Tools, Utilities, and Chains are published to the PyPi registry under [langchain-tableau](https://pypi.org/project/langchain-tableau/) following conventions for [integrations](https://python.langchain.com/docs/contributing/how_to/integrations/) to Langchain. 8 | 9 | > [!IMPORTANT] 10 | > On May 27 2025 this project was moved from the Tableau Solution Engineering GitHub organization to the main Tableau OSS one: 11 | > * from: https://github.com/Tab-SE/tableau_langchain 12 | > * to: https://github.com/tableau/tableau_langchain 13 | > 14 | > Everything should be moved over intact and everyone should still have the same access. 15 | > 16 | > All links to the previous location should automatically redirect to the new location. If you have a copy of this project cloned locally, we recommend updating to point to the new location: 17 | > 18 | > ```sh 19 | > git remote set-url origin https://github.com/tableau/tableau_langchain 20 | > ``` 21 | 22 | 23 | ![tableau logo](experimental/notebooks/assets/tableau_logo_text.png) 24 | 25 | ```bash 26 | pip install langchain-tableau 27 | ``` 28 | 29 | We'd like you to explore how Agentic tools can drive alignment between your organization's data and the day-to-day needs of your users. Consider contributing to this project or creating your own work on a different framework, ultimately we seek to increase the flow of data and help people get answers from it. 30 | 31 | To see live demos of Agents using Tableau, visit: 32 | - [EmbedTableau.com](https://www.embedtableau.com/) | [Github Repository](https://github.com/Tab-SE/embedding_playbook) 33 | 34 |
35 | 36 | # Table of Contents 37 | - [Tableau Langchain](#tableau-langchain) 38 | - [Table of Contents](#table-of-contents) 39 | - [Getting Started](#getting-started) 40 | - [Published Solutions](#published-solutions) 41 | - [Experimental Sandbox](#experimental-sandbox) 42 | - [About This Project](#about-this-project) 43 | - [Published Solutions](#published-solutions-1) 44 | - [Experimental Sandbox](#experimental-sandbox-1) 45 | - [Security](#security) 46 | - [Contributing](#contributing) 47 | 48 | ![area chart](experimental/notebooks/assets/vizart/area_chart_banner.png) 49 | 50 | # Getting Started 51 | 52 | The easiest way to start with `tableau_langchain` is to try the Jupyter Notebooks in the `experimental/notebooks/` folder. These examples will guide you through different use cases and scenarios with increasing complexity. 53 | 54 | ## Published Solutions 55 | 56 | To use the solutions available at [langchain-tableau](https://pypi.org/project/langchain-tableau/) in notebooks and your code, do the following: 57 | 58 | 1. Install `langchain-tableau` 59 | 60 | ```bash 61 | pip install langchain-tableau 62 | ``` 63 | 2. Import `langchain-tableau` and use it with your Agent (in a Python file or Jupyter Notebook) 64 | 65 | ```python 66 | # langchain and langgraph package imports 67 | from langchain_openai import ChatOpenAI 68 | from langgraph.prebuilt import create_react_agent 69 | # langchain_tableau imports 70 | from langchain_tableau.tools.simple_datasource_qa import initialize_simple_datasource_qa 71 | 72 | # initialize an LLM 73 | llm = ChatOpenAI(model='gpt-4o-mini', temperature=0) 74 | 75 | # initalize `simple_datasource_qa` for querying a Tableau Published Datasource through VDS 76 | analyze_datasource = initialize_simple_datasource_qa( 77 | domain='https://your-tableau-cloud-or-server.com', 78 | site='Tableau site name', 79 | jwt_client_id='from an enabled Tableau Connected App', 80 | jwt_secret_id='from an enabled Tableau Connected App', 81 | jwt_secret='from an enabled Tableau Connected App', 82 | tableau_api_version='Tableau REST API version', 83 | tableau_user='user to query the Agent with', 84 | datasource_luid='unique data source ID can be obtained via REST or Metadata APIs', 85 | tooling_llm_model='model to use for the data query tool' 86 | ) 87 | 88 | # Add the tool to the array of tools used by the Agent 89 | tools = [ analyze_datasource ] 90 | 91 | # Build the Agent using the minimum components (LLM + Tools) 92 | tableauAgent = create_react_agent(llm, tools) 93 | 94 | # Run the Agent 95 | messages = tableauAgent.invoke({"messages": [("human",'which states sell the most? Are those the same states with the most profits?')]}) 96 | ``` 97 | 98 |
99 | 100 | ## Experimental Sandbox 101 | 102 | To develop and test solutions for the `langchain-tableau` package, this repository contains an `experimental/` folder organizing Agents, Tools, Utilities and other files that allow contributors to improve the solutions made available to the open-source community. 103 | 104 | To use the sandbox, do the following: 105 | 106 | 1. Clone the repository 107 | 108 | ```bash 109 | git clone https://github.com/Tab-SE/tableau_langchain.git 110 | ``` 111 | 112 | 2. Create a Python environment to isolate project dependencies (optional) 113 | 114 | Note: This example uses `conda` (`environment.yml` file provided). If you use `conda` skip to step #4 since dependencies will already be installed. Other environment management systems should also work (`poetry`, `venv`, `mamba` etc.) 115 | 116 | ```bash 117 | conda env create -f environment.yml 118 | conda activate tableau_langchain 119 | ``` 120 | 121 | 3. Install project dependencies (use this to install anytime with or without isolated Python environments) 122 | 123 | Note: dependencies are listed in the `pyproject.toml` file 124 | 125 | ```bash 126 | pip install . 127 | ``` 128 | 129 | You must also install the `langgraph-cli` developer dependency to run the Langgraph Server (see [langgraph-cli](https://langchain-ai.github.io/langgraph/cloud/reference/cli)) 130 | 131 | ```bash 132 | pip install langgraph-cli 133 | ``` 134 | 135 | If you wish to run the Langgraph Server in local development mode, you will need the `inmem` extra (see [langgraph dev](https://langchain-ai.github.io/langgraph/cloud/reference/cli/#dev) command) 136 | 137 | ```bash 138 | pip install -U "langgraph-cli[inmem]" 139 | ``` 140 | 141 | 4. Declare Environment Variables 142 | 143 | Start by duplicating the template file: 144 | 145 | ```bash 146 | cp .env.template .env 147 | ``` 148 | 149 | Replace the values in the `.env` file with your own. These values are secure and never published to Github 150 | 151 | 5. Run an Agent in the terminal 152 | 153 | ```bash 154 | python main.py 155 | ``` 156 | 157 | 6. Run the Langgraph Server API (see [langgraph-cli](https://langchain-ai.github.io/langgraph/cloud/reference/cli/#commands)) 158 | 159 | Note: Docker Desktop must also be running 160 | 161 | Local Development 162 | ```bash 163 | langgraph dev 164 | ``` 165 | 166 | Docker Container 167 | ```bash 168 | langgraph build 169 | langgraph up 170 | ``` 171 | 172 | ![dual axis area chart](experimental/notebooks/assets/vizart/up_down_area.png) 173 | 174 | # About This Project 175 | 176 | This repository is a monorepo with two components. The main goal is to publish and support a Langchain [integration](https://python.langchain.com/docs/contributing/how_to/integrations/). This produces a need to have a development sandbox to try these solutions before publishing them for open-source use. 177 | 178 | The code base has two top-level folders: `experimental/` and `pkg/`. Experimental is where active development takes place. Use this folder to build and test new tools, chains, and agents, or extend the resources that are already there. The `pkg/` folder packages up well-tested resources for inclusion in our public PyPi package: `langchain-tableau`. If you have a contribution to `pkg/`, first make sure it’s been tested in experimental, and then submit your PR via GitHub. 179 | 180 | The `main` branch will always be the latest stable branch. 181 | 182 | ## Published Solutions 183 | The `pgk` folder contains production code shipped to the [PyPi registry](https://pypi.org/project/langchain-tableau/). 184 | These are available resources: 185 | 186 | 1. `simple_datasource_qa.py` 187 | - Query a Published Datasource in natural language 188 | - Leverage the analytical engine provided by Tableau's VizQL Data Service 189 | - Supports aggregating, filtering, and soon: calcs! 190 | - Scales securely by way of the API interface, preventing SQL injection 191 | 192 | ## Experimental Sandbox 193 | The `experimental` folder organizes agents, tools, utilities, and notebooks for developing and solution testing that may eventually be published ([see Published Agent Tools](#published-agent-tools)) for community use. This folder is a sandbox for Tableau AI. 194 | 195 | ## Security 196 | 197 | Tableau resources are accessed via supported authentication methods such as Connected Apps. Secrets, keys and other credentials used to access Tableau should not be published to Github and instead stored securely via `.env` files. See step #4 in the [Getting Started](#getting-started) section. 198 | 199 | Learn more by reading our [Security](.github/SECURITY.md) article. 200 | 201 | # Contributing 202 | 203 | The Tableau developer community (aka DataDev) is more than welcome to contribute to this project by enhancing either the `experimental` or `pkg` folders. 204 | 205 | This is the founding team for the project. Please consider contributing in your own way to further what's possible when you combine Tableau with AI Agents. 206 | 207 | * Stephen Price [@stephenlprice](https://github.com/stephenlprice) - Architect 208 | * Joe Constantino [@joeconstantino](https://github.com/joeconstantino) - Product Manager 209 | * Joseph Fluckiger [@josephflu](https://github.com/josephflu) - Lead Developer 210 | * Will Sutton [@wjsutton](https://github.com/wjsutton) - Developer 211 | * Cristian Saavedra [@cristiansaavedra](https://github.com/cristiansaavedra) - Developer 212 | * Antoine Issaly [@antoineissaly](https://github.com/antoineissaly) - Developer 213 | 214 | If you wish to contribute to this project, please refer to our [Contribution Guidelines](CONTRIBUTING.md). 215 | Also, check out the [Code of Conduct](CODE_OF_CONDUCT.md). 216 | 217 | ![datadev](experimental/notebooks/assets/datadev.png) 218 | -------------------------------------------------------------------------------- /experimental/tools/simple_datasource_qa.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | from pydantic import BaseModel, Field 4 | 5 | from langchain.prompts import PromptTemplate 6 | from langchain_core.tools import tool, ToolException 7 | 8 | from experimental.tools.prompts import vds_query, vds_prompt_data, vds_response 9 | from experimental.utilities.auth import jwt_connected_app 10 | from experimental.utilities.models import select_model 11 | from experimental.utilities.simple_datasource_qa import ( 12 | env_vars_simple_datasource_qa, 13 | augment_datasource_metadata, 14 | get_headlessbi_data, 15 | prepare_prompt_inputs 16 | ) 17 | 18 | 19 | class DataSourceQAInputs(BaseModel): 20 | """Describes inputs for usage of the simple_datasource_qa tool""" 21 | 22 | user_input: str = Field( 23 | ..., 24 | description="""Describe the user query thoroughly in natural language such as: 'orders and sales for April 20 2025'. 25 | You can ask for relative dates such as last week, 3 days ago, current year, previous 3 quarters or 26 | specific dates: profits and average discounts for last week""", 27 | examples=[ 28 | "sales and orders for April 20 2025" 29 | ] 30 | ) 31 | previous_call_error: Optional[str] = Field( 32 | None, 33 | description="""If the previous interaction resulted in a VizQL Data Service error, include the error otherwise use None: 34 | Error: Quantitative Filters must have a QuantitativeFilterType""", 35 | examples=[ 36 | None, # no errors example 37 | "Error: Quantitative Filters must have a QuantitativeFilterType" 38 | ], 39 | ) 40 | previous_vds_payload: Optional[str] = Field( 41 | None, 42 | description="""If the previous interaction resulted in a VizQL Data Service error, include the faulty VDS JSON payload 43 | otherwise use None: {\"fields\":[{\"fieldCaption\":\"Sub-Category\",\"fieldAlias\":\"SubCategory\",\"sortDirection\":\"DESC\", 44 | \"sortPriority\":1},{\"function\":\"SUM\",\"fieldCaption\":\"Sales\",\"fieldAlias\":\"TotalSales\"}], 45 | \"filters\":[{\"field\":{\"fieldCaption\":\"Order Date\"},\"filterType\":\"QUANTITATIVE_DATE\",\"minDate\":\"2023-04-01\", 46 | \"maxDate\":\"2023-10-01\"},{\"field\":{\"fieldCaption\":\"Sales\"},\"filterType\":\"QUANTITATIVE_NUMERICAL\", 47 | \"quantitativeFilterType\":\"MIN\",\"min\":200000},{\"field\":{\"fieldCaption\":\"Sub-Category\"},\"filterType\":\"MATCH\", 48 | \"exclude\":true,\"contains\":\"Technology\"}]}""", 49 | examples=[ 50 | None, # no errors example 51 | "{\"fields\":[{\"fieldCaption\":\"Sub-Category\",\"fieldAlias\":\"SubCategory\",\"sortDirection\":\"DESC\",\"sortPriority\":1},{\"function\":\"SUM\",\"fieldCaption\":\"Sales\",\"fieldAlias\":\"TotalSales\"}],\"filters\":[{\"field\":{\"fieldCaption\":\"Order Date\"},\"filterType\":\"QUANTITATIVE_DATE\",\"minDate\":\"2023-04-01\",\"maxDate\":\"2023-10-01\"},{\"field\":{\"fieldCaption\":\"Sales\"},\"filterType\":\"QUANTITATIVE_NUMERICAL\",\"quantitativeFilterType\":\"MIN\",\"min\":200000},{\"field\":{\"fieldCaption\":\"Sub-Category\"},\"filterType\":\"MATCH\",\"exclude\":true,\"contains\":\"Technology\"}]}" 52 | ], 53 | ) 54 | 55 | 56 | def initialize_simple_datasource_qa( 57 | domain: Optional[str] = None, 58 | site: Optional[str] = None, 59 | jwt_client_id: Optional[str] = None, 60 | jwt_secret_id: Optional[str] = None, 61 | jwt_secret: Optional[str] = None, 62 | tableau_api_version: Optional[str] = None, 63 | tableau_user: Optional[str] = None, 64 | datasource_luid: Optional[str] = None, 65 | model_provider: Optional[str] = None, 66 | tooling_llm_model: Optional[str] = None 67 | ): 68 | """ 69 | Initializes the Langgraph tool called 'simple_datasource_qa' for analytical 70 | questions and answers on a Tableau Data Source 71 | 72 | Args: 73 | domain (Optional[str]): The domain of the Tableau server. 74 | site (Optional[str]): The site name on the Tableau server. 75 | jwt_client_id (Optional[str]): The client ID for JWT authentication. 76 | jwt_secret_id (Optional[str]): The secret ID for JWT authentication. 77 | jwt_secret (Optional[str]): The secret for JWT authentication. 78 | tableau_api_version (Optional[str]): The version of the Tableau API to use. 79 | tableau_user (Optional[str]): The Tableau user to authenticate as. 80 | datasource_luid (Optional[str]): The LUID of the data source to perform QA on. 81 | tooling_llm_model (Optional[str]): The LLM model to use for tooling operations. 82 | 83 | Returns: 84 | function: A decorated function that can be used as a langgraph tool for data source QA. 85 | 86 | The returned function (datasource_qa) takes the following parameters: 87 | user_input (str): The user's query or command represented in simple SQL. 88 | previous_call_error (Optional[str]): Any error from a previous call, for error handling. 89 | 90 | It returns a dictionary containing the results of the QA operation. 91 | 92 | Note: 93 | If arguments are not provided, the function will attempt to read them from 94 | environment variables, typically stored in a .env file. 95 | """ 96 | # if arguments are not provided, the tool obtains environment variables directly from .env 97 | env_vars = env_vars_simple_datasource_qa( 98 | domain=domain, 99 | site=site, 100 | jwt_client_id=jwt_client_id, 101 | jwt_secret_id=jwt_secret_id, 102 | jwt_secret=jwt_secret, 103 | tableau_api_version=tableau_api_version, 104 | tableau_user=tableau_user, 105 | datasource_luid=datasource_luid, 106 | model_provider=model_provider, 107 | tooling_llm_model=tooling_llm_model 108 | ) 109 | 110 | @tool("simple_datasource_qa", args_schema=DataSourceQAInputs) 111 | def simple_datasource_qa( 112 | user_input: str, 113 | previous_call_error: Optional[str] = None, 114 | previous_vds_payload: Optional[str] = None 115 | ) -> dict: 116 | """ 117 | Queries a Tableau data source for analytical Q&A. Returns a data set you can use to answer user questions. 118 | To be more efficient, describe your entire query in a single request rather than selecting small slices of 119 | data in multiple requests. DO NOT perform multiple queries if all the data can be fetched at once with the 120 | same filters or conditions: 121 | 122 | Good query: "Profits & average discounts by region for last week" 123 | Bad queries: "profits per region last week" & "average discounts per region last week" 124 | 125 | If you received an error after using this tool, mention it in your next attempt to help the tool correct itself. 126 | """ 127 | 128 | # Session scopes are limited to only required authorizations to Tableau resources that support tool operations 129 | access_scopes = [ 130 | "tableau:content:read", # for quering Tableau Metadata API 131 | "tableau:viz_data_service:read" # for querying VizQL Data Service 132 | ] 133 | try: 134 | tableau_session = jwt_connected_app( 135 | tableau_domain=env_vars["domain"], 136 | tableau_site=env_vars["site"], 137 | jwt_client_id=env_vars["jwt_client_id"], 138 | jwt_secret_id=env_vars["jwt_secret_id"], 139 | jwt_secret=env_vars["jwt_secret"], 140 | tableau_api=env_vars["tableau_api_version"], 141 | tableau_user=env_vars["tableau_user"], 142 | scopes=access_scopes 143 | ) 144 | except Exception as e: 145 | auth_error_string = f""" 146 | CRITICAL ERROR: Could not authenticate to the Tableau site successfully. 147 | This tool is unusable as a result. 148 | Error from remote server: {e} 149 | 150 | INSTRUCTION: Do not ask the user to provide credentials directly or in chat since they should 151 | originate from a secure Connected App or similar authentication mechanism. You may inform the 152 | user that you are not able to access their Tableau environment at this time. You can also describe 153 | the nature of the error to help them understand why you can't service their request. 154 | """ 155 | raise ToolException(auth_error_string) 156 | 157 | # credentials to access Tableau environment on behalf of the user 158 | tableau_auth = tableau_session['credentials']['token'] 159 | 160 | # Data source for VDS querying 161 | tableau_datasource = env_vars["datasource_luid"] 162 | 163 | # 0. Obtain metadata about the data source to enhance the query writing prompt 164 | query_writing_data = augment_datasource_metadata( 165 | task = user_input, 166 | api_key = tableau_auth, 167 | url = domain, 168 | datasource_luid = tableau_datasource, 169 | prompt = vds_prompt_data, 170 | previous_errors = previous_call_error, 171 | previous_vds_payload = previous_vds_payload 172 | ) 173 | 174 | # 1. Insert instruction data into the template 175 | query_writing_prompt = PromptTemplate( 176 | input_variables=[ 177 | "task" 178 | "instructions", 179 | "vds_schema", 180 | "sample_queries", 181 | "error_queries", 182 | "data_dictionary", 183 | "data_model", 184 | "previous_call_error", 185 | "previous_vds_payload" 186 | ], 187 | template=vds_query 188 | ) 189 | 190 | # 2. Instantiate language model to execute the prompt to write a VizQL Data Service query 191 | query_writer = select_model( 192 | provider=env_vars["model_provider"], 193 | model_name=env_vars["tooling_llm_model"], 194 | temperature=0 195 | ) 196 | 197 | # 3. Query data from Tableau's VizQL Data Service using the AI written payload 198 | def get_data(vds_query): 199 | payload = vds_query.content 200 | 201 | try: 202 | data = get_headlessbi_data( 203 | api_key = tableau_auth, 204 | url = domain, 205 | datasource_luid = tableau_datasource, 206 | payload = payload 207 | ) 208 | 209 | return { 210 | "vds_query": payload, 211 | "data_table": data, 212 | } 213 | except Exception as e: 214 | query_error_message = f""" 215 | Tableau's VizQL Data Service return an error for the generated query: 216 | 217 | {str(vds_query.content)} 218 | 219 | The user_input used to write this query was: 220 | 221 | {str(user_input)} 222 | 223 | This was the error: 224 | 225 | {str(e)} 226 | 227 | Consider retrying this tool with the same inputs but include the previous query 228 | causing the error and the error itself for the tool to correct itself on a retry. 229 | If the error was an empty array, this usually indicates an incorrect filter value 230 | was applied, thus returning no data 231 | """ 232 | 233 | raise ToolException(query_error_message) 234 | 235 | # 4. Prepare inputs for a structured response to the calling Agent 236 | def response_inputs(input): 237 | metadata = query_writing_data.get('meta') 238 | data = { 239 | "query": input.get('vds_query', ''), 240 | "data_source_name": metadata.get('datasource_name'), 241 | "data_source_description": metadata.get('datasource_description'), 242 | "data_source_maintainer": metadata.get('datasource_owner'), 243 | "data_table": input.get('data_table', ''), 244 | } 245 | inputs = prepare_prompt_inputs(data=data, user_string=user_input) 246 | return inputs 247 | 248 | # 5. Response template for the Agent with further instructions 249 | response_prompt = PromptTemplate( 250 | input_variables=[ 251 | "data_source_name", 252 | "data_source_description", 253 | "data_source_maintainer", 254 | "vds_query", 255 | "data_table", 256 | "user_input" 257 | ], 258 | template=vds_response 259 | ) 260 | 261 | # this chain defines the flow of data through the system 262 | chain = query_writing_prompt | query_writer | get_data | response_inputs | response_prompt 263 | 264 | 265 | # invoke the chain to generate a query and obtain data 266 | vizql_data = chain.invoke(query_writing_data) 267 | 268 | # Return the structured output 269 | return vizql_data 270 | 271 | return simple_datasource_qa 272 | -------------------------------------------------------------------------------- /pkg/langchain_tableau/tools/simple_datasource_qa.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from pydantic import BaseModel, Field 3 | 4 | from langchain.prompts import PromptTemplate 5 | from langchain_core.tools import tool, ToolException 6 | 7 | from langchain_tableau.tools.prompts import vds_query, vds_prompt_data, vds_response 8 | from langchain_tableau.utilities.auth import jwt_connected_app 9 | from langchain_tableau.utilities.models import select_model 10 | from langchain_tableau.utilities.simple_datasource_qa import ( 11 | env_vars_simple_datasource_qa, 12 | augment_datasource_metadata, 13 | get_headlessbi_data, 14 | prepare_prompt_inputs 15 | ) 16 | 17 | 18 | class DataSourceQAInputs(BaseModel): 19 | """Describes inputs for usage of the simple_datasource_qa tool""" 20 | 21 | user_input: str = Field( 22 | ..., 23 | description="""Describe the user query thoroughly in natural language such as: 'orders and sales for April 20 2025'. 24 | You can ask for relative dates such as last week, 3 days ago, current year, previous 3 quarters or 25 | specific dates: profits and average discounts for last week""", 26 | examples=[ 27 | "sales and orders for April 20 2025" 28 | ] 29 | ) 30 | previous_call_error: Optional[str] = Field( 31 | None, 32 | description="""If the previous interaction resulted in a VizQL Data Service error, include the error otherwise use None: 33 | Error: Quantitative Filters must have a QuantitativeFilterType""", 34 | examples=[ 35 | None, # no errors example 36 | "Error: Quantitative Filters must have a QuantitativeFilterType" 37 | ], 38 | ) 39 | previous_vds_payload: Optional[str] = Field( 40 | None, 41 | description="""If the previous interaction resulted in a VizQL Data Service error, include the faulty VDS JSON payload 42 | otherwise use None: {\"fields\":[{\"fieldCaption\":\"Sub-Category\",\"fieldAlias\":\"SubCategory\",\"sortDirection\":\"DESC\", 43 | \"sortPriority\":1},{\"function\":\"SUM\",\"fieldCaption\":\"Sales\",\"fieldAlias\":\"TotalSales\"}], 44 | \"filters\":[{\"field\":{\"fieldCaption\":\"Order Date\"},\"filterType\":\"QUANTITATIVE_DATE\",\"minDate\":\"2023-04-01\", 45 | \"maxDate\":\"2023-10-01\"},{\"field\":{\"fieldCaption\":\"Sales\"},\"filterType\":\"QUANTITATIVE_NUMERICAL\", 46 | \"quantitativeFilterType\":\"MIN\",\"min\":200000},{\"field\":{\"fieldCaption\":\"Sub-Category\"},\"filterType\":\"MATCH\", 47 | \"exclude\":true,\"contains\":\"Technology\"}]}""", 48 | examples=[ 49 | None, # no errors example 50 | "{\"fields\":[{\"fieldCaption\":\"Sub-Category\",\"fieldAlias\":\"SubCategory\",\"sortDirection\":\"DESC\",\"sortPriority\":1},{\"function\":\"SUM\",\"fieldCaption\":\"Sales\",\"fieldAlias\":\"TotalSales\"}],\"filters\":[{\"field\":{\"fieldCaption\":\"Order Date\"},\"filterType\":\"QUANTITATIVE_DATE\",\"minDate\":\"2023-04-01\",\"maxDate\":\"2023-10-01\"},{\"field\":{\"fieldCaption\":\"Sales\"},\"filterType\":\"QUANTITATIVE_NUMERICAL\",\"quantitativeFilterType\":\"MIN\",\"min\":200000},{\"field\":{\"fieldCaption\":\"Sub-Category\"},\"filterType\":\"MATCH\",\"exclude\":true,\"contains\":\"Technology\"}]}" 51 | ], 52 | ) 53 | 54 | 55 | def initialize_simple_datasource_qa( 56 | domain: Optional[str] = None, 57 | site: Optional[str] = None, 58 | jwt_client_id: Optional[str] = None, 59 | jwt_secret_id: Optional[str] = None, 60 | jwt_secret: Optional[str] = None, 61 | tableau_api_version: Optional[str] = None, 62 | tableau_user: Optional[str] = None, 63 | datasource_luid: Optional[str] = None, 64 | model_provider: Optional[str] = None, 65 | tooling_llm_model: Optional[str] = None 66 | ): 67 | """ 68 | Initializes the Langgraph tool called 'simple_datasource_qa' for analytical 69 | questions and answers on a Tableau Data Source 70 | 71 | Args: 72 | domain (Optional[str]): The domain of the Tableau server. 73 | site (Optional[str]): The site name on the Tableau server. 74 | jwt_client_id (Optional[str]): The client ID for JWT authentication. 75 | jwt_secret_id (Optional[str]): The secret ID for JWT authentication. 76 | jwt_secret (Optional[str]): The secret for JWT authentication. 77 | tableau_api_version (Optional[str]): The version of the Tableau API to use. 78 | tableau_user (Optional[str]): The Tableau user to authenticate as. 79 | datasource_luid (Optional[str]): The LUID of the data source to perform QA on. 80 | tooling_llm_model (Optional[str]): The LLM model to use for tooling operations. 81 | 82 | Returns: 83 | function: A decorated function that can be used as a langgraph tool for data source QA. 84 | 85 | The returned function (datasource_qa) takes the following parameters: 86 | user_input (str): The user's query or command represented in simple SQL. 87 | previous_call_error (Optional[str]): Any error from a previous call, for error handling. 88 | 89 | It returns a dictionary containing the results of the QA operation. 90 | 91 | Note: 92 | If arguments are not provided, the function will attempt to read them from 93 | environment variables, typically stored in a .env file. 94 | """ 95 | # if arguments are not provided, the tool obtains environment variables directly from .env 96 | env_vars = env_vars_simple_datasource_qa( 97 | domain=domain, 98 | site=site, 99 | jwt_client_id=jwt_client_id, 100 | jwt_secret_id=jwt_secret_id, 101 | jwt_secret=jwt_secret, 102 | tableau_api_version=tableau_api_version, 103 | tableau_user=tableau_user, 104 | datasource_luid=datasource_luid, 105 | model_provider=model_provider, 106 | tooling_llm_model=tooling_llm_model 107 | ) 108 | 109 | @tool("simple_datasource_qa", args_schema=DataSourceQAInputs) 110 | def simple_datasource_qa( 111 | user_input: str, 112 | previous_call_error: Optional[str] = None, 113 | previous_vds_payload: Optional[str] = None 114 | ) -> dict: 115 | """ 116 | Queries a Tableau data source for analytical Q&A. Returns a data set you can use to answer user questions. 117 | To be more efficient, describe your entire query in a single request rather than selecting small slices of 118 | data in multiple requests. DO NOT perform multiple queries if all the data can be fetched at once with the 119 | same filters or conditions: 120 | 121 | Good query: "Profits & average discounts by region for last week" 122 | Bad queries: "profits per region last week" & "average discounts per region last week" 123 | 124 | If you received an error after using this tool, mention it in your next attempt to help the tool correct itself. 125 | """ 126 | 127 | # Session scopes are limited to only required authorizations to Tableau resources that support tool operations 128 | access_scopes = [ 129 | "tableau:content:read", # for quering Tableau Metadata API 130 | "tableau:viz_data_service:read" # for querying VizQL Data Service 131 | ] 132 | try: 133 | tableau_session = jwt_connected_app( 134 | tableau_domain=env_vars["domain"], 135 | tableau_site=env_vars["site"], 136 | jwt_client_id=env_vars["jwt_client_id"], 137 | jwt_secret_id=env_vars["jwt_secret_id"], 138 | jwt_secret=env_vars["jwt_secret"], 139 | tableau_api=env_vars["tableau_api_version"], 140 | tableau_user=env_vars["tableau_user"], 141 | scopes=access_scopes 142 | ) 143 | except Exception as e: 144 | auth_error_string = f""" 145 | CRITICAL ERROR: Could not authenticate to the Tableau site successfully. 146 | This tool is unusable as a result. 147 | Error from remote server: {e} 148 | 149 | INSTRUCTION: Do not ask the user to provide credentials directly or in chat since they should 150 | originate from a secure Connected App or similar authentication mechanism. You may inform the 151 | user that you are not able to access their Tableau environment at this time. You can also describe 152 | the nature of the error to help them understand why you can't service their request. 153 | """ 154 | raise ToolException(auth_error_string) 155 | 156 | # credentials to access Tableau environment on behalf of the user 157 | tableau_auth = tableau_session['credentials']['token'] 158 | 159 | # Data source for VDS querying 160 | tableau_datasource = env_vars["datasource_luid"] 161 | 162 | # 0. Obtain metadata about the data source to enhance the query writing prompt 163 | query_writing_data = augment_datasource_metadata( 164 | task = user_input, 165 | api_key = tableau_auth, 166 | url = domain, 167 | datasource_luid = tableau_datasource, 168 | prompt = vds_prompt_data, 169 | previous_errors = previous_call_error, 170 | previous_vds_payload = previous_vds_payload 171 | ) 172 | 173 | # 1. Insert instruction data into the template 174 | query_writing_prompt = PromptTemplate( 175 | input_variables=[ 176 | "task" 177 | "instructions", 178 | "vds_schema", 179 | "sample_queries", 180 | "error_queries", 181 | "data_dictionary", 182 | "data_model", 183 | "previous_call_error", 184 | "previous_vds_payload" 185 | ], 186 | template=vds_query 187 | ) 188 | 189 | # 2. Instantiate language model to execute the prompt to write a VizQL Data Service query 190 | query_writer = select_model( 191 | provider=env_vars["model_provider"], 192 | model_name=env_vars["tooling_llm_model"], 193 | temperature=0 194 | ) 195 | 196 | # 3. Query data from Tableau's VizQL Data Service using the AI written payload 197 | def get_data(vds_query): 198 | payload = vds_query.content 199 | try: 200 | data = get_headlessbi_data( 201 | api_key = tableau_auth, 202 | url = domain, 203 | datasource_luid = tableau_datasource, 204 | payload = payload 205 | ) 206 | 207 | return { 208 | "vds_query": payload, 209 | "data_table": data, 210 | } 211 | except Exception as e: 212 | query_error_message = f""" 213 | Tableau's VizQL Data Service return an error for the generated query: 214 | 215 | {str(vds_query.content)} 216 | 217 | The user_input used to write this query was: 218 | 219 | {str(user_input)} 220 | 221 | This was the error: 222 | 223 | {str(e)} 224 | 225 | Consider retrying this tool with the same inputs but include the previous query 226 | causing the error and the error itself for the tool to correct itself on a retry. 227 | If the error was an empty array, this usually indicates an incorrect filter value 228 | was applied, thus returning no data 229 | """ 230 | 231 | raise ToolException(query_error_message) 232 | 233 | # 4. Prepare inputs for a structured response to the calling Agent 234 | def response_inputs(input): 235 | metadata = query_writing_data.get('meta') 236 | data = { 237 | "query": input.get('vds_query', ''), 238 | "data_source_name": metadata.get('datasource_name'), 239 | "data_source_description": metadata.get('datasource_description'), 240 | "data_source_maintainer": metadata.get('datasource_owner'), 241 | "data_table": input.get('data_table', ''), 242 | } 243 | inputs = prepare_prompt_inputs(data=data, user_string=user_input) 244 | return inputs 245 | 246 | # 5. Response template for the Agent with further instructions 247 | response_prompt = PromptTemplate( 248 | input_variables=[ 249 | "data_source_name", 250 | "data_source_description", 251 | "data_source_maintainer", 252 | "vds_query", 253 | "data_table", 254 | "user_input" 255 | ], 256 | template=vds_response 257 | ) 258 | 259 | # this chain defines the flow of data through the system 260 | chain = query_writing_prompt | query_writer | get_data | response_inputs | response_prompt 261 | 262 | 263 | # invoke the chain to generate a query and obtain data 264 | vizql_data = chain.invoke(query_writing_data) 265 | 266 | # Return the structured output 267 | return vizql_data 268 | 269 | return simple_datasource_qa 270 | -------------------------------------------------------------------------------- /experimental/notebooks/lanchain-tableau_cookbook_01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1f302499-eb05-4296-8716-950babc0f10e", 6 | "metadata": {}, 7 | "source": [ 8 | "## Chat with Tableau\n", 9 | "\n", 10 | "Tableau's [VizQL Data Service](https://help.tableau.com/current/api/vizql-data-service/en-us/index.html) (aka VDS) provides developers with programmatic access to their Tableau Published Data Sources, allowing them to extend their business semantics for any custom workload or application, including AI Agents. The simple_datasource_qa tool adds VDS to the Langchain framework. This notebook shows you how you can use it to build agents that answer analytical questions grounded on your enterprise semantic models. \n", 11 | "\n", 12 | "Follow the [tableau-langchain](https://github.com/Tab-SE/tableau_langchain) project for more tools coming soon!\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "4d57b913-819e-4676-9f6e-3afe0a80030e", 18 | "metadata": {}, 19 | "source": [ 20 | "## Requirements\n", 21 | "1. python version 3.12.2 or higher\n", 22 | "2. A Tableau Cloud or Server environment with at least 1 published data source\n", 23 | "\n", 24 | "Get started by installing and/or importing the required packages" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "9b178e95-ffae-4f04-ad77-1fdc2ab05edf", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "%pip install langchain-openai" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "id": "8605e87a-2253-4c89-992a-ecdbec955ef6", 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "%pip install langgraph" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "id": "c13dca76", 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "%pip install langchain-tableau --upgrade" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "id": "bbaa05f4", 60 | "metadata": {}, 61 | "source": [ 62 | "Note you may need to restart your kernal to use updated packages" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "id": "e0b74b8f-67a3-40f4-af32-498057e427b9", 68 | "metadata": {}, 69 | "source": [ 70 | "## Overview\n", 71 | "The initialize_simple_datasource_qa initializes the Langgraph tool called [simple_datasource_qa](https://github.com/Tab-SE/tableau_langchain/blob/3ff9047414479cd55d797c18a78f834d57860761/pip_package/langchain_tableau/tools/simple_datasource_qa.py#L101), which can be used for analytical questions and answers on a Tableau Data Source.\n", 72 | "\n", 73 | "This initializer function:\n", 74 | "1. Authenticates to Tableau using Tableau's connected-app framework for JWT-based authentication. All the required variables must defined at runtime or as environment variables.\n", 75 | "2. Asynchronously queries for the field metadata of the target datasource specified in the datasource_luid variable\n", 76 | "3. Grounds on the metadata of the target datasource to transform natural language questions into the json-formatted query payload required to make VDS /query-datasource requests \n", 77 | "4. Executes a POST request to VDS\n", 78 | "5. Formats and returns the results in a structured response" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "id": "310d21b3", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "#langchain and langgraph package imports\n", 89 | "from langchain_openai import ChatOpenAI\n", 90 | "from langchain.agents import initialize_agent, AgentType\n", 91 | "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n", 92 | "from langgraph.prebuilt import create_react_agent\n", 93 | "\n", 94 | "#langchain_tableau imports\n", 95 | "#from langchain_tableau.tools.simple_datasource_qa import initialize_simple_datasource_qa\n", 96 | "from experimental.tools.simple_datasource_qa import initialize_simple_datasource_qa" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "id": "596d6718-f2e1-44bb-b614-65447862661c", 102 | "metadata": {}, 103 | "source": [ 104 | "## Authentication Variables\n", 105 | "You can declare your environment variables explicitly, as shown in several cases in this cookbook. However, ff these parameters are not provided, the simple_datasource_qa tool will attempt to automatically read them from environment variables.\n", 106 | "\n", 107 | "For the Data Source that you choose, make sure you've updated the VizqlDataApiAccess permission in Tableau to allow the VDS API to access that Data Source via REST. More info [here](https://help.tableau.com/current/server/en-us/permissions_capabilities.htm#data-sources\n", 108 | "). " 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "id": "ccfb4159-34ac-4816-a8f0-795c5442c0b2", 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "import os\n", 119 | "from dotenv import load_dotenv\n", 120 | "load_dotenv()\n", 121 | "\n", 122 | "tableau_server = os.getenv('TABLEAU_DOMAIN') #replace with your Tableau server name\n", 123 | "tableau_site = os.getenv('TABLEAU_SITE') #replace with your Tableau site\n", 124 | "tableau_jwt_client_id = os.getenv('TABLEAU_JWT_CLIENT_ID') #a JWT client ID (obtained through Tableau's admin UI)\n", 125 | "tableau_jwt_secret_id = os.getenv('TABLEAU_JWT_SECRET_ID') #a JWT secret ID (obtained through Tableau's admin UI)\n", 126 | "tableau_jwt_secret = os.getenv('TABLEAU_JWT_SECRET') #a JWT secret ID (obtained through Tableau's admin UI)\n", 127 | "tableau_api_version = '3.21' #the current Tableau REST API Version\n", 128 | "tableau_user = os.getenv('TABLEAU_USER') #replace with the username querying the target Tableau Data Source\n", 129 | "\n", 130 | "# For this cookbook we are connecting to the Superstore dataset that comes by default with every Tableau server\n", 131 | "datasource_luid = os.getenv('DATASOURCE_LUID') #the target data source for this Tool\n", 132 | "\n", 133 | "# Add variables to control LLM models for the Agent and Tools\n", 134 | "os.getenv(\"OPENAI_API_KEY\") #set an your model API key as an environment variable\n", 135 | "tooling_llm_model = os.getenv(\"TOOLING_MODEL\") #set the LLM model for the Agent\n", 136 | "print(f\"Tooling LLM Model: {tooling_llm_model}\")" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "id": "39ae3703-d271-44aa-8f3b-05ddadacc59d", 142 | "metadata": {}, 143 | "source": [ 144 | "## Langchain Example\n", 145 | "First, we'll initlialize the LLM of our choice. Next, we initialize our tool for chatting with tableau data sources and store it in a variable called analyze_datasource. Finally, we define an agent using Langchain legacy initialize_agent constructor and invoke it with a query related to the target data source. " 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "id": "35376fe7-09ae-4a78-ab9b-ec2e652ffbca", 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "from IPython.display import display, Markdown\n", 156 | "\n", 157 | "# Initialize an LLM \n", 158 | "llm = ChatOpenAI(model='o4-mini', temperature=0)\n", 159 | "\n", 160 | "# Initalize simple_datasource_qa for querying Tableau Datasources through VDS\n", 161 | "analyze_datasource = initialize_simple_datasource_qa(\n", 162 | " domain=tableau_server,\n", 163 | " site=tableau_site,\n", 164 | " jwt_client_id=tableau_jwt_client_id,\n", 165 | " jwt_secret_id=tableau_jwt_secret_id,\n", 166 | " jwt_secret=tableau_jwt_secret,\n", 167 | " tableau_api_version=tableau_api_version,\n", 168 | " tableau_user=tableau_user,\n", 169 | " datasource_luid=datasource_luid,\n", 170 | " tooling_llm_model=tooling_llm_model)\n", 171 | "\n", 172 | "# load the List of Tools to be used by the Agent. In this case we will just load our data source Q&A tool.\n", 173 | "tools = [ analyze_datasource ]\n", 174 | "\n", 175 | "tableauHeadlessAgent = initialize_agent(\n", 176 | " tools,\n", 177 | " llm,\n", 178 | " agent=AgentType.OPENAI_FUNCTIONS, # Use OpenAI's function calling\n", 179 | " verbose = False)\n", 180 | "\n", 181 | "# Run the agent\n", 182 | "response = tableauHeadlessAgent.invoke(\"which school are available in the dataset?\")\n", 183 | "response\n", 184 | "#display(Markdown(response['output'])) #display a nicely formatted answer for successful generations" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": null, 190 | "id": "2839c4fd", 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "def env_vars_simple_datasource_qa(\n", 195 | " domain=None,\n", 196 | " site=None,\n", 197 | " jwt_client_id=None,\n", 198 | " jwt_secret_id=None,\n", 199 | " jwt_secret=None,\n", 200 | " tableau_api_version=None,\n", 201 | " tableau_user=None,\n", 202 | " datasource_luid=None,\n", 203 | " model_provider=None,\n", 204 | " tooling_llm_model=None\n", 205 | "):\n", 206 | " \"\"\"\n", 207 | " Retrieves Tableau configuration from environment variables if not provided as arguments.\n", 208 | "\n", 209 | " Args:\n", 210 | " domain (str, optional): Tableau domain\n", 211 | " site (str, optional): Tableau site\n", 212 | " jwt_client_id (str, optional): JWT client ID\n", 213 | " jwt_secret_id (str, optional): JWT secret ID\n", 214 | " jwt_secret (str, optional): JWT secret\n", 215 | " tableau_api_version (str, optional): Tableau API version\n", 216 | " tableau_user (str, optional): Tableau user\n", 217 | " datasource_luid (str, optional): Datasource LUID\n", 218 | " tooling_llm_model (str, optional): Tooling LLM model\n", 219 | "\n", 220 | " Returns:\n", 221 | " dict: A dictionary containing all the configuration values\n", 222 | " \"\"\"\n", 223 | " # Load environment variables before accessing them\n", 224 | " load_dotenv()\n", 225 | "\n", 226 | " config = {\n", 227 | " 'domain': domain if isinstance(domain, str) and domain else os.environ['TABLEAU_DOMAIN'],\n", 228 | " 'site': site or os.environ['TABLEAU_SITE'],\n", 229 | " 'jwt_client_id': jwt_client_id or os.environ['TABLEAU_JWT_CLIENT_ID'],\n", 230 | " 'jwt_secret_id': jwt_secret_id or os.environ['TABLEAU_JWT_SECRET_ID'],\n", 231 | " 'jwt_secret': jwt_secret or os.environ['TABLEAU_JWT_SECRET'],\n", 232 | " 'tableau_api_version': tableau_api_version or os.environ['TABLEAU_API_VERSION'],\n", 233 | " 'tableau_user': tableau_user or os.environ['TABLEAU_USER'],\n", 234 | " 'datasource_luid': datasource_luid or os.environ['DATASOURCE_LUID'],\n", 235 | " 'model_provider': model_provider or os.environ['MODEL_PROVIDER'],\n", 236 | " 'tooling_llm_model': tooling_llm_model or os.environ['TOOLING_MODEL']\n", 237 | " }\n", 238 | "\n", 239 | " return config" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "id": "0ac5daa0-4336-48d0-9c26-20bf2c252bad", 245 | "metadata": {}, 246 | "source": [ 247 | "## Langgraph Example\n", 248 | "This example uses the updated langgraph agent constructor class to achieve the same outcome. " 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "id": "06a1d3f7-79a8-452e-b37e-9070d15445b0", 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "from IPython.display import display, Markdown\n", 259 | "\n", 260 | "# Initalize simple_datasource_qa for querying Tableau Datasources through VDS\n", 261 | "analyze_datasource = initialize_simple_datasource_qa(\n", 262 | " domain=tableau_server,\n", 263 | " site=tableau_site,\n", 264 | " jwt_client_id=tableau_jwt_client_id,\n", 265 | " jwt_secret_id=tableau_jwt_secret_id,\n", 266 | " jwt_secret=tableau_jwt_secret,\n", 267 | " tableau_api_version=tableau_api_version,\n", 268 | " tableau_user=tableau_user,\n", 269 | " datasource_luid=datasource_luid,\n", 270 | " tooling_llm_model=tooling_llm_model)\n", 271 | "\n", 272 | "tools = [analyze_datasource]\n", 273 | "\n", 274 | "model = ChatOpenAI(model='gpt-4o', temperature=0)\n", 275 | "\n", 276 | "tableauAgent = create_react_agent(model, tools)\n", 277 | "\n", 278 | "# Run the agent\n", 279 | "messages = tableauAgent.invoke({\"messages\": [(\"human\",'Rank schools by their average score in Writing where the score is greater than 499, showing their charter numbers.')]})\n", 280 | "messages\n", 281 | "#display(Markdown(messages['messages'][4].content)) #display a nicely formatted answer for successful generations" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "id": "33d01ab9", 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "display(Markdown(messages['messages'][2].content)) #display a nicely formatted answer for successful generations" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": null, 297 | "id": "55ffe130", 298 | "metadata": {}, 299 | "outputs": [], 300 | "source": [] 301 | } 302 | ], 303 | "metadata": { 304 | "kernelspec": { 305 | "display_name": "Python 3", 306 | "language": "python", 307 | "name": "python3" 308 | }, 309 | "language_info": { 310 | "codemirror_mode": { 311 | "name": "ipython", 312 | "version": 3 313 | }, 314 | "file_extension": ".py", 315 | "mimetype": "text/x-python", 316 | "name": "python", 317 | "nbconvert_exporter": "python", 318 | "pygments_lexer": "ipython3", 319 | "version": "3.12.2" 320 | } 321 | }, 322 | "nbformat": 4, 323 | "nbformat_minor": 5 324 | } 325 | --------------------------------------------------------------------------------