{"templateName":"quickstart-page-template","cssClassNames":"page basicpage summit-page","canonicalLink":"https://www.snowflake.com/en/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/","robotsTags":[],"allowedRenditionsWidth":["320","480","640","768","960","1200","1440","1920"],"language":"en","title":"Build an LLM-Powered Dashboard with Posit Connect and Snowflake Cortex AI","analyticsPageType":"quickstart-page-template","analyticsCategory":"general","analyticsSubCategory":"","excludeFromAnalytics":false,":mappedPath":"/en/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/",":type":"snowflake-site/components/structure/page",":items":{"root":{"columnClassNames":{"markup_editor_1950346551":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-banner":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-header":"aem-GridColumn aem-GridColumn--default--12","responsivegrid":"aem-GridColumn aem-GridColumn--default--12","experiencefragment-footer":"aem-GridColumn aem-GridColumn--default--12","modal_container":"aem-GridColumn aem-GridColumn--default--12","markup_editor":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","columnCount":12,":items":{"experiencefragment-banner":{"id":"experiencefragment-7d304b2114","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/pushdown-banner/master.xfmodel.json"},"experiencefragment-header":{"id":"experiencefragment-bbc7f7bb7e","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/mega-nav-header/master.xfmodel.json","languageNavPath":"/content/snowflake-site/global/en/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex.languagenav.json"},"markup_editor_1950346551":{"id":"markup-editor-9ee0449829","title":" ","cssContent":".snowflake-markdown-table code[class*=language-],.snowflake-markdown-table code[class*=language-],.snowflake-markdown .snowflake-text code[class*=language-],.snowflake-markdown .snowflake-text pre[class*=language-]{background-color:rgba(var(--ui-12-rgb),.5);color:var(--text-01);text-shadow:none;padding:var(--spacing-00);border-radius:var(--spacing-00);font-size:smaller}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false},"responsivegrid":{"columnClassNames":{"quickstart_hero":"aem-GridColumn aem-GridColumn--default--12","flexible_column_cont":"aem-GridColumn aem-GridColumn--default--12","markup_editor":"aem-GridColumn aem-GridColumn--default--12"},"gridClassNames":"aem-Grid aem-Grid--12 aem-Grid--default--12","columnCount":12,":items":{"quickstart_hero":{"id":"quickstart-hero-2cbfd3d4a3","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/build-an-llm-powered-dashboard-with-posit-connect-and-cortex",":type":"snowflake-site/components/quickstart/quickstart-hero","isDeveloperGuidesPage":false,"quickstartHeroFirstCertifiedTag":{"tagText":"Quickstart","tagColor":"#29B5E8","tagPath":"/content/cq:tags/snowflake-site/taxonomy/solution-center/certification/quickstart","tagIcon":""},"quickstartHeroTitle":{"lines":["Build an LLM-Powered Dashboard with Posit Connect and Snowflake Cortex AI"],"type":"heading2",":type":"snowflake-site/components/title-v2"},"quickstartHeroAuthor":"Emil Hvitfeldt","quickstartHeroFirstSnowflakeFeatureTag":{"tagText":"Cortex LLM","tagColor":"#29B5E8","tagPath":"/content/cq:tags/snowflake-site/taxonomy/snowflake-feature/cortex-llm-functions","tagIcon":""},"quickstartHeroForkRepoLink":{"id":"button-a3c8ca1636","showOutboundIcon":false,"buttonLink":{"valid":true,"attributes":{"target":"_blank"},"url":"https://github.com/Snowflake-Labs/sfquickstarts/tree/master/site/sfguides/src/build-an-llm-powered-dashboard-with-posit-connect-and-cortex"},"linkTargetContentType":"GENERIC",":type":"snowflake-site/components/button","linkType":"SNOWFLAKE_EXTERNAL","text":"Fork Repo"},"quickstartHeroBreadcrumbs":[{"title":"Build an LLM-Powered Dashboard with Posit Connect and Snowflake Cortex AI","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex","currentPage":true},{"title":"Guides","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers/guides","currentPage":false},{"title":"Snowflake for Developers","url":"https://www.snowflake.com/content/snowflake-site/global/en/developers","currentPage":false}]},"flexible_column_cont":{"id":"flexible-column-container-aac2e6b91a","propertiesId":"quickstart-template-main-flexible-container","type":"2-column-75-25","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"none","bottomPadding":"none","spaceBetween":"small","reverseOnMobile":false,"carouselOnMobile":false,"backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-7862283b64",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"contentfragment":{"id":"contentfragment-2ec1c6fd77","paragraphs":["&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EOverview\u003C/h2\u003E\n","\u003Cp\u003EIn this guide, we'll build a Shiny for Python dashboard over Home Mortgage Disclosure Act (HMDA) data. When designing a dashboard you are constrained by space and it can be hard to answer every possible question a stakeholder might have. This is why we will show how to integrate an AI powered chat into the dashboard, where anyone viewing it can ask questions in plain English and get back tables and charts. The dashboard doesn't ask a model to guess at answers. It asks the model to write SQL, runs that SQL in Snowflake, and shows both the result and the query that produced it.\u003C/p\u003E\n","\u003Cp\u003EThe benefit of this cannot be overstated. Letting the AI agent produce a SQL query as an artifact is exactly what we want. This query can be saved, rerun, and modified at will without having to reprompt the agent. It also keeps the heavy lifting where your data already lives.\u003C/p\u003E\n","\u003Cp\u003EWe'll develop in Posit Workbench inside the Posit Team Native App, use Snowflake Cortex AI as the model backend, and publish to Posit Connect. Because the deployed content carries a Snowflake OAuth integration, each viewer's questions execute under their own Snowflake identity, so the dashboard respects the grants you've already configured. Data, compute, and inference all stay inside your Snowflake account.\u003C/p\u003E\n","\u003Cp\u003EAt a high level, the pieces fit together like this. Everything sits inside the Snowflake account boundary: you develop in Workbench, the deployed dashboard runs on Connect, and both reach the same databases and the same Cortex models.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/overview_architecture.png\" alt=\"An architecture diagram bounded by a Snowflake account. Inside it, the Posit Team Snowflake Native App contains Posit Workbench with its IDEs including Positron Pro, Posit Connect hosting Shiny and other content types, and Posit Package Manager. Arrows connect Workbench and Connect down to Snowflake Databases and Snowflake Cortex, and out from Connect to third party applications, email reports, web apps, and decision makers\"\u003E\u003C/p\u003E\n","\u003Ch3\u003EWhat You Will Learn\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EHow to connect to Snowflake from Python so the same code works in Workbench and on Connect\u003C/li\u003E\u003Cli\u003EHow to use Snowflake Cortex as an LLM backend with the \u003Ccode\u003Echatlas\u003C/code\u003E package\u003C/li\u003E\u003Cli\u003EHow to build a natural language dashboard with \u003Ccode\u003Equerychat\u003C/code\u003E, including LLM-generated charts with \u003Ccode\u003Eggsql\u003C/code\u003E\u003C/li\u003E\u003Cli\u003EHow to keep generated queries running in Snowflake instead of collecting data into memory\u003C/li\u003E\u003Cli\u003EHow to publish to Posit Connect and serve every viewer under their own Snowflake credentials\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EPrerequisites\u003C/h3\u003E\n","\u003Cp\u003EAccess and entitlements that have to be in place before you start, most of which an administrator grants:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EA \u003Ca href=\"https://signup.snowflake.com/\"\u003ESnowflake account\u003C/a\u003E with Cortex AI enabled\u003C/li\u003E\u003Cli\u003EThe \u003Ca href=\"https://app.snowflake.com/marketplace/listing/GZTSZMCB9S/posit-pbc-posit-team\"\u003EPosit Team Snowflake Native App\u003C/a\u003E installed and configured by an administrator with the \u003Ccode\u003Eaccountadmin\u003C/code\u003E role, and access granted to you\u003C/li\u003E\u003Cli\u003EA Snowflake OAuth integration configured for Posit Connect, which your administrator sets up alongside the app\u003C/li\u003E\u003Cli\u003EAccess to the \u003Ca href=\"https://app.snowflake.com/marketplace/listing/GZTSZ290BV255/snowflake-public-data-products-snowflake-public-data-free\"\u003E\u003Ccode\u003ESNOWFLAKE_PUBLIC_DATA_FREE\u003C/code\u003E database\u003C/a\u003E\u003C/li\u003E\u003Cli\u003EFamiliarity with Python and SQL\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EWhat You Will Need\u003C/h3\u003E\n","\u003Cp\u003EThe working environment you'll set up yourself in the first two sections of this guide:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003EA Positron Pro session in Posit Workbench, running Python 3.12\u003C/li\u003E\u003Cli\u003EThe Shiny and Posit Publisher extensions, both bootstrapped in Positron\u003C/li\u003E\u003Cli\u003EA Posit Connect publisher API key\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003EWhat You Will Build\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EAn interactive Shiny for Python dashboard, published on Posit Connect, that turns viewers' plain-English questions into Snowflake SQL and renders the answers as tables and charts\u003C/li\u003E\u003C/ul\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003ESet Up Your Environment\u003C/h2\u003E\n","\u003Ch3\u003EVerify the Data\u003C/h3\u003E\n","\u003Cp\u003EWe'll use the Home Mortgage Disclosure Act (HMDA) dataset from Snowflake's free public data. It contains mortgage application records covering loan types, applicant demographics, property characteristics, amounts, and outcomes across U.S. geographies.\u003C/p\u003E\n","\u003Cp\u003EThe HMDA dataset we'll use is located at:\u003C/p\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Cstrong\u003EDatabase:\u003C/strong\u003E \u003Ccode\u003ESNOWFLAKE_PUBLIC_DATA_FREE\u003C/code\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ESchema:\u003C/strong\u003E \u003Ccode\u003EPUBLIC_DATA_FREE\u003C/code\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ETable:\u003C/strong\u003E \u003Ccode\u003EHOME_MORTGAGE_DISCLOSURE_ATTRIBUTES\u003C/code\u003E\u003C/li\u003E\u003C/ul\u003E\n","\u003Cp\u003ETo verify you have access to this data, navigate to Snowsight and click \u003Cstrong\u003E+\u003C/strong\u003E &gt; \u003Cstrong\u003ESQL File\u003C/strong\u003E and run:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-sql\"\u003ESELECT *\nFROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES\nLIMIT 10;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EYou should see ten rows of mortgage application data.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003EIf you find that you don't have access to this dataset, please contact your account administrator.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Ch3\u003ELaunch Posit Workbench\u003C/h3\u003E\n","\u003Ch4\u003EStep 1: Open the Posit Team Native App\u003C/h4\u003E\n","\u003Cp\u003EIn Snowsight, navigate to \u003Cstrong\u003EHorizon Catalog\u003C/strong\u003E &gt; \u003Cstrong\u003ECatalog\u003C/strong\u003E &gt; \u003Cstrong\u003EApps\u003C/strong\u003E &gt; \u003Cstrong\u003EPosit Team\u003C/strong\u003E, then click \u003Cstrong\u003ELaunch app\u003C/strong\u003E.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/snowflake_launch_app.png\" alt=\"The Posit Team app page in Snowsight, showing version 1.8 installed and Active, with the blue Launch app button highlighted in the upper right\"\u003E\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003EIf you don't see the Posit Team Native App listed, ask your Snowflake account administrator to install it from the Marketplace, \u003Ca href=\"https://docs.posit.co/partnerships/snowflake/posit-team/\"\u003Econfigure\u003C/a\u003E it, and grant you access.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Ch4\u003EStep 2: Open Workbench\u003C/h4\u003E\n","\u003Cp\u003EFrom within the Posit Team Native App, click \u003Cstrong\u003EPosit Workbench\u003C/strong\u003E. You might be prompted to sign in to Snowflake.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/snowflake_launch_workbench.png\" alt=\"The Posit Team landing page with three service tiles, Posit Workbench, Posit Connect, and Posit Package Manager, each showing &quot;Service running&quot;. The Workbench tile is highlighted\"\u003E\u003C/p\u003E\n","\u003Ch3\u003EStart a Positron Pro Session\u003C/h3\u003E\n","\u003Cp\u003EWorkbench offers several IDEs for data science work. For this guide, we'll use Positron Pro, the data science IDE for Python and R.\u003C/p\u003E\n","\u003Ch4\u003EStep 1: Create the session\u003C/h4\u003E\n","\u003Cp\u003EClick \u003Cstrong\u003E+ New Session\u003C/strong\u003E and select the \u003Cstrong\u003EPositron Pro\u003C/strong\u003E IDE.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/workbench_start_new_session.png\" alt=\"The Posit Workbench home page on the Projects tab with an empty session list, and the + New Session button highlighted\"\u003E\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/workbench_create_new_session.png\" alt=\"The New Session dialog's IDE row offering Jupyter Notebook, JupyterLab, Positron Pro, RStudio Pro, and VS Code, with Positron Pro highlighted and the Session Name field below reading &quot;Positron Pro Session&quot;\"\u003E\u003C/p\u003E\n","\u003Ch4\u003EStep 2: Sign in to Snowflake\u003C/h4\u003E\n","\u003Cp\u003EUnder \u003Cstrong\u003ESession Credentials\u003C/strong\u003E, click the button with the Snowflake icon, complete the sign-in prompts, then click \u003Cstrong\u003ELaunch\u003C/strong\u003E.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/workbench_snowflake_login_success.png\" alt=\"The New Session dialog with Positron Pro selected from the IDE row, and a Session Credentials panel below showing a checked Snowflake integration for the account POSIT_SOFTWARE_PBC_DEV:DEVELOPER\"\u003E\u003C/p\u003E\n","\u003Cp\u003EYour session now holds \u003Ca href=\"https://docs.posit.co/partnerships/snowflake/posit-team/managed-credentials.html\"\u003Emanaged credentials\u003C/a\u003E derived from the Snowflake identity you signed in with, and that one identity covers both querying the mortgage table and calling Cortex. You only have to manage access at the Snowflake level, everything is inherited down into Workbench.\u003C/p\u003E\n","\u003Ch4\u003EStep 3: Check the extensions\u003C/h4\u003E\n","\u003Cp\u003EConfirm the \u003Ca href=\"https://open-vsx.org/extension/posit/shiny\"\u003EShiny\u003C/a\u003E and \u003Ca href=\"https://docs.posit.co/connect/user/publishing-positron-vscode/\"\u003EPosit Publisher\u003C/a\u003E extensions are installed and enabled from the Extensions view. Both ship with Positron as bootstrapped extensions.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/positron_extensions.png\" alt=\"The Extensions Marketplace pane filtered to &quot;shiny&quot;, showing the Shiny extension from Posit Software, PBC: &quot;Run and develop Shiny apps in Python or R&quot;\"\u003E\u003C/p\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EConnect Data and Inference\u003C/h2\u003E\n","\u003Ch3\u003EGet the Guide Materials\u003C/h3\u003E\n","\u003Cp\u003EClone the example repository into your Workbench session and open \u003Ccode\u003Equarto.qmd\u003C/code\u003E. It walks through connecting to Snowflake, configuring Cortex, and building the dashboard, with each step runnable via the \u003Cstrong\u003ERun Cell\u003C/strong\u003E button. The repository also contains the finished \u003Ccode\u003Eapp.py\u003C/code\u003E that this guide builds up to and deploys.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-bash\"\u003Egit clone https://github.com/posit-dev/snowflake-posit-llm-dashboard-connect-python/\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EUse \u003Cstrong\u003EPython 3.12\u003C/strong\u003E for this guide. The Snowflake packages don't yet publish wheels for the newest Python releases, so a newer interpreter forces pip to compile from source. The repository pins the version in a \u003Ccode\u003E.python-version\u003C/code\u003E file, which Positron picks up when selecting an interpreter for the session; confirm the interpreter shown in the top right of Positron reads 3.12 before installing anything.\u003C/p\u003E\n","\u003Cp\u003EOnce you are in the repo, you can install the required dependencies.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-bash\"\u003Epython -m pip install --upgrade pip setuptools wheel\npython -m pip install -r requirements.txt\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003Equerychat must be \u003Cstrong\u003E0.7.0 or newer\u003C/strong\u003E. Earlier versions require a data source at construction time, and the deployed app builds one per viewer instead.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003EIf the install can't find a recent enough \u003Ccode\u003Echatlas\u003C/code\u003E or \u003Ccode\u003Equerychat\u003C/code\u003E, your Package Manager mirror may be behind. Adding \u003Ccode\u003E--extra-index-url https://pypi.org/simple\u003C/code\u003E as the first line of \u003Ccode\u003Erequirements.txt\u003C/code\u003E lets pip fall back to PyPI, and Connect honors that line at deploy time too. Ask whoever administers Package Manager first, since a curated mirror is usually deliberate.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Ch3\u003EConnect to Snowflake\u003C/h3\u003E\n","\u003Cp\u003ERemember that the goal of this post is to let the viewers of the dashboard chat with it using their own credentials. We have already gotten those credentials in Workbench for us to use while developing the Shiny app, but the deployed dashboard needs a second path using each viewer's own credentials. We'll come back to it when we assemble the app.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Eimport ibis\nimport snowflake.connector\n\nWAREHOUSE = &quot;DEFAULT_WH&quot;\nDATABASE = &quot;SNOWFLAKE_PUBLIC_DATA_FREE&quot;\nSCHEMA = &quot;PUBLIC_DATA_FREE&quot;\nTABLE = &quot;HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES&quot;\n\ncon = snowflake.connector.connect(\n    connection_name=&quot;workbench&quot;,\n    warehouse=WAREHOUSE,\n    database=DATABASE,\n    schema=SCHEMA,\n)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003ENow wrap the connection with \u003Ca href=\"https://ibis-project.org/\"\u003EIbis\u003C/a\u003E, which gives us a Pythonic way to work with Snowflake tables:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Eibiscon = ibis.snowflake.from_connection(con, create_object_udfs=False)\nmortgage_data = ibiscon.table(TABLE)\n\nmortgage_data\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EIt is important that we don't call \u003Ccode\u003E.to_pandas()\u003C/code\u003E at this stage. \u003Ccode\u003Emortgage_data\u003C/code\u003E is a lazy reference to a Snowflake table. We don't want the app to pull any more data than necessary.\u003C/p\u003E\n","\u003Ch3\u003EReach Cortex from Python\u003C/h3\u003E\n","\u003Cp\u003E\u003Ca href=\"https://posit-dev.github.io/chatlas/\"\u003Echatlas\u003C/a\u003E is a Python package for talking to LLMs, and its \u003Ccode\u003EChatSnowflake\u003C/code\u003E provider targets Cortex. Your Workbench session already holds your Snowflake credentials, so pointing it at Cortex takes one call:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Efrom chatlas import ChatSnowflake\n\nchat = ChatSnowflake(\n    system_prompt=&quot;You are a mortgage lending and housing finance data analysis expert&quot;,\n    model=&quot;claude-haiku-4-5&quot;,  # Choose from the available Cortex AI models\n    connection_name=&quot;workbench&quot;,\n)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EName the model explicitly. \u003Ccode\u003EChatSnowflake\u003C/code\u003E will pick a default and warn you about it, but a dashboard other people depend on shouldn't change models when that default moves.\u003C/p\u003E\n","\u003Cp\u003EIt is generally a good idea to test it before wiring it into an app, as it can be harder to debug later on. This way we can surface credential problems in the console rather than getting a blank app.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Echat.chat(&quot;What patterns do you see in home mortgage lending data?&quot;)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/cortex_chat_response.png\" alt=\"The Positron console showing a chat.chat() call asking what patterns appear in home mortgage lending data, and Cortex's formatted reply with headed sections on interest rate cycles and seasonal trends\"\u003E\u003C/p\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EBuild the Dashboard\u003C/h2\u003E\n","\u003Ch3\u003EUsing querychat\u003C/h3\u003E\n","\u003Cp\u003EThe \u003Ca href=\"https://posit-dev.github.io/querychat/\"\u003Equerychat\u003C/a\u003E package lets us turn questions into SQL. It needs two things: a data source and a model.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Efrom querychat import QueryChat\n\nqc = QueryChat(mortgage_data, TABLE, client=chat)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EThat is the whole configuration: the lazy Ibis table from the previous section, the name the model should use for it in SQL, and the Cortex client. Note that we hand over \u003Ccode\u003Emortgage_data\u003C/code\u003E itself, not a sample. querychat sends the model the table's \u003Cem\u003Eschema\u003C/em\u003E, not its rows, and the SQL the model writes runs against the table by name on your connection, so the model can work over the full table without anything being pulled into Python to get there.\u003C/p\u003E\n","\u003Cp\u003EWe are building the configuration here rather than launching an app, because the app that ships has to create its connection per viewer, and we will assemble it in one piece shortly. Two things are worth adding to \u003Ccode\u003EQueryChat\u003C/code\u003E before then.\u003C/p\u003E\n","\u003Cp\u003Equerychat also gives you SQL you can read, which is the reason to use it over a black-box text-to-answer service. Every response is backed by a query you can inspect, copy, and rerun.\u003C/p\u003E\n","\u003Ch3\u003Equerychat and charts\u003C/h3\u003E\n","\u003Cp\u003Equerychat is not limited to producing SQL queries, it can also produce data visualizations. Charts are not on by default, so we opt in by adding \u003Ccode\u003E&quot;visualize&quot;\u003C/code\u003E to \u003Ccode\u003Etools\u003C/code\u003E.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Eqc = QueryChat(\n    mortgage_data,\n    TABLE,\n    client=chat,\n    tools=(&quot;filter&quot;, &quot;query&quot;, &quot;visualize&quot;),\n)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EBehind this is \u003Ca href=\"https://posit-dev.github.io/ggsql/\"\u003Eggsql\u003C/a\u003E, an extension of SQL that brings the elegance of the Grammar of Graphics to SQL. The model writes one query that both aggregates and describes the chart, querychat runs the SQL part against Snowflake, and the result is rendered as a chart in the chat.\u003C/p\u003E\n","\u003Cp\u003EThis stays efficient because all the aggregation happens in the database, and only the handful of summarized rows the chart needs come back to us.\u003C/p\u003E\n","\u003Ch3\u003ETeach the Model Your Data\u003C/h3\u003E\n","\u003Cp\u003EWhat we have right now works. But we can improve it a bit using our domain knowledge about the data and how we expect users to interact with the app.\u003C/p\u003E\n","\u003Cp\u003EFirstly, a \u003Ccode\u003Edata_description\u003C/code\u003E gives the model your domain vocabulary and your caveats:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Edata_description = &quot;&quot;&quot;\nHome Mortgage Disclosure Act (HMDA) dataset containing mortgage application and\norigination data. Includes loan types, applicant demographics, property\ncharacteristics, loan amounts, interest rates, and loan outcomes across U.S.\ngeographic areas.\n\nOne row is an application, not an approved loan. Loan amounts are in whole\ndollars. Approval rate is not a stored column and must be computed as originated\napplications divided by total applications.\n&quot;&quot;&quot;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003ESecondly, a \u003Ccode\u003Egreeting\u003C/code\u003E tells a first-time viewer what kinds of questions work, so they aren't staring at an empty text box:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Egreeting = &quot;&quot;&quot;\n# Home Mortgage Disclosure Act (HMDA) Data Explorer\n\nAsk questions about mortgage lending patterns, loan characteristics, and geographic trends.\n\n**Example questions:**\n- What are the most common loan types?\n- How do loan approval rates vary by state?\n- Chart loan amounts across different property types\n&quot;&quot;&quot;\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EAdd these two as additional arguments to \u003Ccode\u003EQueryChat()\u003C/code\u003E, which gives us the configuration the deployed app will use:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Eqc = QueryChat(\n    mortgage_data,\n    TABLE,\n    client=chat,\n    greeting=greeting,\n    data_description=data_description,\n    tools=(&quot;filter&quot;, &quot;query&quot;, &quot;visualize&quot;),\n)\n\u003C/code\u003E\u003C/pre\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003EIf you'd rather the model never see raw rows at all, set \u003Ccode\u003Etools=&quot;filter&quot;\u003C/code\u003E. It can then only drive the dashboard's filters, and your own code renders results from the database. The model writes the query; it never reads the data.\u003C/p\u003E\n\u003C/blockquote\u003E\n","\u003Ch3\u003EUse Certified Metrics\u003C/h3\u003E\n","\u003Cp\u003EIf your account defines \u003Ca href=\"https://docs.snowflake.com/en/user-guide/views-semantic/overview\"\u003ESnowflake Semantic Views\u003C/a\u003E, querychat discovers them automatically on a Snowflake connection and includes their definitions in what it sends the model. The model can then answer using \u003Ccode\u003ESEMANTIC_VIEW()\u003C/code\u003E and your organization's certified metrics instead of inventing its own arithmetic. Nothing extra is required in the app code.\u003C/p\u003E\n","\u003Ch3\u003EAssemble the App to Deploy\u003C/h3\u003E\n","\u003Cp\u003ESo far we have used our own connection to work with the data. As mentioned above, we need to do something different, so that the deployed app has each viewer use their own credentials and session tokens to query the data.\u003C/p\u003E\n","\u003Cp\u003EOur deployable artifact is a self-contained \u003Ccode\u003Eapp.py\u003C/code\u003E, and what changes is \u003Cem\u003Ewhere\u003C/em\u003E the connection is created. Everything credential-related moves \u003Cstrong\u003Einside the server function\u003C/strong\u003E, which Shiny calls once per session with that session's request headers in hand.\u003C/p\u003E\n","\u003Cp\u003EFirst, the imports, a few constants, and the credentials. One function, two environments:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Eimport os\n\nimport ibis\nimport snowflake.connector\nfrom chatlas import ChatSnowflake\nfrom posit.connect.external.snowflake import PositAuthenticator\n\nWAREHOUSE = &quot;DEFAULT_WH&quot;\nDATABASE = &quot;SNOWFLAKE_PUBLIC_DATA_FREE&quot;\nSCHEMA = &quot;PUBLIC_DATA_FREE&quot;\nTABLE = &quot;HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES&quot;\n\nMODEL = &quot;claude-haiku-4-5&quot;\nMAX_ROWS = 1000\nSYSTEM_PROMPT = &quot;You are a mortgage lending and housing finance data analysis expert&quot;\n\ndef credentials(session):\n    &quot;&quot;&quot;Snowflake connection parameters for the current viewer.&quot;&quot;&quot;\n    if os.getenv(&quot;RSTUDIO_PRODUCT&quot;) == &quot;CONNECT&quot;:\n        token = session.http_conn.headers.get(&quot;Posit-Connect-User-Session-Token&quot;)\n        if token is None:\n            raise RuntimeError(\n                &quot;No Posit-Connect-User-Session-Token header. Attach the &quot;\n                &quot;Snowflake integration to this content in Connect.&quot;\n            )\n        # Connect sets SNOWFLAKE_ACCOUNT from the attached integration.\n        account = os.getenv(&quot;SNOWFLAKE_ACCOUNT&quot;)\n\n        auth = PositAuthenticator(\n            local_authenticator=&quot;EXTERNALBROWSER&quot;,\n            user_session_token=token,\n        )\n        return {\n            &quot;account&quot;: account,\n            &quot;authenticator&quot;: auth.authenticator,\n            &quot;token&quot;: auth.token,\n        }\n\n    if os.getenv(&quot;SNOWFLAKE_HOME&quot;) is not None:\n        return {&quot;connection_name&quot;: &quot;workbench&quot;}\n\n    raise RuntimeError(&quot;No Snowflake credentials found.&quot;)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EThe \u003Ccode\u003ERuntimeError\u003C/code\u003E names the one Connect-side setup step this guide finishes with, attaching the integration. Raising it by name beats the \u003Ccode\u003EAttributeError\u003C/code\u003E you'd otherwise get. \u003Ccode\u003ESNOWFLAKE_ACCOUNT\u003C/code\u003E needs no such check: Connect sets it on the content from the attached integration, so there is nothing for you to configure.\u003C/p\u003E\n","\u003Cp\u003EThe \u003Ccode\u003Esession\u003C/code\u003E argument is not decoration: the viewer's token arrives as an HTTP header on their session, so this function cannot be called at import time, and that constraint shapes the rest of the file.\u003C/p\u003E\n","\u003Cp\u003ESecond, two helpers that turn those credentials into the table and the chat client. Both take the same \u003Ccode\u003Ecreds\u003C/code\u003E, so one identity covers the query, the model call, and the rows that come back:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Edef get_table(creds):\n    &quot;&quot;&quot;A lazy Ibis reference to the HMDA table, for one viewer.&quot;&quot;&quot;\n    con = snowflake.connector.connect(\n        warehouse=WAREHOUSE, database=DATABASE, schema=SCHEMA, **creds\n    )\n    ibiscon = ibis.snowflake.from_connection(con, create_object_udfs=False)\n    return con, ibiscon.table(TABLE)\n\ndef get_chat(creds):\n    &quot;&quot;&quot;A Cortex chat client for one viewer, using the same credentials.&quot;&quot;&quot;\n    creds = dict(creds)\n    return ChatSnowflake(\n        system_prompt=SYSTEM_PROMPT,\n        model=MODEL,\n        connection_name=creds.pop(&quot;connection_name&quot;, None),\n        account=creds.pop(&quot;account&quot;, None),\n        # Anything else the Snowpark session builder needs (an OAuth\n        # authenticator and token, on Connect) goes through kwargs.\n        kwargs={&quot;warehouse&quot;: WAREHOUSE, **creds},\n    )\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003E\u003Ccode\u003EChatSnowflake\u003C/code\u003E accepts \u003Ccode\u003Econnection_name\u003C/code\u003E, \u003Ccode\u003Eaccount\u003C/code\u003E, \u003Ccode\u003Euser\u003C/code\u003E, \u003Ccode\u003Epassword\u003C/code\u003E, and \u003Ccode\u003Eprivate_key_file\u003C/code\u003E directly; anything else the underlying Snowpark session needs goes through \u003Ccode\u003Ekwargs\u003C/code\u003E. That's how the OAuth authenticator and token reach it on Connect, so inference runs as the viewer too, not just data access.\u003C/p\u003E\n","\u003Cp\u003EThird, the app. The UI is built once and shared, since rendering the chat sidebar needs only the module \u003Ccode\u003Eid\u003C/code\u003E and the tool list. The data source and chat client are built per session:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-python\"\u003Efrom querychat import QueryChat\nfrom shiny import App, Inputs, Outputs, Session, render, ui\n\nQUERYCHAT_ID = &quot;querychat&quot;\nTOOLS = (&quot;filter&quot;, &quot;query&quot;, &quot;visualize&quot;)\n\n# No data source, so this instance is safe to share across sessions.\nqc_ui = QueryChat(id=QUERYCHAT_ID, tools=TOOLS)\n\napp_ui = ui.page_sidebar(\n    qc_ui.sidebar(),\n    ui.card(\n        ui.card_header(ui.output_text(&quot;query_title&quot;, inline=True)),\n        ui.output_code(&quot;sql_query&quot;),\n        fill=False,\n        style=&quot;max-height: 33%;&quot;,\n    ),\n    ui.card(\n        ui.card_header(&quot;Data&quot;),\n        ui.output_data_frame(&quot;data_table&quot;),\n    ),\n    title=&quot;HMDA Mortgage Data Explorer&quot;,\n    fillable=True,\n)\n\ndef server(input: Inputs, output: Outputs, session: Session):\n    creds = credentials(session)\n    con, mortgage_data = get_table(creds)\n    session.on_ended(con.close)\n\n    qc = QueryChat(\n        mortgage_data,\n        TABLE,\n        id=QUERYCHAT_ID,\n        client=get_chat(creds),\n        greeting=greeting,\n        data_description=data_description,\n        tools=TOOLS,\n    )\n    vals = qc.server()\n\n    @render.text\n    def query_title():\n        return vals.title() or &quot;SQL Query&quot;\n\n    @render.code\n    def sql_query():\n        return vals.sql() or &quot;-- Ask a question to generate SQL&quot;\n\n    @render.data_frame\n    def data_table():\n        df = vals.df().head(MAX_ROWS)\n        return df.to_pandas() if hasattr(df, &quot;to_pandas&quot;) else df\n\napp = App(app_ui, server)\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EThat \u003Ccode\u003E.head(MAX_ROWS)\u003C/code\u003E is not cosmetic, and it is the reason we render the table ourselves rather than letting something else do it. \u003Ccode\u003Evals.df()\u003C/code\u003E is a \u003Cem\u003Elazy\u003C/em\u003E Ibis table: before anyone asks a question it is the entire table, and after a filter it is that filter's result, still unevaluated. The filter tool is also required to return every column of the original schema, so a filtered result is narrower in rows but never in columns. Hand either one straight to \u003Ccode\u003Erender.data_frame\u003C/code\u003E and Shiny materializes all of it, so the app renders and then spins while it drags HMDA out of Snowflake a row at a time. Limit before materializing.\u003C/p\u003E\n","\u003Cp\u003EBounding the render is what lets us hand \u003Ccode\u003EQueryChat\u003C/code\u003E the full \u003Ccode\u003Emortgage_data\u003C/code\u003E. The model writes queries against the whole table, aggregates are computed over all of it in the database, and only the rows actually on screen come back.\u003C/p\u003E\n","\u003Cp\u003E\u003Ccode\u003Eqc.server()\u003C/code\u003E returns that session's reactive values, so \u003Ccode\u003Evals.df()\u003C/code\u003E, \u003Ccode\u003Evals.sql()\u003C/code\u003E, and \u003Ccode\u003Evals.title()\u003C/code\u003E are ordinary Shiny reactives. That's the seam for the value boxes and extra tabs above: they read \u003Ccode\u003Evals.df()\u003C/code\u003E and never touch the connection.\u003C/p\u003E\n","\u003Cp\u003ECreate \u003Ccode\u003Eapp.py\u003C/code\u003E next to \u003Ccode\u003Equarto.qmd\u003C/code\u003E and paste these pieces into it, or copy the complete file from the root of the accompanying repository. Run it locally before deploying:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-bash\"\u003Eshiny run app.py\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch3\u003ETry It Out\u003C/h3\u003E\n","\u003Cp\u003EThis is the first time we have had something to ask questions of, so it is worth spending a few minutes here before deploying. Which part of the app answers depends on how you phrase the question, and knowing that up front saves confusion.\u003C/p\u003E\n","\u003Cp\u003EStart with an analytical question:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-text\"\u003EHow do loan approval rates vary by state?\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EThe model answers in the chat, showing the query it ran alongside the result. The SQL card and the data table do not change, and that is correct: you asked for a number, not for a different view of the data.\u003C/p\u003E\n","\u003Cp\u003ENow ask for a different view:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-text\"\u003EShow only loans in Vermont from the most recent year, sorted by loan amount\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EThis time the SQL card and the data table both re-render from the new query. Two different tools sit behind these, and the model chooses between them from your phrasing: &quot;what is&quot;, &quot;how many&quot;, &quot;which is highest&quot; get answered in the chat, while &quot;show me&quot;, &quot;filter to&quot;, &quot;sort by&quot; drive the dashboard. If you ask an analytical question and wonder why the SQL card did not change, that is why, and not a bug.\u003C/p\u003E\n","\u003Cp\u003EKeep filters reasonably narrow while you are exploring. A filter returns whole rows rather than a summary, so filtering to one of the largest states pulls a lot of data through \u003Ccode\u003EMAX_ROWS\u003C/code\u003E before you see anything.\u003C/p\u003E\n","\u003Cp\u003EFinally, ask for a chart, which is the \u003Ccode\u003Evisualize\u003C/code\u003E tool we opted into earlier:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-text\"\u003EChart average loan amount by state for the most recent year\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Cp\u003EThe aggregation runs in Snowflake and only the summarized rows come back to be drawn.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/querychat_ggsql_chart.png\" alt=\"An expanded bar chart titled &quot;Average Loan Amount by State (Most Recent Year)&quot;, with one bar per US state and territory, average loan amount in dollars on the vertical axis, and a Show Query control beneath it\"\u003E\u003C/p\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EDeploy to Posit Connect\u003C/h2\u003E\n","\u003Ch3\u003ECreate an API Key\u003C/h3\u003E\n\u003Col\u003E\u003Cli\u003EOpen Posit Connect from the Posit Team Native App.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/snowflake_launch_connect.png\" alt=\"The Posit Team landing page with three service tiles, Posit Workbench, Posit Connect, and Posit Package Manager, each showing &quot;Service running&quot;. The Connect tile is highlighted\"\u003E\u003C/p\u003E\n\u003Col start=\"2\"\u003E\u003Cli\u003EClick your account in the upper right, then \u003Cstrong\u003EManage Your API Keys\u003C/strong\u003E.\u003C/li\u003E\u003Cli\u003EClick \u003Cstrong\u003E+ New API Key\u003C/strong\u003E, name it, and select the \u003Cstrong\u003EPublisher\u003C/strong\u003E role.\u003C/li\u003E\u003Cli\u003EClick \u003Cstrong\u003ECreate Key\u003C/strong\u003E and copy it somewhere safe.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_manage_api_keys.png\" alt=\"The Posit Connect user menu opened from the account name in the top bar, with the &quot;Manage Your API Keys&quot; entry highlighted\"\u003E\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_new_api_key.png\" alt=\"The API Keys tab of the Posit Connect user settings, with a Key Name of llm-dashboard-deploy, Key Role set to Publisher described as &quot;Can deploy, manage, and share content&quot;, and a Create Key button\"\u003E\u003C/p\u003E\n","\u003Ch3\u003EPublish in One Click\u003C/h3\u003E\n","\u003Cp\u003EBecause Workbench and Connect run inside the same Native App, publishing avoids the network and authentication problems this step usually involves.\u003C/p\u003E\n","\u003Ch4\u003EStep 1: Pin the Python version\u003C/h4\u003E\n","\u003Cp\u003EThe \u003Ccode\u003E.python-version\u003C/code\u003E file that selected your session interpreter also travels into the deployment, so confirm it sits alongside \u003Ccode\u003Eapp.py\u003C/code\u003E and reads \u003Ccode\u003E3.12\u003C/code\u003E. Publisher reads it when generating the deployment manifest, which records \u003Ccode\u003E&gt;=3.12\u003C/code\u003E as an explicit version constraint rather than just noting whichever interpreter you happened to author with. Connect then picks a matching Python installation. It ships in the repository; if you are adapting this pattern in a project of your own, create it with:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode class=\"language-bash\"\u003Eecho &quot;3.12&quot; &gt; .python-version\n\u003C/code\u003E\u003C/pre\u003E\n","\u003Ch4\u003EStep 2: Deploy\u003C/h4\u003E\n\u003Col\u003E\u003Cli\u003EIn the Positron tool menu, click the \u003Cstrong\u003EPosit Publisher\u003C/strong\u003E icon.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/posit_publisher.png\" alt=\"The Positron activity bar with the Posit Publisher icon, an upward arrow in a circle, highlighted among the other tool icons\"\u003E\u003C/p\u003E\n\u003Col start=\"2\"\u003E\u003Cli\u003EUnder \u003Cstrong\u003EDeployment\u003C/strong\u003E, click \u003Cstrong\u003ESelect...\u003C/strong\u003E and create a new deployment for \u003Ccode\u003Eapp.py\u003C/code\u003E.\u003C/li\u003E\u003Cli\u003EChoose the Connect deployment, or create one with the URL \u003Ccode\u003Ehttps://connect/\u003C/code\u003E.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/deploy_connect.png\" alt=\"The Create a New Deployment credential picker, listing an existing credential named &quot;connect&quot; pointing at https://connect and highlighted, above a &quot;Create a New Credential&quot; entry\"\u003E\u003C/p\u003E\n\u003Col start=\"4\"\u003E\u003Cli\u003EEnter the API key you just created.\u003C/li\u003E\u003Cli\u003EInclude both \u003Ccode\u003Erequirements.txt\u003C/code\u003E and \u003Ccode\u003E.python-version\u003C/code\u003E in the file list. \u003Ccode\u003E.python-version\u003C/code\u003E is a hidden file, so it's easy to miss, and leaving it out drops the explicit constraint from the manifest.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/publisher_deployment_config.png\" alt=\"The Posit Publisher pane's Project Files list with .python-version, app.py, and requirements.txt all checked, and collapsed sections below for Secrets, Integration Requests, Python Packages, and Credentials\"\u003E\u003C/p\u003E\n\u003Col start=\"6\"\u003E\u003Cli\u003EClick \u003Cstrong\u003EIntegration requests\u003C/strong\u003E &gt; \u003Cstrong\u003E+\u003C/strong\u003E &gt; the available Snowflake integration.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/publisher_snowflake_integration.png\" alt=\"The lower section of the Posit Publisher pane with Integration Requests expanded and highlighted, showing a requested &quot;Snowflake Dev&quot; integration above the Python Packages, Credentials, and Help and Feedback sections\"\u003E\u003C/p\u003E\n\u003Col start=\"7\"\u003E\u003Cli\u003EClick \u003Cstrong\u003EDeploy your project\u003C/strong\u003E.\u003C/li\u003E\u003C/ol\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/deploy_log_success.png\" alt=\"The Publisher panel in Positron showing a completed deployment, with green checkmarks beside every step from Get Package Descriptions through Validate Deployment Record, and a &quot;Successfully deployed at&quot; line at the bottom\"\u003E\u003C/p\u003E\n","\u003Ch3\u003EFinish the Setup in Connect\u003C/h3\u003E\n","\u003Cp\u003EThe app is deployed now, but it won't work yet. One thing has to be set on the content itself, and it doesn't travel in the bundle.\u003C/p\u003E\n","\u003Cp\u003EOpen the content in Connect, go to \u003Cstrong\u003EContent Settings\u003C/strong\u003E &gt; \u003Cstrong\u003EAccess\u003C/strong\u003E, and add the Snowflake integration there.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_access_integration.png\" alt=\"The Access tab of the Connect content settings, showing the Content URL and Sharing sections above an Integrations section listing a Snowflake Staging integration with a Log out button\"\u003E\u003C/p\u003E\n","\u003Cp\u003ECheck that you're attaching the \u003Cstrong\u003Eviewer\u003C/strong\u003E integration rather than a service account one. That choice is what makes the rest of this guide true.\u003C/p\u003E\n","\u003Cp\u003EAttaching the integration also sets \u003Ccode\u003ESNOWFLAKE_ACCOUNT\u003C/code\u003E on the content, which is where the app's connection gets its account identifier. You don't have to add it yourself.\u003C/p\u003E\n","\u003Cp\u003EAdding an integration restarts the content, so this doesn't need a redeploy. Reload the app and it should come up.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003EThe first time you open content backed by a viewer integration, you may be asked to log in to Snowflake through the \u003Cstrong\u003EAccess\u003C/strong\u003E tab. That's the OAuth session being established, and it happens once.\u003C/p\u003E\n\u003C/blockquote\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EAsk Questions as a Viewer\u003C/h2\u003E\n","\u003Ch3\u003EOpen the Dashboard\u003C/h3\u003E\n","\u003Cp\u003EWe did it! The app has been deployed correctly. Return to Connect in the Posit Team Native App, click the \u003Cstrong\u003EContent\u003C/strong\u003E tab, and open your dashboard. Ask a few questions to confirm it behaves the way it did locally.\u003C/p\u003E\n","\u003Cp\u003E\u003Cimg src=\"https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_content_dashboard.png\" alt=\"The deployed HMDA Mortgage Data Explorer dashboard, with the chat on the left showing a generated bar chart of application outcomes and the model's summary, and on the right a card holding the generated SQL query and a table of the returned application rows\"\u003E\u003C/p\u003E\n","\u003Cp\u003ETo share it, open the \u003Cstrong\u003ESettings\u003C/strong\u003E pane on the content page, copy the URL from the \u003Cstrong\u003EContent URL\u003C/strong\u003E section, and send it to your team.\u003C/p\u003E\n","\u003Ch3\u003EUsing the Viewer's Credentials\u003C/h3\u003E\n","\u003Cp\u003EEach viewer queries Snowflake with their own credentials. Two people can use the same dashboard and see different results, because Snowflake applies the permissions granted to each viewer. Cortex receives only the data Snowflake authorizes for that viewer.\u003C/p\u003E\n\u003Cblockquote\u003E\n","\u003Cp\u003EThe alternative is a service account integration, where all content queries Snowflake as one shared identity. That suits a dashboard meant to show everyone the same governed numbers, but it puts access control back on you. Over sensitive data, the viewer integration is the safer default.\u003C/p\u003E\n\u003C/blockquote\u003E\n&lt;!-- ------------------------ --&gt;\n","\u003Ch2\u003EConclusion and Resources\u003C/h2\u003E\n","\u003Cp\u003EWe've built a dashboard that answers questions in natural language and deployed it somewhere a whole team can use it, without moving data, standing up model infrastructure, or writing a permission layer.\u003C/p\u003E\n","\u003Cp\u003EThree properties carry to your next project: the model writes SQL rather than answers, so results can be checked and reused; queries execute in Snowflake, so the dashboard scales with your warehouse instead of your container; and access is delegated to Snowflake, so security is inherited rather than reimplemented. Point the pattern at your own table and the credentials function and data description are the only parts to rewrite.\u003C/p\u003E\n","\u003Ch3\u003EWhat You Learned\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003EHow to write one Snowflake connection that works in both Workbench and Connect\u003C/li\u003E\u003Cli\u003EHow to use Snowflake Cortex as the model backend for a deployed application\u003C/li\u003E\u003Cli\u003EHow to build a natural language dashboard with \u003Ccode\u003Equerychat\u003C/code\u003E, Shiny, and \u003Ccode\u003Eggsql\u003C/code\u003E\u003C/li\u003E\u003Cli\u003EWhy passing a database connection rather than a data frame keeps queries in Snowflake\u003C/li\u003E\u003Cli\u003EHow to publish to Posit Connect and inherit Snowflake's access controls per viewer\u003C/li\u003E\u003C/ul\u003E\n","\u003Ch3\u003ERelated Resources\u003C/h3\u003E\n\u003Cul\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/snowflake-cortex/llm-functions\"\u003ESnowflake Cortex AI LLM Functions\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.snowflake.com/en/user-guide/views-semantic/overview\"\u003ESnowflake Semantic Views\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://posit-dev.github.io/chatlas/\"\u003Echatlas documentation\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://posit-dev.github.io/querychat/\"\u003Equerychat documentation\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://posit-dev.github.io/ggsql/\"\u003Eggsql documentation\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://shiny.posit.co/py/\"\u003EShiny for Python\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.posit.co/partnerships/snowflake/posit-team/\"\u003EPosit Team Native App documentation\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.posit.co/partnerships/snowflake/posit-team/managed-credentials.html\"\u003EManaged credentials in the Posit Team Native App\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Ca href=\"https://docs.posit.co/connect/user/\"\u003EPosit Connect user guide\u003C/a\u003E\u003C/li\u003E\u003Cli\u003E\u003Cstrong\u003ERelated Guides\u003C/strong\u003E: \u003Ca href=\"https://www.snowflake.com/en/developers/guides/build-and-deploy-interactive-dashboard-with-posit-team-and-cortex/\"\u003EBuild and Deploy an Interactive Shiny Dashboard with the Posit Team Native App and Snowflake Cortex AI\u003C/a\u003E, \u003Ca href=\"https://www.snowflake.com/en/developers/guides/analyze-data-with-python-using-posit-team/\"\u003EAnalyze Data with Python Using Posit Team\u003C/a\u003E\u003C/li\u003E\u003C/ul\u003E"],"description":"","title":"Base Quickstart CF",":type":"snowflake-site/components/contentfragment",":items":{},":itemsOrder":[],"elements":{"quickstartArticleBody":{"dataType":"string","title":"Quickstart Article Body","value":"\u003C!-- ------------------------ --\u003E\n## Overview\n\nIn this guide, we'll build a Shiny for Python dashboard over Home Mortgage Disclosure Act (HMDA) data. When designing a dashboard you are constrained by space and it can be hard to answer every possible question a stakeholder might have. This is why we will show how to integrate an AI powered chat into the dashboard, where anyone viewing it can ask questions in plain English and get back tables and charts. The dashboard doesn't ask a model to guess at answers. It asks the model to write SQL, runs that SQL in Snowflake, and shows both the result and the query that produced it.\n\nThe benefit of this cannot be overstated. Letting the AI agent produce a SQL query as an artifact is exactly what we want. This query can be saved, rerun, and modified at will without having to reprompt the agent. It also keeps the heavy lifting where your data already lives.\n\nWe'll develop in Posit Workbench inside the Posit Team Native App, use Snowflake Cortex AI as the model backend, and publish to Posit Connect. Because the deployed content carries a Snowflake OAuth integration, each viewer's questions execute under their own Snowflake identity, so the dashboard respects the grants you've already configured. Data, compute, and inference all stay inside your Snowflake account.\n\nAt a high level, the pieces fit together like this. Everything sits inside the Snowflake account boundary: you develop in Workbench, the deployed dashboard runs on Connect, and both reach the same databases and the same Cortex models.\n\n![An architecture diagram bounded by a Snowflake account. Inside it, the Posit Team Snowflake Native App contains Posit Workbench with its IDEs including Positron Pro, Posit Connect hosting Shiny and other content types, and Posit Package Manager. Arrows connect Workbench and Connect down to Snowflake Databases and Snowflake Cortex, and out from Connect to third party applications, email reports, web apps, and decision makers](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/overview_architecture.png)\n\n### What You Will Learn\n\n- How to connect to Snowflake from Python so the same code works in Workbench and on Connect\n- How to use Snowflake Cortex as an LLM backend with the `chatlas` package\n- How to build a natural language dashboard with `querychat`, including LLM-generated charts with `ggsql`\n- How to keep generated queries running in Snowflake instead of collecting data into memory\n- How to publish to Posit Connect and serve every viewer under their own Snowflake credentials\n\n### Prerequisites\n\nAccess and entitlements that have to be in place before you start, most of which an administrator grants:\n\n- A [Snowflake account](https://signup.snowflake.com/) with Cortex AI enabled\n- The [Posit Team Snowflake Native App](https://app.snowflake.com/marketplace/listing/GZTSZMCB9S/posit-pbc-posit-team) installed and configured by an administrator with the `accountadmin` role, and access granted to you\n- A Snowflake OAuth integration configured for Posit Connect, which your administrator sets up alongside the app\n- Access to the [`SNOWFLAKE_PUBLIC_DATA_FREE` database](https://app.snowflake.com/marketplace/listing/GZTSZ290BV255/snowflake-public-data-products-snowflake-public-data-free)\n- Familiarity with Python and SQL\n\n### What You Will Need\n\nThe working environment you'll set up yourself in the first two sections of this guide:\n\n- A Positron Pro session in Posit Workbench, running Python 3.12\n- The Shiny and Posit Publisher extensions, both bootstrapped in Positron\n- A Posit Connect publisher API key\n\n### What You Will Build\n\n- An interactive Shiny for Python dashboard, published on Posit Connect, that turns viewers' plain-English questions into Snowflake SQL and renders the answers as tables and charts\n\n\u003C!-- ------------------------ --\u003E\n## Set Up Your Environment\n\n### Verify the Data\n\nWe'll use the Home Mortgage Disclosure Act (HMDA) dataset from Snowflake's free public data. It contains mortgage application records covering loan types, applicant demographics, property characteristics, amounts, and outcomes across U.S. geographies.\n\nThe HMDA dataset we'll use is located at:\n\n- **Database:** `SNOWFLAKE_PUBLIC_DATA_FREE`\n- **Schema:** `PUBLIC_DATA_FREE`\n- **Table:** `HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES`\n\nTo verify you have access to this data, navigate to Snowsight and click **+** \u003E **SQL File** and run:\n\n```sql\nSELECT *\nFROM SNOWFLAKE_PUBLIC_DATA_FREE.PUBLIC_DATA_FREE.HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES\nLIMIT 10;\n```\n\nYou should see ten rows of mortgage application data.\n\n\u003E If you find that you don't have access to this dataset, please contact your account administrator.\n\n### Launch Posit Workbench\n\n#### Step 1: Open the Posit Team Native App\n\nIn Snowsight, navigate to **Horizon Catalog** \u003E **Catalog** \u003E **Apps** \u003E **Posit Team**, then click **Launch app**.\n\n![The Posit Team app page in Snowsight, showing version 1.8 installed and Active, with the blue Launch app button highlighted in the upper right](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/snowflake_launch_app.png)\n\n\u003E If you don't see the Posit Team Native App listed, ask your Snowflake account administrator to install it from the Marketplace, [configure](https://docs.posit.co/partnerships/snowflake/posit-team/) it, and grant you access.\n\n#### Step 2: Open Workbench\n\nFrom within the Posit Team Native App, click **Posit Workbench**. You might be prompted to sign in to Snowflake.\n\n![The Posit Team landing page with three service tiles, Posit Workbench, Posit Connect, and Posit Package Manager, each showing \"Service running\". The Workbench tile is highlighted](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/snowflake_launch_workbench.png)\n\n### Start a Positron Pro Session\n\nWorkbench offers several IDEs for data science work. For this guide, we'll use Positron Pro, the data science IDE for Python and R.\n\n#### Step 1: Create the session\n\nClick **+ New Session** and select the **Positron Pro** IDE.\n\n![The Posit Workbench home page on the Projects tab with an empty session list, and the + New Session button highlighted](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/workbench_start_new_session.png)\n\n![The New Session dialog's IDE row offering Jupyter Notebook, JupyterLab, Positron Pro, RStudio Pro, and VS Code, with Positron Pro highlighted and the Session Name field below reading \"Positron Pro Session\"](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/workbench_create_new_session.png)\n\n#### Step 2: Sign in to Snowflake\n\nUnder **Session Credentials**, click the button with the Snowflake icon, complete the sign-in prompts, then click **Launch**.\n\n![The New Session dialog with Positron Pro selected from the IDE row, and a Session Credentials panel below showing a checked Snowflake integration for the account POSIT_SOFTWARE_PBC_DEV:DEVELOPER](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/workbench_snowflake_login_success.png)\n\nYour session now holds [managed credentials](https://docs.posit.co/partnerships/snowflake/posit-team/managed-credentials.html) derived from the Snowflake identity you signed in with, and that one identity covers both querying the mortgage table and calling Cortex. You only have to manage access at the Snowflake level, everything is inherited down into Workbench.\n\n#### Step 3: Check the extensions\n\nConfirm the [Shiny](https://open-vsx.org/extension/posit/shiny) and [Posit Publisher](https://docs.posit.co/connect/user/publishing-positron-vscode/) extensions are installed and enabled from the Extensions view. Both ship with Positron as bootstrapped extensions.\n\n![The Extensions Marketplace pane filtered to \"shiny\", showing the Shiny extension from Posit Software, PBC: \"Run and develop Shiny apps in Python or R\"](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/positron_extensions.png)\n\n\u003C!-- ------------------------ --\u003E\n## Connect Data and Inference\n\n### Get the Guide Materials\n\nClone the example repository into your Workbench session and open `quarto.qmd`. It walks through connecting to Snowflake, configuring Cortex, and building the dashboard, with each step runnable via the **Run Cell** button. The repository also contains the finished `app.py` that this guide builds up to and deploys.\n\n```bash\ngit clone https://github.com/posit-dev/snowflake-posit-llm-dashboard-connect-python/\n```\n\nUse **Python 3.12** for this guide. The Snowflake packages don't yet publish wheels for the newest Python releases, so a newer interpreter forces pip to compile from source. The repository pins the version in a `.python-version` file, which Positron picks up when selecting an interpreter for the session; confirm the interpreter shown in the top right of Positron reads 3.12 before installing anything.\n\nOnce you are in the repo, you can install the required dependencies.\n\n```bash\npython -m pip install --upgrade pip setuptools wheel\npython -m pip install -r requirements.txt\n```\n\nquerychat must be **0.7.0 or newer**. Earlier versions require a data source at construction time, and the deployed app builds one per viewer instead.\n\n\u003E If the install can't find a recent enough `chatlas` or `querychat`, your Package Manager mirror may be behind. Adding `--extra-index-url https://pypi.org/simple` as the first line of `requirements.txt` lets pip fall back to PyPI, and Connect honors that line at deploy time too. Ask whoever administers Package Manager first, since a curated mirror is usually deliberate.\n\n### Connect to Snowflake\n\nRemember that the goal of this post is to let the viewers of the dashboard chat with it using their own credentials. We have already gotten those credentials in Workbench for us to use while developing the Shiny app, but the deployed dashboard needs a second path using each viewer's own credentials. We'll come back to it when we assemble the app.\n\n```python\nimport ibis\nimport snowflake.connector\n\nWAREHOUSE = \"DEFAULT_WH\"\nDATABASE = \"SNOWFLAKE_PUBLIC_DATA_FREE\"\nSCHEMA = \"PUBLIC_DATA_FREE\"\nTABLE = \"HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES\"\n\ncon = snowflake.connector.connect(\n    connection_name=\"workbench\",\n    warehouse=WAREHOUSE,\n    database=DATABASE,\n    schema=SCHEMA,\n)\n```\n\nNow wrap the connection with [Ibis](https://ibis-project.org/), which gives us a Pythonic way to work with Snowflake tables:\n\n```python\nibiscon = ibis.snowflake.from_connection(con, create_object_udfs=False)\nmortgage_data = ibiscon.table(TABLE)\n\nmortgage_data\n```\n\nIt is important that we don't call `.to_pandas()` at this stage. `mortgage_data` is a lazy reference to a Snowflake table. We don't want the app to pull any more data than necessary.\n\n### Reach Cortex from Python\n\n[chatlas](https://posit-dev.github.io/chatlas/) is a Python package for talking to LLMs, and its `ChatSnowflake` provider targets Cortex. Your Workbench session already holds your Snowflake credentials, so pointing it at Cortex takes one call:\n\n```python\nfrom chatlas import ChatSnowflake\n\nchat = ChatSnowflake(\n    system_prompt=\"You are a mortgage lending and housing finance data analysis expert\",\n    model=\"claude-haiku-4-5\",  # Choose from the available Cortex AI models\n    connection_name=\"workbench\",\n)\n```\n\nName the model explicitly. `ChatSnowflake` will pick a default and warn you about it, but a dashboard other people depend on shouldn't change models when that default moves.\n\nIt is generally a good idea to test it before wiring it into an app, as it can be harder to debug later on. This way we can surface credential problems in the console rather than getting a blank app.\n\n```python\nchat.chat(\"What patterns do you see in home mortgage lending data?\")\n```\n\n![The Positron console showing a chat.chat() call asking what patterns appear in home mortgage lending data, and Cortex's formatted reply with headed sections on interest rate cycles and seasonal trends](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/cortex_chat_response.png)\n\n\u003C!-- ------------------------ --\u003E\n## Build the Dashboard\n\n### Using querychat\n\nThe [querychat](https://posit-dev.github.io/querychat/) package lets us turn questions into SQL. It needs two things: a data source and a model.\n\n```python\nfrom querychat import QueryChat\n\nqc = QueryChat(mortgage_data, TABLE, client=chat)\n```\n\nThat is the whole configuration: the lazy Ibis table from the previous section, the name the model should use for it in SQL, and the Cortex client. Note that we hand over `mortgage_data` itself, not a sample. querychat sends the model the table's *schema*, not its rows, and the SQL the model writes runs against the table by name on your connection, so the model can work over the full table without anything being pulled into Python to get there.\n\nWe are building the configuration here rather than launching an app, because the app that ships has to create its connection per viewer, and we will assemble it in one piece shortly. Two things are worth adding to `QueryChat` before then.\n\nquerychat also gives you SQL you can read, which is the reason to use it over a black-box text-to-answer service. Every response is backed by a query you can inspect, copy, and rerun.\n\n### querychat and charts\n\nquerychat is not limited to producing SQL queries, it can also produce data visualizations. Charts are not on by default, so we opt in by adding `\"visualize\"` to `tools`.\n\n```python\nqc = QueryChat(\n    mortgage_data,\n    TABLE,\n    client=chat,\n    tools=(\"filter\", \"query\", \"visualize\"),\n)\n```\n\nBehind this is [ggsql](https://posit-dev.github.io/ggsql/), an extension of SQL that brings the elegance of the Grammar of Graphics to SQL. The model writes one query that both aggregates and describes the chart, querychat runs the SQL part against Snowflake, and the result is rendered as a chart in the chat.\n\nThis stays efficient because all the aggregation happens in the database, and only the handful of summarized rows the chart needs come back to us.\n\n### Teach the Model Your Data\n\nWhat we have right now works. But we can improve it a bit using our domain knowledge about the data and how we expect users to interact with the app.\n\nFirstly, a `data_description` gives the model your domain vocabulary and your caveats:\n\n```python\ndata_description = \"\"\"\nHome Mortgage Disclosure Act (HMDA) dataset containing mortgage application and\norigination data. Includes loan types, applicant demographics, property\ncharacteristics, loan amounts, interest rates, and loan outcomes across U.S.\ngeographic areas.\n\nOne row is an application, not an approved loan. Loan amounts are in whole\ndollars. Approval rate is not a stored column and must be computed as originated\napplications divided by total applications.\n\"\"\"\n```\n\nSecondly, a `greeting` tells a first-time viewer what kinds of questions work, so they aren't staring at an empty text box:\n\n```python\ngreeting = \"\"\"\n# Home Mortgage Disclosure Act (HMDA) Data Explorer\n\nAsk questions about mortgage lending patterns, loan characteristics, and geographic trends.\n\n**Example questions:**\n- What are the most common loan types?\n- How do loan approval rates vary by state?\n- Chart loan amounts across different property types\n\"\"\"\n```\n\nAdd these two as additional arguments to `QueryChat()`, which gives us the configuration the deployed app will use:\n\n```python\nqc = QueryChat(\n    mortgage_data,\n    TABLE,\n    client=chat,\n    greeting=greeting,\n    data_description=data_description,\n    tools=(\"filter\", \"query\", \"visualize\"),\n)\n```\n\n\u003E If you'd rather the model never see raw rows at all, set `tools=\"filter\"`. It can then only drive the dashboard's filters, and your own code renders results from the database. The model writes the query; it never reads the data.\n\n### Use Certified Metrics\n\nIf your account defines [Snowflake Semantic Views](https://docs.snowflake.com/en/user-guide/views-semantic/overview), querychat discovers them automatically on a Snowflake connection and includes their definitions in what it sends the model. The model can then answer using `SEMANTIC_VIEW()` and your organization's certified metrics instead of inventing its own arithmetic. Nothing extra is required in the app code.\n\n### Assemble the App to Deploy\n\nSo far we have used our own connection to work with the data. As mentioned above, we need to do something different, so that the deployed app has each viewer use their own credentials and session tokens to query the data.\n\nOur deployable artifact is a self-contained `app.py`, and what changes is *where* the connection is created. Everything credential-related moves **inside the server function**, which Shiny calls once per session with that session's request headers in hand.\n\nFirst, the imports, a few constants, and the credentials. One function, two environments:\n\n```python\nimport os\n\nimport ibis\nimport snowflake.connector\nfrom chatlas import ChatSnowflake\nfrom posit.connect.external.snowflake import PositAuthenticator\n\nWAREHOUSE = \"DEFAULT_WH\"\nDATABASE = \"SNOWFLAKE_PUBLIC_DATA_FREE\"\nSCHEMA = \"PUBLIC_DATA_FREE\"\nTABLE = \"HOME_MORTGAGE_DISCLOSURE_ATTRIBUTES\"\n\nMODEL = \"claude-haiku-4-5\"\nMAX_ROWS = 1000\nSYSTEM_PROMPT = \"You are a mortgage lending and housing finance data analysis expert\"\n\ndef credentials(session):\n    \"\"\"Snowflake connection parameters for the current viewer.\"\"\"\n    if os.getenv(\"RSTUDIO_PRODUCT\") == \"CONNECT\":\n        token = session.http_conn.headers.get(\"Posit-Connect-User-Session-Token\")\n        if token is None:\n            raise RuntimeError(\n                \"No Posit-Connect-User-Session-Token header. Attach the \"\n                \"Snowflake integration to this content in Connect.\"\n            )\n        # Connect sets SNOWFLAKE_ACCOUNT from the attached integration.\n        account = os.getenv(\"SNOWFLAKE_ACCOUNT\")\n\n        auth = PositAuthenticator(\n            local_authenticator=\"EXTERNALBROWSER\",\n            user_session_token=token,\n        )\n        return {\n            \"account\": account,\n            \"authenticator\": auth.authenticator,\n            \"token\": auth.token,\n        }\n\n    if os.getenv(\"SNOWFLAKE_HOME\") is not None:\n        return {\"connection_name\": \"workbench\"}\n\n    raise RuntimeError(\"No Snowflake credentials found.\")\n```\n\nThe `RuntimeError` names the one Connect-side setup step this guide finishes with, attaching the integration. Raising it by name beats the `AttributeError` you'd otherwise get. `SNOWFLAKE_ACCOUNT` needs no such check: Connect sets it on the content from the attached integration, so there is nothing for you to configure.\n\nThe `session` argument is not decoration: the viewer's token arrives as an HTTP header on their session, so this function cannot be called at import time, and that constraint shapes the rest of the file.\n\nSecond, two helpers that turn those credentials into the table and the chat client. Both take the same `creds`, so one identity covers the query, the model call, and the rows that come back:\n\n```python\ndef get_table(creds):\n    \"\"\"A lazy Ibis reference to the HMDA table, for one viewer.\"\"\"\n    con = snowflake.connector.connect(\n        warehouse=WAREHOUSE, database=DATABASE, schema=SCHEMA, **creds\n    )\n    ibiscon = ibis.snowflake.from_connection(con, create_object_udfs=False)\n    return con, ibiscon.table(TABLE)\n\ndef get_chat(creds):\n    \"\"\"A Cortex chat client for one viewer, using the same credentials.\"\"\"\n    creds = dict(creds)\n    return ChatSnowflake(\n        system_prompt=SYSTEM_PROMPT,\n        model=MODEL,\n        connection_name=creds.pop(\"connection_name\", None),\n        account=creds.pop(\"account\", None),\n        # Anything else the Snowpark session builder needs (an OAuth\n        # authenticator and token, on Connect) goes through kwargs.\n        kwargs={\"warehouse\": WAREHOUSE, **creds},\n    )\n```\n\n`ChatSnowflake` accepts `connection_name`, `account`, `user`, `password`, and `private_key_file` directly; anything else the underlying Snowpark session needs goes through `kwargs`. That's how the OAuth authenticator and token reach it on Connect, so inference runs as the viewer too, not just data access.\n\nThird, the app. The UI is built once and shared, since rendering the chat sidebar needs only the module `id` and the tool list. The data source and chat client are built per session:\n\n```python\nfrom querychat import QueryChat\nfrom shiny import App, Inputs, Outputs, Session, render, ui\n\nQUERYCHAT_ID = \"querychat\"\nTOOLS = (\"filter\", \"query\", \"visualize\")\n\n# No data source, so this instance is safe to share across sessions.\nqc_ui = QueryChat(id=QUERYCHAT_ID, tools=TOOLS)\n\napp_ui = ui.page_sidebar(\n    qc_ui.sidebar(),\n    ui.card(\n        ui.card_header(ui.output_text(\"query_title\", inline=True)),\n        ui.output_code(\"sql_query\"),\n        fill=False,\n        style=\"max-height: 33%;\",\n    ),\n    ui.card(\n        ui.card_header(\"Data\"),\n        ui.output_data_frame(\"data_table\"),\n    ),\n    title=\"HMDA Mortgage Data Explorer\",\n    fillable=True,\n)\n\ndef server(input: Inputs, output: Outputs, session: Session):\n    creds = credentials(session)\n    con, mortgage_data = get_table(creds)\n    session.on_ended(con.close)\n\n    qc = QueryChat(\n        mortgage_data,\n        TABLE,\n        id=QUERYCHAT_ID,\n        client=get_chat(creds),\n        greeting=greeting,\n        data_description=data_description,\n        tools=TOOLS,\n    )\n    vals = qc.server()\n\n    @render.text\n    def query_title():\n        return vals.title() or \"SQL Query\"\n\n    @render.code\n    def sql_query():\n        return vals.sql() or \"-- Ask a question to generate SQL\"\n\n    @render.data_frame\n    def data_table():\n        df = vals.df().head(MAX_ROWS)\n        return df.to_pandas() if hasattr(df, \"to_pandas\") else df\n\napp = App(app_ui, server)\n```\n\nThat `.head(MAX_ROWS)` is not cosmetic, and it is the reason we render the table ourselves rather than letting something else do it. `vals.df()` is a *lazy* Ibis table: before anyone asks a question it is the entire table, and after a filter it is that filter's result, still unevaluated. The filter tool is also required to return every column of the original schema, so a filtered result is narrower in rows but never in columns. Hand either one straight to `render.data_frame` and Shiny materializes all of it, so the app renders and then spins while it drags HMDA out of Snowflake a row at a time. Limit before materializing.\n\nBounding the render is what lets us hand `QueryChat` the full `mortgage_data`. The model writes queries against the whole table, aggregates are computed over all of it in the database, and only the rows actually on screen come back.\n\n`qc.server()` returns that session's reactive values, so `vals.df()`, `vals.sql()`, and `vals.title()` are ordinary Shiny reactives. That's the seam for the value boxes and extra tabs above: they read `vals.df()` and never touch the connection.\n\nCreate `app.py` next to `quarto.qmd` and paste these pieces into it, or copy the complete file from the root of the accompanying repository. Run it locally before deploying:\n\n```bash\nshiny run app.py\n```\n\n### Try It Out\n\nThis is the first time we have had something to ask questions of, so it is worth spending a few minutes here before deploying. Which part of the app answers depends on how you phrase the question, and knowing that up front saves confusion.\n\nStart with an analytical question:\n\n```text\nHow do loan approval rates vary by state?\n```\n\nThe model answers in the chat, showing the query it ran alongside the result. The SQL card and the data table do not change, and that is correct: you asked for a number, not for a different view of the data.\n\nNow ask for a different view:\n\n```text\nShow only loans in Vermont from the most recent year, sorted by loan amount\n```\n\nThis time the SQL card and the data table both re-render from the new query. Two different tools sit behind these, and the model chooses between them from your phrasing: \"what is\", \"how many\", \"which is highest\" get answered in the chat, while \"show me\", \"filter to\", \"sort by\" drive the dashboard. If you ask an analytical question and wonder why the SQL card did not change, that is why, and not a bug.\n\nKeep filters reasonably narrow while you are exploring. A filter returns whole rows rather than a summary, so filtering to one of the largest states pulls a lot of data through `MAX_ROWS` before you see anything.\n\nFinally, ask for a chart, which is the `visualize` tool we opted into earlier:\n\n```text\nChart average loan amount by state for the most recent year\n```\n\nThe aggregation runs in Snowflake and only the summarized rows come back to be drawn.\n\n![An expanded bar chart titled \"Average Loan Amount by State (Most Recent Year)\", with one bar per US state and territory, average loan amount in dollars on the vertical axis, and a Show Query control beneath it](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/querychat_ggsql_chart.png)\n\n\u003C!-- ------------------------ --\u003E\n## Deploy to Posit Connect\n\n### Create an API Key\n\n1. Open Posit Connect from the Posit Team Native App.\n\n![The Posit Team landing page with three service tiles, Posit Workbench, Posit Connect, and Posit Package Manager, each showing \"Service running\". The Connect tile is highlighted](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/snowflake_launch_connect.png)\n\n2. Click your account in the upper right, then **Manage Your API Keys**.\n3. Click **+ New API Key**, name it, and select the **Publisher** role.\n4. Click **Create Key** and copy it somewhere safe.\n\n![The Posit Connect user menu opened from the account name in the top bar, with the \"Manage Your API Keys\" entry highlighted](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_manage_api_keys.png)\n\n![The API Keys tab of the Posit Connect user settings, with a Key Name of llm-dashboard-deploy, Key Role set to Publisher described as \"Can deploy, manage, and share content\", and a Create Key button](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_new_api_key.png)\n\n### Publish in One Click\n\nBecause Workbench and Connect run inside the same Native App, publishing avoids the network and authentication problems this step usually involves.\n\n#### Step 1: Pin the Python version\n\nThe `.python-version` file that selected your session interpreter also travels into the deployment, so confirm it sits alongside `app.py` and reads `3.12`. Publisher reads it when generating the deployment manifest, which records `\u003E=3.12` as an explicit version constraint rather than just noting whichever interpreter you happened to author with. Connect then picks a matching Python installation. It ships in the repository; if you are adapting this pattern in a project of your own, create it with:\n\n```bash\necho \"3.12\" \u003E .python-version\n```\n\n#### Step 2: Deploy\n\n1. In the Positron tool menu, click the **Posit Publisher** icon.\n\n![The Positron activity bar with the Posit Publisher icon, an upward arrow in a circle, highlighted among the other tool icons](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/posit_publisher.png)\n\n2. Under **Deployment**, click **Select...** and create a new deployment for `app.py`.\n3. Choose the Connect deployment, or create one with the URL `https://connect/`.\n\n![The Create a New Deployment credential picker, listing an existing credential named \"connect\" pointing at https://connect and highlighted, above a \"Create a New Credential\" entry](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/deploy_connect.png)\n\n4. Enter the API key you just created.\n5. Include both `requirements.txt` and `.python-version` in the file list. `.python-version` is a hidden file, so it's easy to miss, and leaving it out drops the explicit constraint from the manifest.\n\n![The Posit Publisher pane's Project Files list with .python-version, app.py, and requirements.txt all checked, and collapsed sections below for Secrets, Integration Requests, Python Packages, and Credentials](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/publisher_deployment_config.png)\n\n6. Click **Integration requests** \u003E **+** \u003E the available Snowflake integration.\n\n![The lower section of the Posit Publisher pane with Integration Requests expanded and highlighted, showing a requested \"Snowflake Dev\" integration above the Python Packages, Credentials, and Help and Feedback sections](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/publisher_snowflake_integration.png)\n\n7. Click **Deploy your project**.\n\n![The Publisher panel in Positron showing a completed deployment, with green checkmarks beside every step from Get Package Descriptions through Validate Deployment Record, and a \"Successfully deployed at\" line at the bottom](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/deploy_log_success.png)\n\n### Finish the Setup in Connect\n\nThe app is deployed now, but it won't work yet. One thing has to be set on the content itself, and it doesn't travel in the bundle.\n\nOpen the content in Connect, go to **Content Settings** \u003E **Access**, and add the Snowflake integration there.\n\n![The Access tab of the Connect content settings, showing the Content URL and Sharing sections above an Integrations section listing a Snowflake Staging integration with a Log out button](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_access_integration.png)\n\n\nCheck that you're attaching the **viewer** integration rather than a service account one. That choice is what makes the rest of this guide true.\n\nAttaching the integration also sets `SNOWFLAKE_ACCOUNT` on the content, which is where the app's connection gets its account identifier. You don't have to add it yourself.\n\nAdding an integration restarts the content, so this doesn't need a redeploy. Reload the app and it should come up.\n\n\u003E The first time you open content backed by a viewer integration, you may be asked to log in to Snowflake through the **Access** tab. That's the OAuth session being established, and it happens once.\n\n\u003C!-- ------------------------ --\u003E\n## Ask Questions as a Viewer\n\n### Open the Dashboard\n\nWe did it! The app has been deployed correctly. Return to Connect in the Posit Team Native App, click the **Content** tab, and open your dashboard. Ask a few questions to confirm it behaves the way it did locally.\n\n![The deployed HMDA Mortgage Data Explorer dashboard, with the chat on the left showing a generated bar chart of application outcomes and the model's summary, and on the right a card holding the generated SQL query and a table of the returned application rows](https://www.snowflake.com/content/dam/snowflake-site/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex/connect_content_dashboard.png)\n\nTo share it, open the **Settings** pane on the content page, copy the URL from the **Content URL** section, and send it to your team.\n\n### Using the Viewer's Credentials\n\nEach viewer queries Snowflake with their own credentials. Two people can use the same dashboard and see different results, because Snowflake applies the permissions granted to each viewer. Cortex receives only the data Snowflake authorizes for that viewer.\n\n\u003E The alternative is a service account integration, where all content queries Snowflake as one shared identity. That suits a dashboard meant to show everyone the same governed numbers, but it puts access control back on you. Over sensitive data, the viewer integration is the safer default.\n\n\u003C!-- ------------------------ --\u003E\n## Conclusion and Resources\n\nWe've built a dashboard that answers questions in natural language and deployed it somewhere a whole team can use it, without moving data, standing up model infrastructure, or writing a permission layer.\n\nThree properties carry to your next project: the model writes SQL rather than answers, so results can be checked and reused; queries execute in Snowflake, so the dashboard scales with your warehouse instead of your container; and access is delegated to Snowflake, so security is inherited rather than reimplemented. Point the pattern at your own table and the credentials function and data description are the only parts to rewrite.\n\n### What You Learned\n\n- How to write one Snowflake connection that works in both Workbench and Connect\n- How to use Snowflake Cortex as the model backend for a deployed application\n- How to build a natural language dashboard with `querychat`, Shiny, and `ggsql`\n- Why passing a database connection rather than a data frame keeps queries in Snowflake\n- How to publish to Posit Connect and inherit Snowflake's access controls per viewer\n\n### Related Resources\n\n- [Snowflake Cortex AI LLM Functions](https://docs.snowflake.com/en/user-guide/snowflake-cortex/llm-functions)\n- [Snowflake Semantic Views](https://docs.snowflake.com/en/user-guide/views-semantic/overview)\n- [chatlas documentation](https://posit-dev.github.io/chatlas/)\n- [querychat documentation](https://posit-dev.github.io/querychat/)\n- [ggsql documentation](https://posit-dev.github.io/ggsql/)\n- [Shiny for Python](https://shiny.posit.co/py/)\n- [Posit Team Native App documentation](https://docs.posit.co/partnerships/snowflake/posit-team/)\n- [Managed credentials in the Posit Team Native App](https://docs.posit.co/partnerships/snowflake/posit-team/managed-credentials.html)\n- [Posit Connect user guide](https://docs.posit.co/connect/user/)\n- **Related Guides**: [Build and Deploy an Interactive Shiny Dashboard with the Posit Team Native App and Snowflake Cortex AI](https://www.snowflake.com/en/developers/guides/build-and-deploy-interactive-dashboard-with-posit-team-and-cortex/), [Analyze Data with Python Using Posit Team](https://www.snowflake.com/en/developers/guides/analyze-data-with-python-using-posit-team/)\n","multiValue":false,":type":"text/x-markdown"},"quickstartArticleLogoImage":{"dataType":"string","title":"Quickstart Article Logo Image","multiValue":false,":type":"text/plain"}},"elementsOrder":["quickstartArticleBody","quickstartArticleLogoImage"],"isDeveloperGuidesPage":false,"model":"snowflake-site/models/quickstart-article"},"flexible_column_cont":{"id":"flexible-column-container-f4fe60ad13","type":"2-column-75-25","alignColumns":"top","containerMaxWidth":"extra-large","topPadding":"none","bottomPadding":"none","spaceBetween":"none","reverseOnMobile":false,"carouselOnMobile":false,"backgroundImageOption":"none","flexible_column_content_container_1":{"layout":"SIMPLE","id":"container-f02ee324a7",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"quickstart_last_modi":{"id":"quickstart-last-modified-c7cdeca3f0","icon":{"id":"icon","icon":"calendar",":type":"snowflake-site/components/icon","appliedCssClassNames":"snowflake-icon-blue"},"lastModifiedDatePrefix":"Updated","lastModifiedDate":"2026-08-27",":type":"snowflake-site/components/quickstart/quickstart-last-modified","appliedCssClassNames":"snowflake-responsive-component-top-padding-small"},"text":{"id":"text-0d42dd0a53","additionalClasses":"qs-disclaimer-text","text":"\u003Cp\u003E\u003Cspan style=\"color: #666;\"\u003EThis content is provided as is, and is not maintained on an ongoing basis. It may be out of date with current Snowflake instances\u003C/span\u003E\u003C/p\u003E\r\n","richText":true,":type":"snowflake-site/components/text","appliedCssClassNames":"snowflake-responsive-component-top-padding-small"}},":itemsOrder":["quickstart_last_modi","text"]},"flexible_column_content_container_2":{"layout":"SIMPLE","id":"container-ecb8b52ef7",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{},":itemsOrder":[]},":type":"snowflake-site/components/flexible-column-container","isBlogPage":false,"isActiveTOC":false}},":itemsOrder":["contentfragment","flexible_column_cont"]},"flexible_column_content_container_2":{"layout":"SIMPLE","id":"container-ea9de762da",":type":"snowflake-site/components/flexible-column-container/flexible-column-content-container",":items":{"quickstart_table_of_":{"layout":"SIMPLE","id":"container-712afd280b","isDeveloperGuidesPage":false,":type":"snowflake-site/components/quickstart/quickstart-table-of-content/quickstart-table-of-content-container",":items":{"quickstart_table_of_":{"id":"quickstart-table-of-content-4a240cafef","headings":["\u003Ch2\u003EOverview\u003C/h2\u003E","\u003Ch2\u003ESet Up Your Environment\u003C/h2\u003E","\u003Ch2\u003EConnect Data and Inference\u003C/h2\u003E","\u003Ch2\u003EBuild the Dashboard\u003C/h2\u003E","\u003Ch2\u003EDeploy to Posit Connect\u003C/h2\u003E","\u003Ch2\u003EAsk Questions as a Viewer\u003C/h2\u003E","\u003Ch2\u003EConclusion and Resources\u003C/h2\u003E"],"fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/build-an-llm-powered-dashboard-with-posit-connect-and-cortex",":type":"snowflake-site/components/quickstart/quickstart-table-of-content"},"quickstart_button":{"id":"quickstart-button-edded8b73f","fragmentPath":"/content/dam/snowflake-site/en/content-fragments/quickstarts/build-an-llm-powered-dashboard-with-posit-connect-and-cortex",":type":"snowflake-site/components/quickstart/quickstart-button","appliedCssClassNames":"snowflake-responsive-component-top-padding-none"}},":itemsOrder":["quickstart_table_of_","quickstart_button"]}},":itemsOrder":["quickstart_table_of_"]},":type":"snowflake-site/components/flexible-column-container","isBlogPage":false,"isActiveTOC":false},"markup_editor":{"id":"markup-editor-b9d916399b","title":"Page CSS","cssContent":"#quickstart-template-main-flexible-container{padding:24px}#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{grid-template-columns:1fr 0}.qs-disclaimer-text p \u003E span{font-size:15px !important}@media (min-width:768px){#quickstart-template-main-flexible-container{padding:24px 32px}#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{grid-template-columns:7fr 3fr;gap:48px}}@media (max-width:767px){#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{gap:0}}@media (min-width:1024px){#quickstart-template-main-flexible-container{padding:0 92px 48px 92px}#quickstart-template-main-flexible-container \u003E .snowflake-flexible-column-container-items{gap:117px}}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false}},":itemsOrder":["quickstart_hero","flexible_column_cont","markup_editor"],":type":"wcm/foundation/components/responsivegrid"},"modal_container":{"layout":"SIMPLE","id":"container-13e4e8a0e3",":type":"snowflake-site/components/modal/modal-container",":items":{},":itemsOrder":[]},"experiencefragment-footer":{"id":"experiencefragment-f525120fa6","localizedFragmentVariationPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master/jcr:content","configured":true,":type":"snowflake-site/components/experiencefragment","xfModelPath":"/content/experience-fragments/snowflake-site/language-masters/en/site/footer/master.xfmodel.json"},"markup_editor":{"id":"markup-editor-c6b8c87927","title":"Quickstarts Overrides","cssContent":".snowflake-markdown blockquote{padding:24px 32px;background:#f6f9fa;border:1px solid #29b5e8;border-radius:16px}.snowflake-markdown .snowflake-image-container img{width:auto !important;max-width:100%}.snowflake-markdown .snowflake-text ol{padding-left:20px !important}.snowflake-markdown .snowflake-text li{margin:0 0 12px 0 !important}.snowflake-markdown h3.snowflake-markdown-h3{font-size:20px !important;font-family:Texta,sans-serif !important}@media (min-width:768px){.snowflake-markdown h3.snowflake-markdown-h3{font-size:28px !important}}",":type":"snowflake-site/components/markup-editor","isGSAPEnabled":false}},":itemsOrder":["experiencefragment-banner","experiencefragment-header","markup_editor_1950346551","responsivegrid","modal_container","experiencefragment-footer","markup_editor"],":type":"wcm/foundation/components/responsivegrid"}},":itemsOrder":["root"],":hierarchyType":"page",":path":"/content/snowflake-site/global/en/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex","isPasswordProtected":false,"analyticsContentTags":["snowflake-site:taxonomy/solution-center/certification/quickstart","snowflake-site:taxonomy/snowflake-feature/cortex-llm-functions","snowflake-site:taxonomy/product/ai","snowflake-site:taxonomy/product/data-engineering","snowflake-site:taxonomy/industry/financial-services"],"analyticsEnabled":true,"coveoConfig":{"pipeline":"snowflake.com","searchHub":"snowflake.com","organizationId":"snowflakecomputingproduction8neljofn","apiKey":"xx335921a6-2a0a-40f2-a167-e390b4766c3d"},"analyticsDebugMode":false,"analyticsData":{"excludeFromAnalytics":false,"subCategory":"","pageType":"quickstart-page-template","templateName":"quickstart-page-template","siteName":"snowflake","pageUrl":"/content/snowflake-site/global/en/developers/guides/build-an-llm-powered-dashboard-with-posit-connect-and-cortex","language":"en","category":"general","pageName":"Build an LLM-Powered Dashboard with Posit Connect and Snowflake Cortex AI","contentTags":["snowflake-site:taxonomy/solution-center/certification/quickstart","snowflake-site:taxonomy/snowflake-feature/cortex-llm-functions","snowflake-site:taxonomy/product/ai","snowflake-site:taxonomy/product/data-engineering","snowflake-site:taxonomy/industry/financial-services"]},"locale":"en"}
  