├── .env.template ├── .gitattributes ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── README.md │ ├── app_backend-test.yml │ ├── app_frontend-vitest.yml │ ├── dockerfile-lint.yml │ ├── python-deps-install-test.yml │ ├── python-static-checks.yml │ ├── shellcheck.yml │ └── yaml-format.yml ├── .gitignore ├── .hadolint.yml ├── .shellcheckrc ├── .yamlfmt.yml ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── Pulumi.yaml ├── README.md ├── Set-Env.ps1 ├── app_backend ├── .gitignore ├── README.md ├── __init__.py ├── app │ ├── __init__.py │ └── main.py ├── metadata.yaml.jinja ├── pyproject.toml ├── requirements.txt ├── start-app.sh ├── static │ ├── assets │ │ ├── Chats-Dd9N05MP.css │ │ ├── Chats-M0pjWu7k.js │ │ ├── Data-Bpsku5AZ.js │ │ ├── DataRobot_white-C2JrcVRN.svg │ │ ├── ResponseMessage-CWOV94tO.js │ │ ├── dm-sans-latin-300-normal-CfSGLRSF.woff2 │ │ ├── dm-sans-latin-300-normal-Cjw7_AC8.woff │ │ ├── dm-sans-latin-400-normal-COF6noiJ.woff2 │ │ ├── dm-sans-latin-400-normal-DmRB7q_B.woff │ │ ├── dm-sans-latin-500-normal-DN3Amd4H.woff │ │ ├── dm-sans-latin-500-normal-kGSpR5A9.woff2 │ │ ├── dm-sans-latin-600-normal-DOZeTYVF.woff │ │ ├── dm-sans-latin-600-normal-DrBIvsIy.woff2 │ │ ├── dm-sans-latin-700-normal-1DREuLwQ.woff2 │ │ ├── dm-sans-latin-700-normal-ryGpXDOP.woff │ │ ├── dm-sans-latin-ext-300-normal-C06zYcNH.woff2 │ │ ├── dm-sans-latin-ext-300-normal-CJf67p0q.woff │ │ ├── dm-sans-latin-ext-400-normal-Cygz-XR6.woff2 │ │ ├── dm-sans-latin-ext-400-normal-NAt9AhwD.woff │ │ ├── dm-sans-latin-ext-500-normal-BBd_G3i-.woff2 │ │ ├── dm-sans-latin-ext-500-normal-hI9Kr37g.woff │ │ ├── dm-sans-latin-ext-600-normal-CFRgRepe.woff2 │ │ ├── dm-sans-latin-ext-600-normal-Czy-B68B.woff │ │ ├── dm-sans-latin-ext-700-normal-BS_Ohp14.woff │ │ ├── dm-sans-latin-ext-700-normal-CGQ_Vo0j.woff2 │ │ ├── index-CMsw26cy.css │ │ ├── index-FhmhbUKT.js │ │ ├── loading-CK9GbiUV.js │ │ ├── react-plotly-BPP4jxfQ.js │ │ └── tabs-3OT-8Yc1.js │ ├── datarobot_favicon.png │ └── index.html └── tests │ ├── __init__.py │ └── test_main.py ├── app_frontend ├── .npmrc ├── .prettierrc.json ├── README.md ├── Taskfile.yaml ├── components.json ├── eslint.config.js ├── index.html ├── package.json ├── postcss.config.mjs ├── public │ └── datarobot_favicon.png ├── src │ ├── App.css │ ├── App.tsx │ ├── api │ │ ├── apiClient.ts │ │ ├── chat-messages │ │ │ ├── api-requests.ts │ │ │ ├── hooks.ts │ │ │ ├── keys.ts │ │ │ ├── mocks.ts │ │ │ └── types.ts │ │ ├── cleansed-datasets │ │ │ ├── api-requests.ts │ │ │ ├── hooks.ts │ │ │ └── keys.ts │ │ ├── database │ │ │ ├── api-requests.ts │ │ │ ├── hooks.ts │ │ │ └── keys.ts │ │ ├── datasets │ │ │ ├── api-requests.ts │ │ │ ├── hooks.ts │ │ │ └── keys.ts │ │ ├── dictionaries │ │ │ ├── api-requests.ts │ │ │ ├── hooks.ts │ │ │ ├── keys.ts │ │ │ └── types.ts │ │ └── user │ │ │ ├── api-requests.ts │ │ │ ├── hooks.ts │ │ │ └── keys.ts │ ├── assets │ │ ├── DataRobotLogo_black.svg │ │ ├── DataRobot_black.svg │ │ ├── DataRobot_white.svg │ │ ├── chat-midnight.svg │ │ ├── loader.svg │ │ └── playground-midnight.svg │ ├── components │ │ ├── AddDataModal.tsx │ │ ├── DataSourceSelector.tsx │ │ ├── DataSourceToggle.tsx │ │ ├── NewChatModal.tsx │ │ ├── RenameChatModal.tsx │ │ ├── SettingsModal.tsx │ │ ├── Sidebar.tsx │ │ ├── WelcomeModal.tsx │ │ ├── chat │ │ │ ├── AnalystDatasetTable.tsx │ │ │ ├── Avatars.tsx │ │ │ ├── CodeTabContent.tsx │ │ │ ├── CollapsiblePanel.tsx │ │ │ ├── ErrorPanel.tsx │ │ │ ├── HeaderSection.tsx │ │ │ ├── InfoText.tsx │ │ │ ├── InitialPrompt.tsx │ │ │ ├── InsightsTabContent.tsx │ │ │ ├── Loading.tsx │ │ │ ├── LoadingIndicator.tsx │ │ │ ├── MarkdownContent.css │ │ │ ├── MarkdownContent.tsx │ │ │ ├── MessageContainer.tsx │ │ │ ├── MessageHeader.tsx │ │ │ ├── PlotPanel.css │ │ │ ├── PlotPanel.tsx │ │ │ ├── ResponseMessage.tsx │ │ │ ├── ResponseTabs.tsx │ │ │ ├── SuggestedPrompt.tsx │ │ │ ├── SuggestedQuestionsSection.tsx │ │ │ ├── SummaryTabContent.tsx │ │ │ ├── UserMessage.tsx │ │ │ ├── UserPrompt.tsx │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── data │ │ │ ├── CleansedDataTable.tsx │ │ │ ├── ClearDatasetsButton.tsx │ │ │ ├── DataViewTabs.tsx │ │ │ ├── DatasetCardDescriptionPanel.tsx │ │ │ ├── DictionaryTable.tsx │ │ │ ├── SearchControl.tsx │ │ │ └── index.ts │ │ ├── ui-custom │ │ │ ├── file-uploader.tsx │ │ │ ├── loading.tsx │ │ │ ├── multi-select.tsx │ │ │ ├── prompt-input.tsx │ │ │ ├── sidebar-menu.tsx │ │ │ └── truncated-text.tsx │ │ └── ui │ │ │ ├── alert.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── toggle-group.tsx │ │ │ └── toggle.tsx │ ├── constants │ │ ├── dataSources.ts │ │ └── dev.ts │ ├── global.d.ts │ ├── index.css │ ├── jest-dom.d.ts │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ ├── pages │ │ ├── Chats.tsx │ │ ├── Data.tsx │ │ ├── index.tsx │ │ └── routes.ts │ ├── state │ │ ├── AppStateContext.ts │ │ ├── AppStateProvider.tsx │ │ ├── constants.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── storage.ts │ │ └── types.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tests │ ├── __mocks__ │ │ ├── handlers.ts │ │ ├── handlers │ │ │ └── app.ts │ │ └── node.ts │ ├── components │ │ ├── LoadingIndicator.test.tsx │ │ ├── MessageContainer.test.tsx │ │ ├── NewChatModal.test.tsx │ │ ├── button.test.tsx │ │ ├── input.test.tsx │ │ └── prompt-input.test.tsx │ ├── setupTests.ts │ ├── state │ │ └── storage.test.ts │ └── test-utils.tsx ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.test.json └── vite.config.ts ├── frontend ├── .streamlit │ └── config.toml ├── 01_connect_and_explore.py ├── 02_chat_with_data.py ├── DataRobot_black.svg ├── DataRobot_white.svg ├── Google_Cloud.svg ├── Snowflake.svg ├── __init__.py ├── app.py ├── app_settings.py ├── bot.jpg ├── csv_File_Logo.svg ├── datarobot_connect.py ├── datarobot_favicon.png ├── helpers.py ├── metadata.yaml.jinja ├── requirements.txt ├── sap.svg ├── start-app.sh ├── style.css └── you.jpg ├── infra ├── __init__.py ├── __main__.py ├── components │ ├── __init__.py │ └── dr_credential.py ├── feature_flag_requirements.yaml ├── settings_app_infra.py ├── settings_database.py ├── settings_generative.py ├── settings_main.py └── settings_proxy_llm.py ├── notebooks └── testing.ipynb ├── pyproject.toml ├── quickstart.py ├── requirements.txt ├── set_env.bat ├── set_env.sh ├── test_pydantic.py ├── trivy-ignore.rego └── utils ├── __init__.py ├── analyst_db.py ├── api.py ├── code_execution.py ├── credentials.py ├── data_cleansing_helpers.py ├── database_helpers.py ├── logging_helper.py ├── prompts.py ├── resources.py ├── rest_api.py ├── schema.py └── tools.py /.env.template: -------------------------------------------------------------------------------- 1 | # Refer to https://docs.datarobot.com/en/docs/api/api-quickstart/index.html#create-a-datarobot-api-key 2 | # and https://docs.datarobot.com/en/docs/api/api-quickstart/index.html#retrieve-the-api-endpoint 3 | # Can be deleted on a DataRobot codespace 4 | DATAROBOT_API_TOKEN= 5 | DATAROBOT_ENDPOINT= 6 | 7 | # Required, unless logged in to pulumi cloud. Choose your own alphanumeric passphrase to be used for encrypting pulumi config 8 | PULUMI_CONFIG_PASSPHRASE=123 9 | 10 | # Optional: Choose which frontend implementation to use (streamlit or react) 11 | # FRONTEND_TYPE=streamlit 12 | 13 | # To use an existing TextGen Model or Deployment: 14 | # 1. Provide either the registered model ID, or the deployment ID 15 | # 2. Set CHAT_MODEL_NAME to the model name expected by the TextGen model (e.g. gpt-4o-mini) 16 | # 3. Set LLM=DEPLOYED_LLM in infra/settings_generative.py 17 | 18 | # TEXTGEN_REGISTERED_MODEL_ID= 19 | # TEXTGEN_DEPLOYMENT_ID= 20 | # CHAT_MODEL_NAME=datarobot-deployed-llm # for NIM models, "datarobot-deployed-llm" will automatically be translated to the correct model name. 21 | 22 | # For Azure OpenAI: 23 | 24 | # Refer to (https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line%2Cjavascript-keyless%2Ctypescript-keyless%2Cpython-new&pivots=programming-language-python#retrieve-key-and-endpoint) 25 | 26 | # OPENAI_API_KEY= 27 | # OPENAI_API_BASE= 28 | # OPENAI_API_VERSION= 29 | # OPENAI_API_DEPLOYMENT_ID= 30 | 31 | # For Snowflake (Optional) 32 | # Either password authentication: 33 | # SNOWFLAKE_USER= 34 | # SNOWFLAKE_PASSWORD= 35 | # Or key file authentication: 36 | # SNOWFLAKE_KEY_PATH= 37 | 38 | # Common Snowflake settings (required if using Snowflake): 39 | # SNOWFLAKE_ACCOUNT= 40 | # SNOWFLAKE_WAREHOUSE= 41 | # SNOWFLAKE_DATABASE= 42 | # SNOWFLAKE_SCHEMA= 43 | # SNOWFLAKE_ROLE= 44 | 45 | # For Google VertexAI 46 | 47 | # You will need a service account JSON with aiplatform.endpoints.predict permission (https://cloud.google.com/iam/docs/keys-create-delete) 48 | # GOOGLE_DB_SCHEMA is required if using BigQuery as a database but can be omitted for Vertex LLMs 49 | # Add the JSON file content enclosed by '' here: 50 | 51 | # GOOGLE_SERVICE_ACCOUNT= 52 | # GOOGLE_REGION= 53 | # GOOGLE_DB_SCHEMA= 54 | 55 | # For AWS Bedrock: 56 | 57 | # Refer to https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html 58 | # AWS_SESSION_TOKEN is optional in case you are using a long term access key 59 | 60 | # AWS_ACCESS_KEY_ID= 61 | # AWS_SECRET_ACCESS_KEY= 62 | # AWS_SESSION_TOKEN= 63 | # AWS_REGION= 64 | 65 | # For SAP DataSphere 66 | # SAP_DATASPHERE_HOST= 67 | # SAP_DATASPHERE_PORT= 68 | # SAP_DATASPHERE_USER= 69 | # SAP_DATASPHERE_PASSWORD= 70 | # SAP_DATASPHERE_SCHEMA= 71 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file configures the Dependabot for the repository. 3 | version: 2 4 | updates: 5 | # Maintain dependencies for GitHub Actions. 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | open-pull-requests-limit: 5 11 | labels: 12 | - "dependencies" 13 | - "github-actions" 14 | 15 | # Maintain dependencies for Python packages. 16 | - package-ecosystem: "pip" 17 | insecure-external-code-execution: allow 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | pull-request-branch-name: 22 | separator: "/" 23 | open-pull-requests-limit: 1 24 | versioning-strategy: "increase-if-necessary" 25 | target-branch: "main" 26 | labels: 27 | - "dependencies" 28 | - "python" 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | 4 | # Rationale 5 | 6 | 7 | # Checklist 8 | - [ ] Implementation 9 | - [ ] Update Changelog -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | ### **GitHub Workflows** 2 | 3 | #### 📌 **Overview** 4 | 5 | This directory (`.github/workflows/`) contains GitHub Actions workflow definitions that automate various tasks such as 6 | CI/CD, releases, and repository synchronization. 7 | 8 | #### ⚙️ **Workflows** 9 | 10 | Below is a list of the workflows included in this repository: 11 | 12 | | Workflow File | Purpose | 13 | |--------------------------------|-------------------------------------------------------------------| 14 | | `dockerfile-lint.yml` | Run Hadolint to check Dockerfiles for best practices. | 15 | | `license-check.yml` | Check and fix license headers and resolve dependencies' licenses. | 16 | | `python-static-checks.yml` | Run Ruff linter and formatter, and MyPy static type checks. | 17 | | `python-deps-install-test.yml` | Verify Python dependencies install for different Python versions. | 18 | | `shellcheck.yml` | Run [shellcheck](https://github.com/koalaman/shellcheck/). | 19 | | `yaml-format.yml` | Run YAML linter tool (yamlfmt). | 20 | 21 | --- 22 | 23 | Feel free to update this document as new workflows are added or modified! ✨ 24 | -------------------------------------------------------------------------------- /.github/workflows/app_backend-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test app_backend 3 | 4 | on: 5 | push: 6 | branches: ["main"] 7 | paths: 8 | - 'app_backend/**.py' 9 | 10 | pull_request: 11 | branches: ["main"] 12 | paths: 13 | - 'app_backend/**.py' 14 | 15 | jobs: 16 | tests: 17 | name: "FastAPI: app_backend ${{ matrix.python-version }}" 18 | runs-on: "ubuntu-latest" 19 | defaults: 20 | run: 21 | working-directory: app_backend 22 | 23 | strategy: 24 | matrix: 25 | python-version: ["3.10", "3.11", "3.12"] 26 | 27 | steps: 28 | - uses: "actions/checkout@v4" 29 | 30 | # Ref: https://docs.astral.sh/uv/guides/integration/github/#installation 31 | - name: Setup uv 32 | uses: astral-sh/setup-uv@v6 33 | with: 34 | working-directory: app_backend 35 | enable-cache: true 36 | cache-dependency-glob: "uv.lock" 37 | python-version: ${{ matrix.python-version }} 38 | 39 | - name: Install Dependencies 40 | working-directory: app_backend 41 | run: uv sync --all-extras --dev 42 | 43 | - name: Run Static Checks 44 | working-directory: app_backend 45 | run: | 46 | uv run ruff format --check . 47 | uv run ruff check . 48 | uv run mypy --pretty --explicit-package-bases . 49 | 50 | - name: Test 51 | working-directory: app_backend 52 | run: uv run pytest --cov --cov-report=html --cov-report=term --cov-report xml:.coverage.xml 53 | 54 | - name: Get Cover 55 | uses: orgoro/coverage@v3.2 56 | with: 57 | coverageFile: app_backend/.coverage.xml 58 | token: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /.github/workflows/app_frontend-vitest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test app_frontend 3 | 4 | on: 5 | push: 6 | branches: ["main"] 7 | paths: 8 | - 'app_frontend/**' 9 | 10 | pull_request: 11 | branches: ["main"] 12 | paths: 13 | - 'app_frontend/**' 14 | 15 | workflow_dispatch: 16 | 17 | jobs: 18 | tests: 19 | name: "React: app_frontend node: ${{ matrix.node-version }}" 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | working-directory: frontend_web 24 | 25 | strategy: 26 | matrix: 27 | node-version: ["18", "20", "22"] 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | 38 | - name: Install dependencies 39 | run: npm ci 40 | working-directory: app_frontend 41 | 42 | - name: Run Vitest 43 | run: npm run test 44 | working-directory: app_frontend 45 | -------------------------------------------------------------------------------- /.github/workflows/dockerfile-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dockerfile Lint 3 | 4 | on: 5 | push: 6 | paths: 7 | - '**Dockerfile' 8 | - '**Dockerfile.*' 9 | branches: 10 | - main 11 | - 'release/*' 12 | pull_request: 13 | paths: 14 | - '**Dockerfile' 15 | - '**Dockerfile.*' 16 | workflow_dispatch: 17 | 18 | jobs: 19 | hadolint: 20 | name: lint-dockerfile 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Run Hadolint 25 | uses: hadolint/hadolint-action@v3.1.0 26 | with: 27 | recursive: true 28 | -------------------------------------------------------------------------------- /.github/workflows/python-deps-install-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependency Install Test 3 | 4 | on: 5 | pull_request: 6 | 7 | jobs: 8 | python-deps-install-test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.11", "3.12"] 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | python3 -m pip install --upgrade pip 26 | python3 -m pip install -r requirements.txt 27 | -------------------------------------------------------------------------------- /.github/workflows/python-static-checks.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Python Static Checks 3 | 4 | on: 5 | push: 6 | paths: 7 | - '**.py' 8 | 9 | jobs: 10 | python-static-checks: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.10' 18 | - name: Install dependencies 19 | run: | 20 | python3 -m pip install --upgrade pip 21 | python3 -m pip install -r requirements.txt 22 | - name: Run Static Checks 23 | run: | 24 | ruff format --check . 25 | ruff check . 26 | mypy --pretty . 27 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Shellcheck 3 | 4 | on: 5 | push: 6 | paths: 7 | - '**.sh' 8 | branches: 9 | - main 10 | - 'release/*' 11 | pull_request: 12 | paths: 13 | - '**.sh' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | shellcheck: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install dependencies 22 | run: | 23 | wget -qO- https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz | tar -xJv 24 | sudo install -o root -g root -m 0755 shellcheck-stable/shellcheck /usr/local/bin 25 | shellcheck --version 26 | - name: Run shellcheck 27 | run: | 28 | find . -type f -name "*.sh" -exec shellcheck {} + 29 | -------------------------------------------------------------------------------- /.github/workflows/yaml-format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: YAML formatter 3 | on: 4 | push: 5 | paths: 6 | - '**.yaml' 7 | - '**.yml' 8 | branches: 9 | - main 10 | - 'release/*' 11 | pull_request: 12 | paths: 13 | - '**.yaml' 14 | - '**.yml' 15 | workflow_dispatch: 16 | 17 | jobs: 18 | yaml-formatter: 19 | name: yamlfmt 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | - name: Install go 25 | uses: actions/setup-go@v5 26 | - name: Install yamlfmt 27 | run: go install github.com/google/yamlfmt/cmd/yamlfmt@latest 28 | - name: Run yamlfmt 29 | run: yamlfmt -conf .yamlfmt.yml -lint . 30 | -------------------------------------------------------------------------------- /.hadolint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ignored: 3 | - DL3013 4 | - DL3018 5 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # ShellCheck configuration 2 | # 3 | # https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files. 4 | 5 | disable=SC1091 # Not following 6 | -------------------------------------------------------------------------------- /.yamlfmt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.com/google/yamlfmt config file 3 | continue_on_error: true 4 | formatter: 5 | include_document_start: true 6 | retain_line_breaks: true 7 | max_line_length: 120 8 | exclude: 9 | - .datarobot/ 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: copyright-check apply-copyright fix-licenses check-licenses 2 | 3 | help: 4 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | copyright-check: ## Copyright checks 7 | docker run -it --rm -v $(CURDIR):/github/workspace apache/skywalking-eyes -c .github/.licenserc.yaml header check 8 | 9 | apply-copyright: ## Add copyright notice to new files 10 | docker run -it --rm -v $(CURDIR):/github/workspace apache/skywalking-eyes -c .github/.licenserc.yaml header fix 11 | 12 | fix-licenses: apply-copyright 13 | 14 | check-licenses: copyright-check 15 | 16 | fix-lint: ## Fix linting issues 17 | ruff format . 18 | ruff check . --fix 19 | mypy --pretty . 20 | 21 | lint: ## Lint the code 22 | ruff format --check . 23 | ruff check . 24 | mypy --pretty . 25 | 26 | check-all: check-licenses lint ## Run all checks 27 | 28 | run-local-dev-backend: 29 | PYTHONPATH=app_backend SERVE_STATIC_FRONTEND=False DEV_MODE=True ./app_backend/start-app.sh 30 | 31 | run-local-static-backend: 32 | PYTHONPATH=app_backend DEV_MODE=True ./app_backend/start-app.sh 33 | -------------------------------------------------------------------------------- /Pulumi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: dataanalyst 3 | runtime: 4 | name: python 5 | main: infra 6 | description: Data Analyst 7 | config: 8 | pulumi:tags: 9 | value: 10 | pulumi:template: python 11 | datarobot:tracecontext: dataanalyst 12 | -------------------------------------------------------------------------------- /Set-Env.ps1: -------------------------------------------------------------------------------- 1 | # Function to load .env file 2 | function Import-EnvFile { 3 | if (-not (Test-Path '.env')) { 4 | Write-Host "Error: .env file not found." 5 | Write-Host "Please create a .env file with VAR_NAME=value pairs." 6 | return $false 7 | } 8 | 9 | $envVarsJson = python -c "import json; from quickstart import load_dotenv; env_vars = load_dotenv(); print(json.dumps(env_vars))" 10 | 11 | $envVars = $envVarsJson | ConvertFrom-Json 12 | 13 | $envVars.PSObject.Properties | ForEach-Object { 14 | [System.Environment]::SetEnvironmentVariable($_.Name, $_.Value) 15 | } 16 | 17 | Write-Host "Environment variables from .env have been set." 18 | return $true 19 | } 20 | # Function to activate the virtual environment if it exists 21 | function Enable-VirtualEnvironment { 22 | if (Test-Path '.venv\Scripts\Activate.ps1') { 23 | Write-Host "Activated virtual environment found at .venv\Scripts\Activate.ps1" 24 | . '.venv\Scripts\Activate.ps1' 25 | } 26 | } 27 | 28 | # Main execution 29 | Enable-VirtualEnvironment 30 | Import-EnvFile -------------------------------------------------------------------------------- /app_backend/.gitignore: -------------------------------------------------------------------------------- 1 | /metadata.yaml -------------------------------------------------------------------------------- /app_backend/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /app_backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /app_backend/app/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import logging 17 | import os 18 | 19 | from fastapi import APIRouter 20 | from fastapi.responses import FileResponse, Response 21 | from fastapi.staticfiles import StaticFiles 22 | from utils.rest_api import app 23 | 24 | # Configure logging to filter out the health check logs 25 | logging.getLogger("uvicorn.access").setLevel(logging.WARNING) 26 | 27 | 28 | class EndpointFilter(logging.Filter): 29 | def filter(self, record: logging.LogRecord) -> bool: 30 | # Filter out "GET /" health check logs 31 | return "GET / HTTP/1.1" not in record.getMessage() 32 | 33 | 34 | logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) 35 | 36 | SCRIPT_NAME = os.environ.get("SCRIPT_NAME", "") 37 | SERVE_STATIC_FRONTEND = os.environ.get("SERVE_STATIC_FRONTEND", "True") 38 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 39 | STATIC_DIR = os.path.join(BASE_DIR, "static") 40 | base_router = APIRouter() 41 | 42 | 43 | @base_router.get("/_dr_env.js") 44 | async def get_env() -> Response: 45 | NOTEBOOK_ID = os.getenv("NOTEBOOK_ID", "") 46 | app_base_url = os.getenv("BASE_PATH", "") 47 | if not app_base_url and NOTEBOOK_ID: 48 | app_base_url = f"notebook-sessions/{NOTEBOOK_ID}" 49 | 50 | env_vars = { 51 | "APP_BASE_URL": app_base_url, 52 | "API_PORT": os.getenv("PORT"), 53 | "DATAROBOT_ENDPOINT": os.getenv("DATAROBOT_ENDPOINT", ""), 54 | "IS_STATIC_FRONTEND": SERVE_STATIC_FRONTEND, 55 | } 56 | js = f"window.ENV = {json.dumps(env_vars)};" 57 | return Response(content=js, media_type="application/javascript") 58 | 59 | 60 | if SERVE_STATIC_FRONTEND: 61 | 62 | @base_router.get(f"{SCRIPT_NAME}/data") 63 | @base_router.get(f"{SCRIPT_NAME}/chats") 64 | @base_router.get(f"{SCRIPT_NAME}/chats/{{chat_id}}") 65 | @base_router.get(f"{SCRIPT_NAME}/") 66 | async def serve_root() -> FileResponse: 67 | """Serve the React index.html for the root route.""" 68 | return FileResponse(os.path.join(STATIC_DIR, "index.html")) 69 | 70 | 71 | app.include_router(base_router) 72 | 73 | if SERVE_STATIC_FRONTEND: 74 | # Important to be last so that we fall back to the static files if the route is not found 75 | app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") 76 | -------------------------------------------------------------------------------- /app_backend/metadata.yaml.jinja: -------------------------------------------------------------------------------- 1 | name: runtime-params 2 | 3 | runtimeParameterDefinitions: 4 | {{ additional_params }} 5 | -------------------------------------------------------------------------------- /app_backend/requirements.txt: -------------------------------------------------------------------------------- 1 | # python sandbox libraries 2 | pandas>=2.2.3,<3.0 3 | numpy>=2.1.3,<3.0 4 | scipy>=1.15.1,<2.0 5 | statsmodels>=0.14.4,<1.0 6 | scikit-learn>=1.6.1,<2.0 7 | lightgbm>=4.5.0,<5.0 8 | tslearn>=0.6.3,<1.0 9 | spacy>=3.8.3,<4.0 10 | pyarrow<19.0.0 11 | polars>=1.22.0,<2.0 12 | 13 | # plotting 14 | kaleido>=0.2.1,<1.0 15 | plotly>=5.24.1,<6.0 16 | textblob>=0.19.0,<1.0 17 | 18 | # data 19 | openpyxl>=3.1.5,<4.0 20 | snowflake-connector-python>=3.12.4,<4.0 21 | google-cloud-bigquery>=3.27.0,<4.0 22 | google-auth>=2.37.0,<3.0 23 | snowflake-sqlalchemy>=1.7.3,<2.0 24 | sqlalchemy>=2.0.37,<3.0 25 | cryptography>=44.0.0,<45.0 26 | hdbcli>=2.23.27,<3.0 27 | pillow 28 | # genai 29 | openai>=1.59.9,<2 30 | instructor>=1.3.4,<2.0 31 | boto3>=1.36.2,<2.0 32 | 33 | # backend 34 | datarobot-asgi-middleware>=0.1.0 35 | fastapi>=0.115.6,<1.0 36 | httpx>=0.28.1 37 | python-dotenv>=1.0.1 38 | datarobot>=3.6.0,<4.0 39 | python-multipart>=0.0.20,<1.0 40 | uvicorn==0.34.0,<1 41 | psutil>=6.1.1,<7.0 42 | pydantic==2.7.4,<3.0 43 | pydantic-settings==2.4.0,<3.0 44 | joblib>=1.4.2,<2.0 45 | duckdb>=1.2.0,<1.3 46 | fastexcel>=0.12.1,<1.0 47 | aiofiles==24.1.0 48 | types-aiofiles==24.1.0.20241221 49 | 50 | # dev & compatibility 51 | eval_type_backport>=0.2.2,<1.0 52 | db-dtypes>=1.3.1,<2.0 53 | typing-extensions>=4.12.2,<5.0 54 | numba>=0.61.0,<1.0 55 | -------------------------------------------------------------------------------- /app_backend/start-app.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PORT=${PORT:-"8080"} 4 | DEV_MODE=${DEV_MODE:-false} 5 | LOG_LEVEL="info" 6 | EXTRA_OPTS="--proxy-headers" 7 | 8 | if [ "$DEV_MODE" = "true" ]; then 9 | EXTRA_OPTS="$EXTRA_OPTS --reload" 10 | fi 11 | 12 | python -m uvicorn app.main:app --host 0.0.0.0 --port "$PORT" --log-level $LOG_LEVEL "$EXTRA_OPTS" 13 | -------------------------------------------------------------------------------- /app_backend/static/assets/Chats-Dd9N05MP.css: -------------------------------------------------------------------------------- 1 | .markdown-content{line-height:1.5}.markdown-content ul,.markdown-content ol{margin-top:.5rem;margin-bottom:.5rem;padding-left:1.5rem}.markdown-content ul{list-style-type:disc}.markdown-content ol{list-style-type:decimal}.markdown-content li{margin-bottom:.25rem}.markdown-content p{margin-bottom:.75rem}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{font-weight:600;margin-top:1rem;margin-bottom:.5rem}.markdown-content code{padding:.2rem .4rem;border-radius:.25rem;font-family:monospace}.markdown-content pre{border-radius:.25rem;overflow-x:auto}.markdown-content blockquote{border-left:3px solid #ccc;padding-left:1rem;margin-left:0;margin-right:0;font-style:italic}.markdown-content hr{border:0;border-top:1px solid #ccc;margin:1rem 0}.plot-container{filter:invert(90%) hue-rotate(180deg)} 2 | -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-300-normal-CfSGLRSF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-300-normal-CfSGLRSF.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-300-normal-Cjw7_AC8.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-300-normal-Cjw7_AC8.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-400-normal-COF6noiJ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-400-normal-COF6noiJ.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-400-normal-DmRB7q_B.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-400-normal-DmRB7q_B.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-500-normal-DN3Amd4H.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-500-normal-DN3Amd4H.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-500-normal-kGSpR5A9.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-500-normal-kGSpR5A9.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-600-normal-DOZeTYVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-600-normal-DOZeTYVF.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-600-normal-DrBIvsIy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-600-normal-DrBIvsIy.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-700-normal-1DREuLwQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-700-normal-1DREuLwQ.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-700-normal-ryGpXDOP.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-700-normal-ryGpXDOP.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-300-normal-C06zYcNH.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-300-normal-C06zYcNH.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-300-normal-CJf67p0q.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-300-normal-CJf67p0q.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-400-normal-Cygz-XR6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-400-normal-Cygz-XR6.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-400-normal-NAt9AhwD.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-400-normal-NAt9AhwD.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-500-normal-BBd_G3i-.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-500-normal-BBd_G3i-.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-500-normal-hI9Kr37g.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-500-normal-hI9Kr37g.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-600-normal-CFRgRepe.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-600-normal-CFRgRepe.woff2 -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-600-normal-Czy-B68B.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-600-normal-Czy-B68B.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-700-normal-BS_Ohp14.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-700-normal-BS_Ohp14.woff -------------------------------------------------------------------------------- /app_backend/static/assets/dm-sans-latin-ext-700-normal-CGQ_Vo0j.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/assets/dm-sans-latin-ext-700-normal-CGQ_Vo0j.woff2 -------------------------------------------------------------------------------- /app_backend/static/datarobot_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_backend/static/datarobot_favicon.png -------------------------------------------------------------------------------- /app_backend/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Talk to My Data 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /app_backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /app_backend/tests/test_main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from fastapi.testclient import TestClient 16 | 17 | from app.main import app 18 | 19 | client = TestClient(app) 20 | 21 | 22 | def test_index() -> None: 23 | response = client.get("/") 24 | assert response.status_code == 200 25 | assert response.headers["content-type"] == "text/html; charset=utf-8" 26 | 27 | 28 | def test_favicon_file() -> None: 29 | response = client.get("/datarobot_favicon.png") 30 | assert response.status_code == 200 31 | assert response.headers["content-type"] == "image/png" 32 | assert response.content.startswith(b"\x89PNG\r\n\x1a\n") 33 | -------------------------------------------------------------------------------- /app_frontend/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /app_frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "jsxSingleQuote": false, 6 | "printWidth": 100, 7 | "quoteProps": "as-needed", 8 | "semi": true, 9 | "singleQuote": true, 10 | "tabWidth": 4, 11 | "trailingComma": "es5", 12 | "useTabs": false, 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /app_frontend/README.md: -------------------------------------------------------------------------------- 1 | # Talk to My Data: React App app_frontend 2 | 3 | This application provides a modern React-based frontend for the **Talk to My Data** application. It allows users to interact with data, perform analyses, and chat with the system to gain insights from their datasets. 4 | 5 | ## Features 6 | 7 | - Interactive chat interface for data analysis 8 | - Data visualization with interactive plots 9 | - Dataset management and cleansing 10 | - Support for multiple data sources (CSV, Data Registry, Snowflake, Google Cloud) 11 | - Code execution and insights generation 12 | 13 | ## Tech Stack 14 | 15 | - React 18 with TypeScript 16 | - Vite for fast development and building 17 | - Tailwind CSS for styling 18 | - Jest for testing 19 | - React Query for API state management 20 | 21 | ## Development 22 | 23 | See README in `app_backend` directory 24 | 25 | ## Testing 26 | 27 | To run the test suite: 28 | 29 | ```bash 30 | npm run test 31 | ``` 32 | 33 | ## Project Structure 34 | 35 | - `src/api`: API client and hooks for data fetching 36 | - `src/components/ui`: shadcn components 37 | - `src/components/ui-custom`: shadcn based generic components 38 | - `src/pages`: Main application pages 39 | - `src/state`: Application state management 40 | - `src/assets`: Static assets like images and icons 41 | 42 | 43 | ## Expanding the ESLint configuration 44 | 45 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 46 | 47 | ```js 48 | export default tseslint.config({ 49 | extends: [ 50 | // Remove ...tseslint.configs.recommended and replace with this 51 | ...tseslint.configs.recommendedTypeChecked, 52 | // Alternatively, use this for stricter rules 53 | ...tseslint.configs.strictTypeChecked, 54 | // Optionally, add this for stylistic rules 55 | ...tseslint.configs.stylisticTypeChecked, 56 | ], 57 | languageOptions: { 58 | // other options... 59 | parserOptions: { 60 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 61 | tsconfigRootDir: import.meta.dirname, 62 | }, 63 | }, 64 | }) 65 | ``` 66 | 67 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 68 | 69 | ```js 70 | // eslint.config.js 71 | import reactX from 'eslint-plugin-react-x' 72 | import reactDom from 'eslint-plugin-react-dom' 73 | 74 | export default tseslint.config({ 75 | plugins: { 76 | // Add the react-x and react-dom plugins 77 | 'react-x': reactX, 78 | 'react-dom': reactDom, 79 | }, 80 | rules: { 81 | // other rules... 82 | // Enable its recommended typescript rules 83 | ...reactX.configs['recommended-typescript'].rules, 84 | ...reactDom.configs.recommended.rules, 85 | }, 86 | }) 87 | ``` 88 | -------------------------------------------------------------------------------- /app_frontend/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | tasks: 4 | install: 5 | desc: 🧱 Install React dependencies 6 | cmds: 7 | - npm install 8 | lint: 9 | desc: 🧹 Lint React code 10 | cmds: 11 | - npm run lint:fix 12 | lint-check: 13 | desc: 🧹 Lint React code 14 | cmds: 15 | - npm run lint 16 | test: 17 | desc: 🧪 Test React code 18 | cmds: 19 | - npm run test 20 | test-watch: 21 | desc: 🧪👀 Continuously Test React code 22 | cmds: 23 | - npm run test:watch 24 | build: 25 | desc: 🏗️ Build React code 26 | cmds: 27 | - npm run build 28 | dev: 29 | desc: 🚀 Start React development server 30 | cmds: 31 | - npm run dev 32 | -------------------------------------------------------------------------------- /app_frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /app_frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /app_frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Talk to My Data 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app_frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app_frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b tsconfig.app.json && cross-env NODE_OPTIONS='--max-old-space-size=4096' vite build", 9 | "build:codespace": "tsc -b tsconfig.app.json && cross-env NODE_OPTIONS='--max-old-space-size=4096' NODE_ENV=development STATIC_CODESPACE=true vite build", 10 | "lint": "npm run prettier && eslint .", 11 | "lint:fix": "npm run prettier:fix && eslint . --fix", 12 | "preview": "vite preview", 13 | "test": "vitest --run", 14 | "test:watch": "vitest", 15 | "prettier": "prettier --check --config .prettierrc.json ./src", 16 | "prettier:fix": "prettier --write --config .prettierrc.json ./src" 17 | }, 18 | "dependencies": { 19 | "@fontsource/dm-sans": "^5.1.1", 20 | "@fortawesome/free-regular-svg-icons": "^6.7.2", 21 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 22 | "@fortawesome/react-fontawesome": "^0.2.2", 23 | "@hookform/resolvers": "^5.0.1", 24 | "@radix-ui/react-collapsible": "^1.1.3", 25 | "@radix-ui/react-dialog": "^1.1.6", 26 | "@radix-ui/react-label": "^2.1.2", 27 | "@radix-ui/react-popover": "^1.1.6", 28 | "@radix-ui/react-progress": "^1.1.2", 29 | "@radix-ui/react-radio-group": "^1.2.3", 30 | "@radix-ui/react-scroll-area": "^1.2.3", 31 | "@radix-ui/react-separator": "^1.1.2", 32 | "@radix-ui/react-slot": "^1.1.2", 33 | "@radix-ui/react-switch": "^1.1.3", 34 | "@radix-ui/react-tabs": "^1.1.3", 35 | "@radix-ui/react-toggle": "^1.1.2", 36 | "@radix-ui/react-toggle-group": "^1.1.2", 37 | "@tanstack/react-query": "^5.74.11", 38 | "axios": "^1.9.0", 39 | "class-variance-authority": "^0.7.1", 40 | "clsx": "^2.1.1", 41 | "cmdk": "1.0.4", 42 | "lucide-react": "^0.475.0", 43 | "plotly.js": "^3.0.1", 44 | "react": "^19.0.0", 45 | "react-dom": "^19.0.0", 46 | "react-dropzone": "^14.3.5", 47 | "react-intersection-observer": "^9.15.1", 48 | "react-markdown": "^10.0.0", 49 | "react-plotly.js": "^2.6.0", 50 | "react-resizable-panels": "^2.1.9", 51 | "react-router": "^7.1.5", 52 | "react-router-dom": "^7.5.3", 53 | "react-syntax-highlighter": "^15.6.1", 54 | "tailwind-merge": "^3.0.1", 55 | "tailwindcss-animate": "^1.0.7" 56 | }, 57 | "devDependencies": { 58 | "@eslint/js": "^9.21.0", 59 | "@tailwindcss/postcss": "^4.0.4", 60 | "@tailwindcss/vite": "^4.1.4", 61 | "@tanstack/eslint-plugin-query": "^5.66.1", 62 | "@testing-library/jest-dom": "^6.6.3", 63 | "@testing-library/react": "^16.3.0", 64 | "@testing-library/user-event": "^14.4.3", 65 | "@types/fast-text-encoding": "^1.0.3", 66 | "@types/node": "^22.15.21", 67 | "@types/react": "^19.0.10", 68 | "@types/react-dom": "^19.0.4", 69 | "@types/react-plotly.js": "^2.6.3", 70 | "@vitejs/plugin-react": "^4.3.4", 71 | "autoprefixer": "^10.4.20", 72 | "cross-env": "^7.0.3", 73 | "eslint": "^9.21.0", 74 | "eslint-plugin-react-hooks": "^5.1.0", 75 | "eslint-plugin-react-refresh": "^0.4.19", 76 | "globals": "^15.15.0", 77 | "jsdom": "^26.1.0", 78 | "msw": "^2.7.4", 79 | "postcss": "^8.5.1", 80 | "prettier": "^3.5.3", 81 | "tailwindcss": "^4.1.4", 82 | "typescript": "~5.7.2", 83 | "typescript-eslint": "^8.24.1", 84 | "vite": "^6.2.0", 85 | "vite-plugin-node-polyfills": "^0.23.0", 86 | "vitest": "^3.1.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app_frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {}, 4 | "@tailwindcss/postcss": {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app_frontend/public/datarobot_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/app_frontend/public/datarobot_favicon.png -------------------------------------------------------------------------------- /app_frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | } 3 | 4 | .logo { 5 | height: 6em; 6 | padding: 1.5em; 7 | will-change: filter; 8 | transition: filter 300ms; 9 | } 10 | .logo:hover { 11 | filter: drop-shadow(0 0 2em #646cffaa); 12 | } 13 | -------------------------------------------------------------------------------- /app_frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import { Sidebar } from '@/components/Sidebar'; 3 | import Pages from './pages'; 4 | import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; 5 | import { useDataRobotInfo } from './api/user/hooks'; 6 | 7 | function App() { 8 | useDataRobotInfo(); 9 | 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /app_frontend/src/api/apiClient.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { isDev, isServedStatic } from '@/lib/utils'; 3 | import { VITE_DEFAULT_PORT } from '@/constants/dev'; 4 | 5 | export const getApiUrl = () => { 6 | // Adjust API URL based on the environment 7 | let apiBaseURL: string = window.location.origin; 8 | if (window.ENV?.APP_BASE_URL) { 9 | apiBaseURL = `${apiBaseURL}/${window.ENV.APP_BASE_URL}`; 10 | } 11 | if (window.ENV?.APP_BASE_URL?.includes('notebook-sessions') && isDev()) { 12 | apiBaseURL += `/ports/` + VITE_DEFAULT_PORT; 13 | } 14 | if ( 15 | window.ENV?.APP_BASE_URL?.includes('notebook-sessions') && 16 | window.ENV?.API_PORT && 17 | isServedStatic() 18 | ) { 19 | apiBaseURL += `/ports/` + window.ENV.API_PORT; 20 | } 21 | apiBaseURL += '/api'; 22 | return apiBaseURL; 23 | }; 24 | 25 | const apiClient = axios.create({ 26 | baseURL: `${getApiUrl()}`, 27 | headers: { 28 | Accept: 'application/json', 29 | 'Content-type': 'application/json', 30 | }, 31 | withCredentials: true, 32 | }); 33 | 34 | export default apiClient; 35 | 36 | const drClient = axios.create({ 37 | baseURL: window.ENV?.DATAROBOT_ENDPOINT || `${window.location.origin}/api/v2`, 38 | headers: { 39 | Accept: 'application/json', 40 | 'Content-type': 'application/json', 41 | }, 42 | withCredentials: true, 43 | }); 44 | 45 | export { drClient, apiClient }; 46 | -------------------------------------------------------------------------------- /app_frontend/src/api/chat-messages/keys.ts: -------------------------------------------------------------------------------- 1 | export const messageKeys = { 2 | all: ['messages', 'chats'], 3 | chats: ['chats'], 4 | messages: (chatId?: string) => ['messages', ...(chatId ? [chatId] : [])], 5 | }; 6 | -------------------------------------------------------------------------------- /app_frontend/src/api/chat-messages/types.ts: -------------------------------------------------------------------------------- 1 | export interface IMetadata { 2 | duration?: number | null; 3 | attempts?: number | null; 4 | datasets_analyzed?: number | null; 5 | total_rows_analyzed?: number | null; 6 | total_columns_analyzed?: number | null; 7 | exception?: { 8 | exception_history?: ICodeExecutionError[] | null; 9 | } | null; 10 | columns_analyzed?: number; 11 | rows_analyzed?: number; 12 | question?: string; 13 | } 14 | 15 | export interface ICodeExecutionError { 16 | code?: string | null; 17 | exception_str?: string | null; 18 | stdout?: string | null; 19 | stderr?: string | null; 20 | traceback_str?: string | null; 21 | } 22 | 23 | export interface IDataset { 24 | name: string; 25 | data_records: Record[]; 26 | } 27 | 28 | export interface IMessageComponent { 29 | enhanced_user_message?: string; 30 | } 31 | 32 | export interface IAnalysisComponent extends IComponent { 33 | type: 'analysis'; 34 | metadata?: IMetadata; 35 | dataset?: IDataset | null; 36 | code?: string | null; 37 | } 38 | 39 | export interface IChartsComponent extends IComponent { 40 | type: 'charts'; 41 | fig1_json?: string | null; 42 | fig2_json?: string | null; 43 | code?: string | null; 44 | } 45 | 46 | export interface IBusinessComponent extends IComponent { 47 | type: 'business'; 48 | bottom_line?: string | null; 49 | additional_insights?: string | null; 50 | follow_up_questions?: string[] | null; 51 | } 52 | 53 | export interface IComponent { 54 | status?: 'success' | 'error'; 55 | metadata?: IMetadata; 56 | } 57 | 58 | export interface IChatMessage { 59 | role: 'user' | 'assistant'; 60 | content: string; 61 | components: (IMessageComponent | IAnalysisComponent | IChartsComponent | IBusinessComponent)[]; 62 | in_progress?: boolean; 63 | created_at?: string; // ISO timestamp for message creation time 64 | chat_id?: string; // ID of the chat this message belongs to 65 | id?: string; // Unique identifier for the message 66 | error?: string; 67 | } 68 | 69 | export interface IUserMessage { 70 | message: string; 71 | chatId?: string; 72 | enableChartGeneration?: boolean; 73 | enableBusinessInsights?: boolean; 74 | dataSource?: string; 75 | } 76 | 77 | export interface IPostMessageContext { 78 | previousMessages: IChatMessage[]; 79 | messagesKey: string[]; 80 | previousChats?: IChat[]; 81 | } 82 | 83 | export interface IChat { 84 | id: string; 85 | name: string; 86 | created_at: string; // ISO date for chat creation time 87 | data_source?: string; 88 | } 89 | -------------------------------------------------------------------------------- /app_frontend/src/api/cleansed-datasets/api-requests.ts: -------------------------------------------------------------------------------- 1 | import apiClient from '../apiClient'; 2 | 3 | export type CleansedColumnReport = { 4 | new_column_name: string; 5 | original_column_name: string | null; 6 | errors: string[]; 7 | warnings: string[]; 8 | original_dtype: string | null; 9 | new_dtype: string | null; 10 | conversion_type: string | null; 11 | }; 12 | 13 | export type CleansedDataset = { 14 | dataset: { 15 | name: string; 16 | data_records: Record[]; 17 | }; 18 | cleaning_report: CleansedColumnReport[]; 19 | name: string; 20 | }; 21 | 22 | export type DatasetMetadata = { 23 | name: string; 24 | dataset_type: string; 25 | original_name: string; 26 | created_at: string; 27 | columns: string[]; 28 | row_count: number; 29 | data_source: string; 30 | file_size: number; 31 | }; 32 | 33 | export const getCleansedDataset = async ({ 34 | name, 35 | skip = 0, 36 | limit = 100, 37 | signal, 38 | }: { 39 | name: string; 40 | skip?: number; 41 | limit?: number; 42 | signal?: AbortSignal; 43 | }): Promise => { 44 | const encodedName = encodeURIComponent(name); 45 | const { data } = await apiClient.get( 46 | `/v1/datasets/${encodedName}/cleansed?skip=${skip}&limit=${limit}`, 47 | { signal } 48 | ); 49 | return data; 50 | }; 51 | 52 | export const getDatasetMetadata = async ({ 53 | name, 54 | signal, 55 | }: { 56 | name: string; 57 | signal?: AbortSignal; 58 | }): Promise => { 59 | const encodedName = encodeURIComponent(name); 60 | const { data } = await apiClient.get(`/v1/datasets/${encodedName}/metadata`, { 61 | signal, 62 | }); 63 | return data; 64 | }; 65 | -------------------------------------------------------------------------------- /app_frontend/src/api/cleansed-datasets/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; 2 | import { cleansedDatasetKeys, datasetMetadataKeys } from './keys'; 3 | import { getCleansedDataset, getDatasetMetadata } from './api-requests'; 4 | 5 | export const useInfiniteCleansedDataset = (name: string, limit = 100) => { 6 | return useInfiniteQuery({ 7 | queryKey: cleansedDatasetKeys.detail(name), 8 | initialPageParam: 0, 9 | queryFn: ({ pageParam = 0, signal }) => 10 | getCleansedDataset({ 11 | name, 12 | skip: pageParam, 13 | limit, 14 | signal, 15 | }), 16 | getNextPageParam: (lastPage, allPages) => { 17 | const totalFetched = allPages.length * limit; 18 | // If we received fewer rows than the limit, we've reached the end 19 | if (lastPage.dataset.data_records.length < limit) return undefined; 20 | return totalFetched; 21 | }, 22 | // Keep data for 5 minutes before refetching 23 | staleTime: 5 * 60 * 1000, 24 | }); 25 | }; 26 | 27 | export const useDatasetMetadata = (name: string) => { 28 | return useQuery({ 29 | queryKey: datasetMetadataKeys.detail(name), 30 | queryFn: ({ signal }) => getDatasetMetadata({ name, signal }), 31 | // Keep data for 5 minutes before refetching 32 | staleTime: 5 * 60 * 1000, 33 | // Don't refetch on window focus for metadata (doesn't change frequently) 34 | refetchOnWindowFocus: false, 35 | }); 36 | }; 37 | 38 | export const useMultipleDatasetMetadata = (names: string[]) => { 39 | return useQuery({ 40 | queryKey: datasetMetadataKeys.list(names), 41 | queryFn: async ({ signal }) => { 42 | const results = []; 43 | for (const name of names) { 44 | const metadata = await getDatasetMetadata({ name, signal }); 45 | results.push({ name, metadata }); 46 | } 47 | return results; 48 | }, 49 | // Keep data for 5 minutes before refetching 50 | staleTime: 5 * 60 * 1000, 51 | // Don't refetch on window focus for metadata (doesn't change frequently) 52 | refetchOnWindowFocus: false, 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /app_frontend/src/api/cleansed-datasets/keys.ts: -------------------------------------------------------------------------------- 1 | export const cleansedDatasetKeys = { 2 | all: ['cleansed_datasets'] as const, 3 | detail: (name: string) => [...cleansedDatasetKeys.all, name] as const, 4 | }; 5 | 6 | export const datasetMetadataKeys = { 7 | all: ['dataset_metadata'] as const, 8 | detail: (name: string) => [...datasetMetadataKeys.all, name] as const, 9 | list: (names: string[]) => [...datasetMetadataKeys.all, 'list', ...names] as const, 10 | }; 11 | -------------------------------------------------------------------------------- /app_frontend/src/api/database/api-requests.ts: -------------------------------------------------------------------------------- 1 | import apiClient from '../apiClient'; 2 | 3 | export type DatabaseTables = Array; 4 | 5 | export const getDatabaseTables = async ({ 6 | signal, 7 | }: { 8 | signal?: AbortSignal; 9 | }): Promise => { 10 | const { data } = await apiClient.get(`/v1/database/tables`, { 11 | signal, 12 | }); 13 | return data; 14 | }; 15 | 16 | export const loadFromDatabase = async ({ 17 | tableNames, 18 | signal, 19 | }: { 20 | tableNames: string[]; 21 | signal?: AbortSignal; 22 | }): Promise => { 23 | const { data } = await apiClient.post( 24 | '/v1/database/select', 25 | { table_names: tableNames }, 26 | { 27 | signal, 28 | } 29 | ); 30 | return data; 31 | }; 32 | -------------------------------------------------------------------------------- /app_frontend/src/api/database/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 2 | import { databaseKeys } from './keys'; 3 | import { getDatabaseTables, loadFromDatabase } from './api-requests'; 4 | import { dictionaryKeys } from '../dictionaries/keys'; 5 | import { DictionaryTable } from '../dictionaries/types'; 6 | 7 | export const useGetDatabaseTables = () => { 8 | const queryResult = useQuery({ 9 | queryKey: databaseKeys.all, 10 | queryFn: ({ signal }) => getDatabaseTables({ signal }), 11 | }); 12 | 13 | return queryResult; 14 | }; 15 | 16 | export const useLoadFromDatabaseMutation = ({ 17 | onSuccess, 18 | onError, 19 | }: { 20 | onSuccess: (data: unknown) => void; 21 | onError: (error: Error) => void; 22 | }) => { 23 | const queryClient = useQueryClient(); 24 | 25 | const mutation = useMutation({ 26 | mutationFn: ({ tableNames }: { tableNames: string[] }) => 27 | loadFromDatabase({ 28 | tableNames, 29 | }), 30 | onMutate: async () => { 31 | const previousDictionaries = 32 | queryClient.getQueryData(dictionaryKeys.all) || []; 33 | return { previousDictionaries }; 34 | }, 35 | onSuccess: data => { 36 | queryClient.invalidateQueries({ queryKey: dictionaryKeys.all }); 37 | onSuccess(data); 38 | }, 39 | onError: (error, _, context) => { 40 | if (context?.previousDictionaries) { 41 | queryClient.setQueryData( 42 | dictionaryKeys.all, 43 | context.previousDictionaries 44 | ); 45 | } 46 | onError(error); 47 | }, 48 | onSettled: () => queryClient.invalidateQueries({ queryKey: databaseKeys.all }), 49 | }); 50 | 51 | return mutation; 52 | }; 53 | -------------------------------------------------------------------------------- /app_frontend/src/api/database/keys.ts: -------------------------------------------------------------------------------- 1 | export const databaseKeys = { 2 | all: ['databaseTables'], 3 | }; 4 | -------------------------------------------------------------------------------- /app_frontend/src/api/datasets/api-requests.ts: -------------------------------------------------------------------------------- 1 | import { AxiosProgressEvent } from 'axios'; 2 | import apiClient from '../apiClient'; 3 | 4 | export type Dataset = { 5 | id: string; 6 | name: string; 7 | created: string; 8 | size: string; 9 | file_size?: number; 10 | }; 11 | 12 | export const getDatasets = async ({ 13 | limit, 14 | signal, 15 | }: { 16 | limit: number; 17 | signal?: AbortSignal; 18 | }): Promise => { 19 | const { data } = await apiClient.get(`/v1/registry/datasets?limit=${limit}`, { 20 | signal, 21 | }); 22 | return data; 23 | }; 24 | 25 | export async function uploadDataset({ 26 | files, 27 | onUploadProgress, 28 | catalogIds, 29 | signal, 30 | }: { 31 | files?: File[]; 32 | catalogIds?: string[]; 33 | onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; 34 | signal?: AbortSignal; 35 | }) { 36 | const formData = new FormData(); 37 | 38 | if (files && files.length > 0) { 39 | files.forEach(file => formData.append('files', file)); 40 | } 41 | 42 | formData.append('registry_ids', JSON.stringify(catalogIds || [])); 43 | 44 | const response = await apiClient.post('/v1/datasets/upload', formData, { 45 | headers: { 46 | 'content-type': 'multipart/form-data', 47 | }, 48 | onUploadProgress, 49 | signal, 50 | }); 51 | 52 | const { data } = response; 53 | 54 | return data; 55 | } 56 | 57 | export const deleteAllDatasets = async (): Promise => { 58 | const { data } = await apiClient.delete(`/v1/datasets`); 59 | 60 | return data; 61 | }; 62 | -------------------------------------------------------------------------------- /app_frontend/src/api/datasets/keys.ts: -------------------------------------------------------------------------------- 1 | export const datasetKeys = { 2 | all: ['datasets'], 3 | upload: ['uploadDataset'], 4 | }; 5 | -------------------------------------------------------------------------------- /app_frontend/src/api/dictionaries/keys.ts: -------------------------------------------------------------------------------- 1 | export const dictionaryKeys = { 2 | all: ['generatedDictionaries'], 3 | }; 4 | -------------------------------------------------------------------------------- /app_frontend/src/api/dictionaries/types.ts: -------------------------------------------------------------------------------- 1 | export type DictionaryTable = { 2 | name: string; 3 | column_descriptions?: Array; 4 | in_progress: boolean; 5 | }; 6 | 7 | export type DictionaryRow = { 8 | column: string; 9 | data_type: string; 10 | description: string; 11 | }; 12 | -------------------------------------------------------------------------------- /app_frontend/src/api/user/api-requests.ts: -------------------------------------------------------------------------------- 1 | import { apiClient, drClient } from '../apiClient'; 2 | 3 | export interface DataRobotInfoResponse { 4 | datarobot_account_info: { 5 | uid: string; 6 | username: string; 7 | email: string; 8 | [key: string]: string | number | boolean | null | undefined; 9 | } | null; 10 | datarobot_api_token: string | null; 11 | datarobot_api_skoped_token: string | null; 12 | } 13 | 14 | export interface DataRobotAccount { 15 | uid: string; 16 | username: string; 17 | email: string; 18 | [key: string]: string | number | boolean | null | undefined; 19 | } 20 | 21 | export interface DataRobotStoreInfoRequest { 22 | account_info?: { 23 | uid: string; 24 | username: string; 25 | email: string; 26 | [key: string]: string | number | boolean | null | undefined; 27 | } | null; 28 | api_token?: string | null; 29 | } 30 | 31 | export const getDataRobotInfo = async (): Promise => { 32 | const response = await apiClient.get('/v1/user/datarobot-account'); 33 | 34 | if (!response.data.datarobot_api_skoped_token && !response.data.datarobot_api_token) { 35 | try { 36 | await fetchAndStoreDataRobotToken(); 37 | const updatedResponse = await apiClient.get( 38 | '/v1/user/datarobot-account' 39 | ); 40 | return updatedResponse.data; 41 | } catch (error) { 42 | console.error('Error fetching DataRobot info:', error); 43 | } 44 | } 45 | 46 | return response.data; 47 | }; 48 | 49 | export const fetchAndStoreDataRobotToken = async (): Promise => { 50 | try { 51 | const apiKeysResponse = await drClient.get('/account/apiKeys/?isScoped=false'); 52 | const apiKeysData = apiKeysResponse.data; 53 | 54 | let apiToken = null; 55 | try { 56 | if (apiKeysData && apiKeysData.data && Array.isArray(apiKeysData.data)) { 57 | const nonExpiringKeys = apiKeysData.data.filter( 58 | (key: { expireAt: string | null; key: string }) => key.expireAt === null 59 | ); 60 | if (nonExpiringKeys.length > 0) { 61 | apiToken = nonExpiringKeys[0].key; 62 | } 63 | } 64 | } catch (apiKeyError) { 65 | console.warn('Could not process API keys response:', apiKeyError); 66 | } 67 | 68 | await apiClient.post('/v1/user/datarobot-account', { 69 | api_token: apiToken, 70 | }); 71 | } catch (error) { 72 | console.error('Error fetching or storing DataRobot info:', error); 73 | throw error; 74 | } 75 | }; 76 | 77 | export const updateApiToken = async (apiToken: string): Promise => { 78 | try { 79 | await apiClient.post('/v1/user/datarobot-account', { 80 | api_token: apiToken, 81 | }); 82 | } catch (error) { 83 | console.error('Error updating API token:', error); 84 | throw error; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /app_frontend/src/api/user/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 2 | import { getDataRobotInfo, updateApiToken } from './api-requests'; 3 | import { dataRobotInfoKey } from './keys'; 4 | import { useEffect } from 'react'; 5 | import { datasetKeys } from '../datasets/keys'; 6 | 7 | export const useDataRobotInfo = () => { 8 | const query = useQuery({ 9 | queryKey: dataRobotInfoKey, 10 | queryFn: getDataRobotInfo, 11 | }); 12 | 13 | useEffect(() => { 14 | try { 15 | getDataRobotInfo(); 16 | } catch (error) { 17 | console.error('Error in DataRobot info effect:', error); 18 | } 19 | }, []); 20 | 21 | return query; 22 | }; 23 | 24 | export const useUpdateApiToken = () => { 25 | const queryClient = useQueryClient(); 26 | 27 | const mutation = useMutation({ 28 | mutationFn: updateApiToken, 29 | onSuccess: () => { 30 | queryClient.invalidateQueries({ queryKey: dataRobotInfoKey }); 31 | queryClient.invalidateQueries({ queryKey: datasetKeys.all }); 32 | }, 33 | }); 34 | 35 | return mutation; 36 | }; 37 | -------------------------------------------------------------------------------- /app_frontend/src/api/user/keys.ts: -------------------------------------------------------------------------------- 1 | export const dataRobotInfoKey = ['datarobot-account']; 2 | -------------------------------------------------------------------------------- /app_frontend/src/assets/DataRobotLogo_black.svg: -------------------------------------------------------------------------------- 1 | 8 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /app_frontend/src/assets/chat-midnight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app_frontend/src/assets/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /app_frontend/src/assets/playground-midnight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app_frontend/src/components/DataSourceSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from '@/components/ui/label'; 2 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; 3 | import { DATA_SOURCES } from '@/constants/dataSources'; 4 | 5 | interface DataSourceSelectorProps { 6 | value: string; 7 | onChange: (value: string) => void; 8 | } 9 | 10 | export const DataSourceSelector: React.FC = ({ value, onChange }) => { 11 | return ( 12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app_frontend/src/components/WelcomeModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import playgroundMidnight from "@/assets/playground-midnight.svg"; 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogTitle, 10 | } from "@/components/ui/dialog"; 11 | import { useAppState } from "@/state"; 12 | import { useState } from "react"; 13 | import { Separator } from "./ui/separator"; 14 | 15 | export const WelcomeModal = () => { 16 | const { showWelcome, hideWelcomeModal } = useAppState(); 17 | const [open, setOpen] = useState(showWelcome); 18 | 19 | return ( 20 | !open && setOpen(open)} 24 | > 25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 | 33 | Talk to my data 34 | 35 | 36 | Use DataRobot’s intuitive chat-based analyst to ask questions about 37 | your data. 38 |
39 |
40 | Get started by selecting the datasets you want to work with. 41 |
42 |
43 | 44 | 45 | 54 | 55 |
56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/AnalystDatasetTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from '@/components/ui/table'; 10 | 11 | interface AnalystDatasetTableProps { 12 | records?: Record[]; 13 | } 14 | 15 | export const AnalystDatasetTable: React.FC = ({ records }) => { 16 | const headerRow = records?.length ? Object.keys(records[0]) : []; 17 | return ( 18 | 19 | 20 | 21 | {headerRow.map(h => ( 22 | {h} 23 | ))} 24 | 25 | 26 | 27 | {records?.map((record, index) => ( 28 | 29 | {Object.keys(record).map(k => ( 30 | {String(record[k])} 31 | ))} 32 | 33 | ))} 34 | 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/Avatars.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { faComment } from '@fortawesome/free-regular-svg-icons/faComment'; 3 | import DataRobotLogo from '@/assets/DataRobotLogo_black.svg'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { ROUTES } from '@/pages/routes'; 6 | 7 | export const DataRobotAvatar = () => { 8 | const navigate = useNavigate(); 9 | 10 | return ( 11 |
12 | navigate(ROUTES.DATA)} 17 | /> 18 |
19 | ); 20 | }; 21 | 22 | export const UserAvatar = () => ( 23 |
24 |
25 | 26 |
27 |
28 | ); 29 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/CodeTabContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IDataset as DatasetType } from '@/api/chat-messages/types'; 3 | import { CollapsiblePanel } from './CollapsiblePanel'; 4 | import { AnalystDatasetTable } from './AnalystDatasetTable'; 5 | // @ts-expect-error ??? 6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 7 | // @ts-expect-error ??? 8 | import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; 9 | import './MarkdownContent.css'; 10 | 11 | interface CodeTabContentProps { 12 | dataset?: DatasetType | null; 13 | code?: string | null; 14 | } 15 | 16 | export const CodeTabContent: React.FC = ({ dataset, code }) => { 17 | return ( 18 |
19 | {/* 20 | DataRobot generates additional content based on your original question. 21 | */} 22 | {dataset && ( 23 | 24 | 25 | 26 | )} 27 | {code && ( 28 | 29 |
30 | 40 | {code} 41 | 42 |
43 |
44 | )} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/CollapsiblePanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faChevronUp } from '@fortawesome/free-solid-svg-icons/faChevronUp'; 4 | import { faChevronDown } from '@fortawesome/free-solid-svg-icons/faChevronDown'; 5 | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; 6 | import { useAppState } from '@/state'; 7 | 8 | interface CollapsiblePanelProps { 9 | header: React.ReactNode; 10 | children: React.ReactNode; 11 | } 12 | 13 | export const CollapsiblePanel: React.FC = ({ header, children }) => { 14 | const { collapsiblePanelDefaultOpen } = useAppState(); 15 | const [isOpen, setIsOpen] = React.useState(collapsiblePanelDefaultOpen); 16 | const ref = useRef(null); 17 | 18 | useEffect(() => { 19 | ref.current?.scrollIntoView(false); 20 | }); 21 | 22 | return ( 23 | 28 | 29 |
30 |
{header}
31 | 32 |
33 |
34 | 35 | {children} 36 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/HeaderSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface HeaderSectionProps { 4 | title: string; 5 | children: React.ReactNode; 6 | } 7 | 8 | export const HeaderSection: React.FC = ({ title, children }) => { 9 | return ( 10 | <> 11 |
{title}
12 |
13 | {children} 14 |
15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/InfoText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface InfoTextProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export const InfoText: React.FC = ({ children, className = 'mb-4' }) => { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/InsightsTabContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HeaderSection } from './HeaderSection'; 3 | import { SuggestedQuestionsSection } from './SuggestedQuestionsSection'; 4 | import { MarkdownContent } from './MarkdownContent'; 5 | 6 | interface InsightsTabContentProps { 7 | additionalInsights?: string | null; 8 | followUpQuestions?: string[] | null; 9 | chatId?: string; 10 | } 11 | 12 | export const InsightsTabContent: React.FC = ({ 13 | additionalInsights, 14 | followUpQuestions, 15 | chatId, 16 | }) => { 17 | return ( 18 |
19 | {/* 20 | DataRobot generates additional content based on your original question. 21 | */} 22 | {additionalInsights && ( 23 | 24 | 25 | 26 | )} 27 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/Loading.tsx: -------------------------------------------------------------------------------- 1 | export const Loading = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck"; 4 | import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle"; 5 | import loader from "@/assets/loader.svg"; 6 | 7 | interface LoadingIndicatorProps { 8 | isLoading?: boolean; 9 | hasError?: boolean; 10 | successTestId?: string; 11 | } 12 | 13 | export const LoadingIndicator: React.FC = ({ 14 | isLoading = true, 15 | hasError = false, 16 | successTestId = "data-loading-success", 17 | }) => { 18 | if (hasError) { 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | return isLoading ? ( 29 | processing 30 | ) : ( 31 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/MarkdownContent.css: -------------------------------------------------------------------------------- 1 | .markdown-content { 2 | line-height: 1.5; 3 | } 4 | 5 | .markdown-content ul, 6 | .markdown-content ol { 7 | margin-top: 0.5rem; 8 | margin-bottom: 0.5rem; 9 | padding-left: 1.5rem; 10 | } 11 | 12 | .markdown-content ul { 13 | list-style-type: disc; 14 | } 15 | 16 | .markdown-content ol { 17 | list-style-type: decimal; 18 | } 19 | 20 | .markdown-content li { 21 | margin-bottom: 0.25rem; 22 | } 23 | 24 | .markdown-content p { 25 | margin-bottom: 0.75rem; 26 | } 27 | 28 | .markdown-content h1, 29 | .markdown-content h2, 30 | .markdown-content h3, 31 | .markdown-content h4, 32 | .markdown-content h5, 33 | .markdown-content h6 { 34 | font-weight: 600; 35 | margin-top: 1rem; 36 | margin-bottom: 0.5rem; 37 | } 38 | 39 | .markdown-content code { 40 | padding: 0.2rem 0.4rem; 41 | border-radius: 0.25rem; 42 | font-family: monospace; 43 | } 44 | 45 | .markdown-content pre { 46 | border-radius: 0.25rem; 47 | overflow-x: auto; 48 | } 49 | 50 | .markdown-content blockquote { 51 | border-left: 3px solid #ccc; 52 | padding-left: 1rem; 53 | margin-left: 0; 54 | margin-right: 0; 55 | font-style: italic; 56 | } 57 | 58 | .markdown-content hr { 59 | border: 0; 60 | border-top: 1px solid #ccc; 61 | margin: 1rem 0; 62 | } 63 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/MarkdownContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import './MarkdownContent.css'; 4 | 5 | interface MarkdownContentProps { 6 | content?: string; 7 | className?: string; 8 | } 9 | 10 | export const MarkdownContent: React.FC = ({ content, className = '' }) => { 11 | if (!content) { 12 | return null; 13 | } 14 | 15 | return ( 16 |
17 |
    , 21 | ol: ({ ...props }) =>
      , 22 | li: ({ ...props }) =>
    1. , 23 | }} 24 | /> 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/MessageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | 3 | interface MessageContainerProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export const MessageContainer: React.FC = React.memo(({ children }) => { 8 | const ref = useRef(null); 9 | useEffect(() => { 10 | ref.current?.scrollIntoView(false); 11 | }); 12 | return ( 13 |
17 | {children} 18 |
19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/MessageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { JSX } from 'react'; 2 | import { UserAvatar } from './Avatars'; 3 | import { Button } from '@/components/ui/button'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash'; 6 | 7 | interface MessageHeaderProps { 8 | avatar?: () => JSX.Element; 9 | name: string; 10 | date: string; 11 | onDelete?: () => void; 12 | } 13 | 14 | export const MessageHeader: React.FC = ({ 15 | avatar = UserAvatar, 16 | name, 17 | date, 18 | onDelete, 19 | }) => { 20 | return ( 21 |
22 |
23 | {avatar()} 24 |
{name}
25 |
{date}
26 |
27 | {onDelete && ( 28 | 40 | )} 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/PlotPanel.css: -------------------------------------------------------------------------------- 1 | .plot-container { 2 | filter: invert(90%) hue-rotate(180deg); 3 | } 4 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/PlotPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, lazy } from 'react'; 2 | import { CollapsiblePanel } from './CollapsiblePanel'; 3 | import { PlotlyData } from './utils'; 4 | import './PlotPanel.css'; 5 | 6 | const Plot = lazy(() => { 7 | return import('react-plotly.js'); 8 | }); 9 | 10 | const PlotLoading = () => ( 11 |
12 |
Loading visualization...
13 |
14 | ); 15 | 16 | interface PlotPanelProps { 17 | plotData: { 18 | data: PlotlyData[]; 19 | layout: { 20 | title?: { 21 | text?: string; 22 | }; 23 | [key: string]: unknown; 24 | }; 25 | }; 26 | className?: string; 27 | width?: string | number; 28 | height?: string | number; 29 | } 30 | 31 | export const PlotPanel: React.FC = ({ 32 | plotData, 33 | className = '', 34 | width = '100%', 35 | height = '500px', 36 | }) => { 37 | if (!plotData || !plotData.data || !plotData.layout) { 38 | return null; 39 | } 40 | 41 | return ( 42 | 43 | }> 44 | 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/ResponseTabs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; 3 | import { LoadingIndicator } from "./LoadingIndicator"; 4 | import { RESPONSE_TABS } from "./constants"; 5 | 6 | export interface TabState { 7 | isLoading?: boolean; 8 | hasError: boolean; 9 | } 10 | 11 | interface ResponseTabsProps { 12 | value: string; 13 | onValueChange: (value: string) => void; 14 | tabStates?: { 15 | summary: TabState; 16 | insights: TabState; 17 | code: TabState; 18 | }; 19 | } 20 | 21 | export const ResponseTabs: React.FC = ({ 22 | value, 23 | onValueChange, 24 | tabStates, 25 | }) => { 26 | const states = tabStates || { 27 | summary: { 28 | isLoading: false, 29 | hasError: false, 30 | }, 31 | insights: { 32 | isLoading: false, 33 | hasError: false, 34 | }, 35 | code: { 36 | isLoading: false, 37 | hasError: false, 38 | }, 39 | }; 40 | 41 | return ( 42 | 48 | 49 | 50 | 55 | Summary 56 | 57 | 58 | 63 | More insights 64 | 65 | 66 | 71 | Behind the scenes 72 | 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/SuggestedPrompt.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faPaperPlane } from '@fortawesome/free-solid-svg-icons/faPaperPlane'; 4 | import { useGeneratedDictionaries } from '@/api/dictionaries/hooks'; 5 | import { usePostMessage } from '@/api/chat-messages/hooks'; 6 | import { useAppState } from '@/state/hooks'; 7 | 8 | interface SuggestedPromptProps { 9 | message: string; 10 | chatId?: string; 11 | } 12 | 13 | export const SuggestedPrompt: React.FC = ({ message, chatId }) => { 14 | const { enableChartGeneration, enableBusinessInsights, dataSource } = useAppState(); 15 | const { data: dictionaries } = useGeneratedDictionaries(); 16 | const isDisabled = !dictionaries?.[0]; 17 | const { mutate } = usePostMessage(); 18 | return ( 19 |
20 |
21 | {message} 22 |
23 |
24 |
25 |
26 | {!isDisabled && ( 27 | { 30 | mutate({ 31 | message, 32 | chatId, 33 | enableChartGeneration, 34 | enableBusinessInsights, 35 | dataSource, 36 | }); 37 | }} 38 | /> 39 | )} 40 |
41 |
42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/SuggestedQuestionsSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SuggestedPrompt } from './SuggestedPrompt'; 3 | 4 | interface SuggestedQuestionsSectionProps { 5 | questions?: string[] | null; 6 | chatId?: string; 7 | } 8 | 9 | export const SuggestedQuestionsSection: React.FC = ({ 10 | questions, 11 | chatId, 12 | }) => { 13 | if (!questions || questions.length === 0) { 14 | return null; 15 | } 16 | 17 | return ( 18 | <> 19 |
20 | Suggested follow-up questions 21 |
22 |
23 | {questions.map(q => ( 24 | 25 | ))} 26 |
27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/SummaryTabContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HeaderSection } from './HeaderSection'; 3 | import { PlotPanel } from './PlotPanel'; 4 | import { parsePlotData } from './utils'; 5 | 6 | interface SummaryTabContentProps { 7 | bottomLine?: string; 8 | fig1: string; 9 | fig2: string; 10 | } 11 | 12 | export const SummaryTabContent: React.FC = ({ bottomLine, fig1, fig2 }) => { 13 | const plot1 = parsePlotData(fig1); 14 | const plot2 = parsePlotData(fig2); 15 | 16 | return ( 17 |
18 | {/* 19 | DataRobot writes as short an answer to your question as possible, 20 | illustrated with supporting charts. 21 | */} 22 | {bottomLine && {bottomLine}} 23 |
24 | {plot1 && } 25 | {plot2 && } 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/UserMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { MessageHeader } from './MessageHeader'; 3 | import { formatMessageDate } from './utils'; 4 | import { useDeleteMessage } from '@/api/chat-messages/hooks'; 5 | 6 | interface UserMessageProps { 7 | id?: string; 8 | date?: string; 9 | timestamp?: string; 10 | message?: string; 11 | chatId?: string; 12 | responseId?: string; 13 | } 14 | 15 | export const UserMessage: React.FC = ({ 16 | id, 17 | date, 18 | timestamp, 19 | message, 20 | chatId, 21 | responseId, 22 | }) => { 23 | const ref = useRef(null); 24 | const { mutate: deleteMessage } = useDeleteMessage(); 25 | 26 | useEffect(() => { 27 | ref.current?.scrollIntoView(false); 28 | }); 29 | 30 | // Use the formatted timestamp if available, otherwise fallback to date prop or default 31 | const displayDate = timestamp ? formatMessageDate(timestamp) : date || ''; 32 | 33 | const handleDelete = () => { 34 | if (id) { 35 | deleteMessage({ 36 | messageId: id, 37 | chatId: chatId, 38 | }); 39 | } 40 | if (responseId) { 41 | deleteMessage({ 42 | messageId: responseId, 43 | chatId: chatId, 44 | }); 45 | } 46 | }; 47 | 48 | return ( 49 |
53 | 54 |
{message}
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/UserPrompt.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { PromptInput } from '@/components/ui-custom/prompt-input'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faPaperPlane } from '@fortawesome/free-solid-svg-icons/faPaperPlane'; 5 | import { usePostMessage, useFetchAllChats } from '@/api/chat-messages/hooks'; 6 | import { useAppState } from '@/state'; 7 | import { DATA_SOURCES } from '@/constants/dataSources'; 8 | 9 | export const UserPrompt = ({ 10 | chatId, 11 | allowSend, 12 | allowedDataSources, 13 | }: { 14 | chatId?: string; 15 | allowSend?: boolean; 16 | allowedDataSources?: string[]; 17 | }) => { 18 | const { mutate } = usePostMessage(); 19 | const { 20 | enableChartGeneration, 21 | enableBusinessInsights, 22 | dataSource: globalDataSource, 23 | } = useAppState(); 24 | const { data: chats } = useFetchAllChats(); 25 | const isDisabled = !allowedDataSources?.[0]; 26 | 27 | const [message, setMessage] = useState(''); 28 | 29 | // Find the active chat to get its data source setting 30 | const activeChat = chatId ? chats?.find(chat => chat.id === chatId) : undefined; 31 | const chatDataSource = useMemo(() => { 32 | const dataSource = activeChat?.data_source || globalDataSource; 33 | // User can only select from the allowed data sources 34 | return allowedDataSources?.includes(dataSource) 35 | ? dataSource 36 | : allowedDataSources?.[0] || DATA_SOURCES.FILE; 37 | }, [activeChat?.data_source, globalDataSource, allowedDataSources]); 38 | 39 | const sendMessage = () => { 40 | if (message.trim()) { 41 | mutate({ 42 | message, 43 | chatId, 44 | enableChartGeneration, 45 | enableBusinessInsights, 46 | dataSource: chatDataSource, 47 | }); 48 | setMessage(''); 49 | } 50 | }; 51 | 52 | return ( 53 | { 66 | if (e.key === 'Enter' && allowSend) { 67 | sendMessage(); 68 | } 69 | }} 70 | disabled={isDisabled} 71 | aria-disabled={isDisabled} 72 | onChange={e => setMessage(e.target.value)} 73 | value={message} 74 | /> 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/constants.ts: -------------------------------------------------------------------------------- 1 | export const RESPONSE_TABS = { 2 | SUMMARY: 'summary', 3 | INSIGHTS: 'insights', 4 | CODE: 'code', 5 | }; 6 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { DataRobotAvatar, UserAvatar } from './Avatars'; 2 | export { AnalystDatasetTable } from './AnalystDatasetTable'; 3 | export { CodeTabContent } from './CodeTabContent'; 4 | export { CollapsiblePanel } from './CollapsiblePanel'; 5 | export { HeaderSection } from './HeaderSection'; 6 | export { InfoText } from './InfoText'; 7 | export { InitialPrompt } from './InitialPrompt'; 8 | export { InsightsTabContent } from './InsightsTabContent'; 9 | export { Loading } from './Loading'; 10 | export { LoadingIndicator } from './LoadingIndicator'; 11 | export { MarkdownContent } from './MarkdownContent'; 12 | export { MessageContainer } from './MessageContainer'; 13 | export { MessageHeader } from './MessageHeader'; 14 | export { PlotPanel } from './PlotPanel'; 15 | export { ResponseTabs } from './ResponseTabs'; 16 | export { SuggestedPrompt } from './SuggestedPrompt'; 17 | export { SuggestedQuestionsSection } from './SuggestedQuestionsSection'; 18 | export { SummaryTabContent } from './SummaryTabContent'; 19 | export { UserMessage } from './UserMessage'; 20 | export { UserPrompt } from './UserPrompt'; 21 | export { RESPONSE_TABS } from './constants'; 22 | export { parsePlotData } from './utils'; 23 | 24 | // lazy loaded 25 | export { ResponseMessage } from './ResponseMessage'; 26 | -------------------------------------------------------------------------------- /app_frontend/src/components/chat/utils.ts: -------------------------------------------------------------------------------- 1 | export interface PlotlyData { 2 | [key: string]: unknown; 3 | } 4 | 5 | export const parsePlotData = ( 6 | jsonString?: string 7 | ): { 8 | data: PlotlyData[]; 9 | layout: { 10 | paper_bgcolor: string; 11 | [key: string]: unknown; 12 | }; 13 | } | null => { 14 | if (!jsonString) { 15 | return null; 16 | } 17 | 18 | try { 19 | return { 20 | paper_bgcolor: 'rgba(255,255,255, 0)', 21 | ...JSON.parse(jsonString), 22 | }; 23 | } catch (error) { 24 | console.error('Failed to parse plot data:', error); 25 | return null; 26 | } 27 | }; 28 | 29 | export const formatMessageDate = (timestamp?: string): string => { 30 | if (!timestamp) { 31 | return ''; 32 | } 33 | 34 | try { 35 | const date = new Date(timestamp); 36 | 37 | return date.toLocaleString('en-US', { 38 | month: 'long', 39 | day: 'numeric', 40 | year: 'numeric', 41 | hour: 'numeric', 42 | minute: '2-digit', 43 | hour12: true, 44 | }); 45 | } catch (error) { 46 | console.error('Error formatting date:', error); 47 | return ''; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /app_frontend/src/components/data/ClearDatasetsButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash"; 5 | import { useDeleteAllDatasets } from "@/api/datasets/hooks"; 6 | 7 | interface ClearDatasetsButtonProps { 8 | onClear?: () => void; 9 | } 10 | 11 | export const ClearDatasetsButton: React.FC = ({ 12 | onClear, 13 | }) => { 14 | const { mutate } = useDeleteAllDatasets(); 15 | 16 | const handleClick = () => { 17 | mutate(); 18 | if (onClear) { 19 | onClear(); 20 | } 21 | }; 22 | 23 | return ( 24 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /app_frontend/src/components/data/DataViewTabs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faQuoteLeft } from "@fortawesome/free-solid-svg-icons/faQuoteLeft"; 5 | import { faTable } from "@fortawesome/free-solid-svg-icons/faTable"; 6 | import { DATA_TABS } from "@/state/constants"; 7 | import { ValueOf } from "@/state/types"; 8 | 9 | interface DataViewTabsProps { 10 | defaultValue?: ValueOf; 11 | onChange?: (value: ValueOf) => void; 12 | } 13 | 14 | export const DataViewTabs: React.FC = ({ 15 | defaultValue = DATA_TABS.DESCRIPTION, 16 | onChange, 17 | }) => { 18 | return ( 19 | 24 | 25 | 29 | 30 | Description 31 | 32 | 33 | 34 | Raw rows 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /app_frontend/src/components/data/SearchControl.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'; 4 | 5 | interface SearchControlProps { 6 | onSearch?: (searchText: string) => void; 7 | } 8 | 9 | export const SearchControl: React.FC = () => { 10 | return ( 11 |
12 | 13 | Search 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /app_frontend/src/components/data/index.ts: -------------------------------------------------------------------------------- 1 | export { ClearDatasetsButton } from './ClearDatasetsButton'; 2 | export { DatasetCardDescriptionPanel } from './DatasetCardDescriptionPanel'; 3 | export { DataViewTabs } from './DataViewTabs'; 4 | export { DictionaryTable } from './DictionaryTable'; 5 | export { SearchControl } from './SearchControl'; 6 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui-custom/loading.tsx: -------------------------------------------------------------------------------- 1 | import loader from '@/assets/loader.svg'; 2 | 3 | export const Loading = () => { 4 | return ( 5 |
6 | processing 7 | Loading... 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui-custom/sidebar-menu.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | export type SidebarMenuOptionType = { 4 | key: string; 5 | name: string; 6 | icon?: React.ReactNode; 7 | active?: boolean; 8 | disabled?: boolean; 9 | onClick: () => void; 10 | testId?: string; 11 | }; 12 | 13 | type Props = { 14 | options: SidebarMenuOptionType[]; 15 | activeKey?: string; 16 | }; 17 | 18 | export const SidebarMenu = ({ options }: Props) => { 19 | return ( 20 |
21 | {options.map((option) => ( 22 | 31 | ))} 32 |
33 | ); 34 | }; 35 | 36 | const SidebarMenuOption = ({ 37 | name, 38 | icon, 39 | active, 40 | disabled, 41 | onClick, 42 | testId, 43 | }: SidebarMenuOptionType) => { 44 | return ( 45 |
{ 50 | if (e.key === "Enter" || (e.key === " " && !disabled)) { 51 | onClick(); 52 | } 53 | }} 54 | className={cn( 55 | "flex gap-2 px-3 py-2 rounded border-l-2 border-transparent overflow-hidden transition-colors cursor-pointer hover:bg-card", 56 | { 57 | "rounded-l-none border-l-2 border-primary bg-card": active, 58 | "opacity-50 cursor-not-allowed": disabled, 59 | } 60 | )} 61 | onClick={!disabled ? onClick : () => null} 62 | > 63 |
64 | {icon &&
{icon}
} 65 |
{name}
66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui-custom/truncated-text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from '@/lib/utils'; 3 | 4 | interface TruncatedTextProps { 5 | text?: string; 6 | maxLength?: number; 7 | tooltip?: boolean; 8 | className?: string; 9 | children?: string; 10 | } 11 | 12 | export const TruncatedText: React.FC = ({ 13 | text, 14 | className, 15 | maxLength = 18, 16 | tooltip = true, 17 | children, 18 | }) => { 19 | text = text || children?.toString() || ''; 20 | const isTruncated = text.length > maxLength; 21 | const truncatedText = isTruncated ? `${text.slice(0, maxLength)}...` : text; 22 | 23 | return ( 24 | 28 | {truncatedText} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-card text-card-foreground', 12 | destructive: 13 | 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | } 20 | ); 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<'div'> & VariantProps) { 27 | return ( 28 |
34 | ); 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { 38 | return ( 39 |
44 | ); 45 | } 46 | 47 | function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { 48 | return ( 49 |
57 | ); 58 | } 59 | 60 | export { Alert, AlertTitle, AlertDescription }; 61 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import * as React from "react"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | destructive: 17 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 18 | outline: "text-foreground border-primary", 19 | success: "text-success border-success", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ); 27 | 28 | export interface BadgeProps 29 | extends React.HTMLAttributes, 30 | VariantProps { 31 | testId?: string; 32 | } 33 | 34 | function Badge({ className, testId, variant, ...props }: BadgeProps) { 35 | return ( 36 |
41 | ); 42 | } 43 | 44 | export { Badge, badgeVariants }; 45 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import * as React from "react"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0 cursor-pointer", 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", 15 | destructive: 16 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", 17 | outline: 18 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 19 | secondary: 20 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 21 | ghost: "hover:bg-accent hover:text-accent-foreground", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | testId, 44 | ...props 45 | }: React.ComponentProps<"button"> & 46 | VariantProps & { 47 | asChild?: boolean; 48 | testId?: string; 49 | }) { 50 | const Comp = asChild ? Slot : "button"; 51 | 52 | return ( 53 | 59 | ); 60 | } 61 | 62 | export { Button, buttonVariants }; 63 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 2 | 3 | function Collapsible({ ...props }: React.ComponentProps) { 4 | return ; 5 | } 6 | 7 | function CollapsibleTrigger({ 8 | ...props 9 | }: React.ComponentProps) { 10 | return ; 11 | } 12 | 13 | function CollapsibleContent({ 14 | ...props 15 | }: React.ComponentProps) { 16 | return ; 17 | } 18 | 19 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 20 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Input({ className, type, onKeyDown, ...props }: React.ComponentProps<'input'>) { 6 | const [isComposing, setIsComposing] = React.useState(false); 7 | return ( 8 | setIsComposing(true)} 12 | onCompositionEnd={() => setIsComposing(false)} 13 | onKeyDown={event => { 14 | if (onKeyDown && !isComposing) { 15 | onKeyDown(event); 16 | } 17 | }} 18 | className={cn( 19 | 'border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4', 20 | className 21 | )} 22 | {...props} 23 | /> 24 | ); 25 | } 26 | 27 | export { Input }; 28 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as LabelPrimitive from '@radix-ui/react-label'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Label({ className, htmlFor, ...props }: React.ComponentProps) { 6 | const isForEnabledRadio = React.useMemo(() => { 7 | if (typeof document !== 'undefined' && htmlFor) { 8 | const element = document.getElementById(htmlFor); 9 | if (element && element instanceof HTMLInputElement && element.type === 'radio') { 10 | return !element.disabled; 11 | } 12 | } 13 | return false; 14 | }, [htmlFor]); 15 | 16 | return ( 17 | 27 | ); 28 | } 29 | 30 | export { Label }; 31 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Popover({ ...props }: React.ComponentProps) { 7 | return ; 8 | } 9 | 10 | function PopoverTrigger({ ...props }: React.ComponentProps) { 11 | return ; 12 | } 13 | 14 | function PopoverContent({ 15 | className, 16 | align = 'center', 17 | sideOffset = 4, 18 | ...props 19 | }: React.ComponentProps) { 20 | return ( 21 | 22 | 32 | 33 | ); 34 | } 35 | 36 | function PopoverAnchor({ ...props }: React.ComponentProps) { 37 | return ; 38 | } 39 | 40 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 41 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ProgressPrimitive from '@radix-ui/react-progress'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Progress({ 9 | className, 10 | value, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 27 | 28 | ); 29 | } 30 | 31 | export { Progress }; 32 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; 3 | import { CircleIcon } from 'lucide-react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | function RadioGroup({ 8 | className, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 17 | ); 18 | } 19 | 20 | function RadioGroupItem({ 21 | className, 22 | disabled, 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 36 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | export { RadioGroup, RadioGroupItem }; 47 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GripVerticalIcon } from 'lucide-react'; 3 | import * as ResizablePrimitive from 'react-resizable-panels'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | function ResizablePanelGroup({ 8 | className, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 20 | ); 21 | } 22 | 23 | function ResizablePanel({ ...props }: React.ComponentProps) { 24 | return ; 25 | } 26 | 27 | function ResizableHandle({ 28 | withHandle, 29 | className, 30 | ...props 31 | }: React.ComponentProps & { 32 | withHandle?: boolean; 33 | }) { 34 | return ( 35 | div]:rotate-90', 39 | className 40 | )} 41 | {...props} 42 | > 43 | {withHandle && ( 44 |
45 | 46 |
47 | )} 48 |
49 | ); 50 | } 51 | 52 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 53 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function ScrollArea({ 7 | className, 8 | children, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 17 | 21 | {children} 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | function ScrollBar({ 31 | className, 32 | orientation = 'vertical', 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 47 | 51 | 52 | ); 53 | } 54 | 55 | export { ScrollArea, ScrollBar }; 56 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Separator({ 9 | className, 10 | orientation = 'horizontal', 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as SwitchPrimitive from '@radix-ui/react-switch'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Switch({ className, ...props }: React.ComponentProps) { 7 | return ( 8 | 16 | 22 | 23 | ); 24 | } 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Table({ className, ...props }: React.ComponentProps<'table'>) { 6 | return ( 7 |
8 | 13 | 14 | ); 15 | } 16 | 17 | function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) { 18 | return ( 19 | 20 | ); 21 | } 22 | 23 | function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) { 24 | return ( 25 | 30 | ); 31 | } 32 | 33 | function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) { 34 | return ( 35 | tr]:last:border-b-0', className)} 38 | {...props} 39 | /> 40 | ); 41 | } 42 | 43 | function TableRow({ className, ...props }: React.ComponentProps<'tr'>) { 44 | return ( 45 | 53 | ); 54 | } 55 | 56 | function TableHead({ className, ...props }: React.ComponentProps<'th'>) { 57 | return ( 58 |
[role=checkbox]]:translate-y-[2px]', 62 | className 63 | )} 64 | {...props} 65 | /> 66 | ); 67 | } 68 | 69 | function TableCell({ className, ...props }: React.ComponentProps<'td'>) { 70 | return ( 71 | [role=checkbox]]:translate-y-[2px]', 75 | className 76 | )} 77 | {...props} 78 | /> 79 | ); 80 | } 81 | 82 | function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) { 83 | return ( 84 |
89 | ); 90 | } 91 | 92 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; 93 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Tabs({ className, ...props }: React.ComponentProps) { 7 | return ( 8 | 13 | ); 14 | } 15 | 16 | function TabsList({ className, ...props }: React.ComponentProps) { 17 | return ( 18 | 26 | ); 27 | } 28 | 29 | function TabsTrigger({ className, ...props }: React.ComponentProps) { 30 | return ( 31 | 39 | ); 40 | } 41 | 42 | function TabsContent({ className, ...props }: React.ComponentProps) { 43 | return ( 44 | 52 | ); 53 | } 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/toggle-group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; 3 | import { type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | import { toggleVariants } from '@/components/ui/toggle'; 7 | 8 | const ToggleGroupContext = React.createContext>({ 9 | size: 'default', 10 | variant: 'default', 11 | }); 12 | 13 | function ToggleGroup({ 14 | className, 15 | variant, 16 | size, 17 | children, 18 | ...props 19 | }: React.ComponentProps & VariantProps) { 20 | return ( 21 | 31 | 32 | {children} 33 | 34 | 35 | ); 36 | } 37 | 38 | function ToggleGroupItem({ 39 | className, 40 | children, 41 | variant, 42 | size, 43 | ...props 44 | }: React.ComponentProps & VariantProps) { 45 | const context = React.useContext(ToggleGroupContext); 46 | 47 | return ( 48 | 62 | {children} 63 | 64 | ); 65 | } 66 | 67 | export { ToggleGroup, ToggleGroupItem }; 68 | -------------------------------------------------------------------------------- /app_frontend/src/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as TogglePrimitive from '@radix-ui/react-toggle'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", 11 | { 12 | variants: { 13 | variant: { 14 | default: 'bg-transparent', 15 | outline: 16 | 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground', 17 | }, 18 | size: { 19 | default: 'h-9 px-2 min-w-9', 20 | sm: 'h-8 px-1.5 min-w-8', 21 | lg: 'h-10 px-2.5 min-w-10', 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: 'default', 26 | size: 'default', 27 | }, 28 | } 29 | ); 30 | 31 | function Toggle({ 32 | className, 33 | variant, 34 | size, 35 | ...props 36 | }: React.ComponentProps & VariantProps) { 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Toggle, toggleVariants }; // eslint-disable-line 47 | -------------------------------------------------------------------------------- /app_frontend/src/constants/dataSources.ts: -------------------------------------------------------------------------------- 1 | export const DATA_SOURCES = { 2 | DATABASE: 'database', 3 | CATALOG: 'catalog', 4 | FILE: 'file', 5 | }; 6 | -------------------------------------------------------------------------------- /app_frontend/src/constants/dev.ts: -------------------------------------------------------------------------------- 1 | export const VITE_DEFAULT_PORT = 5173; 2 | export const VITE_STATIC_DEFAULT_PORT = 8080; 3 | -------------------------------------------------------------------------------- /app_frontend/src/global.d.ts: -------------------------------------------------------------------------------- 1 | // global.d.ts 2 | 3 | export {}; 4 | 5 | declare global { 6 | interface Window { 7 | ENV: { 8 | APP_BASE_URL?: string; 9 | API_PORT?: string; 10 | DATAROBOT_ENDPOINT?: string; 11 | IS_STATIC_FRONTEND?: boolean; 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app_frontend/src/jest-dom.d.ts: -------------------------------------------------------------------------------- 1 | // jest-dom.d.ts 2 | import '@testing-library/jest-dom'; 3 | 4 | declare global { 5 | namespace jest { 6 | interface Matchers { 7 | toBeInTheDocument(): R; 8 | } 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /app_frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function isDev() { 9 | return import.meta.env.MODE === 'development'; 10 | } 11 | 12 | export function isServedStatic() { 13 | return window.ENV?.IS_STATIC_FRONTEND; 14 | } 15 | -------------------------------------------------------------------------------- /app_frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import '@fontsource/dm-sans/300.css'; 5 | import '@fontsource/dm-sans/400.css'; 6 | import '@fontsource/dm-sans/500.css'; 7 | import '@fontsource/dm-sans/600.css'; 8 | import '@fontsource/dm-sans/700.css'; 9 | import { isServedStatic } from '@/lib/utils.ts'; 10 | import './index.css'; 11 | import App from './App.tsx'; 12 | import { AppStateProvider } from './state'; 13 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 14 | 15 | let basename = window.ENV?.APP_BASE_URL ?? undefined; 16 | if ( 17 | window.ENV?.APP_BASE_URL?.includes('notebook-sessions') && 18 | window.ENV?.API_PORT && 19 | isServedStatic() 20 | ) { 21 | basename += `/ports/` + window.ENV.API_PORT; 22 | } 23 | 24 | const queryClient = new QueryClient(); 25 | 26 | createRoot(document.getElementById('root')!).render( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | -------------------------------------------------------------------------------- /app_frontend/src/pages/Data.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Separator } from '@radix-ui/react-separator'; 3 | import { useGeneratedDictionaries } from '@/api/dictionaries/hooks'; 4 | 5 | import { 6 | DatasetCardDescriptionPanel, 7 | DataViewTabs, 8 | SearchControl, 9 | ClearDatasetsButton, 10 | } from '@/components/data'; 11 | import { ValueOf } from '@/state/types'; 12 | import { DATA_TABS } from '@/state/constants'; 13 | import { Loading } from '@/components/ui-custom/loading'; 14 | 15 | export const Data: React.FC = () => { 16 | const { data, status } = useGeneratedDictionaries(); 17 | const [viewMode, setViewMode] = useState>(DATA_TABS.DESCRIPTION); 18 | 19 | return ( 20 |
21 |

22 | Data 23 |

24 |
25 |
26 |
View
27 | setViewMode(value as ValueOf)} 30 | /> 31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | {status === 'pending' ? ( 39 |
40 | 41 |
42 | ) : ( 43 |
44 | {data?.map(dictionary => ( 45 | 51 | ))} 52 |
53 | )} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /app_frontend/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes, Navigate } from 'react-router-dom'; 2 | import { ROUTES } from './routes'; 3 | import { Suspense, lazy } from 'react'; 4 | 5 | const Data = lazy(() => import('./Data').then(module => ({ default: module.Data }))); 6 | const Chats = lazy(() => import('./Chats').then(module => ({ default: module.Chats }))); 7 | 8 | const Loading = () =>
Loading...
; 9 | 10 | const Pages = () => { 11 | return ( 12 |
13 | }> 14 | 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default Pages; 26 | -------------------------------------------------------------------------------- /app_frontend/src/pages/routes.ts: -------------------------------------------------------------------------------- 1 | export const ROUTES = { 2 | DATA: '/data', 3 | CHATS: '/chats', 4 | CHAT_WITH_ID: '/chats/:chatId', 5 | }; 6 | 7 | export const generateChatRoute = (chatId?: string) => { 8 | if (!chatId) return ROUTES.CHATS; 9 | return `/chats/${chatId}`; 10 | }; 11 | -------------------------------------------------------------------------------- /app_frontend/src/state/AppStateContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { AppState } from './types'; 3 | 4 | export const AppStateContext = createContext({} as AppState); 5 | -------------------------------------------------------------------------------- /app_frontend/src/state/AppStateProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import { AppState } from './types'; 3 | import { reducer, createInitialState, actions } from './reducer'; 4 | import { AppStateContext } from './AppStateContext'; 5 | 6 | export const AppStateProvider: React.FC<{ 7 | children: React.ReactNode; 8 | }> = ({ children }) => { 9 | const [state, dispatch] = useReducer(reducer, createInitialState()); 10 | 11 | const hideWelcomeModal = () => { 12 | dispatch(actions.hideWelcomeModal()); 13 | }; 14 | 15 | const setCollapsiblePanelDefaultOpen = (isOpen: boolean) => { 16 | dispatch(actions.setCollapsiblePanelDefaultOpen(isOpen)); 17 | }; 18 | 19 | const setEnableChartGeneration = (enabled: boolean) => { 20 | dispatch(actions.setEnableChartGeneration(enabled)); 21 | }; 22 | 23 | const setEnableBusinessInsights = (enabled: boolean) => { 24 | dispatch(actions.setEnableBusinessInsights(enabled)); 25 | }; 26 | 27 | const setDataSource = (source: string) => { 28 | dispatch(actions.setDataSource(source)); 29 | }; 30 | 31 | const contextValue: AppState = { 32 | ...state, 33 | hideWelcomeModal, 34 | setCollapsiblePanelDefaultOpen, 35 | setEnableChartGeneration, 36 | setEnableBusinessInsights, 37 | setDataSource, 38 | }; 39 | 40 | return {children}; 41 | }; 42 | -------------------------------------------------------------------------------- /app_frontend/src/state/constants.ts: -------------------------------------------------------------------------------- 1 | import { ValueOf } from './types'; 2 | 3 | export const ACTION_TYPES = { 4 | HIDE_WELCOME_MODAL: 'HIDE_WELCOME_MODAL', 5 | SET_COLLAPSIBLE_PANEL_DEFAULT_OPEN: 'SET_COLLAPSIBLE_PANEL_DEFAULT_OPEN', 6 | SET_ENABLE_CHART_GENERATION: 'SET_ENABLE_CHART_GENERATION', 7 | SET_ENABLE_BUSINESS_INSIGHTS: 'SET_ENABLE_BUSINESS_INSIGHTS', 8 | SET_DATA_SOURCE: 'SET_DATA_SOURCE', 9 | } as const; 10 | 11 | export type StateActionType = ValueOf; 12 | 13 | export const STORAGE_KEYS = { 14 | HIDE_WELCOME_MODAL: 'HIDE_WELCOME_MODAL', 15 | COLLAPSIBLE_PANEL_DEFAULT_OPEN: 'COLLAPSIBLE_PANEL_DEFAULT_OPEN', 16 | ENABLE_CHART_GENERATION: 'ENABLE_CHART_GENERATION', 17 | ENABLE_BUSINESS_INSIGHTS: 'ENABLE_BUSINESS_INSIGHTS', 18 | DATA_SOURCE: 'DATA_SOURCE', 19 | } as const; 20 | 21 | export const NEW_CHAT_ID = 'new'; 22 | 23 | export const DATA_TABS = { 24 | DESCRIPTION: 'description', 25 | RAW: 'raw', 26 | }; 27 | -------------------------------------------------------------------------------- /app_frontend/src/state/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AppStateContext } from './AppStateContext'; 3 | import { AppState } from './types'; 4 | 5 | export const useAppState = (): AppState => { 6 | const context = useContext(AppStateContext); 7 | 8 | if (context === undefined) { 9 | throw new Error('useAppState must be used within an AppStateProvider'); 10 | } 11 | 12 | return context; 13 | }; 14 | -------------------------------------------------------------------------------- /app_frontend/src/state/index.ts: -------------------------------------------------------------------------------- 1 | export { AppStateContext } from './AppStateContext'; 2 | export { AppStateProvider } from './AppStateProvider'; 3 | export { useAppState } from './hooks'; 4 | export type { AppState } from './types'; 5 | export { NEW_CHAT_ID } from './constants'; 6 | -------------------------------------------------------------------------------- /app_frontend/src/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AppStateData, Action } from './types'; 2 | import { ACTION_TYPES, STORAGE_KEYS } from './constants'; 3 | import { getStorageItem, setStorageItem } from './storage'; 4 | 5 | import { DATA_SOURCES } from '@/constants/dataSources'; 6 | 7 | export const createInitialState = (): AppStateData => { 8 | return { 9 | showWelcome: getStorageItem(STORAGE_KEYS.HIDE_WELCOME_MODAL) !== 'true', 10 | collapsiblePanelDefaultOpen: 11 | getStorageItem(STORAGE_KEYS.COLLAPSIBLE_PANEL_DEFAULT_OPEN) === 'true', 12 | enableChartGeneration: getStorageItem(STORAGE_KEYS.ENABLE_CHART_GENERATION) !== 'false', // Enable by default 13 | enableBusinessInsights: getStorageItem(STORAGE_KEYS.ENABLE_BUSINESS_INSIGHTS) !== 'false', // Enable by default 14 | dataSource: getStorageItem(STORAGE_KEYS.DATA_SOURCE) || DATA_SOURCES.FILE, // Default to FILE 15 | }; 16 | }; 17 | 18 | export const reducer = (state: AppStateData, action: Action): AppStateData => { 19 | switch (action.type) { 20 | case ACTION_TYPES.HIDE_WELCOME_MODAL: 21 | setStorageItem(STORAGE_KEYS.HIDE_WELCOME_MODAL, 'true'); 22 | return { 23 | ...state, 24 | showWelcome: false, 25 | }; 26 | case ACTION_TYPES.SET_COLLAPSIBLE_PANEL_DEFAULT_OPEN: 27 | setStorageItem( 28 | STORAGE_KEYS.COLLAPSIBLE_PANEL_DEFAULT_OPEN, 29 | action.payload ? 'true' : 'false' 30 | ); 31 | return { 32 | ...state, 33 | collapsiblePanelDefaultOpen: action.payload, 34 | }; 35 | case ACTION_TYPES.SET_ENABLE_CHART_GENERATION: 36 | setStorageItem(STORAGE_KEYS.ENABLE_CHART_GENERATION, action.payload ? 'true' : 'false'); 37 | return { 38 | ...state, 39 | enableChartGeneration: action.payload, 40 | }; 41 | case ACTION_TYPES.SET_ENABLE_BUSINESS_INSIGHTS: 42 | setStorageItem( 43 | STORAGE_KEYS.ENABLE_BUSINESS_INSIGHTS, 44 | action.payload ? 'true' : 'false' 45 | ); 46 | return { 47 | ...state, 48 | enableBusinessInsights: action.payload, 49 | }; 50 | case ACTION_TYPES.SET_DATA_SOURCE: 51 | setStorageItem(STORAGE_KEYS.DATA_SOURCE, action.payload); 52 | return { 53 | ...state, 54 | dataSource: action.payload, 55 | }; 56 | default: 57 | return state; 58 | } 59 | }; 60 | 61 | export const actions = { 62 | hideWelcomeModal: (): Action => ({ 63 | type: ACTION_TYPES.HIDE_WELCOME_MODAL, 64 | }), 65 | setCollapsiblePanelDefaultOpen: (isOpen: boolean): Action => ({ 66 | type: ACTION_TYPES.SET_COLLAPSIBLE_PANEL_DEFAULT_OPEN, 67 | payload: isOpen, 68 | }), 69 | setEnableChartGeneration: (enabled: boolean): Action => ({ 70 | type: ACTION_TYPES.SET_ENABLE_CHART_GENERATION, 71 | payload: enabled, 72 | }), 73 | setEnableBusinessInsights: (enabled: boolean): Action => ({ 74 | type: ACTION_TYPES.SET_ENABLE_BUSINESS_INSIGHTS, 75 | payload: enabled, 76 | }), 77 | setDataSource: (source: string): Action => ({ 78 | type: ACTION_TYPES.SET_DATA_SOURCE, 79 | payload: source, 80 | }), 81 | }; 82 | -------------------------------------------------------------------------------- /app_frontend/src/state/storage.ts: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEYS } from './constants'; 2 | 3 | const getAppIdFromUrl = (): string => { 4 | const url = window.location.href; 5 | const match = url.match(/\/custom_applications\/([^/]+)/); 6 | return match ? match[1] : ''; 7 | }; 8 | 9 | /** 10 | * Creates a prefixed key in the format /custom_applications/{id}/{key} 11 | */ 12 | const getPrefixedKey = (key: string): string => { 13 | const appId = getAppIdFromUrl(); 14 | return appId ? `/custom_applications/${appId}/${key}` : key; 15 | }; 16 | 17 | export const getStorageItem = (key: string): string | null => { 18 | return localStorage.getItem(getPrefixedKey(key)); 19 | }; 20 | 21 | export const setStorageItem = (key: string, value: string): void => { 22 | localStorage.setItem(getPrefixedKey(key), value); 23 | }; 24 | 25 | export const isWelcomeModalHidden = (): boolean => { 26 | return getStorageItem(STORAGE_KEYS.HIDE_WELCOME_MODAL) === 'true'; 27 | }; 28 | 29 | export const isCollapsiblePanelDefaultOpen = (): boolean => { 30 | return getStorageItem(STORAGE_KEYS.COLLAPSIBLE_PANEL_DEFAULT_OPEN) === 'true'; 31 | }; 32 | 33 | export const setCollapsiblePanelDefaultOpen = (isOpen: boolean): void => { 34 | setStorageItem(STORAGE_KEYS.COLLAPSIBLE_PANEL_DEFAULT_OPEN, isOpen ? 'true' : 'false'); 35 | }; 36 | -------------------------------------------------------------------------------- /app_frontend/src/state/types.ts: -------------------------------------------------------------------------------- 1 | export type ValueOf = T[keyof T]; 2 | 3 | export interface AppStateData { 4 | showWelcome: boolean; 5 | collapsiblePanelDefaultOpen: boolean; 6 | enableChartGeneration: boolean; 7 | enableBusinessInsights: boolean; 8 | dataSource: string; 9 | } 10 | 11 | export interface AppStateActions { 12 | hideWelcomeModal: () => void; 13 | setCollapsiblePanelDefaultOpen: (isOpen: boolean) => void; 14 | setEnableChartGeneration: (enabled: boolean) => void; 15 | setEnableBusinessInsights: (enabled: boolean) => void; 16 | setDataSource: (source: string) => void; 17 | } 18 | 19 | export type AppState = AppStateData & AppStateActions; 20 | 21 | export type Action = 22 | | { type: 'HIDE_WELCOME_MODAL' } 23 | | { type: 'SET_COLLAPSIBLE_PANEL_DEFAULT_OPEN'; payload: boolean } 24 | | { type: 'SET_ENABLE_CHART_GENERATION'; payload: boolean } 25 | | { type: 'SET_ENABLE_BUSINESS_INSIGHTS'; payload: boolean } 26 | | { type: 'SET_DATA_SOURCE'; payload: string }; 27 | -------------------------------------------------------------------------------- /app_frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app_frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"], 5 | theme: { 6 | extend: { 7 | colors: { 8 | border: "hsl(var(--border))", 9 | input: "hsl(var(--input))", 10 | ring: "hsl(var(--ring))", 11 | background: "hsl(var(--background))", 12 | foreground: "hsl(var(--foreground))", 13 | primary: { 14 | DEFAULT: "hsl(var(--primary))", 15 | foreground: "hsl(var(--primary-foreground))", 16 | }, 17 | secondary: { 18 | DEFAULT: "hsl(var(--secondary))", 19 | foreground: "hsl(var(--secondary-foreground))", 20 | }, 21 | destructive: { 22 | DEFAULT: "hsl(var(--destructive))", 23 | foreground: "hsl(var(--destructive-foreground))", 24 | }, 25 | muted: { 26 | DEFAULT: "hsl(var(--muted))", 27 | foreground: "hsl(var(--muted-foreground))", 28 | }, 29 | accent: { 30 | DEFAULT: "hsl(var(--accent))", 31 | foreground: "hsl(var(--accent-foreground))", 32 | }, 33 | popover: { 34 | DEFAULT: "hsl(var(--popover))", 35 | foreground: "hsl(var(--popover-foreground))", 36 | }, 37 | card: { 38 | DEFAULT: "hsl(var(--card))", 39 | foreground: "hsl(var(--card-foreground))", 40 | }, 41 | }, 42 | borderRadius: { 43 | lg: `var(--radius)`, 44 | md: `calc(var(--radius) - 2px)`, 45 | sm: "calc(var(--radius) - 4px)", 46 | }, 47 | }, 48 | }, 49 | plugins: [require("tailwindcss-animate")], 50 | } 51 | -------------------------------------------------------------------------------- /app_frontend/tests/__mocks__/handlers.ts: -------------------------------------------------------------------------------- 1 | import { appHandlers } from "./handlers/app"; 2 | 3 | export const handlers = [ 4 | ...appHandlers, 5 | ] 6 | -------------------------------------------------------------------------------- /app_frontend/tests/__mocks__/handlers/app.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw' 2 | 3 | export const appHandlers = [ 4 | http.get('api/v1/welcome', () => { 5 | return HttpResponse.json({ 6 | message: 'Welcome Engineer!' 7 | }) 8 | }), 9 | ] 10 | -------------------------------------------------------------------------------- /app_frontend/tests/__mocks__/node.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node' 2 | import { handlers } from './handlers' 3 | 4 | export const server = setupServer(...handlers) 5 | -------------------------------------------------------------------------------- /app_frontend/tests/components/LoadingIndicator.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { test, describe, expect, vi } from 'vitest'; 3 | import { LoadingIndicator } from '@/components/chat/LoadingIndicator'; 4 | 5 | // Mock the FontAwesomeIcon component 6 | vi.mock('@fortawesome/react-fontawesome', () => ({ 7 | FontAwesomeIcon: () =>
8 | })); 9 | 10 | // Mock the loader SVG import 11 | vi.mock('@/assets/loader.svg', () => ({ 12 | default: 'mock-loader-path', 13 | })); 14 | 15 | describe('LoadingIndicator Component', () => { 16 | test('renders loader when isLoading is true', () => { 17 | render(); 18 | 19 | const loader = screen.getByRole('img'); 20 | expect(loader).toBeInTheDocument(); 21 | expect(loader).toHaveAttribute('src', 'mock-loader-path'); 22 | expect(loader).toHaveAttribute('alt', 'processing'); 23 | expect(loader).toHaveClass('animate-spin'); 24 | 25 | // Check that the icon is not rendered 26 | expect(screen.queryByTestId('mock-icon')).not.toBeInTheDocument(); 27 | }); 28 | 29 | test('renders check icon when isLoading is false', () => { 30 | render(); 31 | 32 | // Check that the loader is not rendered 33 | expect(screen.queryByRole('img')).not.toBeInTheDocument(); 34 | 35 | // Check that the icon is rendered 36 | const icon = screen.getByTestId('mock-icon'); 37 | expect(icon).toBeInTheDocument(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /app_frontend/tests/components/MessageContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { test, describe, expect, vi } from 'vitest'; 3 | import { MessageContainer } from '@/components/chat/MessageContainer'; 4 | 5 | describe('MessageContainer Component', () => { 6 | // Setup and teardown for all tests 7 | let scrollIntoViewMock; 8 | let originalScrollIntoView: typeof HTMLElement.prototype.scrollIntoView; 9 | 10 | beforeEach(() => { 11 | // Store original method before mocking 12 | originalScrollIntoView = HTMLElement.prototype.scrollIntoView; 13 | // Create a mock for scrollIntoView 14 | scrollIntoViewMock = vi.fn(); 15 | 16 | // Mock scrollIntoView on the HTMLElement prototype 17 | Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { 18 | configurable: true, 19 | value: scrollIntoViewMock, 20 | writable: true 21 | }); 22 | }); 23 | 24 | afterEach(() => { 25 | // Restore the original method after each test 26 | Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { 27 | configurable: true, 28 | value: originalScrollIntoView, 29 | writable: true 30 | }); 31 | }); 32 | 33 | test('renders children correctly', () => { 34 | render( 35 | 36 |
Test Child Content
37 |
38 | ); 39 | 40 | const childElement = screen.getByTestId('test-child'); 41 | expect(childElement).toBeInTheDocument(); 42 | expect(childElement).toHaveTextContent('Test Child Content'); 43 | }); 44 | 45 | test('applies correct styling', () => { 46 | render( 47 | 48 |
Content
49 |
50 | ); 51 | 52 | const container = screen.getByText('Content').parentElement; 53 | expect(container).toHaveClass('p-3'); 54 | expect(container).toHaveClass('bg-card'); 55 | expect(container).toHaveClass('rounded'); 56 | expect(container).toHaveClass('flex-col'); 57 | }); 58 | 59 | test('calls scrollIntoView when mounted', () => { 60 | render( 61 | 62 |
Scroll Test
63 |
64 | ); 65 | 66 | // Verify scrollIntoView was called with false 67 | expect(scrollIntoViewMock).toHaveBeenCalledWith(false); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /app_frontend/tests/components/NewChatModal.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from '@testing-library/react'; 2 | import { test, describe, expect, vi, type MockInstance, type Mock } from 'vitest'; 3 | import { NewChatModal } from '@/components/NewChatModal'; 4 | import { useCreateChat } from '@/api/chat-messages/hooks'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { generateChatRoute } from '@/pages/routes'; 7 | 8 | // Mock the dependencies 9 | vi.mock('@/api/chat-messages/hooks', () => ({ 10 | useCreateChat: vi.fn(), 11 | })); 12 | 13 | vi.mock('react-router-dom', () => ({ 14 | useNavigate: vi.fn(), 15 | })); 16 | 17 | vi.mock('@/pages/routes', () => ({ 18 | generateChatRoute: vi.fn(), 19 | })); 20 | 21 | // Mock FontAwesomeIcon 22 | vi.mock('@fortawesome/react-fontawesome', () => ({ 23 | FontAwesomeIcon: () =>
24 | })); 25 | 26 | // Mock lucide-react 27 | vi.mock('lucide-react', () => ({ 28 | XIcon: () =>
29 | })); 30 | 31 | describe('NewChatModal Component', () => { 32 | let mockCreateChat: MockInstance; 33 | let mockNavigate: MockInstance; 34 | 35 | // Setup default mocks before each test 36 | beforeEach(() => { 37 | // Mock the createChat mutation 38 | mockCreateChat = vi.fn(); 39 | (useCreateChat as Mock).mockReturnValue({ 40 | mutate: mockCreateChat, 41 | isPending: false, 42 | }); 43 | 44 | // Mock the navigate function 45 | mockNavigate = vi.fn(); 46 | (useNavigate as Mock).mockReturnValue(mockNavigate); 47 | 48 | // Mock the generateChatRoute function 49 | (generateChatRoute as Mock).mockImplementation((id) => `/chat/${id}`); 50 | }); 51 | 52 | test('renders the trigger button correctly', () => { 53 | render(); 54 | const newChatButton = screen.getByRole('button', { name: /new chat/i }); 55 | expect(newChatButton).toBeInTheDocument(); 56 | expect(screen.getByTestId('mock-icon')).toBeInTheDocument(); 57 | }); 58 | 59 | test('shows loading state when creating a chat', () => { 60 | // Set isPending to true for loading state 61 | (useCreateChat as Mock).mockReturnValue({ 62 | mutate: mockCreateChat, 63 | isPending: true, 64 | }); 65 | 66 | render(); 67 | 68 | // Find and click the New Chat button to open the dialog 69 | const triggerButton = screen.getByRole('button', { name: /new chat/i }); 70 | fireEvent.click(triggerButton); 71 | 72 | // Now the dialog content should have a "Creating..." button 73 | const createButton = screen.getByRole('button', { name: /creating/i }); 74 | expect(createButton).toBeInTheDocument(); 75 | expect(createButton).toBeDisabled(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /app_frontend/tests/components/input.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { test, describe, expect, vi } from 'vitest'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Input } from '@/components/ui/input'; 5 | 6 | describe('Input Component', () => { 7 | test('renders input with default props', () => { 8 | render(); 9 | const input = screen.getByRole('textbox'); 10 | 11 | expect(input).toBeInTheDocument(); 12 | expect(input).toHaveClass('h-9'); 13 | }); 14 | 15 | test('sets the correct input type', () => { 16 | const { rerender } = render(); 17 | let input = screen.getByRole('textbox'); 18 | expect(input).toHaveAttribute('type', 'text'); 19 | 20 | rerender(); 21 | input = screen.getByRole('textbox'); 22 | expect(input).toHaveAttribute('type', 'email'); 23 | 24 | rerender(); 25 | expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); 26 | const passwordInput = screen.getByDisplayValue(''); 27 | expect(passwordInput).toHaveAttribute('type', 'password'); 28 | }); 29 | 30 | test('applies custom className', () => { 31 | render(); 32 | const input = screen.getByRole('textbox'); 33 | 34 | expect(input).toHaveClass('test-class'); 35 | }); 36 | 37 | test('accepts and displays value correctly', async () => { 38 | const user = userEvent.setup(); 39 | render(); 40 | const input = screen.getByRole('textbox'); 41 | 42 | expect(input).toHaveValue('Default text'); 43 | 44 | await user.clear(input); 45 | await user.type(input, 'New text'); 46 | 47 | expect(input).toHaveValue('New text'); 48 | }); 49 | 50 | test('handles disabled state correctly', async () => { 51 | const user = userEvent.setup(); 52 | render(); 53 | const input = screen.getByRole('textbox'); 54 | 55 | expect(input).toBeDisabled(); 56 | 57 | await user.type(input, 'Test text'); 58 | expect(input).not.toHaveValue('Test text'); 59 | }); 60 | 61 | test('passes additional props to the input element', async () => { 62 | const handleChange = vi.fn(); 63 | const user = userEvent.setup(); 64 | render(); 65 | const input = screen.getByPlaceholderText('Enter text here'); 66 | 67 | await user.type(input, 'a'); 68 | expect(handleChange).toHaveBeenCalledTimes(1); 69 | }); 70 | 71 | test('handles aria-invalid attribute', () => { 72 | render(); 73 | const input = screen.getByRole('textbox'); 74 | 75 | expect(input).toHaveAttribute('aria-invalid', 'true'); 76 | expect(input).toHaveClass('aria-invalid:border-destructive/60'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /app_frontend/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { server } from './__mocks__/node.js' 3 | 4 | beforeAll(() => { 5 | server.listen() 6 | }) 7 | 8 | afterEach(() => { 9 | server.resetHandlers() 10 | }) 11 | 12 | afterAll(() => { 13 | server.close() 14 | }) 15 | -------------------------------------------------------------------------------- /app_frontend/tests/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | import { AppStateProvider } from '@/state'; 6 | 7 | const createTestQueryClient = () => 8 | new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | retry: false, 12 | }, 13 | }, 14 | }); 15 | 16 | export function renderWithProviders(children: ReactNode) { 17 | const queryClient = createTestQueryClient(); 18 | 19 | return render( 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app_frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ], 29 | "~/*": [ 30 | "./src/*" 31 | ] 32 | } 33 | }, 34 | "include": ["src"] 35 | } 36 | -------------------------------------------------------------------------------- /app_frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" }, 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"], 11 | "~/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app_frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /app_frontend/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./tsconfig.app.json", "./tsconfig.node.json"], 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom"], 5 | "types": ["vitest/globals", "vitest", "node", "@testing-library/jest-dom"], 6 | "module": "ESNext", 7 | "target": "ESNext", 8 | "isolatedModules": true, 9 | "noEmit": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./src/*"], 13 | "~/*": ["./src/*"] 14 | } 15 | }, 16 | "include": ["setupTests.ts", "src", "tests", "**/*.test.ts", "**/*.test.tsx"] 17 | } 18 | -------------------------------------------------------------------------------- /app_frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { nodePolyfills } from "vite-plugin-node-polyfills"; 5 | import path from "path"; 6 | import { VITE_DEFAULT_PORT, VITE_STATIC_DEFAULT_PORT } from "./src/constants/dev"; 7 | 8 | let base: string = ''; 9 | // 1. if NOTEBOOK_ID is set, use /notebook-sessions/${NOTEBOOK_ID}/ports/5173/ for dev server 10 | // 2. if NOTEBOOK_ID and NODE_ENV === 'development' are set, use /notebook-sessions/${NOTEBOOK_ID}/ports/8080/ for static codespace server 11 | if (process.env.NOTEBOOK_ID && process.env.NODE_ENV === 'development') { 12 | const notebookId = process.env.NOTEBOOK_ID; 13 | const defaultPort = process.env.STATIC_CODESPACE ? VITE_STATIC_DEFAULT_PORT : VITE_DEFAULT_PORT; 14 | base = `/notebook-sessions/${notebookId}/ports/${defaultPort}/`; 15 | } 16 | const proxyBase: string = base === '' ? '/' : base; 17 | 18 | 19 | // https://vite.dev/config/ 20 | export default defineConfig({ 21 | plugins: [ 22 | react(), 23 | tailwindcss(), 24 | nodePolyfills({ 25 | exclude: [], 26 | // for plotly.js 27 | protocolImports: true, 28 | }), 29 | { 30 | name: 'strip-base', 31 | apply: 'serve', 32 | configureServer({ middlewares }) { 33 | middlewares.use((req, _res, next) => { 34 | if (base !== '' && !req.url?.startsWith(base)) { 35 | req.url = base.slice(0, -1) + req.url; 36 | } 37 | next(); 38 | }); 39 | }, 40 | }, 41 | ], 42 | resolve: { 43 | alias: { 44 | "@": path.resolve(__dirname, "./src"), 45 | "~": path.resolve(__dirname, "./src"), 46 | }, 47 | }, 48 | base: base, 49 | build: { 50 | outDir: '../app_backend/static/', 51 | emptyOutDir: true, 52 | rollupOptions: { 53 | external: ['_dr_env.js'], 54 | }, 55 | }, 56 | server: { 57 | host: true, 58 | allowedHosts: ["localhost", "127.0.0.1", ".datarobot.com"], 59 | proxy: { 60 | [`${proxyBase}api/`]: { 61 | target: 'http://localhost:8080', 62 | changeOrigin: true, 63 | rewrite: (path) => path.replace(new RegExp(`^${proxyBase}`), ''), 64 | }, 65 | [`${proxyBase}_dr_env.js`]: { 66 | target: 'http://localhost:8080', 67 | changeOrigin: true, 68 | rewrite: (path) => path.replace(new RegExp(`^${proxyBase}`), ''), 69 | }, 70 | }, 71 | }, 72 | test: { 73 | globals: true, 74 | environment: 'jsdom', 75 | setupFiles: './tests/setupTests.ts', 76 | typecheck: { 77 | tsconfig: './tsconfig.test.json', 78 | } 79 | }, 80 | }) 81 | -------------------------------------------------------------------------------- /frontend/.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | maxUploadSize = 200 3 | maxMessageSize = 200 4 | [theme] 5 | base = "light" 6 | primaryColor = "#909BF5" 7 | textColor = "#0B0B0B" 8 | font = "sans-serif" 9 | -------------------------------------------------------------------------------- /frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/frontend/__init__.py -------------------------------------------------------------------------------- /frontend/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from typing import List 17 | 18 | import streamlit as st 19 | from app_settings import PAGE_ICON, apply_custom_css 20 | from datarobot_connect import DataRobotTokenManager 21 | from streamlit.navigation.page import StreamlitPage 22 | 23 | pages: List[StreamlitPage] = [ 24 | st.Page("01_connect_and_explore.py", title="Connect & Explore"), 25 | st.Page("02_chat_with_data.py", title="AI Data Analyst"), 26 | ] 27 | 28 | st.set_page_config( 29 | page_icon=PAGE_ICON, 30 | layout="wide", 31 | initial_sidebar_state="expanded", 32 | ) 33 | apply_custom_css() 34 | 35 | if "datarobot_connect" not in st.session_state: 36 | datarobot_connect = DataRobotTokenManager() 37 | st.session_state.datarobot_connect = datarobot_connect 38 | 39 | 40 | asyncio.run( 41 | st.session_state.datarobot_connect.display_info( 42 | st.sidebar.container(key="user_info") 43 | ) 44 | ) 45 | 46 | pg = st.navigation(pages) 47 | pg.run() 48 | -------------------------------------------------------------------------------- /frontend/app_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import sys 17 | 18 | import streamlit as st 19 | from streamlit_theme import st_theme 20 | 21 | sys.path.append("..") 22 | from utils.schema import AppInfra 23 | 24 | PAGE_ICON = "./datarobot_favicon.png" 25 | 26 | 27 | def display_page_logo() -> None: 28 | theme = st_theme() 29 | # logo placeholder used for initial load 30 | logo = '' 31 | if theme: 32 | if theme.get("base") == "light": 33 | logo = "./DataRobot_black.svg" 34 | else: 35 | logo = "./DataRobot_white.svg" 36 | with st.container(key="datarobot-logo"): 37 | st.image(logo, width=200) 38 | 39 | 40 | def get_database_logo(app_infra: AppInfra) -> None: 41 | if app_infra.database == "snowflake": 42 | st.image("./Snowflake.svg", width=100) 43 | elif app_infra.database == "bigquery": 44 | st.image("./Google_Cloud.svg", width=100) 45 | elif app_infra.database == "sap": 46 | st.image("./sap.svg", width=100) 47 | return None 48 | 49 | 50 | def get_database_loader_message(app_infra: AppInfra) -> str: 51 | if app_infra.database == "snowflake": 52 | return "Load Datasets from Snowflake" 53 | elif app_infra.database == "bigquery": 54 | return "Load Datasets from BigQuery" 55 | elif app_infra.database == "sap": 56 | return "Load Datasets from SAP" 57 | return "No database available" 58 | 59 | 60 | def apply_custom_css() -> None: 61 | with open("./style.css") as f: 62 | css = f.read() 63 | st.markdown(f"", unsafe_allow_html=True) 64 | -------------------------------------------------------------------------------- /frontend/bot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/frontend/bot.jpg -------------------------------------------------------------------------------- /frontend/datarobot_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/frontend/datarobot_favicon.png -------------------------------------------------------------------------------- /frontend/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import logging 17 | import sys 18 | import traceback 19 | import uuid 20 | from datetime import datetime 21 | from pathlib import Path 22 | from typing import ( 23 | Any, 24 | ) 25 | 26 | import streamlit as st 27 | 28 | sys.path.append("..") 29 | from utils.analyst_db import AnalystDB, DataSourceType 30 | 31 | logger = logging.getLogger("DataAnalyst") 32 | 33 | 34 | # Add enhanced error logging function 35 | def log_error_details(error: BaseException, context: dict[str, Any]) -> None: 36 | """Log detailed error information with context 37 | 38 | Args: 39 | error: The exception that occurred 40 | context: Dictionary containing error context 41 | """ 42 | error_details = { 43 | "timestamp": datetime.now().isoformat(), 44 | "error_type": type(error).__name__, 45 | "error_message": str(error), 46 | "stack_trace": traceback.format_exc(), 47 | **context, 48 | } 49 | 50 | logger.error( 51 | f"\nERROR DETAILS\n=============\n{json.dumps(error_details, indent=2, default=str)}" 52 | ) 53 | 54 | 55 | empty_session_state = { 56 | "initialized": True, 57 | "datasets_names": [], 58 | "cleansed_data_names": [], 59 | "selected_registry_datasets": [], 60 | "data_source": DataSourceType.FILE, 61 | "file_uploader_key": 0, 62 | "processed_file_ids": [], 63 | "chat_messages": [], 64 | "chat_input_key": 0, 65 | "debug_mode": True, 66 | } 67 | 68 | 69 | def state_empty() -> None: 70 | for key, value in empty_session_state.items(): 71 | st.session_state[key] = value 72 | logger.info("Session state has been reset to its initial empty state.") 73 | 74 | 75 | def generate_user_id() -> str | None: 76 | email_header = st.context.headers.get("x-user-email") 77 | if email_header: 78 | new_user_id = str(uuid.uuid5(uuid.NAMESPACE_OID, email_header))[:36] 79 | return new_user_id 80 | else: 81 | return None 82 | 83 | 84 | async def state_init() -> None: 85 | if "initialized" not in st.session_state: 86 | state_empty() 87 | user_id = None 88 | if "datarobot_uid" not in st.session_state: 89 | user_id = generate_user_id() 90 | else: 91 | user_id = st.session_state.datarobot_uid 92 | if user_id: 93 | analyst_db = await AnalystDB.create( 94 | user_id=user_id, 95 | db_path=Path("/tmp"), 96 | dataset_db_name="datasets.db", 97 | chat_db_name="chat.db", 98 | ) 99 | 100 | st.session_state.analyst_db = analyst_db 101 | else: 102 | logger.warning("datarobot-connect not initialised") 103 | pass 104 | -------------------------------------------------------------------------------- /frontend/metadata.yaml.jinja: -------------------------------------------------------------------------------- 1 | name: runtime-params 2 | 3 | runtimeParameterDefinitions: 4 | {{ additional_params }} 5 | -------------------------------------------------------------------------------- /frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | # python sandbox libraries 2 | pandas>=2.2.3,<3.0 3 | numpy>=2.1.3,<3.0 4 | scipy>=1.15.1,<2.0 5 | statsmodels>=0.14.4,<1.0 6 | scikit-learn>=1.6.1,<2.0 7 | lightgbm>=4.5.0,<5.0 8 | tslearn>=0.6.3,<1.0 9 | spacy>=3.8.3,<4.0 10 | pyarrow<19.0.0 11 | polars>=1.22.0,<2.0 12 | 13 | 14 | # plotting 15 | kaleido>=0.2.1,<1.0 16 | plotly>=5.24.1,<6.0 17 | textblob>=0.19.0,<1.0 18 | 19 | # data 20 | openpyxl>=3.1.5,<4.0 21 | snowflake-connector-python>=3.12.4,<4.0 22 | google-cloud-bigquery>=3.27.0,<4.0 23 | google-auth>=2.37.0,<3.0 24 | snowflake-sqlalchemy>=1.7.3,<2.0 25 | sqlalchemy>=2.0.37,<3.0 26 | cryptography>=44.0.0,<45.0 27 | hdbcli>=2.23.27,<3.0 28 | 29 | # genai 30 | openai>=1.59.9,<2 31 | instructor>=1.3.4,<2.0 32 | 33 | # frontend 34 | streamlit==1.44.1 35 | st-theme>=1.2.3,<2.0 36 | streamlit-javascript>=0.1.5,<1.0 37 | 38 | # backend 39 | datarobot>=3.6.0,<4.0 40 | fastapi>=0.115.6,<1.0 41 | python-multipart>=0.0.20,<1.0 42 | psutil>=6.1.1,<7.0 43 | pydantic==2.7.4,<3.0 44 | pydantic-settings==2.4.0,<3.0 45 | joblib>=1.4.2,<2.0 46 | duckdb>=1.2.0,<1.3 47 | fastexcel>=0.12.1,<1.0 48 | aiofiles==24.1.0 49 | 50 | # dev & compatibility 51 | eval_type_backport>=0.2.2,<1.0 52 | db-dtypes>=1.3.1,<2.0 53 | typing-extensions>=4.12.2,<5.0 54 | numba>=0.61.0,<1.0 55 | -------------------------------------------------------------------------------- /frontend/sap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/start-app.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright 2023 DataRobot, Inc. and its affiliates. 4 | # 5 | # All rights reserved. 6 | # This is proprietary source code of DataRobot, Inc. and its affiliates. 7 | # Released under the terms of DataRobot Tool and Utility Agreement. 8 | # 9 | 10 | echo "Starting App" 11 | streamlit run 'app.py' --server.maxUploadSize 200 12 | -------------------------------------------------------------------------------- /frontend/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto"); 2 | 3 | html, 4 | body, 5 | [class*="css"] { 6 | font-family: "Roboto", sans-serif; 7 | } 8 | 9 | .main { 10 | padding: 0rem 1rem; 11 | } 12 | 13 | .stProgress > div > div > div > div { 14 | background-color: #1c83e1; 15 | } 16 | 17 | .stDownloadButton button { 18 | width: 100%; 19 | } 20 | 21 | /* Add rounded corners to images */ 22 | img { 23 | border-radius: 8px; 24 | } 25 | /* Remove rounded corners from DataRobot logo */ 26 | .st-key-datarobot-logo img { 27 | border-radius: 0; 28 | } 29 | /* Add rounded corners to chat input */ 30 | .stChatInput { 31 | border-radius: 12px; 32 | } 33 | 34 | /* Add rounded corners to chat messages */ 35 | .stChatMessage { 36 | border-radius: 12px; 37 | padding-right: 0; 38 | } 39 | 40 | /* Add rounded corners to buttons */ 41 | button { 42 | border-radius: 8px !important; 43 | } 44 | 45 | /* Add rounded corners to input fields */ 46 | .stTextInput input { 47 | border-radius: 8px; 48 | } 49 | 50 | div[class*="st-key-delete_msg_"] { 51 | height: 40px; 52 | width: 30px; 53 | align-self: end; 54 | margin-right: 16px; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/you.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/frontend/you.jpg -------------------------------------------------------------------------------- /infra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/infra/__init__.py -------------------------------------------------------------------------------- /infra/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/infra/components/__init__.py -------------------------------------------------------------------------------- /infra/feature_flag_requirements.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ENABLE_MLOPS: true 3 | ENABLE_CUSTOM_INFERENCE_MODEL: true 4 | ENABLE_PUBLIC_NETWORK_ACCESS_FOR_ALL_CUSTOM_MODELS: true 5 | ENABLE_MLOPS_TEXT_GENERATION_TARGET_TYPE: true 6 | ENABLE_MLOPS_RESOURCE_REQUEST_BUNDLES: true 7 | -------------------------------------------------------------------------------- /infra/settings_database.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import os 15 | 16 | from utils.schema import DatabaseConnectionType 17 | 18 | # Valid values are: "snowflake", "bigquery", "sap" or "no_database" 19 | DATABASE_CONNECTION_TYPE: DatabaseConnectionType = os.getenv( # type: ignore[assignment] 20 | "DATABASE_CONNECTION_TYPE", "no_database" 21 | ) 22 | -------------------------------------------------------------------------------- /infra/settings_generative.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from __future__ import annotations 15 | 16 | import datarobot as dr 17 | import pulumi 18 | import pulumi_datarobot as datarobot 19 | from datarobot_pulumi_utils.pulumi.stack import PROJECT_NAME 20 | from datarobot_pulumi_utils.schema.custom_models import ( 21 | CustomModelArgs, 22 | DeploymentArgs, 23 | RegisteredModelArgs, 24 | ) 25 | from datarobot_pulumi_utils.schema.exec_envs import RuntimeEnvironments 26 | from datarobot_pulumi_utils.schema.llms import ( 27 | LLMBlueprintArgs, 28 | LLMs, 29 | LLMSettings, 30 | PlaygroundArgs, 31 | ) 32 | 33 | from utils.schema import LLMDeploymentSettings 34 | 35 | LLM = LLMs.AZURE_OPENAI_GPT_4_O 36 | 37 | custom_model_args = CustomModelArgs( 38 | resource_name=f"Generative Analyst Custom Model [{PROJECT_NAME}]", 39 | name="Generative Analyst Assistant", # built-in QA app uses this as the AI's name 40 | target_name=LLMDeploymentSettings().target_feature_name, 41 | target_type=dr.enums.TARGET_TYPE.TEXT_GENERATION, 42 | replicas=2, 43 | base_environment_id=RuntimeEnvironments.PYTHON_312_MODERATIONS.value.id, 44 | opts=pulumi.ResourceOptions(delete_before_replace=True), 45 | ) 46 | 47 | registered_model_args = RegisteredModelArgs( 48 | resource_name=f"Generative Analyst Registered Model [{PROJECT_NAME}]", 49 | ) 50 | 51 | 52 | deployment_args = DeploymentArgs( 53 | resource_name=f"Generative Analyst Deployment [{PROJECT_NAME}]", 54 | label=f"Generative Analyst Deployment [{PROJECT_NAME}]", 55 | association_id_settings=datarobot.DeploymentAssociationIdSettingsArgs( 56 | column_names=["association_id"], 57 | auto_generate_id=False, 58 | required_in_prediction_requests=True, 59 | ), 60 | predictions_data_collection_settings=datarobot.DeploymentPredictionsDataCollectionSettingsArgs( 61 | enabled=True, 62 | ), 63 | predictions_settings=( 64 | datarobot.DeploymentPredictionsSettingsArgs(min_computes=0, max_computes=2) 65 | ), 66 | ) 67 | 68 | playground_args = PlaygroundArgs( 69 | resource_name=f"Generative Analyst Playground [{PROJECT_NAME}]", 70 | ) 71 | 72 | llm_blueprint_args = LLMBlueprintArgs( 73 | resource_name=f"Generative Analyst LLM Blueprint [{PROJECT_NAME}]", 74 | llm_id=LLM.name, 75 | llm_settings=LLMSettings( 76 | max_completion_length=2048, 77 | temperature=0.1, 78 | ), 79 | ) 80 | -------------------------------------------------------------------------------- /infra/settings_main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from pathlib import Path 15 | 16 | PROJECT_ROOT = Path(__file__).resolve().parent.parent.absolute() 17 | -------------------------------------------------------------------------------- /infra/settings_proxy_llm.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from __future__ import annotations 15 | 16 | import os 17 | 18 | import datarobot as dr 19 | import pulumi 20 | from datarobot_pulumi_utils.pulumi.stack import PROJECT_NAME 21 | from datarobot_pulumi_utils.schema.custom_models import ( 22 | CustomModelArgs, 23 | DeploymentArgs, 24 | RegisteredModelArgs, 25 | ) 26 | from datarobot_pulumi_utils.schema.exec_envs import RuntimeEnvironments 27 | 28 | from utils.schema import LLMDeploymentSettings 29 | 30 | CHAT_MODEL_NAME = os.getenv("CHAT_MODEL_NAME") 31 | 32 | custom_model_args = CustomModelArgs( 33 | resource_name=f"Data Analyst Proxy LLM Custom Model [{PROJECT_NAME}]", 34 | name=f"Data Analyst Proxy LLM Custom Model [{PROJECT_NAME}]", 35 | target_name=LLMDeploymentSettings().target_feature_name, 36 | target_type=dr.enums.TARGET_TYPE.TEXT_GENERATION, 37 | replicas=2, 38 | base_environment_id=RuntimeEnvironments.PYTHON_312_MODERATIONS.value.id, 39 | opts=pulumi.ResourceOptions(delete_before_replace=True), 40 | ) 41 | 42 | registered_model_args = RegisteredModelArgs( 43 | resource_name=f"Data Analyst Proxy LLM Registered Model [{PROJECT_NAME}]", 44 | ) 45 | 46 | deployment_args = DeploymentArgs( 47 | resource_name=f"Data Analyst Proxy LLM Deployment [{PROJECT_NAME}]", 48 | label=f"Data Analyst Proxy LLM Deployment [{PROJECT_NAME}]", 49 | ) 50 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # infra 2 | pulumi==3.153.0 3 | pulumi-datarobot>=0.8.15,<0.9 4 | datarobot-pulumi-utils>=0.0.2.post8,<0.1 5 | jinja2>=3.1.5,<4.0 6 | PyYAML>=6.0.2,<7.0 7 | 8 | # dev 9 | mypy>=1.14.1,<2.0 10 | pandas-stubs>=2.2.3.241126,<3.0 11 | types-requests>=2.32.0.20241016,<3.0 12 | types-pyyaml>=6.0.12.20241230,<7.0 13 | ruff>=0.9,<1.0 14 | pytest>=8.3.4,<9.0 15 | pytest-cov>=6.0.0,<7.0 16 | pytest-mock>=3.14.0,<4.0 17 | pytest-asyncio>=0.25.2,<1.0 18 | pytest-httpx>=0.30.0,<0.31.0 19 | 20 | # genai testing 21 | rich>=13.9.4,<14.0 22 | inquirer>=3.4.0,<4.0 23 | nbformat>=5.10.0,<6.0 24 | 25 | # ======= frontend/requirements.txt ======= # 26 | 27 | # python sandbox libraries 28 | pandas>=2.2.3,<3.0 29 | numpy>=2.1.3,<3.0 30 | scipy>=1.15.1,<2.0 31 | statsmodels>=0.14.4,<1.0 32 | scikit-learn>=1.6.1,<2.0 33 | lightgbm>=4.5.0,<5.0 34 | tslearn>=0.6.3,<1.0 35 | spacy>=3.8.3,<4.0 36 | pyarrow<19.0.0 37 | polars>=1.22.0,<2.0 38 | 39 | # plotting 40 | kaleido==0.2.0 41 | plotly>=5.24.1,<6.0 42 | textblob>=0.19.0,<1.0 43 | 44 | # data 45 | openpyxl>=3.1.5,<4.0 46 | snowflake-connector-python>=3.12.4,<4.0 47 | google-cloud-bigquery>=3.27.0,<4.0 48 | google-auth>=2.37.0,<3.0 49 | snowflake-sqlalchemy>=1.7.3,<2.0 50 | sqlalchemy>=2.0.37,<3.0 51 | cryptography>=44.0.0,<45.0 52 | hdbcli>=2.23.27,<3.0 53 | 54 | # genai 55 | openai>=1.59.9,<2 56 | instructor>=1.3.4,<2.0 57 | boto3>=1.36.2,<2.0 58 | 59 | # frontend 60 | streamlit==1.44.1 61 | st-theme>=1.2.3,<2.0 62 | streamlit-javascript>=0.1.5,<1.0 63 | 64 | # backend 65 | datarobot>=3.6.0,<4.0 66 | fastapi>=0.115.6,<1.0 67 | python-multipart>=0.0.20,<1.0 68 | uvicorn==0.34.2,<1.0 69 | psutil>=6.1.1,<7.0 70 | pydantic==2.7.4,<3.0 71 | pydantic-settings==2.4.0,<3.0 72 | joblib>=1.4.2,<2.0 73 | duckdb>=1.2.0,<1.3 74 | fastexcel>=0.12.1,<1.0 75 | aiofiles==24.1.0 76 | types-aiofiles==24.1.0.20241221 77 | httpx>=0.23.0,<1.0,<1.0 78 | pillow==11.2.1 79 | 80 | # dev & compatibility 81 | eval_type_backport>=0.2.2,<1.0 82 | db-dtypes>=1.3.1,<2.0 83 | typing-extensions>=4.12.2,<5.0 84 | numba>=0.61.0,<1.0 85 | -------------------------------------------------------------------------------- /set_env.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | :: Define LF (Line Feed) at the start of your script 4 | set ^"LF=^ 5 | 6 | ^" The empty line above is critical - do not remove 7 | 8 | :: Function to load .env file 9 | call :load_env_file || exit /b 10 | 11 | endlocal 12 | 13 | :: Function to activate the virtual environment if it exists 14 | call :activate_virtual_environment 15 | call :set_env_vars 16 | 17 | exit /b 18 | 19 | 20 | :load_env_file 21 | if not exist .env ( 22 | echo Error: .env file not found. 23 | echo Please create a .env file with VAR_NAME=value pairs. 24 | exit /b 1 25 | ) 26 | @echo off 27 | python -c "from quickstart import load_dotenv; env_vars = load_dotenv(); f = open('set_env_vars.bat', 'w'); [f.write(f'set \"{key}={value.replace(\"\n\", \" \").replace(\"\\r\", \" \")}\"\n') for key, value in env_vars.items()]; f.close()" 28 | exit /b 0 29 | 30 | :activate_virtual_environment 31 | if exist .venv\Scripts\activate.bat ( 32 | echo Activated virtual environment found at .venv\Scripts\activate.bat 33 | call .venv\Scripts\activate.bat 34 | ) 35 | exit /b 0 36 | 37 | :set_env_vars 38 | endlocal 39 | call set_env_vars.bat 40 | del set_env_vars.bat 41 | echo Environment variables have been set. 42 | exit /b 0 43 | 44 | -------------------------------------------------------------------------------- /set_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PULUMI_MANAGED_VENV="runtime: name: python options: toolchain: pip virtualenv: .venv" 4 | 5 | MANUAL_DEP_INSTRUCTIONS="Please comment out the following three lines 'Pulumi.yaml' to enable manual dependency management with Pulumi: 6 | # options: 7 | # toolchain: pip 8 | # virtualenv: .venv 9 | 10 | Then install dependencies manually (make sure to address any conflicts pip identifies): 11 | pip install -r requirements.txt" 12 | 13 | # Function to check if script is sourced 14 | check_if_sourced() { 15 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 16 | echo "Please run this script using 'source' or '.' like this:" 17 | echo " source ${0}" 18 | echo " . ${0}" 19 | return 1 20 | fi 21 | } 22 | 23 | # Function to check for DataRobot Codespaces environment 24 | check_datarobot_environment() { 25 | if [[ -n "${DATAROBOT_NOTEBOOK_IMAGE}" ]]; then 26 | if tr -d '\n' < Pulumi.yaml | grep -Fq "$PULUMI_MANAGED_VENV"; then 27 | echo "DR Codespaces requires manual management of dependencies." 28 | echo "$MANUAL_DEP_INSTRUCTIONS" 29 | return 1 30 | fi 31 | fi 32 | } 33 | 34 | # Function to check for active conda environment 35 | check_active_conda_env() { 36 | if [[ -n "${CONDA_DEFAULT_ENV}" ]]; then 37 | if tr -d '\n' < Pulumi.yaml | grep -Fq "$PULUMI_MANAGED_VENV"; then 38 | echo "Using Pulumi with conda requires manual management of dependencies." 39 | echo "$MANUAL_DEP_INSTRUCTIONS" 40 | return 1 41 | fi 42 | fi 43 | } 44 | 45 | 46 | # Function to load .env file 47 | load_env_file() { 48 | if [ ! -f .env ]; then 49 | echo "Error: .env file not found." 50 | echo "Please create a .env file with VAR_NAME=value pairs." 51 | return 1 52 | fi 53 | set -a 54 | source .env 55 | set +a 56 | echo "Environment variables from .env have been set." 57 | } 58 | 59 | # Function to activate the virtual environment if it exists 60 | activate_virtual_environment() { 61 | if [ -f .venv/bin/activate ]; then 62 | echo "Activated virtual environment found at .venv/bin/activate" 63 | source .venv/bin/activate 64 | fi 65 | } 66 | 67 | # Main execution 68 | if check_if_sourced ; then 69 | load_env_file 70 | activate_virtual_environment 71 | check_datarobot_environment 72 | check_active_conda_env 73 | fi 74 | -------------------------------------------------------------------------------- /test_pydantic.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Hashable 16 | 17 | import pandas as pd 18 | from pandas.testing import assert_frame_equal 19 | from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator 20 | 21 | 22 | class AnalystDataset(BaseModel): 23 | name: str 24 | data: pd.DataFrame = Field(default_factory=pd.DataFrame) # Removed exclude=True 25 | 26 | model_config = ConfigDict( 27 | arbitrary_types_allowed=True, 28 | json_encoders={pd.DataFrame: lambda df: df.to_dict(orient="records")}, 29 | ) 30 | 31 | @field_validator("data", mode="before") 32 | @classmethod 33 | def validate_dataframe(cls, v: Any) -> pd.DataFrame: 34 | if isinstance(v, pd.DataFrame): 35 | return v 36 | elif isinstance(v, list): 37 | try: 38 | return pd.DataFrame.from_records(v) 39 | except Exception as e: 40 | raise ValueError("Invalid data format") from e 41 | else: 42 | raise ValueError("data has to be either list of records or pd.DataFrame") 43 | 44 | def to_df(self) -> pd.DataFrame: 45 | return self.data 46 | 47 | @property 48 | def columns(self) -> list[str]: 49 | return self.data.columns.tolist() 50 | 51 | @computed_field 52 | def data_records(self) -> list[dict[Hashable, Any]]: 53 | return self.data.to_dict(orient="records") 54 | 55 | 56 | # Example usage: 57 | if __name__ == "__main__": 58 | # Create a DataFrame. 59 | df = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) 60 | data = {"df": df} 61 | 62 | # Initialize the model. 63 | model = AnalystDataset(name="test", data=data["df"]) 64 | 65 | # Serialize the model to JSON. 66 | serialized = model.model_dump_json() 67 | print(serialized) 68 | 69 | # Deserialize back into an AnalystDataset instance. 70 | deserialized = AnalystDataset.model_validate_json(serialized) 71 | 72 | # Verify that the deserialized DataFrame matches the original. 73 | assert_frame_equal(deserialized.to_df(), data["df"]) 74 | print("DataFrame round-trip successful!") 75 | -------------------------------------------------------------------------------- /trivy-ignore.rego: -------------------------------------------------------------------------------- 1 | package trivy 2 | 3 | import data.lib.trivy 4 | 5 | default ignore := false 6 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datarobot-community/talk-to-my-data-agent/17301f5fad9192aeefa13315f179328b29135334/utils/__init__.py -------------------------------------------------------------------------------- /utils/resources.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from __future__ import annotations 15 | 16 | import json 17 | import subprocess 18 | from typing import Any, Dict, Mapping, Tuple, Type, Union 19 | 20 | from pydantic import AliasChoices, Field 21 | from pydantic_settings import ( 22 | BaseSettings, 23 | EnvSettingsSource, 24 | PydanticBaseSettingsSource, 25 | SettingsConfigDict, 26 | ) 27 | from pydantic_settings.sources import parse_env_vars 28 | 29 | 30 | class PulumiSettingsSource(EnvSettingsSource): 31 | """Pulumi stack outputs as a pydantic settings source.""" 32 | 33 | _PULUMI_OUTPUTS: Dict[str, str] = {} 34 | _PULUMI_CALLED: bool = False 35 | 36 | def __init__(self, *args: Any, **kwargs: Any) -> None: 37 | self.read_pulumi_outputs() 38 | super().__init__(*args, **kwargs) 39 | 40 | def read_pulumi_outputs(self) -> None: 41 | try: 42 | raw_outputs = json.loads( 43 | subprocess.check_output( 44 | ["pulumi", "stack", "output", "-j"], 45 | text=True, 46 | ).strip() 47 | ) 48 | self._PULUMI_OUTPUTS = { 49 | k: v if isinstance(v, str) else json.dumps(v) 50 | for k, v in raw_outputs.items() 51 | } 52 | except BaseException: 53 | self._PULUMI_OUTPUTS = {} 54 | 55 | def _load_env_vars(self) -> Mapping[str, Union[str, None]]: 56 | return parse_env_vars( 57 | self._PULUMI_OUTPUTS, 58 | self.case_sensitive, 59 | self.env_ignore_empty, 60 | self.env_parse_none_str, 61 | ) 62 | 63 | 64 | class DynamicSettings(BaseSettings): 65 | """Settings that come from pulumi stack outputs or DR runtime parameters""" 66 | 67 | model_config = SettingsConfigDict(extra="ignore", populate_by_name=True) 68 | 69 | @classmethod 70 | def settings_customise_sources( 71 | cls, 72 | settings_cls: Type[BaseSettings], 73 | init_settings: PydanticBaseSettingsSource, 74 | env_settings: PydanticBaseSettingsSource, 75 | dotenv_settings: PydanticBaseSettingsSource, 76 | file_secret_settings: PydanticBaseSettingsSource, 77 | ) -> Tuple[PydanticBaseSettingsSource, ...]: 78 | return ( 79 | init_settings, 80 | PulumiSettingsSource(settings_cls), 81 | env_settings, 82 | ) 83 | 84 | 85 | app_settings_env_name: str = "ANALYST_APP_SETTINGS" 86 | 87 | app_env_name: str = "DATAROBOT_APPLICATION_ID" 88 | 89 | llm_deployment_env_name: str = "LLM_DEPLOYMENT_ID" 90 | 91 | 92 | class LLMDeployment(DynamicSettings): 93 | id: str = Field( 94 | validation_alias=AliasChoices( 95 | "MLOPS_RUNTIME_PARAM_" + llm_deployment_env_name, 96 | llm_deployment_env_name, 97 | ) 98 | ) 99 | -------------------------------------------------------------------------------- /utils/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 DataRobot, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # Add additional tools that can be used by the analysis code execution. Remember to include the necessary imports and provide a docstring and signature for each function. 17 | # signature and docstring will be provided to the LLM in the prompt. 18 | # Uncomment the examples below to get started. 19 | 20 | 21 | # import datarobot as dr 22 | # import pandas as pd 23 | # from datarobot_predict.deployment import predict 24 | 25 | # def calculate_summary(df: pd.DataFrame) -> pd.DataFrame: 26 | # """ 27 | # Calculate summary statistics for a DataFrame. 28 | 29 | # Args: 30 | # df (pd.DataFrame): Input DataFrame. 31 | 32 | # Returns: 33 | # pd.DataFrame: Summary statistics including count, mean, std, min, max, and percentiles. 34 | # """ 35 | # description = df.describe(percentiles=[0.2, 0.4, 0.6, 0.8]) 36 | # return description 37 | 38 | 39 | # def filter_data(df: pd.DataFrame, column: str, value: float) -> pd.DataFrame: 40 | # """ 41 | # Filter DataFrame based on a condition. 42 | # Args: 43 | # df (pd.DataFrame): Input DataFrame. 44 | # column (str): Column to apply the filter on. 45 | # value (float): Value to compare against in the filter. 46 | # Returns: 47 | # pd.DataFrame: Filtered DataFrame where the specified column's values are greater than the given value. 48 | # """ 49 | # filtered_df = df[df[column] > value] 50 | # return filtered_df 51 | 52 | 53 | # def call_datarobot_deployment(df: pd.DataFrame, deployment_id: str) -> pd.DataFrame: 54 | # """ 55 | # Call a DataRobot deployment to get predictions. 56 | 57 | # Args: 58 | # df (pd.DataFrame): Input DataFrame with features for prediction. 59 | # deployment_id (str): ID of the DataRobot deployment to use for predictions. 60 | 61 | # Returns: 62 | # pd.DataFrame: DataFrame containing the predictions from DataRobot. The prediction column is named 'predictions'. 63 | # """ 64 | # deployment = dr.Deployment.get(deployment_id) # type: ignore[attr-defined] 65 | # prediction_response: pd.DataFrame = predict( 66 | # deployment=deployment, data_frame=df 67 | # ).dataframe 68 | 69 | # prediction_response.columns = [ 70 | # c.replace("_PREDICTION", "") 71 | # for c in prediction_response.columns # type: ignore[assignment] 72 | # ] 73 | 74 | # if deployment.model is not None: 75 | # target_column = deployment.model.get("target_name") 76 | # if target_column: 77 | # prediction_response["predictions"] = prediction_response[target_column] 78 | 79 | # return prediction_response 80 | --------------------------------------------------------------------------------