Dhruv-Ty commited on
Commit
b510d01
·
verified ·
1 Parent(s): 4307606

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +290 -91
src/streamlit_app.py CHANGED
@@ -10,13 +10,13 @@ from model import (
10
  extract_and_link_sources,
11
  parse_doctor_response
12
  )
13
- from report_agent import (
14
- orchestrate_report_generation,
15
- detect_report_request,
16
- add_report_download_buttons,
17
- MedicalReport
18
- )
19
  import base64
 
 
 
 
 
 
20
 
21
 
22
  # Set page config with dark theme
@@ -369,12 +369,6 @@ if 'use_rag' not in st.session_state:
369
  st.session_state.use_rag = True
370
  if 'processing' not in st.session_state:
371
  st.session_state.processing = False
372
- if 'report_generation' not in st.session_state:
373
- st.session_state.report_generation = False
374
- if 'report_data' not in st.session_state:
375
- st.session_state.report_data = MedicalReport().model_dump()
376
- if 'report_files' not in st.session_state:
377
- st.session_state.report_files = None
378
 
379
  # Helper function to check if explanation has meaningful content
380
  def has_meaningful_content(text):
@@ -523,16 +517,277 @@ assistant_logo_base64 = get_image_base64(assistant_logo_path)
523
  user_avatar = f"data:image/png;base64,{user_logo_base64}" if user_logo_base64 else "👤"
524
  assistant_avatar = f"data:image/png;base64,{assistant_logo_base64}" if assistant_logo_base64 else "🤖"
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  # Put the toggle in the sidebar - this is the most reliable approach
527
  with st.sidebar:
528
  st.header("Features")
529
  st.session_state.use_rag = st.toggle("Database Search", value=False,
530
  help="Toggle to enable or disable medical database search")
531
- st.session_state.report_generation = st.toggle("Report Generation", value=False,
532
- help="Toggle to enable or disable medical report generation")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
  # Main app area
535
- st.title("What are you Diagnosing Today?")
536
 
537
  # Display chat history
538
  for message in st.session_state.history:
@@ -564,11 +819,6 @@ for message in st.session_state.history:
564
  st.markdown("---")
565
  st.markdown("**Legend:** 🔓 = Open Access (full text available)")
566
 
567
- # Display report download buttons if available
568
- if st.session_state.report_files and st.session_state.report_generation:
569
- html_path, pdf_path, docx_path = st.session_state.report_files
570
- add_report_download_buttons(html_path, pdf_path, docx_path)
571
-
572
  # Display typing indicator if processing
573
  if st.session_state.processing:
574
  with st.container():
@@ -603,77 +853,26 @@ if st.session_state.processing:
603
  if msg["role"] == "user"), None)
604
 
605
  if last_user_message:
606
- # Check if this is a report generation request when toggle is on
607
- if st.session_state.report_generation:
608
- # Call the report generation orchestrator
609
- response, report_files = orchestrate_report_generation(
610
- st.session_state.history,
611
- last_user_message,
612
- st.session_state.report_data
613
- )
614
-
615
- # If we got a response (either a prompt for missing data or a confirmation)
616
- if response:
617
- # Update session state with the report data
618
- st.session_state.report_data = response
619
-
620
- # If we got report files, store them in session state
621
- if report_files:
622
- st.session_state.report_files = report_files
623
-
624
- # Add assistant response to history with report confirmation
625
- st.session_state.history.append({
626
- "role": "assistant",
627
- "content": response
628
- })
629
- else:
630
- # This is a prompt for missing data
631
- st.session_state.history.append({
632
- "role": "assistant",
633
- "content": response
634
- })
635
- else:
636
- # Process as normal conversation if it's not a report request
637
- reply, explanation, evidence = orchestrator_chat(
638
- [msg for msg in st.session_state.history if msg["role"] == "assistant" or
639
- (msg["role"] == "user" and msg["content"] != last_user_message)],
640
- last_user_message,
641
- use_rag=st.session_state.use_rag,
642
- is_follow_up=len(st.session_state.history) > 1
643
- )
644
-
645
- # Clean up the response and explanation
646
- cleaned_reply = remove_reasoning_and_sources(reply)
647
- cleaned_explanation = clean_explanation(explanation) if explanation else ""
648
-
649
- # Add assistant response to history
650
- st.session_state.history.append({
651
- "role": "assistant",
652
- "content": cleaned_reply,
653
- "explanation": cleaned_explanation,
654
- "evidence": evidence if evidence else []
655
- })
656
- else:
657
- # Normal conversation mode (report generation disabled)
658
- reply, explanation, evidence = orchestrator_chat(
659
- [msg for msg in st.session_state.history if msg["role"] == "assistant" or
660
- (msg["role"] == "user" and msg["content"] != last_user_message)],
661
- last_user_message,
662
- use_rag=st.session_state.use_rag,
663
- is_follow_up=len(st.session_state.history) > 1
664
- )
665
-
666
- # Clean up the response and explanation
667
- cleaned_reply = remove_reasoning_and_sources(reply)
668
- cleaned_explanation = clean_explanation(explanation) if explanation else ""
669
-
670
- # Add assistant response to history
671
- st.session_state.history.append({
672
- "role": "assistant",
673
- "content": cleaned_reply,
674
- "explanation": cleaned_explanation,
675
- "evidence": evidence if evidence else []
676
- })
677
  finally:
