├── 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 |
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 |
12 | {% for item in results %}
13 | -
14 | Name: {{ item.name }}
15 | URI: {{ item.uri }}
16 | LUID: {{ item.luid }}
17 | Certified?: {{ item.isCertified }}
18 | Last Update: {{ item.updatedAt }}
19 | Vector Distance: {{ item.distance }}
20 |
21 |
22 | {% endfor %}
23 |
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 | [](https://badge.fury.io/py/langchain-tableau)
4 | [](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 | 
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 | [](https://www.tableau.com/support-levels-it-and-developer-tools)
4 | [](https://badge.fury.io/py/langchain-tableau)
5 | [](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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------