wardbuddy-test / wardbuddy /learning_interface.py
dyadd's picture
Upload folder using huggingface_hub
4acc379 verified
"""Gradio interface"""
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_learning_interface.ipynb.
# %% auto 0
__all__ = ['logger', 'create_dashboard_css', 'LearningInterface', 'launch_learning_interface']
# %% ../nbs/02_learning_interface.ipynb 4
from typing import Dict, List, Optional, Tuple, Any
import gradio as gr
from pathlib import Path
import asyncio
from datetime import datetime
import pandas as pd
from .clinical_tutor import ClinicalTutor
from .learning_context import setup_logger
logger = setup_logger(__name__)
# %% ../nbs/02_learning_interface.ipynb 6
def create_dashboard_css() -> str:
"""Create custom CSS for dashboard styling"""
return """
/* Global styles */
.gradio-container {
background-color: #0f172a !important; /* slate-900 */
}
/* Card styling */
.dashboard-card {
background-color: #1e293b !important; /* slate-800 */
border: 1px solid #334155 !important; /* slate-700 */
border-radius: 0.5rem !important;
padding: 1rem !important;
margin: 0.5rem 0 !important;
color: #f1f5f9 !important; /* slate-100 */
}
/* Chat container */
.chatbot {
background-color: #1e293b !important; /* slate-800 */
border-color: #334155 !important; /* slate-700 */
}
/* Message bubbles */
.chatbot .message.user {
background-color: #334155 !important; /* slate-700 */
border: 1px solid #475569 !important; /* slate-600 */
color: #f1f5f9 !important; /* slate-100 */
}
.chatbot .message.bot {
background-color: #1e40af !important; /* blue-800 */
border: 1px solid #1e3a8a !important; /* blue-900 */
color: #f1f5f9 !important; /* slate-100 */
}
/* Input fields */
textarea, input[type="text"] {
background-color: #334155 !important; /* slate-700 */
color: #f1f5f9 !important; /* slate-100 */
border: 1px solid #475569 !important; /* slate-600 */
}
textarea:focus, input[type="text"]:focus {
border-color: #3b82f6 !important; /* blue-500 */
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important;
}
/* Buttons */
button.primary {
background-color: #2563eb !important; /* blue-600 */
color: white !important;
}
button.primary:hover {
background-color: #3b82f6 !important; /* blue-500 */
}
button.secondary {
background-color: #475569 !important; /* slate-600 */
color: white !important;
}
button.secondary:hover {
background-color: #64748b !important; /* slate-500 */
}
/* Tabs */
.tab-nav {
background-color: #1e293b !important; /* slate-800 */
border-bottom: 1px solid #334155 !important; /* slate-700 */
}
.tab-nav button {
color: #f1f5f9 !important; /* slate-100 */
}
.tab-nav button.selected {
border-bottom-color: #3b82f6 !important; /* blue-500 */
}
/* Status indicators */
.status-active {
color: #22c55e !important; /* green-500 */
font-weight: 500 !important;
}
.status-completed {
color: #94a3b8 !important; /* slate-400 */
}
/* Headers */
.dashboard-header {
color: #f1f5f9 !important; /* slate-100 */
font-size: 1.5rem !important;
font-weight: 600 !important;
margin-bottom: 1rem !important;
}
/* Tables */
table {
background-color: #1e293b !important; /* slate-800 */
color: #f1f5f9 !important; /* slate-100 */
}
th, td {
border-color: #334155 !important; /* slate-700 */
}
"""
# %% ../nbs/02_learning_interface.ipynb 8
class LearningInterface:
"""
Gradio interface for clinical learning interactions.
Features:
- Natural case discussion chat
- Dynamic learning dashboard
- Post-discussion analysis
- Progress tracking
"""
def __init__(
self,
context_path: Optional[Path] = None,
theme: str = "default"
):
"""Initialize learning interface."""
self.tutor = ClinicalTutor(context_path)
self.theme = theme
self.context_path = context_path
# Track current discussion state
self.current_discussion = {
"started": None,
"case_type": None,
"messages": []
}
logger.info("Learning interface initialized")
async def process_chat(
self,
message: str,
history: List[List[str]],
state: Dict[str, Any]
) -> Tuple[List[List[str]], str, Dict[str, Any]]:
"""
Process chat messages with state management.
Args:
message: User input message
history: Chat history
state: Current interface state
Returns:
tuple: (updated history, cleared message, updated state)
"""
try:
if not message.strip():
return history, "", state
# Start new discussion if none active
if not state.get("discussion_active"):
state["discussion_active"] = True
state["discussion_start"] = datetime.now().isoformat()
# Get tutor response
response = await self.tutor.discuss_case(message)
# Update history - now using list pairs instead of dicts
if history is None:
history = []
history.append([message, response]) # Changed from dict format to list pair
state["last_message"] = datetime.now().isoformat()
return history, "", state
except Exception as e:
logger.error(f"Error in chat: {str(e)}")
return history or [], "", state
async def end_discussion(
self,
history: List[List[str]],
state: Dict[str, Any]
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""
Analyze completed discussion and prepare summary.
Args:
history: Chat history as list of [user_message, assistant_message] pairs
state: Current interface state
Returns:
tuple: (analysis results, updated state)
"""
try:
if not history:
return {
"learning_points": [],
"gaps": {},
"strengths": [],
"suggested_objectives": []
}, state
# Convert history format for analysis
formatted_history = []
for user_msg, assistant_msg in history:
formatted_history.extend([
{"role": "user", "content": user_msg},
{"role": "assistant", "content": assistant_msg}
])
# Get analysis
analysis = await self.tutor.analyze_discussion(formatted_history)
# Reset discussion state
state["discussion_active"] = False
state["discussion_start"] = None
state["last_message"] = None
return analysis, state
except Exception as e:
logger.error(f"Error analyzing discussion: {str(e)}")
return {
"learning_points": [],
"gaps": {},
"strengths": [],
"suggested_objectives": []
}, state
def update_rotation(
self,
specialty: str,
start_date: str,
end_date: str,
focus_areas: str
) -> Tuple[str, str, str, str]:
"""
Update rotation details and return updated values.
Args:
specialty: Rotation specialty
start_date: Start date string
end_date: End date string
focus_areas: Comma-separated focus areas
Returns:
tuple: Updated field values
"""
try:
# Parse focus areas
focus_list = [
area.strip()
for area in focus_areas.split(",")
if area.strip()
]
# Update context
rotation = {
"specialty": specialty,
"start_date": start_date,
"end_date": end_date,
"key_focus_areas": focus_list
}
self.tutor.learning_context.update_rotation(rotation)
# Return updated values
return (
specialty,
start_date,
end_date,
",".join(focus_list)
)
except Exception as e:
logger.error(f"Error updating rotation: {str(e)}")
current = self.tutor.learning_context.current_rotation
return (
current["specialty"],
current["start_date"] or "",
current["end_date"] or "",
",".join(current["key_focus_areas"])
)
def add_objective(
self,
objective: str,
objectives_df: pd.DataFrame
) -> pd.DataFrame:
"""
Add new learning objective and return updated dataframe.
Args:
objective: New objective text
objectives_df: Current objectives dataframe
Returns:
pd.DataFrame: Updated objectives list
"""
try:
if not objective.strip():
return objectives_df
# Add to context
self.tutor.learning_context.add_learning_objective(objective)
# Convert to dataframe
return pd.DataFrame([
[obj["objective"], obj["status"], obj["added"]]
for obj in self.tutor.learning_context.learning_objectives
], columns=["Objective", "Status", "Date Added"])
except Exception as e:
logger.error(f"Error adding objective: {str(e)}")
return objectives_df
def toggle_objective_status(
self,
evt: gr.SelectData, # Updated to use gr.SelectData
objectives_df: pd.DataFrame
) -> pd.DataFrame:
"""
Toggle objective status between active and completed.
Args:
evt: Gradio select event containing row index
objectives_df: Current objectives dataframe
Returns:
pd.DataFrame: Updated objectives list
"""
try:
objective_idx = evt.index[0] # Get selected row index
if objective_idx >= len(objectives_df):
return objectives_df
# Get objective
objective = objectives_df.iloc[objective_idx]["Objective"]
current_status = objectives_df.iloc[objective_idx]["Status"]
# Toggle in context
if current_status == "active":
self.tutor.learning_context.complete_objective(objective)
else:
self.tutor.learning_context.add_learning_objective(objective)
# Update dataframe
return pd.DataFrame([
[obj["objective"], obj["status"], obj["added"]]
for obj in self.tutor.learning_context.learning_objectives
], columns=["Objective", "Status", "Date Added"])
except Exception as e:
logger.error(f"Error toggling objective: {str(e)}")
return objectives_df
def add_feedback_focus(
self,
focus: str,
feedback_df: pd.DataFrame
) -> pd.DataFrame:
"""Add new feedback focus area."""
try:
if not focus.strip():
return feedback_df
# Add to context
self.tutor.learning_context.toggle_feedback_focus(focus, True)
# Update dataframe
return pd.DataFrame([
[pref["focus"], pref["active"]]
for pref in self.tutor.learning_context.feedback_preferences
], columns=["Focus Area", "Active"])
except Exception as e:
logger.error(f"Error adding feedback focus: {str(e)}")
return feedback_df
def toggle_feedback_status(
self,
evt: gr.SelectData, # Updated to use gr.SelectData
feedback_df: pd.DataFrame
) -> pd.DataFrame:
"""Toggle feedback focus active status."""
try:
focus_idx = evt.index[0] # Get selected row index
if focus_idx >= len(feedback_df):
return feedback_df
# Get focus area
focus = feedback_df.iloc[focus_idx]["Focus Area"]
current_status = feedback_df.iloc[focus_idx]["Active"]
# Toggle in context
self.tutor.learning_context.toggle_feedback_focus(
focus,
not current_status
)
# Update dataframe
return pd.DataFrame([
[pref["focus"], pref["active"]]
for pref in self.tutor.learning_context.feedback_preferences
], columns=["Focus Area", "Active"])
except Exception as e:
logger.error(f"Error toggling feedback: {str(e)}")
return feedback_df
def create_interface(self) -> gr.Blocks:
"""Create and configure the Gradio interface"""
with gr.Blocks(
title="Clinical Learning Assistant",
theme=self.theme,
css=create_dashboard_css()
) as interface:
# State management
state = gr.State({
"discussion_active": False,
"discussion_start": None,
"last_message": None
})
# Header
with gr.Row():
gr.Markdown(
"# Clinical Learning Assistant",
elem_classes=["dashboard-header"]
)
with gr.Row():
# Left column - Chat interface
with gr.Column(scale=2):
# Active discussion indicator
discussion_status = gr.Markdown(
"Start a new case discussion",
elem_classes=["dashboard-card"]
)
# Chat interface
chatbot = gr.Chatbot(
height=500,
label="Case Discussion",
show_label=True,
elem_classes=["dashboard-card"]
)
with gr.Row():
msg = gr.Textbox(
label="Present your case or ask questions",
placeholder=(
"Present your case as you would to your supervisor:\n"
"- Start with the chief complaint\n"
"- Include relevant history and findings\n"
"- Share your assessment and plan"
),
lines=5
)
# Add voice input with updated syntax
audio_msg = gr.Audio(
label="Or speak your case",
sources=["microphone"],
type="numpy",
streaming=True
)
with gr.Row():
clear = gr.Button("Clear Discussion")
end_discussion = gr.Button(
"End Discussion & Review",
variant="primary"
)
# Right column - Learning dashboard
with gr.Column(scale=1):
with gr.Tabs():
# Current Rotation tab
with gr.Tab("Current Rotation"):
with gr.Column(elem_classes=["dashboard-card"]):
specialty = gr.Textbox(
label="Specialty",
value=self.tutor.learning_context.current_rotation["specialty"]
)
start_date = gr.Textbox(
label="Start Date (YYYY-MM-DD)",
value=self.tutor.learning_context.current_rotation["start_date"]
)
end_date = gr.Textbox(
label="End Date (YYYY-MM-DD)",
value=self.tutor.learning_context.current_rotation["end_date"]
)
focus_areas = gr.Textbox(
label="Key Focus Areas (comma-separated)",
value=",".join(
self.tutor.learning_context.current_rotation["key_focus_areas"]
)
)
update_rotation_btn = gr.Button(
"Update Rotation",
variant="secondary"
)
# Learning Objectives tab
with gr.Tab("Learning Objectives"):
with gr.Column(elem_classes=["dashboard-card"]):
objectives_df = gr.DataFrame(
headers=["Objective", "Status", "Date Added"],
value=[[
obj["objective"],
obj["status"],
obj["added"]
] for obj in self.tutor.learning_context.learning_objectives],
interactive=True,
wrap=True
)
with gr.Row():
new_objective = gr.Textbox(
label="New Learning Objective",
placeholder="Enter objective..."
)
add_objective_btn = gr.Button(
"Add",
variant="secondary"
)
# Feedback Preferences tab
with gr.Tab("Feedback Focus"):
with gr.Column(elem_classes=["dashboard-card"]):
feedback_df = gr.DataFrame(
headers=["Focus Area", "Active"],
value=[[
pref["focus"],
pref["active"]
] for pref in self.tutor.learning_context.feedback_preferences],
interactive=True,
wrap=True
)
with gr.Row():
new_feedback = gr.Textbox(
label="New Feedback Focus",
placeholder="Enter focus area..."
)
add_feedback_btn = gr.Button(
"Add",
variant="secondary"
)
# Knowledge Profile tab
with gr.Tab("Knowledge Profile"):
with gr.Column(elem_classes=["dashboard-card"]):
# Knowledge Gaps
gr.Markdown("### Knowledge Gaps")
gaps_display = gr.DataFrame(
headers=["Topic", "Confidence"],
value=[[
topic, confidence
] for topic, confidence in
self.tutor.learning_context.knowledge_profile["gaps"].items()
],
interactive=False
)
# Strengths Display
gr.Markdown("### Strengths")
strengths_display = gr.DataFrame(
headers=["Area"],
value=[[strength] for strength in
self.tutor.learning_context.knowledge_profile["strengths"]
],
interactive=False
)
# Recent Progress
gr.Markdown("### Recent Progress")
progress_display = gr.DataFrame(
headers=["Topic", "Improvement", "Date"],
value=[[
prog["topic"],
f"{prog['improvement']:.2f}",
prog["date"]
] for prog in
self.tutor.learning_context.knowledge_profile["recent_progress"]
],
interactive=False
)
# Discussion summary section
summary_section = gr.Column(visible=False)
with summary_section:
gr.Markdown("## Discussion Summary")
# Overview section
with gr.Row():
with gr.Column():
gr.Markdown("### Session Overview")
session_overview = gr.JSON(
label="Discussion Details",
value={
"duration": "0 minutes",
"messages": 0,
"topics_covered": []
}
)
# Learning Points and Gaps
with gr.Row():
with gr.Column():
gr.Markdown("### Key Learning Points")
learning_points = gr.JSON(label="Points to Remember")
with gr.Column():
gr.Markdown("### Knowledge Profile Updates")
with gr.Row():
gaps = gr.JSON(label="Areas for Improvement")
strengths = gr.JSON(label="Demonstrated Strengths")
# Future Learning section
gr.Markdown("### Planning Ahead")
with gr.Row():
with gr.Column():
gr.Markdown("#### Suggested Learning Objectives")
objectives = gr.JSON(label="Consider Adding")
with gr.Column():
gr.Markdown("#### Recommended Focus Areas")
recommendations = gr.JSON(label="Next Steps")
# Action buttons
with gr.Row():
add_selected_objectives = gr.Button(
"Add Selected Objectives",
variant="primary"
)
close_summary = gr.Button("Close Summary")
# Event handlers
# Add new event handler for voice input
def process_audio(audio):
if audio is None:
return None
# Convert audio to text using your preferred method
# For example, you could use transformers pipeline here
try:
from transformers import pipeline
transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-small")
text = transcriber(audio)["text"]
return text
except Exception as e:
logger.error(f"Error transcribing audio: {str(e)}")
return None
# Update the event handler:
audio_msg.stop_recording(
fn=process_audio,
outputs=[msg]
).then(
fn=self.process_chat,
inputs=[msg, chatbot, state],
outputs=[chatbot, msg, state]
).then(
fn=self._update_discussion_status,
inputs=[state],
outputs=[discussion_status]
)
msg.submit(
self.process_chat,
inputs=[msg, chatbot, state],
outputs=[chatbot, msg, state]
).then(
self._update_discussion_status,
inputs=[state],
outputs=[discussion_status]
)
clear.click(
lambda: ([], "", {
"discussion_active": False,
"discussion_start": None,
"last_message": None
}),
outputs=[chatbot, msg, state]
).then(
lambda: "Start a new case discussion",
outputs=[discussion_status]
)
end_discussion.click(
self.end_discussion,
inputs=[chatbot, state],
outputs=[
session_overview,
learning_points,
gaps,
strengths,
objectives,
recommendations
]
).then(
lambda: gr.update(visible=True),
None,
summary_section
).then(
self._refresh_knowledge_profile,
outputs=[gaps_display, strengths_display, progress_display]
)
close_summary.click(
lambda: gr.update(visible=False),
None,
summary_section
)
# Rotation management
update_rotation_btn.click(
self.update_rotation,
inputs=[specialty, start_date, end_date, focus_areas],
outputs=[specialty, start_date, end_date, focus_areas]
)
# Learning objectives management
add_objective_btn.click(
self.add_objective,
inputs=[new_objective, objectives_df],
outputs=[objectives_df]
).then(
lambda: "",
None,
new_objective
)
objectives_df.select(
self.toggle_objective_status,
inputs=[objectives_df],
outputs=[objectives_df]
)
# Feedback preferences management
add_feedback_btn.click(
self.add_feedback_focus,
inputs=[new_feedback, feedback_df],
outputs=[feedback_df]
).then(
lambda: "",
None,
new_feedback
)
feedback_df.select(
self.toggle_feedback_status,
inputs=[feedback_df],
outputs=[feedback_df]
)
# Add selected objectives from summary
add_selected_objectives.click(
self._add_suggested_objectives,
inputs=[objectives],
outputs=[objectives_df]
)
return interface
def _update_discussion_status(self, state: Dict[str, Any]) -> str:
"""Update discussion status display"""
try:
if not state.get("discussion_active"):
return "Start a new case discussion"
start = datetime.fromisoformat(state["discussion_start"])
duration = datetime.now() - start
minutes = int(duration.total_seconds() / 60)
return f"Active discussion ({minutes} minutes)"
except Exception as e:
logger.error(f"Error updating status: {str(e)}")
return "Discussion status unknown"
def _refresh_knowledge_profile(
self
) -> Tuple[List[List[str]], List[List[str]], List[List[str]]]:
"""Refresh knowledge profile displays"""
try:
# Gaps
gaps_data = [[
topic, f"{confidence:.2f}"
] for topic, confidence in
self.tutor.learning_context.knowledge_profile["gaps"].items()
]
# Strengths
strengths_data = [[
strength
] for strength in
self.tutor.learning_context.knowledge_profile["strengths"]
]
# Progress
progress_data = [[
prog["topic"],
f"{prog['improvement']:.2f}",
prog["date"]
] for prog in
self.tutor.learning_context.knowledge_profile["recent_progress"]
]
return gaps_data, strengths_data, progress_data
except Exception as e:
logger.error(f"Error refreshing profile: {str(e)}")
return [], [], []
def _add_suggested_objectives(
self,
evt: gr.SelectData, # Updated to use gr.SelectData
suggested_objectives: List[str]
) -> pd.DataFrame:
"""Add selected suggested objectives to learning objectives"""
try:
selected_indices = [evt.index[0]] # Get selected row index
for idx in selected_indices:
if idx < len(suggested_objectives):
objective = suggested_objectives[idx]
self.tutor.learning_context.add_learning_objective(objective)
return pd.DataFrame([
[obj["objective"], obj["status"], obj["added"]]
for obj in self.tutor.learning_context.learning_objectives
], columns=["Objective", "Status", "Date Added"])
except Exception as e:
logger.error(f"Error adding objectives: {str(e)}")
return pd.DataFrame()
# %% ../nbs/02_learning_interface.ipynb 10
async def launch_learning_interface(
port: Optional[int] = None,
context_path: Optional[Path] = None,
share: bool = False,
theme: str = "default"
) -> None:
"""Launch the learning interface application."""
try:
interface = LearningInterface(context_path, theme)
app = interface.create_interface()
app.launch(
server_port=port,
share=share
)
logger.info(f"Interface launched on port: {port}")
except Exception as e:
logger.error(f"Error launching interface: {str(e)}")
raise