678
  # Set processing back to false regardless of success/failure
679
  st.session_state.processing = False
 
10
  extract_and_link_sources,
11
  parse_doctor_response
12
  )
 
 
 
 
 
 
13
  import base64
14
+ from reportlab.lib.pagesizes import A4
15
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
+ from reportlab.lib.enums import TA_LEFT
17
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
18
+ from reportlab.lib import colors
19
+ import tempfile
20
 
21
 
22
  # Set page config with dark theme
 
369
  st.session_state.use_rag = True
370
  if 'processing' not in st.session_state:
371
  st.session_state.processing = False
 
 
 
 
 
 
372
 
373
  # Helper function to check if explanation has meaningful content
374
  def has_meaningful_content(text):
 
517
  user_avatar = f"data:image/png;base64,{user_logo_base64}" if user_logo_base64 else "👤"
518
  assistant_avatar = f"data:image/png;base64,{assistant_logo_base64}" if assistant_logo_base64 else "🤖"
519
 
520
+ # Add report generation functions
521
+ def extract_medical_json(conversation_text: str) -> dict:
522
+ """
523
+ Extract medical report data from conversation text into structured JSON.
524
+ Uses the existing orchestrator_chat model.
525
+ """
526
+ system_prompt = """
527
+ You are an expert medical report generator. Extract ALL info from the patient-assistant conversation into this JSON schema:
528
+
529
+ {
530
+ "patient": {"name":"","age":"","gender":""},
531
+ "visit_date":"YYYY-MM-DD",
532
+ "chief_complaint":"",
533
+ "history_of_present_illness":"",
534
+ "past_medical_history":"",
535
+ "medications":"",
536
+ "allergies":"",
537
+ "examination":"",
538
+ "diagnosis":"",
539
+ "recommendations":"",
540
+ "reasoning":"",
541
+ "sources":""
542
+ }
543
+
544
+ Do NOT invent any data. Return ONLY the JSON object.
545
+ """
546
+ # Use our existing orchestrator_chat function
547
+ result, _, _ = orchestrator_chat(
548
+ [], # No chat history needed
549
+ conversation_text,
550
+ use_rag=True,
551
+ is_system_prompt=True,
552
+ system_message=system_prompt
553
+ )
554
+
555
+ # Extract JSON from response
556
+ try:
557
+ # Find JSON object in the response
558
+ json_match = re.search(r'({[\s\S]*})', result)
559
+ if json_match:
560
+ json_str = json_match.group(1)
561
+ return json.loads(json_str)
562
+ else:
563
+ # If no clear JSON format found, try to parse the whole response
564
+ return json.loads(result)
565
+ except json.JSONDecodeError:
566
+ # Fallback with basic structure
567
+ st.error("Failed to parse report data")
568
+ return {
569
+ "patient": {"name":"", "age":"", "gender":""},
570
+ "visit_date": datetime.now().strftime("%Y-%m-%d"),
571
+ "chief_complaint": "Error generating report"
572
+ }
573
+
574
+ def build_medical_report(data: dict) -> bytes:
575
+ """
576
+ Generates a PDF from the extracted JSON and returns PDF bytes.
577
+ """
578
+ # Create a temporary file for the PDF
579
+ fd, temp_path = tempfile.mkstemp(suffix='.pdf')
580
+ os.close(fd)
581
+
582
+ doc = SimpleDocTemplate(
583
+ temp_path,
584
+ pagesize=A4,
585
+ rightMargin=40,
586
+ leftMargin=40,
587
+ topMargin=60,
588
+ bottomMargin=60
589
+ )
590
+
591
+ styles = getSampleStyleSheet()
592
+ styles.add(ParagraphStyle(
593
+ name='SectionHeading',
594
+ fontSize=14,
595
+ leading=16,
596
+ spaceAfter=8,
597
+ alignment=TA_LEFT,
598
+ textColor=colors.darkblue
599
+ ))
600
+
601
+ styles.add(ParagraphStyle(
602
+ name='NormalLeft',
603
+ fontSize=11,
604
+ leading=14,
605
+ spaceAfter=6,
606
+ alignment=TA_LEFT
607
+ ))
608
+
609
+ elems = []
610
+
611
+ # Title
612
+ elems.append(Paragraph("Medical Consultation Report", styles['Title']))
613
+ elems.append(Spacer(1, 12))
614
+
615
+ # Patient info
616
+ patient = data.get('patient', {})
617
+ info = [
618
+ ['Name:', patient.get('name', '–')],
619
+ ['Age:', patient.get('age', '–')],
620
+ ['Gender:', patient.get('gender', '–')],
621
+ ['Date:', data.get('visit_date', datetime.now().strftime("%Y-%m-%d"))],
622
+ ]
623
+
624
+ tbl = Table(info, colWidths=[80, 250])
625
+ tbl.setStyle(TableStyle([
626
+ ('BACKGROUND', (0, 0), (1, 0), colors.lightgrey),
627
+ ('BOX', (0, 0), (-1, -1), 0.5, colors.grey),
628
+ ('INNERGRID', (0, 0), (-1, -1), 0.5, colors.grey),
629
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
630
+ ]))
631
+
632
+ elems += [tbl, Spacer(1, 16)]
633
+
634
+ # Helper to add sections
635
+ def add_section(title, text):
636
+ elems.append(Paragraph(title, styles['SectionHeading']))
637
+ elems.append(Paragraph(text or '–', styles['NormalLeft']))
638
+ elems.append(Spacer(1, 12))
639
+
640
+ # Populate all sections
641
+ add_section("Chief Complaint", data.get('chief_complaint'))
642
+ add_section("History of Present Illness", data.get('history_of_present_illness'))
643
+ add_section("Past Medical History", data.get('past_medical_history'))
644
+ add_section("Medications", data.get('medications'))
645
+ add_section("Allergies", data.get('allergies'))
646
+ add_section("Examination Findings", data.get('examination'))
647
+ add_section("Diagnosis", data.get('diagnosis'))
648
+ add_section("Recommendations", data.get('recommendations'))
649
+ add_section("Reasoning", data.get('reasoning'))
650
+ add_section("Sources", data.get('sources'))
651
+
652
+ doc.build(elems)
653
+
654
+ # Read PDF bytes
655
+ with open(temp_path, 'rb') as file:
656
+ pdf_bytes = file.read()
657
+
658
+ # Clean up the temporary file
659
+ os.unlink(temp_path)
660
+
661
+ return pdf_bytes
662
+
663
+ def format_conversation_history(history, patient_info=None):
664
+ """
665
+ Format the conversation history into a string suitable for LLM processing.
666
+ Optionally adds patient info at the beginning.
667
+ """
668
+ formatted_text = "# Medical Consultation\n\n"
669
+
670
+ # Add patient info if provided
671
+ if patient_info:
672
+ formatted_text += "## Patient Information\n"
673
+ formatted_text += f"* Name: {patient_info.get('name', '')}\n"
674
+ formatted_text += f"* Age: {patient_info.get('age', '')}\n"
675
+ formatted_text += f"* Gender: {patient_info.get('gender', '')}\n\n"
676
+
677
+ formatted_text += "## Conversation Transcript\n\n"
678
+
679
+ for message in history:
680
+ role = message.get("role", "").strip()
681
+ content = message.get("content", "").strip()
682
+
683
+ if not content:
684
+ continue # Skip empty messages
685
+
686
+ if role.lower() == "user":
687
+ formatted_text += f"PATIENT: {content}\n\n"
688
+ elif role.lower() == "assistant":
689
+ formatted_text += f"ASSISTANT: {content}\n\n"
690
+ # Include explanations which often contain diagnostic reasoning
691
+ if "explanation" in message and message["explanation"]:
692
+ explanation = message.get("explanation", "").strip()
693
+ if explanation:
694
+ formatted_text += f"REASONING: {explanation}\n\n"
695
+
696
+ return formatted_text
697
+
698
+ # Function to handle the report generation process
699
+ def generate_and_download_report():
700
+ # Store the current conversation step
701
+ if 'report_step' not in st.session_state:
702
+ st.session_state.report_step = 1
703
+ st.session_state.patient_info = {"name": "", "age": "", "gender": ""}
704
+
705
+ # Step 1: Collect patient name
706
+ if st.session_state.report_step == 1:
707
+ name = st.text_input("Patient Name:")
708
+ if st.button("Next"):
709
+ st.session_state.patient_info["name"] = name
710
+ st.session_state.report_step = 2
711
+ st.rerun()
712
+
713
+ # Step 2: Collect age
714
+ elif st.session_state.report_step == 2:
715
+ age = st.text_input("Patient Age:")
716
+ if st.button("Next"):
717
+ st.session_state.patient_info["age"] = age
718
+ st.session_state.report_step = 3
719
+ st.rerun()
720
+
721
+ # Step 3: Collect gender
722
+ elif st.session_state.report_step == 3:
723
+ gender = st.selectbox("Patient Gender:", ["Male", "Female", "Other", "Prefer not to say"])
724
+ if st.button("Generate Report"):
725
+ st.session_state.patient_info["gender"] = gender
726
+ st.session_state.report_step = 4
727
+ st.rerun()
728
+
729
+ # Step 4: Generate report
730
+ elif st.session_state.report_step == 4:
731
+ with st.spinner("Generating medical report..."):
732
+ # Format conversation with patient info
733
+ conversation_text = format_conversation_history(
734
+ st.session_state.history,
735
+ st.session_state.patient_info
736
+ )
737
+
738
+ # Extract structured data
739
+ report_json = extract_medical_json(conversation_text)
740
+
741
+ # Override with collected patient info
742
+ report_json["patient"] = st.session_state.patient_info
743
+ report_json["visit_date"] = datetime.now().strftime("%Y-%m-%d")
744
+
745
+ # Generate PDF
746
+ pdf_bytes = build_medical_report(report_json)
747
+
748
+ # Offer download
749
+ st.download_button(
750
+ label="📥 Download Medical Report",
751
+ data=pdf_bytes,
752
+ file_name=f"medical_report_{datetime.now().strftime('%Y%m%d')}.pdf",
753
+ mime="application/pdf",
754
+ key="report_download"
755
+ )
756
+
757
+ # Reset the report process for next time
758
+ st.session_state.report_step = 0
759
+
760
+ # Display completion message
761
+ st.success("Your medical report is ready for download!")
762
+
763
  # Put the toggle in the sidebar - this is the most reliable approach
