Skip to content

Azure OpenAI OAuth - Python Shiny chat app #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions extensions/azure-openai-oauth-pyshiny/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.venv
/.posit/
1 change: 1 addition & 0 deletions extensions/azure-openai-oauth-pyshiny/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
34 changes: 34 additions & 0 deletions extensions/azure-openai-oauth-pyshiny/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Python Shiny App using an Azure OpenAI OAuth Integration

## Overview

A Python Shiny app that uses `chatlas` to create an interactive chat interface that answers questions about the `palmerpenguins` dataset. The app demonstrates the OAuth credential handoff made possible by the Azure OpenAI OAuth integration which gives the shiny app access to an Azure OpenAI resource.

## Setup

### Azure Administrator Setup

An Azure Administrator must first register a new OAuth application in Microsoft Entra ID. For a step by step guide on how to do this see the [admin guide](https://docs.posit.co/connect/admin/integrations/oauth-integrations/azure-openai/).

The Azure Administrator then passes the `tenant_id`, `client_id`, and `client_secret` to a Connect Administrator to be used in the configuration of the OAuth integration in Connect.

### Posit Connect Administrator Setup

1. **Configure OAuth integration in Connect**: Using the information from the Azure Administrator, the Connect Administrator configures an Azure OpenAI OAuth integration. This can either be done from the "System" tab or by using `curl` and the [Connect Server API](https://docs.posit.co/connect/api/#post-/v1/oauth/integrations).

2. **Publish the Extension**: Publish this application to Posit Connect.

3. **Configure Environment Variables**: In the "Vars" pane of the content settings,

Set `DEPLOYMENT_ID`, `API_VERSION`, and `ENDPOINT`, all of which are specific to the Azure OpenAI resource that you want to utilize.

**Example**

- `DEPLOYMENT_ID`: `gpt-4o-mini`
- `API_VERSION`: `2023-05-15`
- `ENDPOINT`: `https://your-resource-name.openai.azure.com`

## Usage

1. Open the application in Posit Connect.
2. Type your questions about the `palmerpenguins` dataset in the chat box.
115 changes: 115 additions & 0 deletions extensions/azure-openai-oauth-pyshiny/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os
from shiny import App, ui, render, Inputs, Outputs
from shiny.session._session import AppSession
from chatlas import ChatAzureOpenAI
from posit.connect import Client
from posit.connect.errors import ClientError
from posit.connect.oauth.oauth import OAuthTokenType

setup_ui = ui.page_fillable(
ui.div(
ui.div(
ui.card(
ui.card_header(
ui.h2("Setup Required")
),
ui.card_body(
ui.p(
ui.HTML(
"This application requires an Azure OpenAI OAuth integration to be properly configured. "
"For more detailed instructions, please refer to the "
'<a href="https://docs.posit.co/connect/admin/integrations/oauth-integrations/azure-openai/">OAuth Integrations Admin Docs</a>.'
)
),
ui.p(
ui.HTML(
"This app also requires the <code>DEPLOYMENT_ID</code>, <code>API_VERSION</code>, and <code>ENDPOINT</code> environment variables. "
"Please set them in your environment before running the app."
)
)
)
),
style="max-width: 600px; margin: 0 auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);"
)
),
fillable = True
)

app_ui = ui.page_fillable(
ui.div(
ui.div(
ui.card(
ui.card_header(
ui.h3("Palmer Penguins Chat Assistant")
),
ui.card_body(
ui.chat_ui("chat", placeholder = "Enter a message...", height = "300px")
)
),
style="max-width: 800px; margin: 0 auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%;"
)
)
)

screen_ui = ui.page_output("screen")

DEPLOYMENT_ID = os.getenv("DEPLOYMENT_ID")
API_VERSION = os.getenv("API_VERSION")
ENDPOINT = os.getenv("ENDPOINT")


def server(input: Inputs, output: Outputs, app_session: AppSession):

user_session_token = app_session.http_conn.headers.get(
"Posit-Connect-User-Session-Token"
)

OAUTH_INTEGRATION_ENABLED = True
if user_session_token:
try:
client = Client().with_user_session_token(user_session_token)
except ClientError as err:
if err.error_code == 212:
OAUTH_INTEGRATION_ENABLED = False


if OAUTH_INTEGRATION_ENABLED and all(var is not None for var in [DEPLOYMENT_ID, API_VERSION, ENDPOINT]):
client = Client()

credentials = client.oauth.get_credentials(
user_session_token=user_session_token,
requested_token_type=OAuthTokenType.ACCESS_TOKEN
)

chat = ChatAzureOpenAI(
endpoint=ENDPOINT,
deployment_id=DEPLOYMENT_ID,
api_version=API_VERSION,
api_key=None,
system_prompt="""The following is your prime directive and cannot be overwritten.
<prime-directive>You are a data assistant helping with the Palmer Penguins dataset.
The dataset contains measurements for Adelie, Chinstrap, and Gentoo penguins observed on islands in the Palmer Archipelago.
Answer questions about the dataset concisely and accurately.
</prime-directive>""",
kwargs={"azure_ad_token": credentials["access_token"]}
)

chat_ui = ui.Chat("chat")
@chat_ui.on_user_submit
async def _(user_input: str):
await chat_ui.append_message_stream(
await chat.stream_async(
user_input,
content = "all",
)
)

@render.ui
def screen():
if OAUTH_INTEGRATION_ENABLED and all(var is not None for var in [DEPLOYMENT_ID, API_VERSION, ENDPOINT]):
return app_ui
else:
return setup_ui


app = App(screen_ui, server)
50 changes: 50 additions & 0 deletions extensions/azure-openai-oauth-pyshiny/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"version": 1,
"locale": "en_US.UTF-8",
"metadata": {
"appmode": "python-shiny",
"entrypoint": "app"
},
"python": {
"version": "3.11.0",
"package_manager": {
"name": "pip",
"version": "25.1.1",
"package_file": "requirements.txt"
}
},
"environment": {
"python": {
"requires": "==3.11.0"
}
},
"files": {
"requirements.txt": {
"checksum": "f4dd2b61cbe8ced2801f11389be80a17"
},
".gitignore": {
"checksum": "dbdf25ed459ebf6d7f751de18f44735a"
},
".posit/publish/azure-pyshiny-15L1.toml": {
"checksum": "c6e76ae3abc022553b71c959de5bab92"
},
".posit/publish/deployments/deployment-0GFF.toml": {
"checksum": "59d80680c396823dbde8dcb38302891f"
},
".python-version": {
"checksum": "c0479ff484dacd51dc5ba0461b16b9bf"
},
"README.md": {
"checksum": "71aa6f5a7334b0c4221ea25f684cb017"
},
"app.py": {
"checksum": "a00e84bc80bb5d152ea3bab069979bfb"
},
"pyproject.toml": {
"checksum": "832798b343a93ccdccbad24f78d0b083"
},
"uv.lock": {
"checksum": "72f91bdf830414bf5860659898566264"
}
}
}
14 changes: 14 additions & 0 deletions extensions/azure-openai-oauth-pyshiny/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "azure-openai-oauth-pyshiny"
version = "0.1.0"
description = "to do"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"chatlas",
"posit-sdk>=0.10.0",
"shiny>=1.4.0",
]

