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:
140
app/app.py
140
app/app.py
@@ -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
37
app/app_styles.py
Normal 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
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
36
app/data_chat.py
Normal 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
|
||||
Reference in New Issue
Block a user