# -*- coding: utf-8 -*- """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. """ # Commented out IPython magic to ensure Python compatibility. # %pip install gradio pandas matplotlib plotly langchain groq """# 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 # from google.colab import userdata # This is Colab-specific, will remove import time # Import time module for potential delays if needed import datetime import random # Import the random module """# Create Default Data - Default profile data - Example chat - 100 measurements """ # --- Generate Default Profile Data --- 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}") # --- Generate Default Measurement Data --- MEASUREMENTS_FILE = "/content/tokai_measurements.csv" default_measurements_list = [] base_time = datetime.datetime.now() - datetime.timedelta(days=7) # Start a week ago # Generate 100 measurements over a week for i in range(100): timestamp = base_time + datetime.timedelta(hours=i * (168 / 100)) # Distribute over 7 days (168 hours) glucose_value = round(random.uniform(80, 250), 1) # Random glucose value 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 "" # Add some random notes 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}") # --- Generate Default Chat History --- 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), # Placeholder, LLM would fill this in ("I've been feeling more tired lately, could that be related to my glucose?", None), # Placeholder ("My fasting glucose seems higher than usual, what could cause that?", None), # Placeholder ("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}") # Explicitly import DatePicker for clarity and potential fallback check try: from gradio.components import DatePicker except ImportError: DatePicker = None # Fallback if explicit import fails # --- Global File Paths --- HISTORY_FILE = "/content/tokai_chat_history.json" MEASUREMENTS_FILE = "/content/tokai_measurements.csv" PROFILE_FILE = "/content/tokai_profile.json" # --- Profile Functions --- 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 success message and the updated state 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 = "" # Attempt to delete the profile file 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." # Reset the profile state new_profile_state = {} # Return status, clear input fields, and reset state # Need to return empty strings for the textboxes linked to this function return status_message, "", "", "", "", new_profile_state # --- Chat History Functions --- 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}") # The save_chat_history_button_click is no longer needed as we have a download button # --- Measurement Functions --- 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) # Ensure Timestamp is datetime df['Timestamp'] = pd.to_datetime(df['Timestamp']) # Ensure 'ID' column exists and is unique, regenerate if necessary 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) # Ensure 'Note' column exists, add if missing if 'Note' not in df.columns: print("Adding 'Note' column to existing measurements.") df['Note'] = "" # Add with empty string default # Ensure all required columns are present, add missing ones with default values for col in required_cols: if col not in df.columns: df[col] = "" # Add any other missing required columns with default empty string # Reorder columns to match the required order 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 DataFrame is empty and file exists, remove the file 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}") # The save_measurements_button_click is no longer needed as we have a download button # --- Combined Data Deletion Function --- 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 = [] # Attempt to delete chat history file 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.") # Attempt to delete measurements file 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.") # Reset states new_chat_history_state = [] new_blood_glucose_state = pd.DataFrame(columns=["ID", "Timestamp", "Glucose Value", "Meal Tag"]) # Combine status messages overall_status = "\n".join(status_messages) # Return status, new states, and update download button visibility by setting value to None return overall_status, new_chat_history_state, new_blood_glucose_state, gr.update(value=None, visible=False), gr.update(value=None, visible=False) # --- Groq API Setup --- # Get the Groq API key from environment variables # You need to add your Groq API key as an environment variable named "GROQ_API_KEY" 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 # --- End Groq API Setup --- # --- Response Function (using Groq API) --- 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). """ # Use Groq API if client is initialized, otherwise use mock response if groq_client: try: # Prepare data context for the LLM # Provide the DataFrame content to the LLM, including the 'Note' column 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: # Limit the data context to avoid exceeding token limits for very large DataFrames # You might want a more sophisticated summary for large datasets # Ensure 'Note' column is included in the to_string() output data_context += blood_glucose_df.tail().to_string(index=False) # Use tail for recent data and exclude index else: data_context += "No data available yet." # Construct the prompt for the LLM # Include previous chat history as context in the required format for Groq API 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."}] # Include previous chat history from state for context, formatting for the LLM for human, assistant in chat_history_state: messages.append({"role": "user", "content": human}) if assistant is not None: messages.append({"role": "assistant", "content": assistant}) # Add the current user message with data context messages.append({"role": "user", "content": data_context + "\n\nUser query: " + message}) chat_completion = groq_client.chat.completions.create( messages=messages, model="llama3-8b-8192", # Using Llama 3 8B model temperature=0.7, max_tokens=500, # Reduced max tokens for potentially faster responses 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}") # Log the error else: # Mock response if API key is not set 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." # Append user message and bot response to history state chat_history_state.append((message, bot_message)) # Return empty string to clear the input box, the updated display history for chatbot, and the updated state return "", chat_history_state, chat_history_state # --- Data Addition Functions --- 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: # Convert timestamp string to datetime object timestamp = pd.to_datetime(timestamp_str) # Determine the next ID next_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 # Create a DataFrame for the new measurement new_measurement = pd.DataFrame([{ "ID": next_id, "Timestamp": timestamp, "Glucose Value": glucose_value, "Meal Tag": meal_tag, "Note": note # Include the note }]) # Concatenate the new measurement to the state DataFrame updated_df = pd.concat([current_df_state, new_measurement], ignore_index=True) print(f"Manual measurement added. Total rows: {len(updated_df)}") # For debugging # Return success message and the updated state return "Measurement added successfully! You can add another or view graphs.", updated_df except ValueError: # Handle invalid timestamp format error return "Error: Invalid timestamp format. Please use YYYY-MM-DD HH:MM:SS.", current_df_state except Exception as e: # Handle any other errors during addition 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 if no file was uploaded return "No file uploaded.", current_df_state try: # Read the CSV file into a pandas DataFrame uploaded_df = pd.read_csv(csv_file.name) # Basic validation: Check if required columns exist (excluding ID and Note, which we will generate/handle) required_core_cols = ["Timestamp", "Glucose Value", "Meal Tag"] if not all(col in uploaded_df.columns for col in required_core_cols): # Return error if required columns is missing return f"Error: CSV must contain columns: {', '.join(required_core_cols)}", current_df_state # Convert timestamp column to datetime objects uploaded_df["Timestamp"] = pd.to_datetime(uploaded_df["Timestamp"]) # Handle the 'Note' column: add if missing in the uploaded CSV if 'Note' not in uploaded_df.columns: print("Adding 'Note' column to uploaded CSV data as it was missing.") uploaded_df['Note'] = "" # Add with empty string default # Determine the starting ID for the new data start_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 # Assign incrementing IDs to the uploaded data uploaded_df['ID'] = range(start_id, start_id + len(uploaded_df)) # Define the desired column order including 'Note' desired_col_order = ["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"] # Ensure the order of columns matches the desired order for consistent concatenation # Only select columns that exist in the uploaded_df after handling 'Note' cols_to_select = [col for col in desired_col_order if col in uploaded_df.columns] uploaded_df = uploaded_df[cols_to_select] # Concatenate the uploaded data to the state DataFrame updated_df = pd.concat([current_df_state, uploaded_df], ignore_index=True) print(f"CSV data added. Total rows: {len(updated_df)}") # For debugging # Return success message and the updated state return f"Successfully uploaded and processed {len(uploaded_df)} measurements from CSV. View graphs to see trends.", updated_df except Exception as e: # Handle any errors during CSV processing return f"Error processing CSV file: {e}", current_df_state # --- Plotting Function --- 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. """ # Close any open plots before creating a new one to prevent memory issues plt.close('all') if current_df_state.empty: # Create a plot indicating no data if the DataFrame is empty fig, ax = plt.subplots() ax.set_title("No data available. Add measurements first.") return fig # Ensure Timestamp column is in datetime format # Create a copy to avoid SettingWithCopyWarning if type conversion is needed plot_df = current_df_state.copy() plot_df['Timestamp'] = pd.to_datetime(plot_df['Timestamp']) # Filter data by the selected date range try: # Convert start and end dates to datetime objects for comparison # Handle potential None values from DatePicker if not set if start_date is None: start_datetime = plot_df['Timestamp'].min() # Use min timestamp if start date not set else: start_datetime = pd.to_datetime(start_date) if end_date is None: end_datetime = plot_df['Timestamp'].max() # Use max timestamp if end date not set else: # Include the entire end day by adding almost a full day end_datetime = pd.to_datetime(end_date) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1) except ValueError: # Handle potential errors if date conversion fails 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') # Sort by timestamp for chronological plotting if filtered_df.empty: # Create a plot indicating no data for the selected range fig, ax = plt.subplots() ax.set_title(f"No data for the selected date range: {start_date} to {end_date}") return fig # Generate the plot based on the selected chart type fig, ax = plt.subplots(figsize=(12, 6)) # Set figure size for better readability 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(): # Ensure Meal Tag is treated as categorical for correct grouping filtered_df['Meal Tag'] = filtered_df['Meal Tag'].astype('category') # Calculate average glucose by meal tag 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 # --- Gradio UI Tab Creation Functions --- 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(): # Use a Row to place inputs side-by-side with gr.Column(): # Left half for the three input fields 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(): # Right half for the Note textbox note_input = gr.Textbox(label="Note", placeholder="Optional notes about this measurement", lines=4) # Add the new textbox and make it multiline 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"], # Update headers to include 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], # Pass the new note_input value 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 # Return note_input as well 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(): # Using DownloadButton to allow users to download the files 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) # Link the Save Profile button 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] ) # Link the Delete All Data button - update states and download buttons 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] ) # Link the Delete Profile button - update states and clear inputs delete_profile_btn.click( fn=delete_profile_button_click, inputs=[profile_state], # Only pass profile state to the function outputs=[profile_status_output, name_input, email_input, medical_conditions_input, glucose_monitor_input, profile_state] # Return status, clear inputs, and updated state ) # Return all created components in the profile tab 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) # Ensure Timestamp is datetime df['Timestamp'] = pd.to_datetime(df['Timestamp']) # Ensure 'ID' column exists and is unique, regenerate if necessary 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) # Ensure 'Note' column exists, add if missing if 'Note' not in df.columns: print("Adding 'Note' column to existing measurements.") df['Note'] = "" # Add with empty string default # Ensure all required columns are present, add missing ones with default values for col in required_cols: if col not in df.columns: df[col] = "" # Add any other missing required columns with default empty string # Reorder columns to match the required order 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. """ # --- Data Addition Functions --- 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: # Convert timestamp string to datetime object timestamp = pd.to_datetime(timestamp_str) # Determine the next ID next_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 # Create a DataFrame for the new measurement new_measurement = pd.DataFrame([{ "ID": next_id, "Timestamp": timestamp, "Glucose Value": glucose_value, "Meal Tag": meal_tag, "Note": note # Include the note }]) # Concatenate the new measurement to the state DataFrame updated_df = pd.concat([current_df_state, new_measurement], ignore_index=True) print(f"Manual measurement added. Total rows: {len(updated_df)}") # For debugging # Return success message and the updated state return "Measurement added successfully! You can add another or view graphs.", updated_df except ValueError: # Handle invalid timestamp format error return "Error: Invalid timestamp format. Please use YYYY-MM-DD HH:MM:SS.", current_df_state except Exception as e: # Handle any other errors during addition return f"Error adding measurement: {e}", current_df_state # --- Gradio UI Tab Creation Functions --- 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 the new textbox 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"], # Update headers to include 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], # Pass the new note_input value 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 # Return note_input as well """## 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. """ # --- Data Addition Functions --- # add_manual_measurement remains the same as in the previous step 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 if no file was uploaded return "No file uploaded.", current_df_state try: # Read the CSV file into a pandas DataFrame uploaded_df = pd.read_csv(csv_file.name) # Basic validation: Check if required columns exist (excluding ID and Note, which we will generate/handle) required_core_cols = ["Timestamp", "Glucose Value", "Meal Tag"] if not all(col in uploaded_df.columns for col in required_core_cols): # Return error if required columns is missing return f"Error: CSV must contain columns: {', '.join(required_core_cols)}", current_df_state # Convert timestamp column to datetime objects uploaded_df["Timestamp"] = pd.to_datetime(uploaded_df["Timestamp"]) # Handle the 'Note' column: add if missing in the uploaded CSV if 'Note' not in uploaded_df.columns: print("Adding 'Note' column to uploaded CSV data as it was missing.") uploaded_df['Note'] = "" # Add with empty string default # Determine the starting ID for the new data start_id = 1 if current_df_state.empty else current_df_state['ID'].max() + 1 # Assign incrementing IDs to the uploaded data uploaded_df['ID'] = range(start_id, start_id + len(uploaded_df)) # Define the desired column order including 'Note' desired_col_order = ["ID", "Timestamp", "Glucose Value", "Meal Tag", "Note"] # Ensure the order of columns matches the desired order for consistent concatenation # Only select columns that exist in the uploaded_df after handling 'Note' cols_to_select = [col for col in desired_col_order if col in uploaded_df.columns] uploaded_df = uploaded_df[cols_to_select] # Concatenate the uploaded data to the state DataFrame updated_df = pd.concat([current_df_state, uploaded_df], ignore_index=True) print(f"CSV data added. Total rows: {len(updated_df)}") # For debugging # Return success message and the updated state return f"Successfully uploaded and processed {len(uploaded_df)} measurements from CSV. View graphs to see trends.", updated_df except Exception as e: # Handle any errors during CSV processing return f"Error processing CSV file: {e}", current_df_state # The rest of the Gradio UI setup remains the same as in the previous step, # as the changes are internal to the process_csv_upload function. # Only including the function definition here as requested by the instructions. """## 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(): # Use a Row to place inputs side-by-side with gr.Column(): # Left half for the three input fields 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(): # Right half for the Note textbox note_input = gr.Textbox(label="Note", placeholder="Optional notes about this measurement", lines=4) # Add the new textbox and make it multiline 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"], # Update headers to include 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], # Pass the new note_input value 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 # Return note_input as well # The rest of the Gradio UI setup remains the same as in the previous step. # Only including the modified function definition here as requested by the instructions. """## 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. """ # --- Response Function (using Groq API) --- 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). """ # Use Groq API if client is initialized, otherwise use mock response if groq_client: try: # Prepare data context for the LLM # Provide the DataFrame content to the LLM, including the 'Note' column 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: # Limit the data context to avoid exceeding token limits for very large DataFrames # You might want a more sophisticated summary for large datasets # Ensure 'Note' column is included in the to_string() output data_context += blood_glucose_df.tail().to_string(index=False) # Use tail for recent data and exclude index else: data_context += "No data available yet." # Construct the prompt for the LLM # Include previous chat history as context in the required format for Groq API 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."}] # Include previous chat history from state for context, formatting for the LLM for human, assistant in chat_history_state: messages.append({"role": "user", "content": human}) if assistant is not None: messages.append({"role": "assistant", "content": assistant}) # Add the current user message with data context messages.append({"role": "user", "content": data_context + "\n\nUser query: " + message}) chat_completion = groq_client.chat.completions.create( messages=messages, model="llama3-8b-8192", # Using Llama 3 8B model temperature=0.7, max_tokens=500, # Reduced max tokens for potentially faster responses 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}") # Log the error else: # Mock response if API key is not set 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." # Append user message and bot response to history state chat_history_state.append((message, bot_message)) # Return empty string to clear the input box, the updated display history for chatbot, and the updated state return "", chat_history_state, chat_history_state # The rest of the code (UI creation, launch) remains the same as it was in the previous step. # Only including the modified function definition here as requested by the instructions. with gr.Blocks(title="Tokai: Your Diabetes Assistant") as demo: # Initialize session state variables using gr.State 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) # Store history as list of tuples 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 each tab by calling the respective functions create_home_tab(chat_history_state, blood_glucose_state) create_measurements_tab(blood_glucose_state) # Call the modified function create_graphs_tab(blood_glucose_state) create_profile_tab(profile_state, chat_history_state, blood_glucose_state) # Pass all necessary states # Launch the Gradio application with sharing enabled demo.launch(share=True, debug=True) # Enabled debug for better error reporting """**Reasoning**: Consolidate all the Python code into a single file and create the requirements.txt file. """