| |
| """Tokai.ipynb |
| |
| Automatically generated by Colab. |
| |
| Original file is located at |
| https://colab.research.google.com/drive/1jxHZrsdegbSDQ0cluIV7zpbIK8ynS7a3 |
| |
| # Task |
| Build a web app in Google Colab using Gradio that acts as a conversational diabetes assistant named Tokai. The app should have a sidebar with three sections: Home, Measurements, and Graphs. In Home, users can chat with Tokai (an LLM hosted via Groq using Llama 3 8B) to gain insights about their blood glucose data, such as spotting trends like “Your fasting glucose has increased over 5 days.” The conversation history should be stored. In Measurements, let users manually enter blood glucose values (with timestamp and meal tags), and also upload CSV files containing glucose logs. The manual entry and CSV upload sections should be side by side at the top, and a history of all entered and uploaded measurements should be displayed at the bottom. In Graphs, plot blood glucose trends using line graphs, scatter plots (glucose vs. time of day), bar charts (average glucose by meal tag), or heatmaps, and allow date range filtering. Use pandas for data processing and matplotlib or Plotly for visualizations. Maintain all user data in session state. Prioritize simplicity, modular code, and clear UX for people with diabetes. The app must run entirely within Colab and open a shareable Gradio link at the end. |
| """ |
|
|
| |
| |
|
|
| """# Import all Python packages""" |
|
|
| import gradio as gr |
| import pandas as pd |
| import datetime |
| import matplotlib.pyplot as plt |
| from groq import Groq |
| import os |
| import json |
| |
| import time |
| import datetime |
| import random |
|
|
| """# Create Default Data |
| - Default profile data |
| - Example chat |
| - 100 measurements |
| """ |
|
|
| |
| default_profile = { |
| 'name': 'John Doe', |
| 'email': 'john.doe@gmail.com', |
| 'medical_conditions': 'Type 2 Diabetes', |
| 'glucose_monitor': 'Libre Link 2' |
| } |
|
|
| PROFILE_FILE = "/content/tokai_profile.json" |
| try: |
| with open(PROFILE_FILE, "w") as f: |
| json.dump(default_profile, f, indent=4) |
| print(f"Generated and saved default profile to {PROFILE_FILE}") |
| except Exception as e: |
| print(f"Error generating/saving default profile: {e}") |
|
|
|
|
| |
| MEASUREMENTS_FILE = "/content/tokai_measurements.csv" |
| default_measurements_list = [] |
| base_time = datetime.datetime.now() - datetime.timedelta(days=7) |
|
|
| |
| for i in range(100): |
| timestamp = base_time + datetime.timedelta(hours=i * (168 / 100)) |
| glucose_value = round(random.uniform(80, 250), 1) |
| meal_tag = random.choice(["Before Meal", "After Meal", "Fasting", "Other"]) |
| note = "" |
| if glucose_value > 180: |
| note = "Feeling a bit tired." if random.random() > 0.5 else "High reading." |
| elif glucose_value < 100 and meal_tag != "After Meal": |
| note = "Feeling shaky." if random.random() > 0.5 else "Low reading." |
| elif meal_tag == "After Meal" and glucose_value > 140: |
| note = "Ate a large meal." if random.random() > 0.5 else "Post-meal spike." |
| elif meal_tag == "Fasting" and glucose_value > 120: |
| note = "Woke up high." if random.random() > 0.5 else "High fasting glucose." |
| else: |
| note = random.choice(["Feeling good.", "Normal reading.", "Quick check."]) if random.random() > 0.7 else "" |
|
|
| default_measurements_list.append({ |
| "ID": i + 1, |
| "Timestamp": timestamp.strftime("%Y-%m-%d %H:%M:%S"), |
| "Glucose Value": glucose_value, |
| "Meal Tag": meal_tag, |
| "Note": note |
| }) |
|
|
| default_measurements_df = pd.DataFrame(default_measurements_list) |
|
|
| try: |
| default_measurements_df.to_csv(MEASUREMENTS_FILE, index=False) |
| print(f"Generated and saved default measurements to {MEASUREMENTS_FILE}") |
| except Exception as e: |
| print(f"Error generating/saving default measurements: {e}") |
|
|
|
|
| |
| HISTORY_FILE = "/content/tokai_chat_history.json" |
| default_chat_history = [ |
| ("Hi Tokai, can you tell me about my recent blood sugar levels?", "Hello John! I can analyze your recent blood glucose data to look for trends. Please make sure your latest measurements are entered or uploaded in the Measurements tab."), |
| ("What was my average blood sugar this past week?", None), |
| ("I've been feeling more tired lately, could that be related to my glucose?", None), |
| ("My fasting glucose seems higher than usual, what could cause that?", None), |
| ("Can you show me a graph of my glucose over the last 3 days?", "I can help you visualize your data! Go to the Graphs tab, select the date range you're interested in, and choose a chart type like 'Line Plot' to see your trends.") |
| ] |
|
|
| try: |
| with open(HISTORY_FILE, "w") as f: |
| json.dump(default_chat_history, f, indent=4) |
| print(f"Generated and saved default chat history to {HISTORY_FILE}") |
| except Exception as e: |
| print(f"Error generating/saving default chat history: {e}") |
|
|
| |
| try: |
| from gradio.components import DatePicker |
| except ImportError: |
| DatePicker = None |
|
|
|
|
| |
| HISTORY_FILE = "/content/tokai_chat_history.json" |
| MEASUREMENTS_FILE = "/content/tokai_measurements.csv" |
| PROFILE_FILE = "/content/tokai_profile.json" |
|
|
|
|
| |
| def load_profile(): |
| """Loads user profile data from a JSON file.""" |
| if os.path.exists(PROFILE_FILE): |
| try: |
| with open(PROFILE_FILE, "r") as f: |
| profile_data = json.load(f) |
| print(f"Loaded profile from {PROFILE_FILE}") |
| return profile_data |
| except json.JSONDecodeError: |
| print(f"Error decoding JSON from {PROFILE_FILE}. Starting with empty profile.") |
| return {} |
| except Exception as e: |
| print(f"Error loading profile from {PROFILE_FILE}: {e}. Starting with empty profile.") |
| return {} |
| else: |
| print("No profile file found. Starting with empty profile.") |
| return {} |
|
|
| def save_profile(name, email, medical_conditions, glucose_monitor, current_profile_state): |
| """Saves user profile data to a JSON file.""" |
| updated_profile = { |
| 'name': name, |
| 'email': email, |
| 'medical_conditions': medical_conditions, |
| 'glucose_monitor': glucose_monitor |
| } |
| try: |
| with open(PROFILE_FILE, "w") as f: |
| json.dump(updated_profile, f, indent=4) |
| print(f"Saved profile to {PROFILE_FILE}") |
| |
| return "Profile saved successfully!", updated_profile |
| except Exception as e: |
| print(f"Error saving profile to {PROFILE_FILE}: {e}") |
| return f"Error saving profile: {e}", current_profile_state |
|
|
| def delete_profile_button_click(profile_state): |
| """ |
| Deletes the profile file and resets the profile state. |
| |
| Args: |
| profile_state (dict): The current profile state. |
| |
| Returns: |
| tuple: A tuple containing a status message, empty strings for input fields, |
| and an empty dictionary for the profile state. |
| """ |
| status_message = "" |
| |
| if os.path.exists(PROFILE_FILE): |
| try: |
| os.remove(PROFILE_FILE) |
| status_message = f"Profile file '{PROFILE_FILE}' deleted." |
| except Exception as e: |
| status_message = f"Error deleting profile file '{PROFILE_FILE}': {e}" |
| else: |
| status_message = "Profile file not found. Nothing to delete." |
|
|
| |
| new_profile_state = {} |
|
|
| |
| |
| return status_message, "", "", "", "", new_profile_state |
|
|
|
|
| |
| def load_chat_history(): |
| """Loads chat history from a JSON file.""" |
| if os.path.exists(HISTORY_FILE): |
| try: |
| with open(HISTORY_FILE, "r") as f: |
| history = json.load(f) |
| print(f"Loaded chat history from {HISTORY_FILE}") |
| return history |
| except json.JSONDecodeError: |
| print(f"Error decoding JSON from {HISTORY_FILE}. Starting with empty history.") |
| return [] |
| except Exception as e: |
| print(f"Error loading chat history from {HISTORY_FILE}: {e}. Starting with empty history.") |
| return [] |
| else: |
| print("No chat history file found. Starting with empty history.") |
| return [] |
|
|
| def save_chat_history(history): |
| """Saves chat history to a JSON file.""" |
| try: |
| with open(HISTORY_FILE, "w") as f: |
| json.dump(history, f, indent=4) |
| print(f"Saved chat history to {HISTORY_FILE}") |
| except Exception as e: |
| print(f"Error saving chat history to {HISTORY_FILE}: {e}") |
|
|
| |
|
|
|
|
| |
| def load_measurements(): |
| """Loads blood glucose measurements from a CSV file, ensuring 'ID' and 'Note' columns exist.""" |
| required_cols = ["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"] |
| if os.path.exists(MEASUREMENTS_FILE): |
| try: |
| df = pd.read_csv(MEASUREMENTS_FILE) |
| |
| df['Timestamp'] = pd.to_datetime(df['Timestamp']) |
| |
| if 'ID' not in df.columns or df['ID'].duplicated().any() or not all(df['ID'] == range(1, len(df) + 1)): |
| print("Regenerating 'ID' column for consistency.") |
| df['ID'] = range(1, len(df) + 1) |
|
|
| |
| if 'Note' not in df.columns: |
| print("Adding 'Note' column to existing measurements.") |
| df['Note'] = "" |
|
|
| |
| for col in required_cols: |
| if col not in df.columns: |
| df[col] = "" |
|
|
| |
| df = df[required_cols] |
|
|
|
|
| print(f"Loaded {len(df)} measurements from {MEASUREMENTS_FILE}") |
| return df |
| except Exception as e: |
| print(f"Error loading measurements from {MEASUREMENTS_FILE}: {e}. Starting with empty DataFrame.") |
| return pd.DataFrame(columns=required_cols) |
| else: |
| print("No measurements file found. Starting with empty DataFrame with required columns.") |
| return pd.DataFrame(columns=required_cols) |
|
|
| def save_measurements(df): |
| """Saves blood glucose measurements DataFrame to a CSV file.""" |
| if not df.empty: |
| try: |
| df.to_csv(MEASUREMENTS_FILE, index=False) |
| print(f"Saved {len(df)} measurements to {MEASUREMENTS_FILE}") |
| except Exception as e: |
| print(f"Error saving measurements to {MEASUREMENTS_FILE}: {e}") |
| else: |
| |
| if os.path.exists(MEASUREMENTS_FILE): |
| try: |
| os.remove(MEASUREMENTS_FILE) |
| print(f"Removed empty measurements file {MEASUREMENTS_FILE}") |
| except Exception as e: |
| print(f"Error removing empty measurements file {MEASUREMENTS_FILE}: {e}") |
|
|
| |
|
|
|
|
| |
| def delete_all_data_button_click(chat_history_state, blood_glucose_state): |
| """ |
| Deletes chat history and measurements files and resets the corresponding states. |
| |
| Args: |
| chat_history_state (list): The current chat history state. |
| blood_glucose_state (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing a status message, an empty chat history state (list), |
| an empty blood glucose DataFrame state, and updated values/visibility for download buttons. |
| """ |
| status_messages = [] |
|
|
| |
| if os.path.exists(HISTORY_FILE): |
| try: |
| os.remove(HISTORY_FILE) |
| status_messages.append(f"Chat history file '{HISTORY_FILE}' deleted.") |
| except Exception as e: |
| status_messages.append(f"Error deleting chat history file '{HISTORY_FILE}': {e}") |
| else: |
| status_messages.append("Chat history file not found. Nothing to delete.") |
|
|
| |
| if os.path.exists(MEASUREMENTS_FILE): |
| try: |
| os.remove(MEASUREMENTS_FILE) |
| status_messages.append(f"Measurements file '{MEASUREMENTS_FILE}' deleted.") |
| except Exception as e: |
| status_messages.append(f"Error deleting measurements file '{MEASUREMENTS_FILE}': {e}") |
| else: |
| status_messages.append("Measurements file not found. Nothing to delete.") |
|
|
| |
| new_chat_history_state = [] |
| new_blood_glucose_state = pd.DataFrame(columns=["ID", "Timestamp", "Glucose Value", "Meal Tag"]) |
|
|
| |
| overall_status = "\n".join(status_messages) |
|
|
| |
| return overall_status, new_chat_history_state, new_blood_glucose_state, gr.update(value=None, visible=False), gr.update(value=None, visible=False) |
|
|
|
|
| |
| |
| |
| try: |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY") |
| if not GROQ_API_KEY: |
| print("Warning: GROQ_API_KEY environment variable not set.") |
| print("Please add your Groq API key as an environment variable named 'GROQ_API_KEY'.") |
| groq_client = None |
| else: |
| groq_client = Groq(api_key=GROQ_API_KEY) |
| print("Groq client initialized.") |
| except Exception as e: |
| print(f"Error initializing Groq client: {e}") |
| print("Please ensure the GROQ_API_KEY environment variable is set correctly.") |
| groq_client = None |
|
|
| |
|
|
|
|
| |
| def respond(message, chat_history_state, blood_glucose_df): |
| """ |
| Handles chatbot responses using Groq API, updating chat history in state. |
| If Groq API key is not set, provides a mock response. |
| |
| Args: |
| message (str): The user's input message. |
| chat_history_state (list): The current chat history state (list of tuples). |
| blood_glucose_df (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing an empty string (to clear the input textbox), |
| the updated chat history for display (list of tuples), |
| and the updated chat history state (list of tuples). |
| """ |
| |
| if groq_client: |
| try: |
| |
| |
| data_context = "Here is the user's blood glucose data, including Timestamp, Glucose Value, Meal Tag, and Note:\n" |
| if not blood_glucose_df.empty: |
| |
| |
| |
| data_context += blood_glucose_df.tail().to_string(index=False) |
| else: |
| data_context += "No data available yet." |
|
|
| |
| |
| messages = [{"role": "system", "content": "You are Tokai, a friendly and helpful diabetes assistant. Analyze the provided blood glucose data and respond to the user's questions or comments. The data includes Timestamp, Glucose Value, Meal Tag, and Note. If no data is available, mention that. Focus on providing insights related to blood glucose trends and management, incorporating information from the notes where relevant. Keep your responses concise and easy to understand."}] |
| |
| for human, assistant in chat_history_state: |
| messages.append({"role": "user", "content": human}) |
| if assistant is not None: |
| messages.append({"role": "assistant", "content": assistant}) |
|
|
|
|
| |
| messages.append({"role": "user", "content": data_context + "\n\nUser query: " + message}) |
|
|
|
|
| chat_completion = groq_client.chat.completions.create( |
| messages=messages, |
| model="llama3-8b-8192", |
| temperature=0.7, |
| max_tokens=500, |
| top_p=1, |
| stream=False, |
| stop=None, |
| ) |
| bot_message = chat_completion.choices[0].message.content |
|
|
| except Exception as e: |
| bot_message = f"Tokai encountered an error while trying to respond: {e}" |
| print(f"Groq API error: {e}") |
| else: |
| |
| bot_message = f"Tokai says: Hello! Your Groq API key is not set (looked for environment variable 'GROQ_API_KEY'), so I'm providing a placeholder response. You asked: '{message}'. Please add your key as an environment variable named 'GROQ_API_KEY' in your hosting environment." |
|
|
|
|
| |
| chat_history_state.append((message, bot_message)) |
|
|
| |
| return "", chat_history_state, chat_history_state |
|
|
|
|
| |
| def add_manual_measurement(timestamp_str, glucose_value, meal_tag, note, current_df_state): |
| """ |
| Adds a manual measurement to the blood glucose DataFrame state with an incrementing ID. |
| |
| Args: |
| timestamp_str (str): The timestamp string (YYYY-MM-DD HH:MM:SS). |
| glucose_value (float): The blood glucose value. |
| meal_tag (str): The meal tag. |
| note (str): The note for the measurement. |
| current_df_state (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing a status message and the updated DataFrame state. |
| """ |
| try: |
| |
| timestamp = pd.to_datetime(timestamp_str) |
|
|
| |
| next_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 |
|
|
| |
| new_measurement = pd.DataFrame([{ |
| "ID": next_id, |
| "Timestamp": timestamp, |
| "Glucose Value": glucose_value, |
| "Meal Tag": meal_tag, |
| "Note": note |
| }]) |
| |
| updated_df = pd.concat([current_df_state, new_measurement], ignore_index=True) |
| print(f"Manual measurement added. Total rows: {len(updated_df)}") |
| |
| return "Measurement added successfully! You can add another or view graphs.", updated_df |
| except ValueError: |
| |
| return "Error: Invalid timestamp format. Please use YYYY-MM-DD HH:MM:SS.", current_df_state |
| except Exception as e: |
| |
| return f"Error adding measurement: {e}", current_df_state |
|
|
| def process_csv_upload(csv_file, current_df_state): |
| """ |
| Processes an uploaded CSV file and adds data to the blood glucose DataFrame state, |
| assigning incrementing IDs and handling a 'Note' column. |
| |
| Args: |
| csv_file (gr.File): The uploaded Gradio File object. |
| current_df_state (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing a status message and the updated DataFrame state. |
| """ |
| if csv_file is None: |
| |
| return "No file uploaded.", current_df_state |
| try: |
| |
| uploaded_df = pd.read_csv(csv_file.name) |
|
|
| |
| required_core_cols = ["Timestamp", "Glucose Value", "Meal Tag"] |
| if not all(col in uploaded_df.columns for col in required_core_cols): |
| |
| return f"Error: CSV must contain columns: {', '.join(required_core_cols)}", current_df_state |
|
|
| |
| uploaded_df["Timestamp"] = pd.to_datetime(uploaded_df["Timestamp"]) |
|
|
| |
| if 'Note' not in uploaded_df.columns: |
| print("Adding 'Note' column to uploaded CSV data as it was missing.") |
| uploaded_df['Note'] = "" |
|
|
| |
| start_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 |
|
|
| |
| uploaded_df['ID'] = range(start_id, start_id + len(uploaded_df)) |
|
|
| |
| desired_col_order = ["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"] |
|
|
| |
| |
| cols_to_select = [col for col in desired_col_order if col in uploaded_df.columns] |
| uploaded_df = uploaded_df[cols_to_select] |
|
|
| |
| updated_df = pd.concat([current_df_state, uploaded_df], ignore_index=True) |
| print(f"CSV data added. Total rows: {len(updated_df)}") |
| |
| return f"Successfully uploaded and processed {len(uploaded_df)} measurements from CSV. View graphs to see trends.", updated_df |
| except Exception as e: |
| |
| return f"Error processing CSV file: {e}", current_df_state |
|
|
|
|
| |
| def plot_glucose_trends(start_date, end_date, current_df_state, chart_type): |
| """ |
| Generates a blood glucose trend plot from the DataFrame state based on a date range |
| and selected chart type. |
| |
| Args: |
| start_date (datetime.date or str): The start date for filtering. |
| end_date (datetime.date or str): The end date for filtering. |
| current_df_state (pd.DataFrame): The current blood glucose DataFrame state. |
| chart_type (str): The type of chart to generate (e.g., "Line Plot", "Scatter Plot", "Bar Chart"). |
| |
| Returns: |
| matplotlib.figure.Figure: The generated matplotlib figure. |
| """ |
| |
| plt.close('all') |
|
|
| if current_df_state.empty: |
| |
| fig, ax = plt.subplots() |
| ax.set_title("No data available. Add measurements first.") |
| return fig |
|
|
| |
| |
| plot_df = current_df_state.copy() |
| plot_df['Timestamp'] = pd.to_datetime(plot_df['Timestamp']) |
|
|
| |
| try: |
| |
| |
| if start_date is None: |
| start_datetime = plot_df['Timestamp'].min() |
| else: |
| start_datetime = pd.to_datetime(start_date) |
|
|
| if end_date is None: |
| end_datetime = plot_df['Timestamp'].max() |
| else: |
| |
| end_datetime = pd.to_datetime(end_date) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1) |
|
|
| except ValueError: |
| |
| fig, ax = plt.subplots() |
| ax.set_title("Invalid date format for filtering.") |
| return fig |
|
|
|
|
| filtered_df = plot_df[ |
| (plot_df['Timestamp'] >= start_datetime) & |
| (plot_df['Timestamp'] <= end_datetime) |
| ].sort_values(by='Timestamp') |
|
|
| if filtered_df.empty: |
| |
| fig, ax = plt.subplots() |
| ax.set_title(f"No data for the selected date range: {start_date} to {end_date}") |
| return fig |
|
|
| |
| fig, ax = plt.subplots(figsize=(12, 6)) |
|
|
| if chart_type == "Line Plot": |
| ax.plot(filtered_df['Timestamp'], filtered_df['Glucose Value'], marker='o', linestyle='-') |
| ax.set_title('Blood Glucose Trends Over Time (Line Plot)') |
| ax.set_xlabel('Date and Time') |
| ax.set_ylabel('Glucose Value (mg/dL)') |
| elif chart_type == "Scatter Plot": |
| ax.scatter(filtered_df['Timestamp'], filtered_df['Glucose Value']) |
| ax.set_title('Blood Glucose Trends Over Time (Scatter Plot)') |
| ax.set_xlabel('Date and Time') |
| ax.set_ylabel('Glucose Value (mg/dL)') |
| elif chart_type == "Bar Chart": |
| if 'Meal Tag' in filtered_df.columns and not filtered_df['Meal Tag'].isnull().all(): |
| |
| filtered_df['Meal Tag'] = filtered_df['Meal Tag'].astype('category') |
| |
| avg_glucose_by_meal = filtered_df.groupby('Meal Tag')['Glucose Value'].mean().reset_index() |
| ax.bar(avg_glucose_by_meal['Meal Tag'], avg_glucose_by_meal['Glucose Value']) |
| ax.set_title('Average Blood Glucose by Meal Tag') |
| ax.set_xlabel('Meal Tag') |
| ax.set_ylabel('Average Glucose Value (mg/dL)') |
| else: |
| ax.set_title("No meal tag data available for Bar Chart.") |
| ax.text(0.5, 0.5, "Add measurements with Meal Tags to see this chart.", |
| horizontalalignment='center', verticalalignment='center', transform=ax.transAxes) |
| else: |
| ax.set_title("Invalid chart type selected.") |
|
|
| ax.grid(True) |
| plt.xticks(rotation=45) |
| plt.tight_layout() |
|
|
| return fig |
|
|
|
|
| |
|
|
| def create_home_tab(chat_history_state, blood_glucose_state): |
| """Creates the UI components for the Home (Chat) tab.""" |
| with gr.TabItem("🏠 Home", id="home_tab"): |
| gr.Markdown( |
| """ |
| ## Chat with Tokai |
| Ask Tokai questions about your blood glucose data. |
| (Note: Tokai can currently answer general questions or use a placeholder response if no API key is set.) |
| """ |
| ) |
| with gr.Column(): |
| chatbot = gr.Chatbot(value=chat_history_state.value, label="Tokai Chat") |
| msg = gr.Textbox(label="Enter your message here:") |
|
|
| msg.submit( |
| fn=respond, |
| inputs=[msg, chat_history_state, blood_glucose_state], |
| outputs=[msg, chatbot, chat_history_state] |
| ).then( |
| fn=save_chat_history, |
| inputs=[chat_history_state], |
| outputs=None |
| ) |
| return chatbot |
|
|
|
|
| def create_graphs_tab(blood_glucose_state): |
| """Creates the UI components for the Graphs tab.""" |
| with gr.TabItem("📈 Graphs", id="graphs_tab"): |
| gr.Markdown( |
| """ |
| ## Blood Glucose Trends |
| Visualize your blood glucose data over a selected date range and chart type. |
| """ |
| ) |
| with gr.Row(): |
| if DatePicker: |
| start_date_input = DatePicker(label="Select Start Date", value=datetime.date.today() - datetime.timedelta(days=7)) |
| end_date_input = DatePicker(label="Select End Date", value=datetime.date.today()) |
| else: |
| start_date_input = gr.Textbox(label="Start Date (YYYY-MM-DD)", value=(datetime.date.today() - datetime.timedelta(days=7)).strftime("%Y-%m-%d")) |
| end_date_input = gr.Textbox(label="End Date (YYYY-MM-DD)", value=datetime.date.today().strftime("%Y-%m-%d")) |
|
|
| chart_type_selector = gr.Radio( |
| label="Select Chart Type", |
| choices=["Line Plot", "Scatter Plot", "Bar Chart"], |
| value="Line Plot" |
| ) |
|
|
| plot_button = gr.Button("Generate Plot") |
| glucose_plot = gr.Plot(label="Blood Glucose Trends Plot") |
|
|
| plot_button.click( |
| fn=plot_glucose_trends, |
| inputs=[start_date_input, end_date_input, blood_glucose_state, chart_type_selector], |
| outputs=[glucose_plot] |
| ) |
|
|
| if DatePicker: |
| start_date_input.change( |
| fn=plot_glucose_trends, |
| inputs=[start_date_input, end_date_input, blood_glucose_state, chart_type_selector], |
| outputs=[glucose_plot] |
| ) |
| end_date_input.change( |
| fn=plot_glucose_trends, |
| inputs=[start_date_input, end_date_input, blood_glucose_state, chart_type_selector], |
| outputs=[glucose_plot] |
| ) |
|
|
| chart_type_selector.change( |
| fn=plot_glucose_trends, |
| inputs=[start_date_input, end_date_input, blood_glucose_state, chart_type_selector], |
| outputs=[glucose_plot] |
| ) |
|
|
| return start_date_input, end_date_input, plot_button, glucose_plot, chart_type_selector |
|
|
|
|
| def create_measurements_tab(blood_glucose_state): |
| """Creates the UI components for the Measurements tab.""" |
| with gr.TabItem("📝 Measurements", id="measurements_tab"): |
| gr.Markdown( |
| """ |
| ## Add Your Blood Glucose Measurements |
| Enter your blood glucose readings manually or upload a CSV file. |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Accordion("➕ Manual Entry", open=True): |
| gr.Markdown("Enter a single blood glucose measurement.") |
| with gr.Row(): |
| with gr.Column(): |
| glucose_input = gr.Number(label="Blood Glucose Value (mg/dL)") |
| timestamp_input = gr.Textbox(label="Timestamp (YYYY-MM-DD HH:MM:SS)", value=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), placeholder="Defaults to current time") |
| meal_tag_input = gr.Dropdown(label="Meal Tag", choices=["Before Meal", "After Meal", "Fasting", "Other"], value="Other") |
| with gr.Column(): |
| note_input = gr.Textbox(label="Note", placeholder="Optional notes about this measurement", lines=4) |
| add_manual_btn = gr.Button("Add Measurement") |
| manual_output = gr.Textbox(label="Status", interactive=False) |
|
|
|
|
| with gr.Accordion("⬆️ Upload CSV", open=True): |
| gr.Markdown("Upload a CSV file with 'Timestamp', 'Glucose Value', and 'Meal Tag' columns.") |
| upload_csv_input = gr.File(label="Choose CSV file to upload") |
| csv_upload_output = gr.Textbox(label="Status", interactive=False) |
|
|
| gr.Markdown("### Your Measurements") |
| measurements_display_df = gr.Dataframe( |
| value=blood_glucose_state.value, |
| headers=["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"], |
| interactive=False, |
| wrap=True |
| ) |
|
|
| add_manual_btn.click( |
| fn=add_manual_measurement, |
| inputs=[timestamp_input, glucose_input, meal_tag_input, note_input, blood_glucose_state], |
| outputs=[manual_output, blood_glucose_state] |
| ).then( |
| fn=save_measurements, |
| inputs=[blood_glucose_state], |
| outputs=None |
| ).then( |
| fn=lambda state: state, |
| inputs=[blood_glucose_state], |
| outputs=[measurements_display_df] |
| ) |
|
|
| upload_csv_input.upload( |
| fn=process_csv_upload, |
| inputs=[upload_csv_input, blood_glucose_state], |
| outputs=[csv_upload_output, blood_glucose_state] |
| ).then( |
| fn=save_measurements, |
| inputs=[blood_glucose_state], |
| outputs=None |
| ).then( |
| fn=lambda state: state, |
| inputs=[blood_glucose_state], |
| outputs=[measurements_display_df] |
| ) |
| return glucose_input, timestamp_input, meal_tag_input, note_input, add_manual_btn, manual_output, upload_csv_input, csv_upload_output, measurements_display_df |
|
|
|
|
| def create_profile_tab(profile_state, chat_history_state, blood_glucose_state): |
| """Creates the UI components for the Profile tab.""" |
| with gr.TabItem("👤 Profile", id="profile_tab"): |
| gr.Markdown( |
| """ |
| ## Your Profile |
| Enter your profile information here. This can help Tokai provide more personalized insights. |
| """ |
| ) |
| name_input = gr.Textbox(label="Name", value=profile_state.value.get('name', '')) |
| email_input = gr.Textbox(label="Email", value=profile_state.value.get('email', '')) |
| medical_conditions_input = gr.Textbox(label="Medical Conditions", value=profile_state.value.get('medical_conditions', '')) |
| glucose_monitor_input = gr.Textbox(label="Type of glucose monitor", value=profile_state.value.get('glucose_monitor', '')) |
|
|
| save_profile_btn = gr.Button("Save Profile") |
|
|
| with gr.Row(): |
| |
| download_chat_history_btn = gr.DownloadButton("Download Chat History", value=HISTORY_FILE if os.path.exists(HISTORY_FILE) else None, visible=os.path.exists(HISTORY_FILE), interactive=os.path.exists(HISTORY_FILE)) |
| download_measurements_btn = gr.DownloadButton("Download Measurements", value=MEASUREMENTS_FILE if os.path.exists(MEASUREMENTS_FILE) else None, visible=os.path.exists(MEASUREMENTS_FILE), interactive=os.path.exists(MEASUREMENTS_FILE)) |
| delete_all_data_btn = gr.Button("Delete All Data") |
| delete_profile_btn = gr.Button("Delete Profile") |
|
|
| profile_status_output = gr.Textbox(label="Status", interactive=False) |
|
|
| |
| save_profile_btn.click( |
| fn=save_profile, |
| inputs=[name_input, email_input, medical_conditions_input, glucose_monitor_input, profile_state], |
| outputs=[profile_status_output, profile_state] |
| ) |
|
|
| |
| delete_all_data_btn.click( |
| fn=delete_all_data_button_click, |
| inputs=[chat_history_state, blood_glucose_state], |
| outputs=[profile_status_output, chat_history_state, blood_glucose_state, download_chat_history_btn, download_measurements_btn] |
| ) |
|
|
| |
| delete_profile_btn.click( |
| fn=delete_profile_button_click, |
| inputs=[profile_state], |
| outputs=[profile_status_output, name_input, email_input, medical_conditions_input, glucose_monitor_input, profile_state] |
| ) |
|
|
|
|
| |
| return name_input, email_input, medical_conditions_input, glucose_monitor_input, save_profile_btn, download_chat_history_btn, download_measurements_btn, delete_all_data_btn, delete_profile_btn, profile_status_output |
|
|
| """# Task |
| Modify the application to allow users to add a note when entering blood glucose measurements, either manually or via CSV upload, display these notes in the measurement history, and make the notes accessible to the LLM for context. |
| |
| ## Modify measurement data structure |
| |
| ### Subtask: |
| Update the data structure (the pandas DataFrame) to include a new 'Note' column. |
| |
| **Reasoning**: |
| The subtask requires modifying the `load_measurements` function to include a 'Note' column in the DataFrame, both when initializing an empty DataFrame and when loading existing data that might be missing the column. This ensures the data structure is consistent for subsequent operations. |
| """ |
|
|
| def load_measurements(): |
| """Loads blood glucose measurements from a CSV file, ensuring 'ID' and 'Note' columns exist.""" |
| required_cols = ["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"] |
| if os.path.exists(MEASUREMENTS_FILE): |
| try: |
| df = pd.read_csv(MEASUREMENTS_FILE) |
| |
| df['Timestamp'] = pd.to_datetime(df['Timestamp']) |
| |
| if 'ID' not in df.columns or df['ID'].duplicated().any() or not all(df['ID'] == range(1, len(df) + 1)): |
| print("Regenerating 'ID' column for consistency.") |
| df['ID'] = range(1, len(df) + 1) |
|
|
| |
| if 'Note' not in df.columns: |
| print("Adding 'Note' column to existing measurements.") |
| df['Note'] = "" |
|
|
| |
| for col in required_cols: |
| if col not in df.columns: |
| df[col] = "" |
|
|
| |
| df = df[required_cols] |
|
|
|
|
| print(f"Loaded {len(df)} measurements from {MEASUREMENTS_FILE}") |
| return df |
| except Exception as e: |
| print(f"Error loading measurements from {MEASUREMENTS_FILE}: {e}. Starting with empty DataFrame.") |
| return pd.DataFrame(columns=required_cols) |
| else: |
| print("No measurements file found. Starting with empty DataFrame with required columns.") |
| return pd.DataFrame(columns=required_cols) |
|
|
| """## Update manual measurement entry |
| |
| ### Subtask: |
| Add a textbox for the 'Note' in the manual entry section and modify the `add_manual_measurement` function to include the note when adding data. |
| |
| **Reasoning**: |
| Add a textbox for the 'Note' input in the manual entry section and modify the `add_manual_measurement` function and its usage in the Gradio interface to handle this new input. |
| """ |
|
|
| |
| def add_manual_measurement(timestamp_str, glucose_value, meal_tag, note, current_df_state): |
| """ |
| Adds a manual measurement to the blood glucose DataFrame state with an incrementing ID. |
| |
| Args: |
| timestamp_str (str): The timestamp string (YYYY-MM-DD HH:MM:SS). |
| glucose_value (float): The blood glucose value. |
| meal_tag (str): The meal tag. |
| note (str): The note for the measurement. |
| current_df_state (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing a status message and the updated DataFrame state. |
| """ |
| try: |
| |
| timestamp = pd.to_datetime(timestamp_str) |
|
|
| |
| next_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 |
|
|
| |
| new_measurement = pd.DataFrame([{ |
| "ID": next_id, |
| "Timestamp": timestamp, |
| "Glucose Value": glucose_value, |
| "Meal Tag": meal_tag, |
| "Note": note |
| }]) |
| |
| updated_df = pd.concat([current_df_state, new_measurement], ignore_index=True) |
| print(f"Manual measurement added. Total rows: {len(updated_df)}") |
| |
| return "Measurement added successfully! You can add another or view graphs.", updated_df |
| except ValueError: |
| |
| return "Error: Invalid timestamp format. Please use YYYY-MM-DD HH:MM:SS.", current_df_state |
| except Exception as e: |
| |
| return f"Error adding measurement: {e}", current_df_state |
|
|
| |
|
|
| def create_measurements_tab(blood_glucose_state): |
| """Creates the UI components for the Measurements tab.""" |
| with gr.TabItem("📝 Measurements", id="measurements_tab"): |
| gr.Markdown( |
| """ |
| ## Add Your Blood Glucose Measurements |
| Enter your blood glucose readings manually or upload a CSV file. |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Accordion("➕ Manual Entry", open=True): |
| gr.Markdown("Enter a single blood glucose measurement.") |
| glucose_input = gr.Number(label="Blood Glucose Value (mg/dL)") |
| timestamp_input = gr.Textbox(label="Timestamp (YYYY-MM-DD HH:MM:SS)", value=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), placeholder="Defaults to current time") |
| meal_tag_input = gr.Dropdown(label="Meal Tag", choices=["Before Meal", "After Meal", "Fasting", "Other"], value="Other") |
| note_input = gr.Textbox(label="Note", placeholder="Optional notes about this measurement") |
| add_manual_btn = gr.Button("Add Measurement") |
| manual_output = gr.Textbox(label="Status", interactive=False) |
|
|
| with gr.Accordion("⬆️ Upload CSV", open=True): |
| gr.Markdown("Upload a CSV file with 'Timestamp', 'Glucose Value', and 'Meal Tag' columns.") |
| upload_csv_input = gr.File(label="Choose CSV file to upload") |
| csv_upload_output = gr.Textbox(label="Status", interactive=False) |
|
|
| gr.Markdown("### Your Measurements") |
| measurements_display_df = gr.Dataframe( |
| value=blood_glucose_state.value, |
| headers=["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"], |
| interactive=False, |
| wrap=True |
| ) |
|
|
| add_manual_btn.click( |
| fn=add_manual_measurement, |
| inputs=[timestamp_input, glucose_input, meal_tag_input, note_input, blood_glucose_state], |
| outputs=[manual_output, blood_glucose_state] |
| ).then( |
| fn=save_measurements, |
| inputs=[blood_glucose_state], |
| outputs=None |
| ).then( |
| fn=lambda state: state, |
| inputs=[blood_glucose_state], |
| outputs=[measurements_display_df] |
| ) |
|
|
| upload_csv_input.upload( |
| fn=process_csv_upload, |
| inputs=[upload_csv_input, blood_glucose_state], |
| outputs=[csv_upload_output, blood_glucose_state] |
| ).then( |
| fn=save_measurements, |
| inputs=[blood_glucose_state], |
| outputs=None |
| ).then( |
| fn=lambda state: state, |
| inputs=[blood_glucose_state], |
| outputs=[measurements_display_df] |
| ) |
| return glucose_input, timestamp_input, meal_tag_input, note_input, add_manual_btn, manual_output, upload_csv_input, csv_upload_output, measurements_display_df |
|
|
| """## Update csv upload processing |
| |
| ### Subtask: |
| Modify the `process_csv_upload` function to handle a 'Note' column in the uploaded CSV and include it in the DataFrame. |
| |
| **Reasoning**: |
| Modify the `process_csv_upload` function to handle the 'Note' column in the uploaded CSV and ensure it's included in the DataFrame, adding an empty 'Note' column if it's missing in the CSV. |
| """ |
|
|
| |
| |
|
|
| def process_csv_upload(csv_file, current_df_state): |
| """ |
| Processes an uploaded CSV file and adds data to the blood glucose DataFrame state, |
| assigning incrementing IDs and handling a 'Note' column. |
| |
| Args: |
| csv_file (gr.File): The uploaded Gradio File object. |
| current_df_state (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing a status message and the updated DataFrame state. |
| """ |
| if csv_file is None: |
| |
| return "No file uploaded.", current_df_state |
| try: |
| |
| uploaded_df = pd.read_csv(csv_file.name) |
|
|
| |
| required_core_cols = ["Timestamp", "Glucose Value", "Meal Tag"] |
| if not all(col in uploaded_df.columns for col in required_core_cols): |
| |
| return f"Error: CSV must contain columns: {', '.join(required_core_cols)}", current_df_state |
|
|
| |
| uploaded_df["Timestamp"] = pd.to_datetime(uploaded_df["Timestamp"]) |
|
|
| |
| if 'Note' not in uploaded_df.columns: |
| print("Adding 'Note' column to uploaded CSV data as it was missing.") |
| uploaded_df['Note'] = "" |
|
|
| |
| start_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 |
|
|
| |
| uploaded_df['ID'] = range(start_id, start_id + len(uploaded_df)) |
|
|
| |
| desired_col_order = ["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"] |
|
|
| |
| |
| cols_to_select = [col for col in desired_col_order if col in uploaded_df.columns] |
| uploaded_df = uploaded_df[cols_to_select] |
|
|
| |
| updated_df = pd.concat([current_df_state, uploaded_df], ignore_index=True) |
| print(f"CSV data added. Total rows: {len(updated_df)}") |
| |
| return f"Successfully uploaded and processed {len(uploaded_df)} measurements from CSV. View graphs to see trends.", updated_df |
| except Exception as e: |
| |
| return f"Error processing CSV file: {e}", current_df_state |
|
|
| |
| |
| |
|
|
| """## Update measurement display |
| |
| ### Subtask: |
| Modify the `measurements_display_df` in the UI to display the new 'Note' column. |
| |
| **Reasoning**: |
| Update the headers in the `gr.Dataframe` component within the `create_measurements_tab` function to include the 'Note' column, as specified in the instructions. |
| """ |
|
|
| def create_measurements_tab(blood_glucose_state): |
| """Creates the UI components for the Measurements tab.""" |
| with gr.TabItem("📝 Measurements", id="measurements_tab"): |
| gr.Markdown( |
| """ |
| ## Add Your Blood Glucose Measurements |
| Enter your blood glucose readings manually or upload a CSV file. |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Accordion("➕ Manual Entry", open=True): |
| gr.Markdown("Enter a single blood glucose measurement.") |
| with gr.Row(): |
| with gr.Column(): |
| glucose_input = gr.Number(label="Blood Glucose Value (mg/dL)") |
| timestamp_input = gr.Textbox(label="Timestamp (YYYY-MM-DD HH:MM:SS)", value=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), placeholder="Defaults to current time") |
| meal_tag_input = gr.Dropdown(label="Meal Tag", choices=["Before Meal", "After Meal", "Fasting", "Other"], value="Other") |
| with gr.Column(): |
| note_input = gr.Textbox(label="Note", placeholder="Optional notes about this measurement", lines=4) |
| add_manual_btn = gr.Button("Add Measurement") |
| manual_output = gr.Textbox(label="Status", interactive=False) |
|
|
|
|
| with gr.Accordion("⬆️ Upload CSV", open=True): |
| gr.Markdown("Upload a CSV file with 'Timestamp', 'Glucose Value', and 'Meal Tag' columns.") |
| upload_csv_input = gr.File(label="Choose CSV file to upload") |
| csv_upload_output = gr.Textbox(label="Status", interactive=False) |
|
|
| gr.Markdown("### Your Measurements") |
| measurements_display_df = gr.Dataframe( |
| value=blood_glucose_state.value, |
| headers=["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"], |
| interactive=False, |
| wrap=True |
| ) |
|
|
| add_manual_btn.click( |
| fn=add_manual_measurement, |
| inputs=[timestamp_input, glucose_input, meal_tag_input, note_input, blood_glucose_state], |
| outputs=[manual_output, blood_glucose_state] |
| ).then( |
| fn=save_measurements, |
| inputs=[blood_glucose_state], |
| outputs=None |
| ).then( |
| fn=lambda state: state, |
| inputs=[blood_glucose_state], |
| outputs=[measurements_display_df] |
| ) |
|
|
| upload_csv_input.upload( |
| fn=process_csv_upload, |
| inputs=[upload_csv_input, blood_glucose_state], |
| outputs=[csv_upload_output, blood_glucose_state] |
| ).then( |
| fn=save_measurements, |
| inputs=[blood_glucose_state], |
| outputs=None |
| ).then( |
| fn=lambda state: state, |
| inputs=[blood_glucose_state], |
| outputs=[measurements_display_df] |
| ) |
| return glucose_input, timestamp_input, meal_tag_input, note_input, add_manual_btn, manual_output, upload_csv_input, csv_upload_output, measurements_display_df |
|
|
| |
| |
|
|
| """## Update llm context |
| |
| ### Subtask: |
| Modify the `respond` function to include the 'Note' column data when providing blood glucose data context to the LLM. |
| |
| **Reasoning**: |
| Modify the respond function to include the 'Note' column data when providing blood glucose data context to the LLM and update the prompt to reflect the inclusion of notes. |
| """ |
|
|
| |
| def respond(message, chat_history_state, blood_glucose_df): |
| """ |
| Handles chatbot responses using Groq API, updating chat history in state. |
| If Groq API key is not set, provides a mock response. |
| |
| Args: |
| message (str): The user's input message. |
| chat_history_state (list): The current chat history state (list of tuples). |
| blood_glucose_df (pd.DataFrame): The current blood glucose DataFrame state. |
| |
| Returns: |
| tuple: A tuple containing an empty string (to clear the input textbox), |
| the updated chat history for display (list of tuples), |
| and the updated chat history state (list of tuples). |
| """ |
| |
| if groq_client: |
| try: |
| |
| |
| data_context = "Here is the user's blood glucose data, including Timestamp, Glucose Value, Meal Tag, and Note:\n" |
| if not blood_glucose_df.empty: |
| |
| |
| |
| data_context += blood_glucose_df.tail().to_string(index=False) |
| else: |
| data_context += "No data available yet." |
|
|
| |
| |
| messages = [{"role": "system", "content": "You are Tokai, a friendly and helpful diabetes assistant. Analyze the provided blood glucose data and respond to the user's questions or comments. The data includes Timestamp, Glucose Value, Meal Tag, and Note. If no data is available, mention that. Focus on providing insights related to blood glucose trends and management, incorporating information from the notes where relevant. Keep your responses concise and easy to understand."}] |
| |
| for human, assistant in chat_history_state: |
| messages.append({"role": "user", "content": human}) |
| if assistant is not None: |
| messages.append({"role": "assistant", "content": assistant}) |
|
|
|
|
| |
| messages.append({"role": "user", "content": data_context + "\n\nUser query: " + message}) |
|
|
|
|
| chat_completion = groq_client.chat.completions.create( |
| messages=messages, |
| model="llama3-8b-8192", |
| temperature=0.7, |
| max_tokens=500, |
| top_p=1, |
| stream=False, |
| stop=None, |
| ) |
| bot_message = chat_completion.choices[0].message.content |
|
|
| except Exception as e: |
| bot_message = f"Tokai encountered an error while trying to respond: {e}" |
| print(f"Groq API error: {e}") |
| else: |
| |
| bot_message = f"Tokai says: Hello! Your Groq API key is not set (looked for secret named 'Groq'), so I'm providing a placeholder response. You asked: '{message}'. Please add your key to Colab secrets with the name 'Groq' to enable full LLM capabilities." |
|
|
|
|
| |
| chat_history_state.append((message, bot_message)) |
|
|
| |
| return "", chat_history_state, chat_history_state |
|
|
| |
| |
|
|
| with gr.Blocks(title="Tokai: Your Diabetes Assistant") as demo: |
| |
| initial_measurements = load_measurements() |
| blood_glucose_state = gr.State(initial_measurements) |
|
|
| initial_chat_history = load_chat_history() |
| chat_history_state = gr.State(initial_chat_history) |
|
|
| initial_profile = load_profile() |
| profile_state = gr.State(initial_profile) |
|
|
|
|
| gr.Markdown( |
| """ |
| # Tokai: Your Diabetes Assistant |
| Welcome to Tokai, your personal assistant for managing blood glucose data. |
| Use the tabs below to chat with Tokai, view your data trends, or add new measurements. |
| """ |
| ) |
|
|
| with gr.Tabs() as tabs: |
| |
| create_home_tab(chat_history_state, blood_glucose_state) |
| create_measurements_tab(blood_glucose_state) |
| create_graphs_tab(blood_glucose_state) |
| create_profile_tab(profile_state, chat_history_state, blood_glucose_state) |
|
|
|
|
| |
| demo.launch(share=True, debug=True) |
|
|
| """**Reasoning**: |
| Consolidate all the Python code into a single file and create the requirements.txt file. |
| |
| |
| """ |