764
  with st.sidebar:
765
  st.header("Features")
766
  st.session_state.use_rag = st.toggle("Database Search", value=False,
767
  help="Toggle to enable or disable medical database search")
768
+
769
+ # Add Generate Report button
770
+ if st.button("Generate Report", use_container_width=True):
771
+ st.session_state.show_report_form = True
772
+ # Reset report step for a new report
773
+ st.session_state.report_step = 1
774
+ st.rerun()
775
+
776
+ # Show report form when button is clicked
777
+ if st.session_state.get('show_report_form', False):
778
+ st.subheader("Report Information")
779
+ generate_and_download_report()
780
+
781
+ # Initialize new session state variables
782
+ if 'show_report_form' not in st.session_state:
783
+ st.session_state.show_report_form = False
784
+ if 'report_step' not in st.session_state:
785
+ st.session_state.report_step = 0
786
+ if 'patient_info' not in st.session_state:
787
+ st.session_state.patient_info = {"name": "", "age": "", "gender": ""}
788
 
789
  # Main app area
790
+ st.title("How are you Feeling Today?")
791
 
792
  # Display chat history
793
  for message in st.session_state.history:
 
819
  st.markdown("---")
820
  st.markdown("**Legend:** 🔓 = Open Access (full text available)")
821
 
 
 
 
 
 
822
  # Display typing indicator if processing
823
  if st.session_state.processing:
824
  with st.container():
 
853
  if msg["role"] == "user"), None)
854
 
855
  if last_user_message:
856
+ # Process as normal conversation
857
+ reply, explanation, evidence = orchestrator_chat(
858
+ [msg for msg in st.session_state.history if msg["role"] == "assistant" or
859
+ (msg["role"] == "user" and msg["content"] != last_user_message)],
860
+ last_user_message,
861
+ use_rag=st.session_state.use_rag,
862
+ is_follow_up=len(st.session_state.history) > 1
863
+ )
864
+
865
+ # Clean up the response and explanation
866
+ cleaned_reply = remove_reasoning_and_sources(reply)
867
+ cleaned_explanation = clean_explanation(explanation) if explanation else ""
868
+
869
+ # Add assistant response to history
870
+ st.session_state.history.append({
871
+ "role": "assistant",
872
+ "content": cleaned_reply,
873
+ "explanation": cleaned_explanation,
874
+ "evidence": evidence if evidence else []
875
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
  finally:
877
  # Set processing back to false regardless of success/failure
878
  st.session_state.processing = False