From 4e73f1b190445fd7e3fc80d72fd8b46a56229bf9 Mon Sep 17 00:00:00 2001 From: taichan03 Date: Sun, 10 Aug 2025 08:33:17 -0400 Subject: [PATCH 1/2] this is to display the sources in the reults and also fix the PDF auto scroller --- frontend/src/api/apiClient.ts | 17 +- frontend/src/pages/DrugSummary/PDFViewer.tsx | 139 +- .../pages/PatientManager/NewPatientForm.tsx | 1567 +++++++++-------- .../pages/PatientManager/PatientManager.tsx | 201 +-- .../pages/PatientManager/PatientSummary.tsx | 1040 ++++++----- .../src/pages/PatientManager/PatientTypes.ts | 13 +- server/api/models/model_medRule.py | 7 +- server/api/views/conversations/views.py | 48 +- server/api/views/listMeds/views.py | 33 +- server/api/views/risk/urls.py | 6 +- server/api/views/risk/views.py | 5 +- .../api/views/risk/views_riskWithSources.py | 303 ++++ 12 files changed, 1916 insertions(+), 1463 deletions(-) create mode 100644 server/api/views/risk/views_riskWithSources.py diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 451c1c41..73b74caf 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -67,6 +67,18 @@ const handleRuleExtraction = async (guid: string) => { } }; +const fetchRiskDataWithSources = async (medication: string, source: "include" | "diagnosis" = "include") => { + try { + const response = await api.post(`/v1/api/riskWithSources`, { + drug: medication, + source: source, + }); + return response.data; + } catch (error) { + console.error("Error fetching risk data: ", error); + throw error; + } +}; interface StreamCallbacks { onContent?: (content: string) => void; @@ -165,7 +177,6 @@ const handleSendDrugSummaryStream = async ( } }; - // Legacy function for backward compatibility const handleSendDrugSummaryStreamLegacy = async ( message: string, @@ -256,7 +267,6 @@ const updateConversationTitle = async ( } }; - export { handleSubmitFeedback, handleSendDrugSummary, @@ -268,5 +278,6 @@ export { deleteConversation, updateConversationTitle, handleSendDrugSummaryStream, - handleSendDrugSummaryStreamLegacy + handleSendDrugSummaryStreamLegacy, + fetchRiskDataWithSources }; \ No newline at end of file diff --git a/frontend/src/pages/DrugSummary/PDFViewer.tsx b/frontend/src/pages/DrugSummary/PDFViewer.tsx index d0cd8636..c2e610c7 100644 --- a/frontend/src/pages/DrugSummary/PDFViewer.tsx +++ b/frontend/src/pages/DrugSummary/PDFViewer.tsx @@ -11,6 +11,8 @@ interface DocumentLoadSuccess { numPages: number; } +const PAGE_INIT_DELAY = 800; + const PDFViewer = () => { const [numPages, setNumPages] = useState(null); const [pageNumber, setPageNumber] = useState(1); @@ -24,14 +26,12 @@ const PDFViewer = () => { null ); - const manualScrollInProgress = useRef(false); - const PAGE_INIT_DELAY = 800; - const headerRef = useRef(null); const containerRef = useRef(null); const contentRef = useRef(null); const pageRefs = useRef>({}); - const initializationRef = useRef(false); + const prevGuidRef = useRef(null); + const isFetchingRef = useRef(false); const location = useLocation(); const navigate = useNavigate(); @@ -39,66 +39,83 @@ const PDFViewer = () => { const guid = params.get("guid"); const pageParam = params.get("page"); - const baseURL = import.meta.env.VITE_API_BASE_URL; - const pdfUrl = useMemo( - () => (guid ? `${baseURL}/v1/api/uploadFile/${guid}` : null), - [guid, baseURL] - ); + const baseURL = import.meta.env.VITE_API_BASE_URL as string | undefined; + + const pdfUrl = useMemo(() => { + const url = guid && baseURL ? `${baseURL}/v1/api/uploadFile/${guid}` : null; + + return url; + }, [guid, baseURL]); useEffect(() => { - pageRefs.current = {}; - setIsDocumentLoaded(false); - initializationRef.current = false; + const nextPage = pageParam ? parseInt(pageParam, 10) : 1; + const guidChanged = guid !== prevGuidRef.current; + + if (guidChanged) { + pageRefs.current = {}; + setIsDocumentLoaded(false); + setNumPages(null); + setPdfData(null); + setPageNumber(1); + } - if (pageParam) { - const page = parseInt(pageParam, 10); - if (!isNaN(page) && page > 0) setTargetPageAfterLoad(page); + if (!isNaN(nextPage) && nextPage > 0) { + setTargetPageAfterLoad(nextPage); } else { setTargetPageAfterLoad(1); } - }, [guid, pageParam]); + + prevGuidRef.current = guid; + }, [guid, pageParam, location.pathname, location.search]); const scrollToPage = useCallback( (page: number) => { - if (page < 1 || !numPages || page > numPages) return; + if (!numPages || page < 1 || page > numPages) { + return; + } + const targetRef = pageRefs.current[page]; - if (!targetRef) return; - - manualScrollInProgress.current = true; - targetRef.scrollIntoView({ behavior: "smooth", block: "start" }); - - const observer = new IntersectionObserver( - (entries, obs) => { - const entry = entries[0]; - if (entry?.isIntersecting) { - manualScrollInProgress.current = false; - obs.disconnect(); - } - }, - { threshold: 0.5 } - ); - observer.observe(targetRef); + if (!targetRef) { + setTimeout(() => scrollToPage(page), 100); + return; + } - const newParams = new URLSearchParams(location.search); - newParams.set("page", String(page)); - navigate(`${location.pathname}?${newParams.toString()}`, { - replace: true, + targetRef.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", }); + + const newParams = new URLSearchParams(location.search); + const oldPage = newParams.get("page"); + if (oldPage !== String(page)) { + newParams.set("page", String(page)); + const newUrl = `${location.pathname}?${newParams.toString()}`; + navigate(newUrl, { replace: true }); + } + setPageNumber(page); }, - [numPages, navigate, location.pathname, location.search] + [numPages, location.pathname, location.search, navigate, pageNumber] ); + // Preload-aware navigation: if not loaded yet, just remember target page. const goToPage = useCallback( (page: number) => { if (typeof page !== "number" || isNaN(page)) return; - if (page < 1) page = 1; - else if (numPages && page > numPages) page = numPages; - setPageNumber(page); - scrollToPage(page); + const clamped = Math.max(1, numPages ? Math.min(page, numPages) : page); + + if (!isDocumentLoaded || !numPages) { + setTargetPageAfterLoad(clamped); + return; + } + + if (clamped === pageNumber) return; + setPageNumber(clamped); + scrollToPage(clamped); }, - [numPages, scrollToPage] + [isDocumentLoaded, numPages, pageNumber, scrollToPage] ); useEffect(() => { @@ -115,21 +132,16 @@ const PDFViewer = () => { }, [goToPage]); useEffect(() => { - if ( - isDocumentLoaded && - numPages && - targetPageAfterLoad && - Object.keys(pageRefs.current).length > 0 - ) { + if (isDocumentLoaded && numPages && targetPageAfterLoad) { const validPage = Math.min(Math.max(1, targetPageAfterLoad), numPages); setPageNumber(validPage); - const timeoutId = setTimeout(() => { + const timer = setTimeout(() => { scrollToPage(validPage); setTargetPageAfterLoad(null); }, PAGE_INIT_DELAY); - return () => clearTimeout(timeoutId); + return () => clearTimeout(timer); } }, [isDocumentLoaded, numPages, targetPageAfterLoad, scrollToPage]); @@ -169,7 +181,13 @@ const PDFViewer = () => { const fetchPdf = useCallback(async () => { if (!pdfUrl) return; + if (isFetchingRef.current) { + console.log("⏳ fetchPdf already in progress, skipping duplicate call"); + return; + } + try { + isFetchingRef.current = true; setLoading(true); setError(null); const token = localStorage.getItem("access"); @@ -193,6 +211,7 @@ const PDFViewer = () => { setPdfData(null); } finally { setLoading(false); + isFetchingRef.current = false; } }, [pdfUrl, isPDF]); @@ -230,13 +249,11 @@ const PDFViewer = () => { ← - Page {pageNumber} of {numPages || "-"} + Page {pageNumber} of {numPages ?? "-"} + +
+
+
+ +
+
+ +
+
+
+

