feat(ai-chat): Add first version of ai chat as well as frontend

Includes the first version of a rudimentary chat app, still without the
SQL capabilities that we want later. For now, we can connect to the
Azure OpenAI source and then have the response displayed in a plotly
dash webapp.

Some styling and UI elements were also added, such as logos. UI
components are designed that the user cannot enter the same query twice
and cannot click the submit button as long as the query is running.
This commit is contained in:
Tobias Quadfasel
2024-08-31 23:38:14 +02:00
parent c61c355ee6
commit 923dc3b439
4 changed files with 210 additions and 3 deletions

View File

@@ -1,8 +1,142 @@
from dash import Dash, html
from typing import Any, Dict, Tuple
app = Dash()
from dash import (
Dash,
Input,
Output,
State,
callback,
dcc,
get_asset_url,
html,
no_update,
)
from dash.exceptions import PreventUpdate
from .app_styles import header_style
from .data_chat import send_message
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
app = Dash(__name__, external_stylesheets=external_stylesheets)
app.index_string = header_style
err_style = {
"height": "0px",
"overflow": "hidden",
"transition": "height 0.5s ease-in-out",
"border-radius": "15px",
"background-color": "#FFCCCB",
"text-align": "center",
"color": "#FF6B6B",
"margin-top": "20px",
"margin-left": "20px",
"margin-right": "20px",
"font-weight": "bold",
"display": "flex",
"justify-content": "center",
"align-items": "center",
}
start_value = "Stelle deine Frage an die Datenbank..."
app.layout = html.Div(
[
html.Div(
[
html.H1("Datenbank-Chat", className="heading"),
html.Img(src=get_asset_url("logo.png"), className="logo"),
],
className="header-container",
), # Header
dcc.Store(
id="tmp-value", data=start_value, storage_type="session"
), # Store previous prompt
dcc.Textarea(
id="input-field",
value=start_value,
style={"width": "96%", "height": 200, "margin-left": "20px"},
), # Input field
html.Div([]), # Needed for keeping the layout clean
html.Button(
"Abschicken",
id="submit-button",
n_clicks=0,
disabled=False,
style={"margin-left": "20px"},
), # Submit button
html.Div(
[html.P("Bitte eine neue Anfrage eingeben.")], id="error", style=err_style
), # Error message (only visible if input is not updated but submit button is clicked)
dcc.Loading(
id="loading",
type="default",
children=[
html.Div(
"Hier erscheint die Antwort der Datenbank.",
id="text-output",
style={
"whiteSpace": "pre-line",
"margin-top": "30px",
"margin-left": "20px",
"margin-right": "20px",
"border": "2px solid #86bc25",
"border-radius": "15px",
"padding": "20px",
},
)
],
),
], # Output field
className="container",
)
@callback(
Output("text-output", "children"),
Output("tmp-value", "data"),
Output("error", "style"),
Input("submit-button", "n_clicks"),
State("input-field", "value"),
State("tmp-value", "data"),
prevent_initial_call=True,
running=[
(Output("submit-button", "disabled"), True, False),
(
Output("submit-button", "style"),
{"opacity": 0.5, "margin-left": "20px"},
{"opacity": 1.0, "margin-left": "20px"},
),
],
)
def update_output(n_clicks: int, value: str, data: str) -> Tuple[str, str, Dict[str, Any]]:
"""Update the output based on user input and button clicks.
Parameters
----------
n_clicks : int
Number of times the submit button has been clicked.
value : str
Current value of the input field.
data : str
Previously stored value.
Returns
-------
Tuple[str, str, Dict[str, Any]]
Updated output text, new stored value, and error style.
"""
global err_style
if n_clicks > 0 and value != data:
result = send_message(value)
err_style["height"] = "0px"
return result, value, err_style
elif value == data:
err_style["height"] = "50px"
return no_update, no_update, err_style
raise PreventUpdate
app.layout = [html.Div(children="Hello World")]
server = app.server

37
app/app_styles.py Normal file
View File

@@ -0,0 +1,37 @@
header_style = """
<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
<style>
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background-color: #f8f9fa;
}
.heading {
font-size: 2.5em;
font-weight: bold;
color: #333;
}
.logo {
height: 30px;
width: auto;
}
</style>
</head>
<body>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>
"""

BIN
app/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

36
app/data_chat.py Normal file
View File

@@ -0,0 +1,36 @@
import os
from openai import AzureOpenAI
# Set up credentials
# NOTE: When running locally, these have to be set in the environment
client = AzureOpenAI(
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
api_key=os.getenv("AZURE_OPENAI_KEY"),
api_version="2024-02-01",
)
deployment_name = "sqlai"
def send_message(message: str) -> str:
"""Send a message to the openai chat completion API and return the response.
Parameters
----------
message : str
The user's message to be sent to the chat completion API.
Returns
-------
str
The content of the assistant's response message.
"""
response = client.chat.completions.create(
model=deployment_name,
messages=[
{"role": "system", "content": "Du bist ein hilfreicher Assistent."},
{"role": "user", "content": message},
],
)
return response.choices[0].message.content