Spaces:
Sleeping
Sleeping
""" | |
Chatbot App for Cognitive Debriefing Interview | |
Author: Dr Musashi Hinck | |
Version Log: | |
- 02.04.24: Initial demo with passed values from Qualtrics survey | |
- 07.04.24: Added configurations for survey edition | |
Notes: | |
- Need to call Request from start state | |
- Example URL: localhost:7860/?user=123&session=456&questionid=0&response=0 | |
TODO: | |
- Test interview ending behavior: does it get triggered reliably? | |
- Add password protection | |
Pre-flight: | |
- Check dotenv values match Gradio secrets | |
""" | |
from __future__ import annotations | |
import os | |
import json | |
import logging | |
import gradio as gr | |
from uuid import uuid4 | |
from typing import Generator, Any | |
from pathlib import Path | |
from utils import ( | |
PromptTemplate, | |
convert_gradio_to_openai, | |
initialize_client, | |
load_dotenv, | |
upload_azure, | |
record_chat, | |
ChatLoggerHandler | |
) | |
# %% Initialize common assets | |
base_logger = logging.getLogger(__name__) | |
chat_logger = ChatLoggerHandler() | |
if os.environ.get("AZURE_ENDPOINT") is None: # Set Azure credentials from local files | |
load_dotenv() | |
client = initialize_client() # Shared across sessions | |
question_mapping: dict[str, str] = json.loads(Path("assets/question_mapping.json").read_text()) | |
# %% (functions) | |
# Initialization | |
# - Record user and session id | |
# - Record question and response | |
# - Build system message | |
# - Build initial message | |
# - Wrapper - start_survey | |
def initialize_interview(request: gr.Request) -> tuple: | |
""" | |
Read: Request | |
Set: values of userId, sessionId, questionWording, initialMessage, systemMessage | |
""" | |
# Parse request | |
request_params = request.query_params | |
user_id: str = request_params.get("user", "testUser") | |
session_id: str = request_params.get("session", "testSession") | |
base_logger.info(f"User: {user_id} (Session: {session_id})") | |
# Parse question | |
question_id: str = request_params.get("questionid", "0") | |
response_id: str = request_params.get("response", "0") | |
question_data: dict = json.loads(Path(f"./assets/questions/{question_mapping[question_id]}").read_text()) | |
question_wording: str = question_data["question"] | |
question_choices: str = question_data["choices"] | |
response_text: str = question_choices[int(response_id)] | |
base_logger.info(f"Question: {question_wording} ({response_text})") | |
# Load initial and system messages | |
initial_message: str = PromptTemplate.from_file("assets/initial_message.txt").format(surveyQuestion=question_wording) | |
system_message: str = PromptTemplate.from_file("assets/system_message.txt").format(surveyQuestion=question_wording, responseVal=response_text) | |
base_logger.info(f"Initial message: {initial_message}") | |
base_logger.info(f"System message: {system_message}") | |
# Return all | |
return ( | |
user_id, | |
session_id, | |
question_wording, | |
initial_message, | |
system_message | |
) | |
def initialize_interface(initial_message: str) -> tuple: | |
""" | |
Change interface to interactive mode. | |
Read: initial_message | |
Set: | |
instruction_text: modify (to empty) | |
chat_display: set initial_message | |
chat_input: update placeholder, make interactive | |
chat_submit: make interactive | |
start_button: hide | |
""" | |
instruction_text = gr.Markdown("") | |
chat_display = gr.Chatbot( | |
value=[[None, initial_message]], | |
elem_id="chatDisplay", | |
show_label=False, | |
visible=True, | |
) | |
chat_input = gr.Textbox( | |
placeholder="Type response here. Hit `Enter` or click the arrow to submit.", | |
visible=True, | |
interactive=True, | |
show_label=False, | |
scale=10, | |
) | |
chat_submit = gr.Button( | |
"", | |
variant="primary", | |
interactive=True, | |
icon="./arrow_icon.svg", | |
visible=True, | |
) | |
start_button = gr.Button("Start Interview", visible=False, variant="primary") | |
return (instruction_text, chat_display, chat_input, chat_submit, start_button) | |
# Interaction | |
# - User message | |
# - Bot message | |
# - Check if interview finished | |
# - Record interaction (local log) | |
def user_message( | |
message: str, chat_history: list[list[str | None]] | |
) -> tuple[str, list[list[str | None]]]: | |
"Display user message immediately" | |
return "", chat_history + [[message, None]] | |
def bot_message( | |
chat_history: list[list[str | None]], | |
system_message: str, | |
model_args: dict = {"model": "gpt-4o-default", "temperature": 0.0}, | |
) -> Generator[Any, Any, Any]: | |
"Streams response from OpenAI API to chat interface." | |
# Prep messages | |
user_msg = chat_history[-1][0] | |
messages = convert_gradio_to_openai(chat_history[:-1]) | |
messages = ( | |
[{"role": "system", "content": system_message}] | |
+ messages | |
+ [{"role": "user", "content": user_msg}] | |
) | |
# API call | |
response = client.chat.completions.create( | |
messages=messages, stream=True, **model_args | |
) | |
# Streaming | |
chat_history[-1][1] = "" | |
for chunk in response: | |
delta = chunk.choices[0].delta.content | |
if delta: | |
chat_history[-1][1] += delta | |
yield chat_history | |
def log_interaction( | |
chat_history: list[list[str | None]], | |
session_id: str, | |
) -> None: | |
"Record last pair of interactions" | |
record_chat(chat_logger, session_id, "user", chat_history[-1][0]) | |
record_chat(chat_logger, session_id, "bot", chat_history[-1][1]) | |
def interview_end_check( | |
chat_history: list[list[str | None]], | |
limit: int = 20, | |
end_of_interview: str = "<end_of_survey>", | |
) -> tuple[list[list[str | None]], gr.Button, gr.Textbox, gr.Button]: | |
""" | |
Checks if interview has completed using two conditions: | |
1. If the last bot message contains `end_of_interview` (default: "<end_of_survey>". Replaced "<end_interview>" with this new default token by Kentaro) | |
2. Conversation length has reached `limit` (default: 10) | |
If either condition is met, the end of interview button is displayed. | |
""" | |
flag = False | |
if len(chat_history) >= limit: | |
flag = True | |
if end_of_interview in chat_history[-1][1]: | |
chat_history[-1][1] = chat_history[-1][1].replace(end_of_interview, "") | |
flag = True | |
input_button = gr.Textbox( | |
placeholder="Type response here. Hit `Enter` or click the arrow to submit.", | |
visible= not flag, | |
interactive=True, | |
show_label=False, | |
scale=10, | |
) | |
submit_button = gr.Button( | |
"", | |
variant="primary", | |
interactive=True, | |
icon="./arrow_icon.svg", | |
visible= not flag, | |
) | |
button = gr.Button("Save and Exit", visible=flag, variant="stop") | |
return chat_history, button, input_button, submit_button | |
# Completion | |
# - Create completion code | |
# - Append to message history | |
# - Display completion code | |
def generate_completion_code(prefix: str = "cd-") -> str: | |
return prefix + str(uuid4()) | |
def upload_interview( | |
session_id: str, | |
chat_history: list[list[str | None]], | |
) -> None: | |
"Upload chat history to Azure blob storage" | |
upload_azure(session_id, chat_history) | |
def end_interview( | |
session_id: str, | |
chat_history: list[list[str | None]], | |
) -> tuple[list[list[str | None]], gr.Text]: | |
"""Create completion code and display in chat interface.""" | |
completion_message = ( | |
"Thank you for participating.\n\n" | |
"Your completion code is: {}\n\n" | |
"Please now return to the Qualtrics survey " | |
"and paste this code into the completion " | |
"code box.".format(generate_completion_code()) | |
) | |
upload_interview(session_id, chat_history) | |
EndMessage = gr.Text(completion_message, visible=True, show_label=False, scale=10) | |
return chat_history, EndMessage | |
# LAYOUT | |
with gr.Blocks(theme="sudeepshouche/minimalist") as demo: | |
# Header and instructions | |
gr.Markdown("# SurveyGPT Interview") | |
instructionText = gr.Markdown( | |
"Use this chat interface to talk to SurveyGPT.\n" | |
"To start, click 'Start Interview' and follow the instructions.\n\n" | |
"You can type your answer into the box below and hit 'Enter' or click the arrow to submit.\n\n" | |
"The interview will end either after 2 minutes, or if the chatbot decides the interview is done.\n" | |
"At this point, you will see a 'Save and Exit' button. Click this to save your responses and receive a completion code." | |
) | |
# Initialize empty hidden values. | |
userId = gr.State() | |
sessionId = gr.State() | |
questionWording = gr.State() | |
initialMessage = gr.State() | |
systemMessage = gr.State() | |
modelArgs = gr.State(value={"model": "gpt-4o-default", "temperature": 0.0}) | |
# Chat app (display, input, submit button) | |
startButton = gr.Button("Start Interview", visible=True, variant="primary") | |
chatDisplay = gr.Chatbot( | |
value=None, | |
elem_id="chatDisplay", | |
show_label=False, | |
visible=True, | |
) | |
EndMessage = gr.Text("", visible=False, show_label=False, scale=10) | |
with gr.Row(): # Interaction | |
chatInput = gr.Textbox( | |
placeholder="Click 'Start Interview' to begin.", | |
visible=False, | |
interactive=False, | |
show_label=False, | |
scale=10, | |
) | |
chatSubmit = gr.Button( | |
"", | |
variant="primary", | |
visible=False, | |
interactive=False, | |
icon="./arrow_icon.svg", | |
) | |
exitButton = gr.Button("Generate Completion Code", visible=False, variant="stop") | |
# testExitButton = gr.Button("Save and Exit", visible=True, variant="stop") | |
# Footer | |
disclaimer = gr.HTML( | |
""" | |
<div | |
style='font-size: 1em; | |
font-style: italic; | |
position: fixed; | |
left: 50%; | |
bottom: 20px; | |
transform: translate(-50%, -50%); | |
margin: 0 auto; | |
' | |
>{}</div> | |
""".format( | |
"Statements by the chatbot may contain factual inaccuracies." | |
) | |
) | |
# INTERACTIONS | |
# Initialization | |
startButton.click( | |
initialize_interview, # Reads in request params | |
inputs=None, | |
outputs=[ | |
userId, | |
sessionId, | |
questionWording, | |
initialMessage, | |
systemMessage, | |
], | |
).then( | |
initialize_interface, # Changes interface to interactive mode | |
inputs=[initialMessage], | |
outputs=[ | |
instructionText, | |
chatDisplay, | |
chatInput, | |
chatSubmit, | |
startButton, | |
], | |
) | |
# Chat interaction | |
# "Enter" | |
chatInput.submit( | |
user_message, | |
inputs=[chatInput, chatDisplay], | |
outputs=[chatInput, chatDisplay], | |
queue=False, | |
).then( | |
bot_message, | |
inputs=[chatDisplay, systemMessage, modelArgs], | |
outputs=[chatDisplay], | |
).then( | |
log_interaction, | |
inputs=[chatDisplay, sessionId], | |
).then( | |
interview_end_check, inputs=[chatDisplay], outputs=[chatDisplay, exitButton, chatInput, chatSubmit] | |
) | |
# Button | |
chatSubmit.click( | |
user_message, | |
inputs=[chatInput, chatDisplay], | |
outputs=[chatInput, chatDisplay], | |
queue=False, | |
).then( | |
bot_message, | |
inputs=[chatDisplay, systemMessage, modelArgs], | |
outputs=[chatDisplay], | |
).then( | |
log_interaction, | |
inputs=[chatDisplay, sessionId], | |
).then( | |
interview_end_check, inputs=[chatDisplay], outputs=[chatDisplay, exitButton, chatInput, chatSubmit] | |
) | |
# Reset button | |
exitButton.click( | |
end_interview, inputs=[sessionId, chatDisplay], outputs=[chatDisplay, EndMessage] | |
) | |
# testExitButton.click( | |
# end_interview, inputs=[sessionId, chatDisplay], outputs=[chatDisplay] | |
# ) | |
if __name__ == "__main__": | |
demo.launch()#auth=auth_no_user) | |