+ Select patient characteristics +

+
+
+ Currently psychotic +
+ +
+
+ handleRadioChange(e, "Psychotic")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "Psychotic")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> +
- )} - {enterNewPatient && ( -
-
-
-

- {isEditing - ? `Edit Patient ${patientInfo.ID} Details` - : "Enter Patient Details"} - {/* Details */} -

-
- -
- -
-
- -
-
- -
- -
-
-

- Select patient characteristics -

-
-
- Currently psychotic -
- -
-
- handleRadioChange(e, "Psychotic")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "Psychotic")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
-
- History of suicide attempt(s) - +
+
+
+
+ History of suicide attempt(s) + info - -
- -
-
- handleRadioChange(e, "Suicide")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "Suicide")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
-
- History or risk of kidney disease - + +
+ +
+
+ handleRadioChange(e, "Suicide")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "Suicide")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+
+
+
+ History or risk of kidney disease + info - -
-
-
- handleRadioChange(e, "Kidney")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "Kidney")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
-
- History or risk of liver disease - + +
+
+
+ handleRadioChange(e, "Kidney")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "Kidney")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+
+
+
+ History or risk of liver disease + info - -
-
-
- handleRadioChange(e, "Liver")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "Liver")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
- -
-
- - History or risk of low blood pressure, or concern for - falls - + +
+
+
+ handleRadioChange(e, "Liver")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "Liver")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+
+ +
+
+ + History or risk of low blood pressure, or concern for + falls + info - -
- -
-
- handleRadioChange(e, "blood_pressure")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "blood_pressure")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
-
- Has weight gain concerns - + +
+ +
+
+ handleRadioChange(e, "blood_pressure")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "blood_pressure")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+
+
+
+ Has weight gain concerns + info - -
- -
-
- handleRadioChange(e, "weight_gain")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "weight_gain")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
-
- Wants to conceive in next 2 years - + +
+ +
+
+ handleRadioChange(e, "weight_gain")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "weight_gain")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+
+
+
+ Wants to conceive in next 2 years + info - -
- -
-
- handleRadioChange(e, "risk_pregnancy")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - - handleRadioChange(e, "risk_pregnancy")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
-
- Any possibility of becoming pregnant - + +
+ +
+
+ handleRadioChange(e, "risk_pregnancy")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + + handleRadioChange(e, "risk_pregnancy")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+
+
+
+ Any possibility of becoming pregnant + info - -
-
-
- handleRadioChange(e, "any_pregnancy")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - - handleRadioChange(e, "any_pregnancy")} - className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" - /> - -
-
-
-
- - {/*
+ + +
+
+ handleRadioChange(e, "any_pregnancy")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + + handleRadioChange(e, "any_pregnancy")} + className="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-600" + /> + +
+
+ +
+ + {/*
*/} -
-
- +
+
+ med.name)} + value={ + (newPatientInfo.PriorMedications && + newPatientInfo.PriorMedications?.split(",")) || + [] + } + placeholder="Start typing..." + label="" + onChange={(chips) => + setNewPatientInfo({ + ...newPatientInfo, + PriorMedications: chips.join(","), + }) + } + /> +
+
+ +
+
+ +
+
- - ); + ) : ( +

+ {isEditing ? "Edit Form" : "Submit"} +

+ )} + + + + + )} +
+ + + ); }; export default NewPatientForm; diff --git a/frontend/src/pages/PatientManager/PatientManager.tsx b/frontend/src/pages/PatientManager/PatientManager.tsx index a0b6e46a..2cb8ede3 100644 --- a/frontend/src/pages/PatientManager/PatientManager.tsx +++ b/frontend/src/pages/PatientManager/PatientManager.tsx @@ -1,19 +1,45 @@ -import {useState} from "react"; -import {Link} from "react-router-dom"; +import { useState } from "react"; +import { Link } from "react-router-dom"; import NewPatientForm from "./NewPatientForm.tsx"; import PatientHistory from "./PatientHistory.tsx"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import PatientSummary from "./PatientSummary.tsx"; -import {Diagnosis, PatientInfo} from "./PatientTypes.ts"; -import {copy} from "../../assets/index.js"; +import { Diagnosis, PatientInfo } from "./PatientTypes.ts"; +import { copy } from "../../assets/index.js"; import Welcome from "../../components/Welcome/Welcome.tsx"; -import {useGlobalContext} from "../../contexts/GlobalContext.tsx"; +import { useGlobalContext } from "../../contexts/GlobalContext.tsx"; const PatientManager = () => { + const [patientInfo, setPatientInfo] = useState({ + ID: "", + Diagnosis: Diagnosis.Manic, + OtherDiagnosis: "", + Description: "", + CurrentMedications: "", + PriorMedications: "", + Depression: "", + Hypomania: "", + Mania: "", + Psychotic: "", + Suicide: "", + Kidney: "", + Liver: "", + blood_pressure: "", + weight_gain: "", + Reproductive: "", + risk_pregnancy: "", + PossibleMedications: { + first: [], + second: [], + third: [], + }, + any_pregnancy: "", + }); - - const [patientInfo, setPatientInfo] = useState({ + const handlePatientDeleted = (deletedId: string) => { + if (patientInfo.ID === deletedId) { + setPatientInfo({ ID: "", Diagnosis: Diagnosis.Manic, OtherDiagnosis: "", @@ -31,106 +57,83 @@ const PatientManager = () => { weight_gain: "", Reproductive: "", risk_pregnancy: "", + any_pregnancy: "", PossibleMedications: { - first: "", - second: "", - third: "", + first: [], + second: [], + third: [], }, - any_pregnancy: "" - }); - - const handlePatientDeleted = (deletedId: string) => { - if (patientInfo.ID === deletedId) { - setPatientInfo({ - ID: "", - Diagnosis: Diagnosis.Manic, - OtherDiagnosis: "", - Description: "", - CurrentMedications: "", - PriorMedications: "", - Depression: "", - Hypomania: "", - Mania: "", - Psychotic: "", - Suicide: "", - Kidney: "", - Liver: "", - blood_pressure: "", - weight_gain: "", - Reproductive: "", - risk_pregnancy: "", - any_pregnancy: "" - }); + }); - setIsPatientDeleted(true); - } - }; + setIsPatientDeleted(true); + } + }; - const [allPatientInfo, setAllPatientInfo] = useState([]); - const [isPatientDeleted, setIsPatientDeleted] = useState(false); - const { - showSummary, - setShowSummary, - enterNewPatient, - setEnterNewPatient, - isEditing, - setIsEditing - } = useGlobalContext(); + const [allPatientInfo, setAllPatientInfo] = useState([]); + const [isPatientDeleted, setIsPatientDeleted] = useState(false); + const { + showSummary, + setShowSummary, + enterNewPatient, + setEnterNewPatient, + isEditing, + setIsEditing, + } = useGlobalContext(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - // TODO: add error and loading state guards + // TODO: add error and loading state guards - const descriptionEl = ( -
- Use our tool to get medication suggestions for bipolar disorder based on - patient characteristics.{" "} - - Read about where we get our data. - -
- ); + const descriptionEl = ( +
+ Use our tool to get medication suggestions for bipolar disorder based on + patient characteristics.{" "} + + Read about where we get our data. + +
+ ); - return ( -
- -
- - - -
-
- ); + return ( +
+ +
+ + + +
+
+ ); }; export default PatientManager; diff --git a/frontend/src/pages/PatientManager/PatientSummary.tsx b/frontend/src/pages/PatientManager/PatientSummary.tsx index 4e9c44b0..2e9e1f61 100644 --- a/frontend/src/pages/PatientManager/PatientSummary.tsx +++ b/frontend/src/pages/PatientManager/PatientSummary.tsx @@ -1,522 +1,626 @@ -import React, {useState, useEffect, useRef} from "react"; -import axios from "axios"; -import {PatientInfo} from "./PatientTypes"; +import React, { useState, useEffect, useRef } from "react"; +import { PatientInfo } from "./PatientTypes"; import Tooltip from "../../components/Tooltip"; import TypingAnimation from "../../components/Header/components/TypingAnimation.tsx"; -import {FaPencilAlt, FaPrint, FaMinus, FaRegThumbsDown} from "react-icons/fa"; +import { FaPencilAlt, FaPrint, FaMinus, FaRegThumbsDown } from "react-icons/fa"; import FeedbackForm from "../Feedback/FeedbackForm"; import Modal from "../../components/Modal/Modal"; -import {EllipsisVertical} from "lucide-react"; - +import { EllipsisVertical } from "lucide-react"; +import { fetchRiskDataWithSources } from "../../api/apiClient.ts"; interface PatientSummaryProps { - showSummary: boolean; - setShowSummary: (state: boolean) => void; - setEnterNewPatient: (isEnteringNewPatient: boolean) => void; - setIsEditing: (isEditing: boolean) => void; - patientInfo: PatientInfo; - isPatientDeleted: boolean; - setPatientInfo: React.Dispatch>; + showSummary: boolean; + setShowSummary: (state: boolean) => void; + setEnterNewPatient: (isEnteringNewPatient: boolean) => void; + setIsEditing: (isEditing: boolean) => void; + patientInfo: PatientInfo; + isPatientDeleted: boolean; + setPatientInfo: React.Dispatch>; } +type SourceItem = { + title: string | null; + publication: string | null; + text: string; + rule_type?: "INCLUDE" | "EXCLUDE" | "include" | "exclude"; + history_type?: string; + guid?: string | null; + page?: number | null; + link_url?: string | null; +}; type RiskData = { - benefits: string[]; - risks: string[]; + benefits: string[]; + risks: string[]; + source?: string; + sources?: SourceItem[]; }; +type MedicationWithSource = { + name: string; + source: "include" | "diagnosis"; +}; + +const truncate = (s = "", n = 220) => + s.length > n ? s.slice(0, n).trim() + "…" : s; +const badge = (label: string) => ( + + {label} + +); const MedicationItem = ({ - medication, - isClicked, - riskData, - loading, - onClick, - }: { - medication: string; - isClicked: boolean; - riskData: RiskData | null; - loading: boolean; - onClick: () => void; + medication, + source, + isClicked, + riskData, + loading, + onClick, +}: { + medication: string; + source: string; + isClicked: boolean; + riskData: RiskData | null; + loading: boolean; + onClick: () => void; }) => { - if (medication === "None") { - return ( -
  • -
    -
    - {medication} -
    -
    -
  • - ); - } - + if (medication === "None") { return ( -
    -
  • -
    -
    - {medication} - {loading && isClicked && ( -
    - -
    - )} -
    -
    -
    - +
  • +
    +
    + {medication} +
    +
    +
  • + ); + } + + return ( +
    +
  • +
    +
    + + {medication} + ({source}) + + {loading && isClicked && ( +
    + +
    + )} +
    +
    +
    + + Sources + +
    +
    + Benefits and risks -
    -
  • - - {isClicked && riskData && ( -
    -
    -
    -

    - Benefits: -

    -
      - {riskData.benefits.map((benefit, index) => ( -
    • - {benefit} -
    • - ))} -
    -
    -
    -

    - Risks: -

    -
      - {riskData.risks.map((risk, index) => ( -
    • - {risk} -
    • - ))} -
    -
    +
    + + + {isClicked && riskData && ( +
    +
    +
    +

    + Benefits: +

    +
      + {riskData.benefits.map((b, i) => ( +
    • + {b} +
    • + ))} +
    +
    +
    +

    + Risks: +

    +
      + {riskData.risks.map((r, i) => ( +
    • + {r} +
    • + ))} +
    +
    +
    + + {/* SOURCES */} + {!!riskData.sources?.length && ( +
    +
    +

    Sources

    + {riskData.source && badge(riskData.source.toUpperCase())} +
    + +
      + {riskData.sources.map((s, idx) => ( +
    • +
      + {s.rule_type && badge(s.rule_type)} + {s.history_type && badge(s.history_type)}
      -
    - )} + +
    + {s.title || "Untitled source"} + + {s.link_url && ( + + View PDF + + )} +
    + + {s.publication && ( +
    + {s.publication} +
    + )} + +

    + {truncate(s.text)} +

    + + {s.page && ( +
    + Page {s.page} +
    + )} + + ))} + +
    + )}
    - ); + )} +
    + ); }; const MedicationTier = ({ - title, - medications, - clickedMedication, - riskData, - loading, - onMedicationClick, - }: { - title: string; - medications: string[]; - clickedMedication: string | null; - riskData: RiskData | null; - loading: boolean; - onMedicationClick: (medication: string) => void; + title, + medications, + clickedMedication, + riskData, + loading, + onMedicationClick, +}: { + title: string; + medications: MedicationWithSource[]; + clickedMedication: string | null; + riskData: RiskData | null; + loading: boolean; + onMedicationClick: (medication: MedicationWithSource) => void; }) => ( - <> -
    - {title}: -
    -
      - {medications.map((medication) => ( - onMedicationClick(medication)} - /> - ))} -
    - + <> +
    + {title}: +
    +
      + {medications.map((medicationObj) => ( + onMedicationClick(medicationObj)} + /> + ))} +
    + ); const PatientSummary = ({ - showSummary, - setShowSummary, - setEnterNewPatient, - setIsEditing, - patientInfo, - isPatientDeleted, - }: PatientSummaryProps) => { - const [loading, setLoading] = useState(false); - const [riskData, setRiskData] = useState(null); - const [clickedMedication, setClickedMedication] = useState( - null - ); + showSummary, + setShowSummary, + setEnterNewPatient, + setIsEditing, + patientInfo, + isPatientDeleted, +}: PatientSummaryProps) => { + const [loading, setLoading] = useState(false); + const [riskData, setRiskData] = useState(null); + const [clickedMedication, setClickedMedication] = useState( + null + ); - const [isModalOpen, setIsModalOpen] = useState({status: false, id: ""}); + const [isModalOpen, setIsModalOpen] = useState({ status: false, id: "" }); - const handleOpenModal = (id: string, event: React.MouseEvent) => { - event.stopPropagation(); - setIsModalOpen({status: true, id: id}); - }; + const handleOpenModal = (id: string, event: React.MouseEvent) => { + event.stopPropagation(); + setIsModalOpen({ status: true, id: id }); + }; - const handleCloseModal = (event: React.MouseEvent) => { - event.stopPropagation(); - setIsModalOpen({status: false, id: ""}); - }; + const handleCloseModal = (event: React.MouseEvent) => { + event.stopPropagation(); + setIsModalOpen({ status: false, id: "" }); + }; - useEffect(() => { - if (isPatientDeleted) { - setShowSummary(true); - setLoading(false); - setRiskData(null); - setClickedMedication(null); - } - }, [isPatientDeleted]); - - useEffect(() => { - setRiskData(null); - setClickedMedication(null); - }, [patientInfo]); - - const handleClickSummary = () => { - setShowSummary(!showSummary); - }; + useEffect(() => { + if (isPatientDeleted) { + setShowSummary(true); + setLoading(false); + setRiskData(null); + setClickedMedication(null); + } + }, [isPatientDeleted, setShowSummary]); - const handleMedicationClick = async (medication: string) => { - if (clickedMedication === medication) { - setClickedMedication(null); - setRiskData(null); - return; - } - - setClickedMedication(medication); - setLoading(true); - try { - const baseUrl = import.meta.env.VITE_API_BASE_URL; - const response = await axios.post(`${baseUrl}/chatgpt/risk`, { - diagnosis: medication, - }); - setRiskData(response.data); - } catch (error) { - console.error("Error fetching data: ", error); - } finally { - setLoading(false); - } - }; + useEffect(() => { + setRiskData(null); + setClickedMedication(null); + }, [patientInfo]); - const handlePatientEdit = () => { - setIsEditing(true); - setEnterNewPatient(true); - handleClickSummary(); - console.log({editingPatient: patientInfo}); - }; + const handleClickSummary = () => { + setShowSummary(!showSummary); + }; + const handleMedicationClick = async (medicationObj: MedicationWithSource) => { + const { name: medication, source } = medicationObj; - const handlePatientPrint = (e: any) => { - e.preventDefault(); - window.print(); - }; + if (clickedMedication === medication) { + setClickedMedication(null); + setRiskData(null); + return; + } - const [isMobileDropDownOpen, setIsMobileDropDownOpen] = useState(false) - const mobileMenuRef = useRef(null); + setClickedMedication(medication); + setLoading(true); - const handleMobileDropDownMenu = () => { - setIsMobileDropDownOpen(!isMobileDropDownOpen) + try { + const data = await fetchRiskDataWithSources(medication, source); + setRiskData(data as RiskData); + } catch (error) { + console.error("Error fetching risk data: ", error); + setRiskData(null); + } finally { + setLoading(false); } + }; + + const handlePatientEdit = () => { + setIsEditing(true); + setEnterNewPatient(true); + handleClickSummary(); + console.log({ editingPatient: patientInfo }); + }; + + const handlePatientPrint = (e: any) => { + e.preventDefault(); + window.print(); + }; - const MobileMenuItem = ({item, onClick}: { item: string, onClick: (e: React.MouseEvent) => void }) => { - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation(); - onClick?.(e); - setIsMobileDropDownOpen(false) - } - return (
    - {item}
    ) + const [isMobileDropDownOpen, setIsMobileDropDownOpen] = useState(false); + const mobileMenuRef = useRef(null); + + const handleMobileDropDownMenu = () => { + setIsMobileDropDownOpen(!isMobileDropDownOpen); + }; + + const MobileMenuItem = ({ + item, + onClick, + }: { + item: string; + onClick: (e: React.MouseEvent) => void; + }) => { + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClick?.(e); + setIsMobileDropDownOpen(false); + }; + return ( +
    + {item} +
    + ); + }; + + useEffect(() => { + const handleClickOutsideMenu = (event: MouseEvent) => { + if ( + mobileMenuRef.current && + !mobileMenuRef.current.contains(event.target as Node) + ) { + setIsMobileDropDownOpen(false); + } + }; + if (isMobileDropDownOpen) { + document.addEventListener("mousedown", handleClickOutsideMenu); } + return () => { + document.removeEventListener("mousedown", handleClickOutsideMenu); + }; + }, [isMobileDropDownOpen]); + const renderMedicationsSection = () => ( +
    +
    + Possible Medications: +
    +
    + {patientInfo.PossibleMedications && ( + <> + +
    + +
    +
    + +
    + + )} +
    +
    + ); - useEffect(() => { - const handleClickOutsideMenu = (event: MouseEvent) => { - if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) { - setIsMobileDropDownOpen(false) - } - } - if (isMobileDropDownOpen) { - document.addEventListener('mousedown', handleClickOutsideMenu); - } - return () => { - document.removeEventListener('mousedown', handleClickOutsideMenu); - }; - }, [isMobileDropDownOpen]); - - const renderMedicationsSection = () => ( -
    -
    - Possible Medications: -
    -
    - {patientInfo.PossibleMedications && ( - <> - +
    +
    + {patientInfo.ID && ( + <> +
    + {!showSummary && ( +
    +
    +

    + Patient Summary +

    +
    + + -
    - +
    +
    +
    + )} + {showSummary && ( +
    +
    +
    +

    + Summary +

    + {isMobileDropDownOpen ? ( +
    +
    + -
    -
    - + { + if (patientInfo.ID) { + handleOpenModal(patientInfo.ID, event); + } + }} + /> +
    +
    +
    + +
    +
    + × +
    +
    - - )} -
    -
    - ); - - return ( -
    -
    -
    - {patientInfo.ID && ( - <> -
    - {!showSummary && ( -
    -
    -

    - Patient Summary -

    -
    - - - -
    -
    -
    + ) : ( +
    +
    + +
    +
    + × +
    +
    + )} + +
    +
    +
    +

    + {" "} + {patientInfo.ID} +

    +

    + Patient details and application +

    +
    +
    +
    +
    +
    +
    + Current State: +
    +
    + {patientInfo.Diagnosis} +
    +
    +
    +
    +
    + Risk Assessment: +
    +
    +
      + {patientInfo.Psychotic === "Yes" && ( +
    • + Currently psychotic +
    • )} - {showSummary && ( -
      -
      -
      -

      - Summary -

      - {isMobileDropDownOpen ? ( -
      -
      - - - { - if (patientInfo.ID) { - handleOpenModal(patientInfo.ID, event); - } - }} - /> -
      -
      -
      - -
      -
      - × -
      -
      -
      - ) : ( -
      -
      - -
      -
      - × -
      -
      - )} - -
      -
      -
      -

      - {" "} - {patientInfo.ID} -

      -

      - Patient details and application -

      -
      -
      -
      -
      -
      -
      - Current State: -
      -
      - {patientInfo.Diagnosis} -
      -
      -
      -
      -
      - Risk Assessment: -
      -
      -
        - {/* Risk Assessment Items */} - {patientInfo.Psychotic === "Yes" && ( -
      • - Currently psychotic -
      • - )} - {patientInfo.Suicide === "Yes" && ( -
      • - - Patient has a history of suicide attempts - + {patientInfo.Suicide === "Yes" && ( +
      • + + Patient has a history of suicide attempts + info - -
      • - )} - {/* Add other risk assessment items similarly */} -
      -
      -
      -
      -
      -
      -
    +
    +
    +
    +
    +
    + -
    - {patientInfo.PriorMedications?.split(",").join( - ", " - )} -
    -
    -
    - {renderMedicationsSection()} -
    -
    -
    + + +
    + {patientInfo.PriorMedications?.split(",").join( + ", " )} +
    - - - - - )} + + {renderMedicationsSection()} + + + + )} - - ); + + + + + )} + + + ); }; export default PatientSummary; diff --git a/frontend/src/pages/PatientManager/PatientTypes.ts b/frontend/src/pages/PatientManager/PatientTypes.ts index 0216bf27..26e40d01 100644 --- a/frontend/src/pages/PatientManager/PatientTypes.ts +++ b/frontend/src/pages/PatientManager/PatientTypes.ts @@ -1,3 +1,8 @@ +export type MedicationWithSource = { + name: string; + source: "include" | "diagnosis"; +}; + export interface PatientInfo { ID?: string; Diagnosis?: Diagnosis; @@ -8,10 +13,10 @@ export interface PatientInfo { Mania?: string; CurrentMedications?: string; PriorMedications?: string; - PossibleMedications?: { - first?: string; - second?: string; - third?: string; + PossibleMedications: { + first: MedicationWithSource[]; + second: MedicationWithSource[]; + third: MedicationWithSource[]; }; Psychotic: string; Suicide: string; diff --git a/server/api/models/model_medRule.py b/server/api/models/model_medRule.py index 3a48c2bc..b8bf4d1b 100644 --- a/server/api/models/model_medRule.py +++ b/server/api/models/model_medRule.py @@ -8,28 +8,24 @@ class MedRule(models.Model): ('INCLUDE', 'Include'), ('EXCLUDE', 'Exclude'), ] - rule_type = models.CharField(max_length=7, choices=RULE_TYPE_CHOICES) history_type = models.CharField(max_length=255) reason = models.TextField(blank=True, null=True) label = models.CharField(max_length=255, blank=True, null=True) explanation = models.TextField(blank=True, null=True) - medications = models.ManyToManyField( Medication, related_name='med_rules' ) - sources = models.ManyToManyField( Embeddings, related_name='med_rules', blank=True, - through='api.MedRuleSource' # Correct fully-qualified through model reference + through='api.MedRuleSource' ) class Meta: db_table = 'api_medrule' - # list of tuples is preferred unique_together = [('rule_type', 'history_type')] def __str__(self): @@ -43,7 +39,6 @@ class MedRuleSource(models.Model): class Meta: db_table = 'api_medrule_sources' - # list of tuples unique_together = [('medrule', 'embedding', 'medication')] def __str__(self): diff --git a/server/api/views/conversations/views.py b/server/api/views/conversations/views.py index d46f8222..342033ab 100644 --- a/server/api/views/conversations/views.py +++ b/server/api/views/conversations/views.py @@ -7,7 +7,7 @@ from bs4 import BeautifulSoup from nltk.stem import PorterStemmer import requests -import openai +from openai import OpenAI, OpenAIError import tiktoken import os import json @@ -25,7 +25,7 @@ def extract_text(request: str) -> JsonResponse: Currently only uses the first 3500 tokens. """ - openai.api_key = os.environ.get("OPENAI_API_KEY") + OpenAI.api_key = os.environ.get("OPENAI_API_KEY") data = json.loads(request.body) webpage_url = data["webpage_url"] @@ -42,7 +42,7 @@ def extract_text(request: str) -> JsonResponse: tokens = get_tokens(text_contents, "cl100k_base") - ai_response = openai.ChatCompletion.create( + ai_response = OpenAI.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ { @@ -64,6 +64,7 @@ def get_tokens(string: str, encoding_name: str) -> str: output_string = encoding.decode(tokens) return output_string + class OpenAIAPIException(APIException): """Custom exception for OpenAI API errors.""" status_code = status.HTTP_500_INTERNAL_SERVER_ERROR @@ -77,6 +78,7 @@ def __init__(self, detail=None, code=None): self.detail = {"error": self.default_detail} self.status_code = code or self.status_code + class ConversationViewSet(viewsets.ModelViewSet): serializer_class = ConversationSerializer permission_classes = [IsAuthenticated] @@ -135,15 +137,15 @@ def update_title(self, request, pk=None): return Response({"status": "Title updated successfully", "title": conversation.title}) def get_chatgpt_response(self, conversation, user_message, page_context=None): + client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) messages = [{ - "role": "system", + "role": "system", "content": "You are a knowledgeable assistant. Balancer is a powerful tool for selecting bipolar medication for patients. We are open-source and available for free use. Your primary role is to assist licensed clinical professionals with information related to Balancer and bipolar medication selection. If applicable, use the supplied tools to assist the professional." }] if page_context: context_message = f"If applicable, please use the following content to ask questions. If not applicable, please answer to the best of your ability: {page_context}" messages.append({"role": "system", "content": context_message}) - for msg in conversation.messages.all(): role = "user" if msg.is_user else "assistant" messages.append({"role": role, "content": msg.content}) @@ -151,7 +153,7 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): messages.append({"role": "user", "content": user_message}) try: - response = openai.ChatCompletion.create( + response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, tools=tools, @@ -159,43 +161,45 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): ) response_message = response.choices[0].message - tool_calls = response_message.get('tool_calls', []) + tool_calls = getattr(response_message, "tool_calls", []) + + tool_calls = response_message.model_dump().get("tool_calls", []) if not tool_calls: return response_message['content'] - # Handle tool calls # Add the assistant's message with tool calls to the conversation messages.append({ "role": "assistant", - "content": response_message.get('content', ''), + "content": response_message.content or "", "tool_calls": tool_calls }) - + # Process each tool call for tool_call in tool_calls: tool_call_id = tool_call['id'] tool_function_name = tool_call['function']['name'] - tool_arguments = json.loads(tool_call['function'].get('arguments', '{}')) - + tool_arguments = json.loads( + tool_call['function'].get('arguments', '{}')) + # Execute the tool results = execute_tool(tool_function_name, tool_arguments) - + # Add the tool response message messages.append({ "role": "tool", "content": str(results), # Convert results to string "tool_call_id": tool_call_id }) - + # Final API call with tool results - final_response = openai.ChatCompletion.create( + final_response = client.chat.completions.create( model="gpt-3.5-turbo", - messages=messages - ) - return final_response.choices[0].message['content'] - except openai.error.OpenAIError as e: + messages=messages + ) + return final_response.choices[0].message.content + except OpenAI.error.OpenAIError as e: logging.error("OpenAI API Error: %s", str(e)) raise OpenAIAPIException(detail=str(e)) except Exception as e: @@ -203,12 +207,12 @@ def get_chatgpt_response(self, conversation, user_message, page_context=None): raise OpenAIAPIException(detail="An unexpected error occurred.") def generate_title(self, conversation): - # Get the first two messages messages = conversation.messages.all()[:2] context = "\n".join([msg.content for msg in messages]) prompt = f"Based on the following conversation, generate a short, descriptive title (max 6 words):\n\n{context}" - response = openai.ChatCompletion.create( + client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a helpful assistant that generates short, descriptive titles."}, @@ -216,4 +220,4 @@ def generate_title(self, conversation): ] ) - return response.choices[0].message['content'].strip() + return response.choices[0].message.content.strip() diff --git a/server/api/views/listMeds/views.py b/server/api/views/listMeds/views.py index 0e7fe584..36ae478f 100644 --- a/server/api/views/listMeds/views.py +++ b/server/api/views/listMeds/views.py @@ -32,21 +32,26 @@ def post(self, request): if diag_query.count() <= 0: return Response({'error': 'Diagnosis not found'}, status=status.HTTP_404_NOT_FOUND) diagnosis = diag_query[0] - meds = {'first': '', 'second': '', 'third': ''} + meds = {'first': [], 'second': [], 'third': []} + + included_set = set(include_result) + excluded_set = set(exclude_result) + for med in include_result: - meds['first'] += med + ", " - for i, line in enumerate(['first', 'second', 'third']): - for suggestion in Suggestion.objects.filter(diagnosis=diagnosis, tier=(i + 1)): - to_exclude = False - for med in exclude_result: - if med in suggestion.medication.name: - to_exclude = True - break - if i > 0 and suggestion.medication.name in include_result: - to_exclude = True - if not to_exclude: - meds[line] += suggestion.medication.name + ", " - meds[line] = meds[line][:-2] if meds[line] else 'None' + meds['first'].append({'name': med, 'source': 'include'}) + + for i, tier_label in enumerate(['first', 'second', 'third']): + suggestions = Suggestion.objects.filter( + diagnosis=diagnosis, tier=i+1) + for suggestion in suggestions: + med_name = suggestion.medication.name + if med_name in excluded_set: + continue + if i > 0 and med_name in included_set: + continue + meds[tier_label].append( + {'name': med_name, 'source': 'diagnosis'}) + return Response(meds) diff --git a/server/api/views/risk/urls.py b/server/api/views/risk/urls.py index 30e53424..8f0c8dd4 100644 --- a/server/api/views/risk/urls.py +++ b/server/api/views/risk/urls.py @@ -1,6 +1,8 @@ from django.urls import path from api.views.risk import views +from api.views.risk.views_riskWithSources import RiskWithSourcesView urlpatterns = [ - path("chatgpt/risk", views.medication, name="risk") -] + path("chatgpt/risk", views.medication, name="risk"), + path("v1/api/riskWithSources", RiskWithSourcesView.as_view()), +] diff --git a/server/api/views/risk/views.py b/server/api/views/risk/views.py index ea1a77a8..99327a8d 100644 --- a/server/api/views/risk/views.py +++ b/server/api/views/risk/views.py @@ -14,10 +14,11 @@ def medication(request): data = json.loads(request.body) if data is not None: - diagnosis = data["diagnosis"] # the variable name is diagnosis but this variable contain the medication name + # the variable name is diagnosis but this variable contain the medication name + diagnosis = data["diagnosis"] else: return JsonResponse({"error": "Diagnosis not found. Request must include diagnosis."}) - + try: med = Medication.objects.get(name=diagnosis) benefits = [f'- {benefit}' for benefit in med.benefits.split(', ')] diff --git a/server/api/views/risk/views_riskWithSources.py b/server/api/views/risk/views_riskWithSources.py new file mode 100644 index 00000000..93795274 --- /dev/null +++ b/server/api/views/risk/views_riskWithSources.py @@ -0,0 +1,303 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from api.views.listMeds.models import Medication, Diagnosis, Suggestion +from api.models.model_medRule import MedRule, MedRuleSource +import openai +import os + + +class RiskWithSourcesView(APIView): + def post(self, request): + openai.api_key = os.environ.get("OPENAI_API_KEY") + + drug = request.data.get("drug") + if not drug: + return Response({"error": "Drug not found. Request must include 'drug'."}, status=status.HTTP_400_BAD_REQUEST) + + source = request.data.get("source") + if source not in ["include", "diagnosis"]: + return Response({"error": "Source must be either 'include' or 'diagnosis'."}, status=status.HTTP_400_BAD_REQUEST) + + # Handle diagnosis source by linking to medrules + if source == "diagnosis": + return self._handle_diagnosis_source(drug) + + if source == "include": + return self._handle_include_source(drug) + + # Handle include source (existing logic) + try: + med = Medication.objects.get(name=drug) + benefits = [f'- {b.strip()}' for b in med.benefits.split(',')] + risks = [f'- {r.strip()}' for r in med.risks.split(',')] + return Response({ + 'benefits': benefits, + 'risks': risks + }) + + except Medication.DoesNotExist: + prompt = ( + f"You are to provide a concise list of 5 key benefits and 5 key risks " + f"for the medication suggested when taking it for Bipolar. Each point should be short, " + f"clear and be kept under 10 words. Begin the benefits section with !!!benefits!!! and " + f"the risks section with !!!risk!!!. Please provide this information for the medication: {drug}." + ) + + try: + ai_response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{"role": "system", "content": prompt}] + ) + except Exception as e: + return Response({"error": f"OpenAI request failed: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + content = ai_response['choices'][0]['message']['content'] + + if '!!!benefits!!!' not in content or '!!!risks!!!' not in content: + return Response({"error": "Unexpected format in OpenAI response."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + benefits_raw = content.split('!!!risks!!!')[0].replace( + '!!!benefits!!!', '').strip() + risks_raw = content.split('!!!risks!!!')[1].strip() + + benefits = [line.strip() + for line in benefits_raw.split('\n') if line.strip()] + risks = [line.strip() + for line in risks_raw.split('\n') if line.strip()] + + return Response({ + 'benefits': benefits, + 'risks': risks + }) + + def _handle_include_source(self, drug): + """Handle include source by looking up medrules for the medication""" + try: + # Get the medication + medication = Medication.objects.get(name=drug) + + print( + f"Found medication '{medication.name}' for '{drug}' with ID {medication.id}") + + # Find medrules that include this medication + medrule_ids = MedRuleSource.objects.filter( + medication=medication, + medrule__rule_type='INCLUDE' + ).values_list('medrule_id', flat=True).distinct() + + medrules = MedRule.objects.filter(id__in=medrule_ids) + print(f"Found {medrules.count()} medrules for {drug}") + benefits = [] + risks = [] + sources_info = [] + + # Extract benefits and sources + for medrule in medrules: + if medrule.explanation: + benefits.append(f"- {medrule.explanation}") + + # Get associated sources through MedRuleSource + medrule_sources = MedRuleSource.objects.filter( + medrule=medrule, + medication=medication + ) + print( + f"Found {medrule_sources.count()} sources for medrule {medrule.id}") + + for source_link in medrule_sources: + embedding = source_link.embedding + + source_info = { + 'title': getattr(embedding, 'title', 'Unknown source'), + 'publication': getattr(embedding, 'publication', ''), + 'text': getattr(embedding, 'text', ''), + 'rule_type': medrule.rule_type, + 'history_type': medrule.history_type, + # Add link data for PDF navigation + 'upload_fileid': getattr(embedding, 'upload_file_id', None), + 'page': getattr(embedding, 'page_num', None), + 'link_url': self._build_pdf_link(embedding) + } + + sources_info.append(source_info) + + # Check EXCLUDE rules for risks + exclude_rules = MedRule.objects.filter( + medications=medication, + rule_type='EXCLUDE' + ) + + for rule in exclude_rules: + if rule.explanation: + risks.append(f"- {rule.explanation}") + + if not benefits and not risks: + basic_benefits = [ + f'- {b.strip()}' for b in medication.benefits.split(',')] + basic_risks = [ + f'- {r.strip()}' for r in medication.risks.split(',')] + + return Response({ + 'benefits': basic_benefits, + 'risks': basic_risks, + 'source': 'include', + 'note': 'No specific medrule sources found, showing general medication information' + }) + + return Response({ + 'benefits': [f'- {b.strip()}' for b in medication.benefits.split(',')], + 'risks': risks if risks else [f'- {r.strip()}' for r in medication.risks.split(',')], + 'source': 'include', + 'sources': sources_info, + 'medrules_found': len(medrules) + len(exclude_rules) + }) + + except Medication.DoesNotExist: + return Response({"error": f"Medication '{drug}' not found."}, status=status.HTTP_404_NOT_FOUND) + + def _handle_diagnosis_source(self, drug): + """Handle diagnosis source by looking up medrules for the medication""" + try: + # Get the medication + medication = Medication.objects.get(name=drug) + + # Find medrules that include this medication + medrules = MedRule.objects.filter( + medications=medication, + rule_type='INCLUDE' + ) + + benefits = [] + risks = [] + sources_info = [] + + # Extract information from medrules and their sources + for medrule in medrules: + if medrule.explanation: + benefits.append(f"- {medrule.explanation}") + + # Get associated sources through MedRuleSource + medrule_sources = MedRuleSource.objects.filter( + medrule=medrule, + medication=medication + ) + + for source_link in medrule_sources: + embedding = source_link.embedding + source_info = { + 'title': getattr(embedding, 'title', 'Unknown source'), + 'publication': getattr(embedding, 'publication', ''), + 'text': getattr(embedding, 'text', ''), + 'rule_type': medrule.rule_type, + 'history_type': medrule.history_type, + # Add link data for PDF navigation + 'guid': getattr(embedding, 'guid', None), + 'page': getattr(embedding, 'page_num', None), + 'link_url': self._build_pdf_link(embedding) + } + sources_info.append(source_info) + + # Also check for exclude rules (risks) + exclude_rules = MedRule.objects.filter( + medications=medication, + rule_type='EXCLUDE' + ) + + for rule in exclude_rules: + if rule.explanation: + risks.append(f"- {rule.explanation}") + + # If no medrule data found, fall back to basic medication data + if not benefits and not risks: + basic_benefits = [ + f'- {b.strip()}' for b in medication.benefits.split(',')] + basic_risks = [ + f'- {r.strip()}' for r in medication.risks.split(',')] + + return Response({ + 'benefits': basic_benefits, + 'risks': basic_risks, + 'source': 'diagnosis', + 'note': 'No specific medrule sources found, showing general medication information' + }) + + return Response({ + 'benefits': benefits if benefits else [f'- {b.strip()}' for b in medication.benefits.split(',')], + 'risks': risks if risks else [f'- {r.strip()}' for r in medication.risks.split(',')], + 'source': 'diagnosis', + 'sources': sources_info, + 'medrules_found': len(medrules) + len(exclude_rules) + }) + + except Medication.DoesNotExist: + # If medication not in database, use AI fallback with diagnosis context + return self._get_ai_response_for_diagnosis(drug) + + def _build_pdf_link(self, embedding): + """Build the PDF viewer link URL by getting the document GUID from UploadFile""" + try: + # Get the upload_fileid from the embedding + upload_fileid = getattr(embedding, 'upload_file_id', None) + page = getattr(embedding, 'page_num', None) + + if not upload_fileid: + return None + + from api.views.uploadFile.models import UploadFile + + # Get the UploadFile record to get the document GUID + upload_file = UploadFile.objects.get(id=upload_fileid) + document_guid = upload_file.guid + + if document_guid: + base_url = "/drugsummary" + if page: + return f"{base_url}?guid={document_guid}&page={page}" + else: + return f"{base_url}?guid={document_guid}" + + except Exception as e: + print(f"Error building PDF link: {e}") + return None + + return None + + def _get_ai_response_for_diagnosis(self, drug): + """Get AI response with diagnosis-specific context""" + prompt = ( + f"You are providing medication information from a diagnosis/clinical perspective. " + f"Provide a concise list of 5 key benefits and 5 key risks for the medication {drug} " + f"when prescribed for Bipolar disorder, focusing on clinical evidence and diagnostic considerations. " + f"Each point should be short, clear and be kept under 10 words. " + f"Begin the benefits section with !!!benefits!!! and the risks section with !!!risk!!!." + ) + + try: + ai_response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[{"role": "system", "content": prompt}] + ) + except Exception as e: + return Response({"error": f"OpenAI request failed: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + content = ai_response['choices'][0]['message']['content'] + + if '!!!benefits!!!' not in content or '!!!risks!!!' not in content: + return Response({"error": "Unexpected format in OpenAI response."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + benefits_raw = content.split('!!!risks!!!')[0].replace( + '!!!benefits!!!', '').strip() + risks_raw = content.split('!!!risks!!!')[1].strip() + + benefits = [line.strip() + for line in benefits_raw.split('\n') if line.strip()] + risks = [line.strip() + for line in risks_raw.split('\n') if line.strip()] + + return Response({ + 'benefits': benefits, + 'risks': risks, + 'source': 'diagnosis', + 'note': 'Generated from AI with diagnosis context - medication not found in database' + }) From 0add944e28e003db5e6d0a6cffead7d5fdd220f8 Mon Sep 17 00:00:00 2001 From: taichan03 Date: Sun, 10 Aug 2025 08:35:28 -0400 Subject: [PATCH 2/2] delete libraries not used --- server/api/views/conversations/views.py | 2 +- server/api/views/risk/views_riskWithSources.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api/views/conversations/views.py b/server/api/views/conversations/views.py index 342033ab..d5921eaf 100644 --- a/server/api/views/conversations/views.py +++ b/server/api/views/conversations/views.py @@ -7,7 +7,7 @@ from bs4 import BeautifulSoup from nltk.stem import PorterStemmer import requests -from openai import OpenAI, OpenAIError +from openai import OpenAI import tiktoken import os import json diff --git a/server/api/views/risk/views_riskWithSources.py b/server/api/views/risk/views_riskWithSources.py index 93795274..d1c01615 100644 --- a/server/api/views/risk/views_riskWithSources.py +++ b/server/api/views/risk/views_riskWithSources.py @@ -1,7 +1,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status -from api.views.listMeds.models import Medication, Diagnosis, Suggestion +from api.views.listMeds.models import Medication from api.models.model_medRule import MedRule, MedRuleSource import openai import os