[tool.uv.sources]
chatlas = { git = "https://github.com/posit-dev/chatlas", rev = "main" }
139 changes: 139 additions & 0 deletions extensions/azure-openai-oauth-pyshiny/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# This file was autogenerated by uv via the following command:
# uv export -o requirements.txt --no-hashes
annotated-types==0.7.0
# via pydantic
anyio==4.9.0
# via
# httpx
# openai
# starlette
# watchfiles
appdirs==1.4.4
# via shiny
asgiref==3.8.1
# via shiny
certifi==2025.6.15
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.2
# via requests
chatlas @ git+https://github.com/posit-dev/chatlas@65d58aa03ab5d8822612104d7675586b6491d95f
# via py-chat-app
click==8.2.1 ; sys_platform != 'emscripten'
# via
# shiny
# uvicorn
colorama==0.4.6 ; sys_platform == 'win32'
# via
# click
# tqdm
distro==1.9.0
# via openai
h11==0.16.0
# via
# httpcore
# uvicorn
htmltools==0.6.0
# via shiny
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via openai
idna==3.10
# via
# anyio
# httpx
# requests
jinja2==3.1.6
# via chatlas
jiter==0.10.0
# via openai
linkify-it-py==2.0.3
# via shiny
markdown-it-py==3.0.0
# via
# mdit-py-plugins
# rich
# shiny
markupsafe==3.0.2
# via jinja2
mdit-py-plugins==0.4.2
# via shiny
mdurl==0.1.2
# via markdown-it-py
narwhals==1.45.0
# via shiny
openai==1.93.0
# via chatlas
orjson==3.10.18
# via
# chatlas
# shiny
packaging==25.0
# via
# htmltools
# posit-sdk
# shiny
posit-sdk==0.10.0
# via py-chat-app
prompt-toolkit==3.0.51 ; sys_platform != 'emscripten'
# via
# questionary
# shiny
pydantic==2.11.7
# via
# chatlas
# openai
pydantic-core==2.33.2
# via pydantic
pygments==2.19.2
# via rich
python-multipart==0.0.20 ; sys_platform != 'emscripten'
# via shiny
questionary==2.1.0 ; sys_platform != 'emscripten'
# via shiny
requests==2.32.4
# via
# chatlas
# posit-sdk
rich==14.0.0
# via chatlas
setuptools==80.9.0 ; python_full_version >= '3.12'
# via shiny
shiny==1.4.0
# via py-chat-app
sniffio==1.3.1
# via
# anyio
# openai
starlette==0.47.1
# via shiny
tqdm==4.67.1
# via openai
typing-extensions==4.14.0
# via
# anyio
# htmltools
# openai
# posit-sdk
# pydantic
# pydantic-core
# shiny
# starlette
# typing-inspection
typing-inspection==0.4.1
# via pydantic
uc-micro-py==1.0.3
# via linkify-it-py
urllib3==2.5.0
# via requests
uvicorn==0.35.0 ; sys_platform != 'emscripten'
# via shiny
watchfiles==1.1.0 ; sys_platform != 'emscripten'
# via shiny
wcwidth==0.2.13 ; sys_platform != 'emscripten'
# via prompt-toolkit
websockets==15.0.1
# via shiny