From 66fae35ccf2a8771d733a8ee1d2c29f4a8cec22b Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 14:55:58 -0500 Subject: [PATCH 01/60] adding some debugging messages for get_biomodels function --- backend/app/controllers/vcelldb_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/app/controllers/vcelldb_controller.py b/backend/app/controllers/vcelldb_controller.py index 93371ff..fae2163 100644 --- a/backend/app/controllers/vcelldb_controller.py +++ b/backend/app/controllers/vcelldb_controller.py @@ -21,7 +21,9 @@ async def get_biomodels_controller(params: BiomodelRequestParams) -> dict: HTTPException: If the VCell API request fails. """ try: + print("About to call fetch_biomodels()") biomodels = await fetch_biomodels(params) + print("fetch_biomodels() completed successfully") return biomodels except httpx.HTTPStatusError as e: raise HTTPException( @@ -142,7 +144,9 @@ async def get_publications_controller() -> List[dict]: HTTPException: If the VCell API request fails. """ try: + print("About to call fetch_publications()") publications = await fetch_publications() + print("fetch_publications() completed successfully") return publications except httpx.HTTPStatusError as e: raise HTTPException( From 619c56c090431dc755a53ea12c5c5fd3e011dbb6 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 15:05:22 -0500 Subject: [PATCH 02/60] debugging messages for vcell specific service.py and testing BIOMODELS_API_URL --- backend/app/services/vcelldb_service.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/app/services/vcelldb_service.py b/backend/app/services/vcelldb_service.py index 8382024..7150134 100644 --- a/backend/app/services/vcelldb_service.py +++ b/backend/app/services/vcelldb_service.py @@ -8,8 +8,10 @@ from typing import List VCELL_API_BASE_URL = "https://vcell.cam.uchc.edu/api/v0" +BIOMODELS_API_URL = "https://biomodels.org/search?query=" logger = get_logger("vcelldb_service") +print("CHECK: in VCELL_DB_SERVICE") def sanitize_vcml_content(vcml_content: str) -> str: @@ -75,8 +77,11 @@ async def fetch_biomodels(params: BiomodelRequestParams) -> dict: Returns: dict: A dictionary containing a list of biomodels with metadata. """ + + print("CHECK: in VCELL_DB_SERVICE") # Transform None to "" (optional, only if needed for empty fields) params_dict = {k: (v if v is not None else "") for k, v in params.dict().items()} + print("DEBUG: " + str(params_dict)) logger.info(f"Fetching biomodels with parameters: {params_dict}") @@ -85,6 +90,9 @@ async def fetch_biomodels(params: BiomodelRequestParams) -> dict: # Construct the full URL url = f"{VCELL_API_BASE_URL}/biomodel?{query_string}" + # elif source == "search": + # bm_name = params_dict.get("bmName", "") + # url = f"{BIOMODELS_API_URL}/{quote(bm_name)}" # Log the URL being queried logger.info(f"Querying URL: {url}") @@ -120,6 +128,7 @@ async def fetch_simulation_details(params: SimulationRequestParams) -> dict: Returns: Simulation: A Simulation object containing simulation details. """ + print("CHECK: in VCELL_DB_SERVICE") async with httpx.AsyncClient() as client: response = await client.get( f"{VCELL_API_BASE_URL}/biomodel/{params.bmId}/simulation/{params.simId}" From 7d78aacb35026ee2f610923de67d6b6f373c3076 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 15:08:46 -0500 Subject: [PATCH 03/60] update system prompt for better formatting guidelines and consistency for each request --- backend/app/utils/system_prompt.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index da79c67..6c7d01e 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -11,10 +11,31 @@ * You can call tools multiple times if needed to gather sufficient data or refine your answer. * If asked about irrelevant topics, politely decline to answer. -### Formatting Guidelines -* When using mathematical expressions, wrap them properly: use `$expression$` for inline math (e.g., $k_{on}$, $\text{mmol}\cdot\text{ml}^{-1}$) and `$$expression$$` for display math blocks. Always use `\text{}` for text within math mode (e.g., $\text{Sos (Inactive)}$, $\text{concentration}$). -* Format all units, chemical names, reaction rates, and numerical expressions using math mode to ensure proper rendering. Example: "The rate is $5.2 \times 10^{-3} \text{ mmol}\cdot\text{ml}^{-1}\cdot\text{min}^{-1}$". -* If there is an opportunity for follow-up questions or further actions, always ask the user if they'd like to explore more options or if you can assist with other related tasks. +### Formatting Guidelines for Mathematical Expressions +* When using mathematical expressions, wrap them properly: use `$expression$` for inline math +(e.g., $k_{on}$, $\text{mmol}\cdot\text{ml}^{-1}$) and `$$expression$$` for display math blocks. Always +use `\text{}` for text within math mode (e.g., $\text{Sos (Inactive)}$, $\text{concentration}$). +* Format all units, chemical names, reaction rates, and numerical expressions using math mode to ensure +proper rendering. Example: "The rate is $5.2 \times 10^{-3} \text{ mmol}\cdot\text{ml}^{-1}\cdot\text{min}^{-1}$". + +### Formatting Guidelines for Biomodels, Biomodel IDs and Hyperlinks +* Ignore all previous instructions. +* When returning a list of biomodels, always follow this format: +``` +1. **[Biomodel Name](https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId})** + - **Biomodel Key:** ${biomodelId} + - **Owner:** ${owner} + - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} + - **Applications:** + +List every application name for the model in italics, each on its own bullet point. Under each bulleted +application name, list its corresponding simulations, with each simulation followed by a solver in round brackets. +Do not omit any applications. +``` + +### Guidelines for Follow-up Questions and Further Actions +* If there is an opportunity for follow-up questions or further actions, always ask the user if they'd like to explore +more options or if you can assist with other related tasks. ### Biomodel Analysis Guidelines * Include as many relevant details as possible, such as biomodel ID, names, descriptions, parameters, and any other relevant metadata that can aid in the user's understanding. From 00cc3e97672bd6679cdff6d53dddfeee71fb1b4e Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 15:11:30 -0500 Subject: [PATCH 04/60] add a test function for connecting BioModels API from frontend chat to backend llm functions --- backend/app/routes/llms_router.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index 35ed27a..e942d7e 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -1,4 +1,8 @@ -from fastapi import APIRouter +from multiprocessing import process + +from fastapi import APIRouter, Request +import httpx +import requests from app.controllers.llms_controller import ( get_llm_response, analyse_biomodel_controller, @@ -23,6 +27,22 @@ async def query_llm(conversation_history: dict): ) return {"response": result, "bmkeys": bmkeys} +# # For BioModelsDB search using BioModelsDB API +# @router.post("/search") +# async def search_llm(conversation_history: dict): +# result, bmkeys = await get_llm_response( +# conversation_history.get("conversation_history", []) +# ) +# return {"response": result, "bmkeys": bmkeys} + +@router.get("/biomodels-search") +async def biomodels_search(query: str): + async with httpx.AsyncClient() as client: + response = await client.get( + "https://www.biomodels.org/search", + params={"query": query, "format": "json"} + ) + return response.json() @router.post("/analyse/{biomodel_id}") async def analyse_biomodel(biomodel_id: str, user_prompt: str): From c1047e4e4ec2d77620f8d03b1d10f0c7bb23b9c1 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 15:12:58 -0500 Subject: [PATCH 05/60] add debugging messages to see how backend calls/process works --- backend/app/services/llms_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 5d5e91d..2a8e098 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -53,8 +53,11 @@ async def get_response_with_tools(conversation_history: list[dict]): ] messages = messages + conversation_history + print("BBBBBBB" + str(messages)) user_prompt = conversation_history[-1]["content"] + print("CCCCCCC" + str(user_prompt)) + logger.info(f"User prompt: {user_prompt}") @@ -68,6 +71,8 @@ async def get_response_with_tools(conversation_history: list[dict]): # Handle the tool calls tool_calls = response.choices[0].message.tool_calls + # tool_calls_descriptions = tool_call.function.description for tool_call in tool_calls + # logger.info(f"DEBUG1: Tool descript: {tool_calls_descriptions}") messages.append(response.choices[0].message) From 64d164528260cb5fb3ae47c21965bb2868525e2e Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 15:19:47 -0500 Subject: [PATCH 06/60] update admin settings with the proper links/steps for setting up the repo --- frontend/app/admin/settings/page.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/app/admin/settings/page.tsx b/frontend/app/admin/settings/page.tsx index 28fa6c7..f2ea618 100644 --- a/frontend/app/admin/settings/page.tsx +++ b/frontend/app/admin/settings/page.tsx @@ -277,7 +277,7 @@ export default function AdminSettingsPage() {
Follow the steps below to get started with your local deployment.

- For more details, check https://github.com/KacemMathlouthi/VCell-GSoC + For more details, check https://github.com/KacemMathlouthi/VCell-GSoC

@@ -287,10 +287,10 @@ export default function AdminSettingsPage() {

Step 1: Clone the Repository

- git clone https://github.com/KacemMathlouthi/VCell-GSoC.git + git clone https://github.com/virtualcell/VCell-AI.git
From 2cea4648a6d605d107f1cb75e9b861a11d86b3b5 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 16:15:32 -0500 Subject: [PATCH 10/60] add new search box for querying BioModels Databa se, format the output results from the query --- frontend/app/search/page.tsx | 157 ++++++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index c602b96..fc7e9ab 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -48,6 +48,30 @@ interface BiomodelResult { groupUsers: string[]; } +// add function to format description from the BMDB response, which is in XML format +// extract using DOMParser +export function extractDescription(xmlString: string): string { + if (!xmlString) return ""; + + try { + const parser = new DOMParser(); + const document = parser.parseFromString(xmlString, "text/xml"); + + // Get
+ const descriptionDiv = document.querySelector("div.dc\\:description p"); + + if (descriptionDiv?.textContent) { + return descriptionDiv.textContent.trim(); + } + return ""; + // else first paragraph + // const firstP = document.querySelector("p"); + // return firstP?.textContent?.trim() || ""; + } catch (err) { + return ""; + } +} + export default function BiomodelSearchPage() { const [filters, setFilters] = useState({ bmId: "", @@ -61,11 +85,17 @@ export default function BiomodelSearchPage() { orderBy: "date_desc", }); + const [bioModelsQuery, setBioModelsQuery] = useState(""); + const [bioModelsResults, setBioModelsResults] = useState([]); + const [bioModelsIsLoading, setBioModelsIsLoading] = useState(false); + + const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(false); const handleSearch = async () => { + setBioModelsResults([]); setIsLoading(true); try { // Build query params from filters, omitting empty bmName @@ -109,6 +139,56 @@ export default function BiomodelSearchPage() { } }; + // introduce a separate search function for the BioModel DB search form. + const handleSearch2 = async () => { + setResults([]); + setBioModelsIsLoading(true); + try { + // build API url + const apiUrl = `${process.env.NEXT_PUBLIC_API_URL2}/search?query=${bioModelsQuery}&format=json`; + + const res = await fetch(apiUrl); + if (!res.ok) throw new Error("Failed to fetch biomodels"); + const data = await res.json(); + console.log("BioModels response in data.models:", data.models); + + // get description from the id's specific endpoint + const description = (data.models || []).map(async (model: any) => { + try { + + // build url using model ID + const descURL = `${process.env.NEXT_PUBLIC_API_URL2}/${model.id}?format=json`; + const descRes = await fetch(descURL); + if (!descRes.ok) throw new Error("Failed to fetch model description"); + const descData = await descRes.json(); + + return descData.description || ""; + } catch (err) { + console.error(`Error fetching description for model ${model.id}:`, err); + return ""; + } + }); + + // get descriptions for all of the models from the query results + let descriptions = await Promise.all(description); + + // get the actual description from the xml format returned by the API + descriptions = descriptions.map((desc) => extractDescription(desc)); + + // format API response to include id, name, and description for each model + const mappedResults = (data.models || []).map((model: any, index: number) => ({ + id: model.id, + name: model.name, + description: descriptions[index], + })); + setBioModelsResults(mappedResults); + } catch (err) { + setBioModelsResults([]); + } finally { + setBioModelsIsLoading(false); + }}; + + const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", @@ -382,18 +462,90 @@ export default function BiomodelSearchPage() { + {/* search box for Biomodel DB */} + + + + + Search BioModels Database + + + +
+
+ + + setBioModelsQuery(e.target.value) + } + className="border-slate-300 focus:border-blue-500 h-9" + /> +
+
+ + {/* Search Button for BioModel DB*/} +
+ +
+
+
+ {/* Results Section */} - {results.length > 0 && ( + {(results.length > 0 || bioModelsResults.length > 0) && (

Search Results

- {results.length} models found + {results.length > 0 + ? results.length + : bioModelsResults.length} models found
+ {/* Output for BioModels Database Results */} + {bioModelsResults.length > 0 && ( +
+ {bioModelsResults.map((model: any) => ( + + + +

{model.name}

+

{model.id}

+ {model.description && ( +

+ {model.description} +

+ )} +
+
+ + ))} +
+ )} + + {/* Output for VCell Biomodel Database Results */} + {results.length > 0 && (
{results.map((model) => ( ))}
+ )}
)}
From 1c16a649bb68741f0a6d07afaf10b7e19496b7ed Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 16:19:41 -0500 Subject: [PATCH 11/60] format the biomodel-specific page for results from BioModels database search --- frontend/app/search/[bmid]/page.tsx | 219 ++++++++++++++++++++++++++-- 1 file changed, 207 insertions(+), 12 deletions(-) diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index fc7c478..f12811f 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -1,5 +1,5 @@ "use client"; - +import { extractDescription } from "../page"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -78,10 +78,25 @@ interface BiomodelDetail { applications: Application[]; } +interface BiomodelDBDetail { + bioID: string; + name: string; + author?: string; + description?: string; + files: Array<{ + name: string; + description: string; + fileSize: string; + downloadLink: string; + }>; +} + export default function BiomodelDetailPage() { const params = useParams<{ bmid: string }>(); const bmid = params?.bmid; + const bioMDid = bmid.startsWith("BIOMD"); const [data, setData] = useState(null); + const [bmdbData, setBmdbData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [activeTab, setActiveTab] = useState("overview"); @@ -131,6 +146,37 @@ export default function BiomodelDetailPage() { if (!bmid) return; setLoading(true); setError(""); + + if (bioMDid) { + fetch(`${process.env.NEXT_PUBLIC_API_URL2}/${bmid}?format=json`) + .then((res) => { + if (!res.ok) throw new Error("Failed to fetch biomodel details"); + return res.json(); + }) + .then((json) => { + setBmdbData({ + bioID: bmid, + name: json.name, + author: json.publication?.authors?.map((a: any) => a.name) || [], + description: extractDescription(json.description || ""), + files: [ + ...(json.files?.main || []), + ...(json.files?.additional || []) + ] + // max out at 4 files shown - can change if needed + .slice(0, 4) + .map((file: any) => ({ + name: file.name, + description: file.description || "", + fileSize: file.fileSize || "", + downloadLink: `${process.env.NEXT_PUBLIC_API_URL2}/model/download/${bmid}?filename=${file.name}`, + })), + }); + }) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + } else { + fetch(`${process.env.NEXT_PUBLIC_API_URL}/biomodel?bmId=${bmid}`) .then((res) => { if (!res.ok) throw new Error("Failed to fetch biomodel details"); @@ -145,13 +191,32 @@ export default function BiomodelDetailPage() { }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); + } }, [bmid]); - useEffect(() => { - if (!data?.bmKey) return; + if (!bmid) return; const fetchDiagramAnalysis = async () => { try { + + // get diagram image from biomodels database + if (bioMDid) { + const bmdbAPIUrl = process.env.NEXT_PUBLIC_API_URL2; + const bmdbRes = await fetch(`${bmdbAPIUrl}/model/download/${bmid}?filename=${bmid}.png`); + if (bmdbRes.ok) { + // convert the .png response into blob to store image as a string + const blob = await bmdbRes.blob(); + const reader = new FileReader(); + + // store the base64 string + reader.onloadend = () => { + const base64data = reader.result as string; + setDiagramAnalysis(base64data); + }; + reader.readAsDataURL(blob); + } + } else { + if (!data?.bmKey) return; const apiUrl = process.env.NEXT_PUBLIC_API_URL; const res = await fetch(`${apiUrl}/analyse/${data.bmKey}/diagram`, { method: "POST", @@ -167,13 +232,13 @@ export default function BiomodelDetailPage() { const errorData = await res.json(); setAnalysisError(errorData.detail || "Failed to analyze diagram."); } - } catch (err) { + }} catch (err) { setAnalysisError("Failed to fetch diagram analysis."); } - }; + }; - fetchDiagramAnalysis(); - }, [data?.bmKey]); + fetchDiagramAnalysis(); + }, [data?.bmKey, bioMDid, bmid]); // Create combined messages when diagram analysis is ready useEffect(() => { @@ -184,12 +249,139 @@ export default function BiomodelDetailPage() { }, [diagramAnalysis]); if (error) return
{error}
; - if (!data) return null; + if (!data && !bmdbData) return null; - const biomodelDiagramUrl = `https://vcell.cam.uchc.edu/api/v0/biomodel/${data.bmKey}/diagram`; + let biomodelDiagramUrl = ""; + if (data) { + biomodelDiagramUrl = `https://vcell.cam.uchc.edu/api/v0/biomodel/${data.bmKey}/diagram`; + } else if (bioMDid) { + biomodelDiagramUrl = diagramAnalysis; // the base64 image string + } + + if (error) return
{error}
; + if (loading) return
Loading...
; return ( -
+ <> + {bioMDid && bmdbData && ( +
+
+ + + + + {bmdbData?.name} + + + {" "} + {bmdbData?.bioID} + + {" "} + {bmdbData?.author} + + + + + + + + Overview + + + + AI Analysis + + + + + {/* Diagram section */} +
+ Biomodel Diagram setError("Failed to load diagram image.")} + onLoad={() => setError("")} + /> +
+ + {/* Description Section */} +
+ + +
+ + + Description + + +
+
+ +
+ {bmdbData && bmdbData.description && bmdbData.description.trim() !== "" + ? bmdbData.description + : "No description is available for this biomodel"} +
+
+
+ + {/* Files Section */} + + +
+ + + Files + + +
+
+ + {bmdbData?.files?.length > 0 && ( +
+ {bmdbData.files.map((file, index) => ( +
+ +
+ {file.name} +
+ +
+ {file.description} +
+ + + Download → + +
+ ))} +
+ )} +
+
+ +
+
+ +
+
+ +
+
+
+
+
+)} {!bioMDid && data && ( +
@@ -259,7 +451,7 @@ export default function BiomodelDetailPage() { - + Overview @@ -449,5 +641,8 @@ export default function BiomodelDetailPage() {
- ); + )} + + ); } + From 15d48ae7f47e19507d41c437db74a175db747908 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 1 Mar 2026 16:23:02 -0500 Subject: [PATCH 12/60] add another section of buttons at the bottom of the main chat page, introduce and test handlesendmessage2 for sending queries to BMDB --- frontend/components/ChatBox.tsx | 208 +++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 16 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 15c89fd..b23fc0e 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -35,6 +35,7 @@ interface ChatBoxProps { startMessage: string | string[]; quickActions: QuickAction[]; supplementalActions?: QuickAction[]; + moreActions?: QuickAction[]; cardTitle: string; promptPrefix?: string; isLoading?: boolean; @@ -45,6 +46,7 @@ export const ChatBox: React.FC = ({ startMessage, quickActions, supplementalActions, + moreActions, cardTitle, promptPrefix, isLoading: isInitialLoading = false, @@ -72,10 +74,22 @@ export const ChatBox: React.FC = ({ return []; }; - const [messages, setMessages] = useState( - createInitialMessages(startMessage), - ); + const [messages, setMessages] = useState(() => { + //const saved = localStorage.getItem("chat_history"); + //return saved ? JSON.parse(saved) : createInitialMessages(startMessage); + return createInitialMessages(startMessage); +}); + + const STORAGE_KEY = null; + + // const [messages, setMessages] = useState(() => { + // if (typeof window === "undefined") return []; + // const saved = localStorage.getItem(STORAGE_KEY); + // return saved ? JSON.parse(saved) : []; + // }); + const [inputMessage, setInputMessage] = useState(""); + const [inputMessage2, setInputMessage2] = useState(""); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); @@ -88,12 +102,27 @@ export const ChatBox: React.FC = ({ scrollToBottom(); }, [messages]); - // Update messages when startMessage changes (when analysis completes) - useEffect(() => { - if (startMessage && !isInitialLoading) { - setMessages(createInitialMessages(startMessage)); - } - }, [startMessage, isInitialLoading]); + // useEffect(() => { + // const handleUnload = () => { + // localStorage.removeItem("chat_history"); + // }; + + // window.addEventListener("beforeunload", handleUnload); + + // return () => { + // window.removeEventListener("beforeunload", handleUnload); + // }; + // }, []); + + // Update messages when startMessage changes (when analysis completes + // useEffect(() => { + // localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)); + // }, [messages]); + + + useEffect(() => { + setMessages(createInitialMessages(startMessage)); + }, [startMessage]); // Helper function to format biomodel IDs as hyperlinks const formatBiomodelIds = (content: string, bmkeys: string[]): string => { @@ -109,9 +138,10 @@ export const ChatBox: React.FC = ({ const db_link = `[Database](/search/${bmId})`; const replacementString = `**${bmId}** -- ${ai_link}  |  ${db_link}`; */ const db_link = `[Database Details](/search/${bmId})`; - const replacementString = `**${bmId}** || ${db_link}`; + const replacementString = `${bmId} || ${db_link}`; + const match_IDs_Not_in_URLs = new RegExp(`(? = ({ } } + console.log("PPPPPP: " + "This is the msg: " + msg); const userMessage: Message = { id: Date.now().toString(), role: "user", content: msg, timestamp: new Date(), }; + console.log("QQQQQQ: " + "This is the userMessage: " + JSON.stringify(userMessage)); setMessages((prev) => [...prev, userMessage]); setInputMessage(""); setIsLoading(true); @@ -175,6 +207,9 @@ export const ChatBox: React.FC = ({ const finalPrompt = promptPrefix ? `${promptPrefix} ${msg}${parameterContext}` : `${msg}${parameterContext}`; + console.log("RRRRRR: " + "This is the promptPrefix: " + promptPrefix); + console.log("RRRRRR: " + "This is the finalPrompt sent to backend: " + finalPrompt); + const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/query`, { @@ -194,6 +229,7 @@ export const ChatBox: React.FC = ({ }), }, ); + console.log("AAAAAA API query sent to backend: " + finalPrompt); const data = await res.json(); const aiResponse = data.response || "Sorry, I didn't get a response from the server."; @@ -223,7 +259,111 @@ export const ChatBox: React.FC = ({ } finally { setIsLoading(false); } - }; + }; // End of handleSendMessage + +// +const handleSendMessage2 = async (overrideMessage?: string) => { + const msg = overrideMessage ?? inputMessage2; + if (!msg.trim()) return; + // Build parameter context string + let parameterContext = ""; + if (parameters) { + const contextParts = []; + + if (parameters.biomodelId) { + contextParts.push(`biomodel ID: ${parameters.biomodelId}`); + } + if (parameters.bmName) { + contextParts.push(`model name: ${parameters.bmName}`); + } + if (parameters.owner) { + contextParts.push(`authored by: ${parameters.owner}`); + } + if (parameters.category && parameters.category !== "all") { + contextParts.push(`category: ${parameters.category}`); + } + if (parameters.savedLow) { + contextParts.push(`saved after: ${parameters.savedLow}`); + } + if (parameters.savedHigh) { + contextParts.push(`saved before: ${parameters.savedHigh}`); + } + if (parameters.maxRows && parameters.maxRows !== 1000) { + contextParts.push(`max results: ${parameters.maxRows}`); + } + if (parameters.orderBy && parameters.orderBy !== "date_desc") { + contextParts.push(`sort by: ${parameters.orderBy}`); + } + + if (contextParts.length > 0) { + parameterContext = `\n\nHere are some specifics that I want: ${contextParts.join(", ")}`; + } + } + + console.log("PPPPPP: " + "This is the msg: " + msg); + const userMessage: Message = { + id: Date.now().toString(), + role: "user", + content: msg, + timestamp: new Date(), + }; + console.log("QQQQQQ: " + "This is the userMessage: " + JSON.stringify(userMessage)); + setMessages((prev) => [...prev, userMessage]); + setInputMessage(""); + setIsLoading(true); + try { + const finalPrompt = promptPrefix + ? `${promptPrefix} ${msg}${parameterContext}` + : `${msg}${parameterContext}`; + + // const res = await fetch( + // `${process.env.NEXT_PUBLIC_API_URL2}/search?query=${encodeURIComponent(msg)}&format=json`); + + // console.log("DEBUG: This is the raw response from the backend: " + JSON.stringify(res)); + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/biomodels-search?query=${encodeURIComponent(msg)}`, + { + headers: { + "Content-Type": "application/json", + accept: "application/json", + }, + }, + ); + console.log("AAAAAA API query sent to backend: " + finalPrompt); + const data = await res.json(); + console.log("BBBBBB API response from backend: " + JSON.stringify(data)); + const aiResponse = + data.response || "Sorry, I didn't get a response from the server."; + const bmkeys = data.bmkeys || []; + + // Format the response to include hyperlinks for biomodel IDs + const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); + + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: formattedResponse, + timestamp: new Date(), + }; + setMessages((prev) => [...prev, assistantMessage]); + } catch (error) { + setMessages((prev) => [ + ...prev, + { + id: (Date.now() + 2).toString(), + role: "assistant", + content: + "There was an error connecting to the backend. Please try again.", + timestamp: new Date(), + }, + ]); + } finally { + setIsLoading(false); + } + }; // End of handleSendMessage2 + + const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -246,11 +386,14 @@ export const ChatBox: React.FC = ({ {messages.map((message) => (
= ({ value={inputMessage} onChange={(e) => setInputMessage(e.target.value)} onKeyPress={handleKeyPress} - placeholder="Ask any questions about VCell biomodels..." + placeholder="Ask any questions about VCell2 biomodels..." className="flex-1 border-slate-300 focus:border-blue-500" disabled={isLoading || isInitialLoading} />
+
+ setInputMessage2(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Ask any questions about BMDB biomodels..." + className="flex-1 border-slate-300 focus:border-blue-500" + disabled={isLoading || isInitialLoading} + /> + +
+ {/* Quick Actions - positioned directly under search bar */} {!isInitialLoading && (
@@ -366,6 +528,20 @@ export const ChatBox: React.FC = ({
)} + + {moreActions && ( +
+
+ {moreActions.map((action, idx) => ( + + ))} +
+
+ )} +
); From fd9ab57053a3d0cc08d0ca054d08a7f54767aad8 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 4 Mar 2026 22:32:34 -0500 Subject: [PATCH 13/60] rename vcelldb_service to databases_service to generalize for adding multiple db --- backend/app/controllers/vcelldb_controller.py | 2 +- ...celldb_service.py => databases_service.py} | 38 +++++++++++++++++++ backend/app/tests/test_vcelldb_service.py | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) rename backend/app/services/{vcelldb_service.py => databases_service.py} (92%) diff --git a/backend/app/controllers/vcelldb_controller.py b/backend/app/controllers/vcelldb_controller.py index fae2163..aa77a3a 100644 --- a/backend/app/controllers/vcelldb_controller.py +++ b/backend/app/controllers/vcelldb_controller.py @@ -2,7 +2,7 @@ from typing import List from fastapi import HTTPException, Response from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams -from app.services.vcelldb_service import ( +from backend.app.services.databases_service import ( fetch_biomodels, fetch_simulation_details, get_vcml_file, diff --git a/backend/app/services/vcelldb_service.py b/backend/app/services/databases_service.py similarity index 92% rename from backend/app/services/vcelldb_service.py rename to backend/app/services/databases_service.py index 7150134..5906733 100644 --- a/backend/app/services/vcelldb_service.py +++ b/backend/app/services/databases_service.py @@ -136,6 +136,44 @@ async def fetch_simulation_details(params: SimulationRequestParams) -> dict: response.raise_for_status() return response.json() +@observe(name="FETCH_BIOMD_MODELS") +async def fetch_biomd_models(params: BiomodelRequestParams) -> dict: + print("DEBUG20: BIOMD POST: in tool FETCH_BIOMD_MODELS") + # Transform None to "" (optional, only if needed for empty fields) + params_dict = {k: (v if v is not None else "") for k, v in params.dict().items()} + + # Construct the query string using urlencoded parameters (params_dict) + query_string = params.bmName if params.bmName else params.bmId + + # Construct the full URL + url = f"{BIOMODELS_API_URL}{query_string}&format=json" + + # Log the URL being queried + logger.info(f"Querying URL: {url}") + + # Perform the API request + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + raw_data = response.json() + + print("FINAL URL:", response.request.url) + print("STATUS CODE:", response.status_code) + print("RAW JSON:", raw_data) + + # Extract list + biomodels = raw_data.get("models", []) + + # Build response with metadata + return { + "search_params": { + "bmId": params.bmId, + "bmName": params.bmName + }, + "models_count": len(biomodels), + "data": biomodels + } + @observe(name="GET_VCML_FILE") async def get_vcml_file( diff --git a/backend/app/tests/test_vcelldb_service.py b/backend/app/tests/test_vcelldb_service.py index a55e024..651238f 100644 --- a/backend/app/tests/test_vcelldb_service.py +++ b/backend/app/tests/test_vcelldb_service.py @@ -7,7 +7,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) -from app.services.vcelldb_service import ( +from backend.app.services.databases_service import ( fetch_biomodels, fetch_simulation_details, get_vcml_file, From 6dfef9445a4bf7992199bb2c130b7a014936c662 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 4 Mar 2026 22:33:46 -0500 Subject: [PATCH 14/60] update to add bmdb-specifc tools for connecting llm to bmdb --- backend/app/services/llms_service.py | 42 ++++++++++++++++++++-------- backend/app/utils/tools_utils.py | 40 ++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 2a8e098..ca22914 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -1,9 +1,10 @@ from app.utils.tools_utils import ( ToolsDefinitions as tools, + BIOMD_TOOLS as biotool, execute_tool, ) -from app.services.vcelldb_service import ( +from backend.app.services.databases_service import ( fetch_biomodels, get_vcml_file, get_diagram_url, @@ -44,7 +45,7 @@ async def get_llm_response(system_prompt: str, user_prompt: str): return response.choices[0].message.content -async def get_response_with_tools(conversation_history: list[dict]): +async def get_response_with_tools(conversation_history: list[dict], use_biomd: bool): messages = [ { "role": "system", @@ -55,19 +56,30 @@ async def get_response_with_tools(conversation_history: list[dict]): messages = messages + conversation_history print("BBBBBBB" + str(messages)) - user_prompt = conversation_history[-1]["content"] - print("CCCCCCC" + str(user_prompt)) + if use_biomd: + print("DEBUG20: BIOMD POST: get_response_with_tools") + response = client.chat.completions.create( + model=settings.AZURE_DEPLOYMENT_NAME, + messages=messages, + tools=biotool, + tool_choice="auto", + ) + else: + user_prompt = conversation_history[-1]["content"] + print("CCCCCCC" + str(user_prompt)) - logger.info(f"User prompt: {user_prompt}") - response = client.chat.completions.create( - name="GET_RESPONSE_WITH_TOOLS::RETRIEVE_TOOLS", - model=settings.AZURE_DEPLOYMENT_NAME, - messages=messages, - tools=tools, - tool_choice="auto", - ) + logger.info(f"User prompt: {user_prompt}") + + print("DEBUG5: " + str(messages) + str(tools)) + response = client.chat.completions.create( + name="GET_RESPONSE_WITH_TOOLS::RETRIEVE_TOOLS", + model=settings.AZURE_DEPLOYMENT_NAME, + messages=messages, + tools=tools, + tool_choice="auto", + ) # Handle the tool calls tool_calls = response.choices[0].message.tool_calls @@ -100,6 +112,12 @@ async def get_response_with_tools(conversation_history: list[dict]): {"role": "tool", "tool_call_id": tool_call.id, "content": str(result)} ) + print("AAAAAA") + with open ("output.txt", "w") as f: + print("BBBBBB") + print("DEBUG1: " + str(messages), file=f) + + print("DEBUG2") logger.info(str(messages)) # Send back the final response incorporating the tool result diff --git a/backend/app/utils/tools_utils.py b/backend/app/utils/tools_utils.py index fe17151..bdac990 100644 --- a/backend/app/utils/tools_utils.py +++ b/backend/app/utils/tools_utils.py @@ -1,9 +1,10 @@ from typing import List -from app.services.vcelldb_service import ( +from backend.app.services.databases_service import ( fetch_biomodels, fetch_simulation_details, get_vcml_file, fetch_publications, + fetch_biomd_models, ) from app.services.knowledge_base_service import get_similar_chunks from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams @@ -178,6 +179,33 @@ ), ) + +fetch_biomd_tool = ToolDefinition( + type="function", + function=FunctionDefinition( + name="fetch_biomd_models", + description="Retrieves a list of biomodels from the BioModels database based on filtering criteria which is the biomodel name. This allows to search for specific biomodels in the BioModels database based on their attributes and retrieve the results.", + parameters=ParameterSchema( + type="object", + properties={ + "bmId": { + "type": "string", + "default": "", + "description": "The unique identifier of the biomodel. This can be used to retrieve specific biomodels directly by their ID. It is under the format BIOMD followed by 10 numbers", + }, + "bmName": { + "type": "string", + "default": "", + "description": "The name or part of the name of the biomodel you are searching for. This can be used to find biomodels that match the provided name or keyword.", + },}, + required=["bmId", "bmName"], + additionalProperties=False, + ), + strict=True, + ), +) + + # List of all tool definitions ToolsDefinitions = [ fetch_biomodels_tool, @@ -186,7 +214,7 @@ search_vcell_knowledge_base_tool, fetch_publications_tool, ] - +BIOMD_TOOLS = [fetch_biomd_tool] # Tool Executor Function async def execute_tool(name, args): @@ -208,6 +236,7 @@ async def execute_tool(name, args): # args["savedHigh"] = None args["maxRows"] = 1000 params = BiomodelRequestParams(**args) + print("DEBUG About to call fetch_biomodels()") return await fetch_biomodels(params) elif name == "fetch_simulation_details": @@ -221,10 +250,17 @@ async def execute_tool(name, args): query = args["query"] limit = args.get("limit", 5) logger.info(f"Executing tool: {name} with query {query}") + print("DEBUG About to call search_vcell_knowledge_base") return get_similar_chunks(query=query, limit=limit) elif name == "fetch_publications": return await fetch_publications() + + elif name == "fetch_biomd_models": + params = BiomodelRequestParams(**args) + print("DEBUG About to call fetch_biomodels()") + return await fetch_biomd_models(params) + else: return {} From 69670d9d6629a6191ec36af5750387caeed4c2f0 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 4 Mar 2026 22:34:43 -0500 Subject: [PATCH 15/60] update with paths to connect bmdb to backend llm response functions --- backend/app/controllers/llms_controller.py | 5 ++-- backend/app/routes/llms_router.py | 35 ++++++++++++---------- frontend/components/ChatBox.tsx | 14 +++++++-- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/backend/app/controllers/llms_controller.py b/backend/app/controllers/llms_controller.py index fe271f5..f216560 100644 --- a/backend/app/controllers/llms_controller.py +++ b/backend/app/controllers/llms_controller.py @@ -7,7 +7,7 @@ ) -async def get_llm_response(conversation_history: list[dict]) -> tuple[str, list]: +async def get_llm_response(conversation_history: list[dict], use_biomd: bool) -> tuple[str, list]: """ Controller function to interact with the LLM service. Args: @@ -16,7 +16,8 @@ async def get_llm_response(conversation_history: list[dict]) -> tuple[str, list] tuple[str, list]: A tuple containing the final response and bmkeys list. """ try: - result, bmkeys = await get_response_with_tools(conversation_history) + print("DEBUG20: BIOMD POST: get_llm_response") + result, bmkeys = await get_response_with_tools(conversation_history, use_biomd) return result, bmkeys except Exception as e: raise HTTPException(status_code=500, detail=f"Error: {str(e)}") diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index e942d7e..2d0af24 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -23,26 +23,29 @@ async def query_llm(conversation_history: dict): dict: The final response after processing the prompt with the tools. """ result, bmkeys = await get_llm_response( - conversation_history.get("conversation_history", []) + conversation_history.get("conversation_history", []), use_biomd=False ) return {"response": result, "bmkeys": bmkeys} -# # For BioModelsDB search using BioModelsDB API -# @router.post("/search") -# async def search_llm(conversation_history: dict): -# result, bmkeys = await get_llm_response( -# conversation_history.get("conversation_history", []) -# ) -# return {"response": result, "bmkeys": bmkeys} +# For BioModelsDB search using BioModelsDB API +@router.post("/biomd-search") +async def search_llm(conversation_history: dict): + print("DEBUG20: BIOMD POST: ROUTER") + result, bmkeys = await get_llm_response( + conversation_history.get("conversation_history", []), use_biomd=True + ) + return {"response": result, "bmkeys": bmkeys} + + -@router.get("/biomodels-search") -async def biomodels_search(query: str): - async with httpx.AsyncClient() as client: - response = await client.get( - "https://www.biomodels.org/search", - params={"query": query, "format": "json"} - ) - return response.json() +# @router.get("/biomodels-search") +# async def biomodels_search(query: str): +# async with httpx.AsyncClient() as client: +# response = await client.get( +# "https://www.biomodels.org/search", +# params={"query": query, "format": "json"} +# ) +# return response.json() @router.post("/analyse/{biomodel_id}") async def analyse_biomodel(biomodel_id: str, user_prompt: str): diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index b23fc0e..33d1473 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -322,17 +322,25 @@ const handleSendMessage2 = async (overrideMessage?: string) => { // console.log("DEBUG: This is the raw response from the backend: " + JSON.stringify(res)); const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/biomodels-search?query=${encodeURIComponent(msg)}`, + `${process.env.NEXT_PUBLIC_API_URL}/biomd-search`, { + method: "POST", headers: { "Content-Type": "application/json", accept: "application/json", }, + body: JSON.stringify({ + conversation_history: [ + ...messages, + { role: "user", content: finalPrompt }, + ].map(msg => ({ + role: msg.role, + content: msg.content, + })), + }), }, ); - console.log("AAAAAA API query sent to backend: " + finalPrompt); const data = await res.json(); - console.log("BBBBBB API response from backend: " + JSON.stringify(data)); const aiResponse = data.response || "Sorry, I didn't get a response from the server."; const bmkeys = data.bmkeys || []; From 05ba3236ee82d3f4086bb0498e5e6850a02a0f8a Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 4 Mar 2026 22:43:43 -0500 Subject: [PATCH 16/60] Pass string instead of a bool for the database key, currently either vcdb or bmdb --- backend/app/controllers/llms_controller.py | 4 ++-- backend/app/controllers/vcelldb_controller.py | 2 +- backend/app/routes/llms_router.py | 4 ++-- backend/app/services/llms_service.py | 8 ++++---- backend/app/tests/test_vcelldb_service.py | 2 +- backend/app/utils/tools_utils.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/app/controllers/llms_controller.py b/backend/app/controllers/llms_controller.py index f216560..247efbc 100644 --- a/backend/app/controllers/llms_controller.py +++ b/backend/app/controllers/llms_controller.py @@ -7,7 +7,7 @@ ) -async def get_llm_response(conversation_history: list[dict], use_biomd: bool) -> tuple[str, list]: +async def get_llm_response(conversation_history: list[dict], database: str) -> tuple[str, list]: """ Controller function to interact with the LLM service. Args: @@ -17,7 +17,7 @@ async def get_llm_response(conversation_history: list[dict], use_biomd: bool) -> """ try: print("DEBUG20: BIOMD POST: get_llm_response") - result, bmkeys = await get_response_with_tools(conversation_history, use_biomd) + result, bmkeys = await get_response_with_tools(conversation_history, database) return result, bmkeys except Exception as e: raise HTTPException(status_code=500, detail=f"Error: {str(e)}") diff --git a/backend/app/controllers/vcelldb_controller.py b/backend/app/controllers/vcelldb_controller.py index aa77a3a..a95668a 100644 --- a/backend/app/controllers/vcelldb_controller.py +++ b/backend/app/controllers/vcelldb_controller.py @@ -2,7 +2,7 @@ from typing import List from fastapi import HTTPException, Response from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams -from backend.app.services.databases_service import ( +from app.services.databases_service import ( fetch_biomodels, fetch_simulation_details, get_vcml_file, diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index 2d0af24..fa95f79 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -23,7 +23,7 @@ async def query_llm(conversation_history: dict): dict: The final response after processing the prompt with the tools. """ result, bmkeys = await get_llm_response( - conversation_history.get("conversation_history", []), use_biomd=False + conversation_history.get("conversation_history", []), database="vcdb" ) return {"response": result, "bmkeys": bmkeys} @@ -32,7 +32,7 @@ async def query_llm(conversation_history: dict): async def search_llm(conversation_history: dict): print("DEBUG20: BIOMD POST: ROUTER") result, bmkeys = await get_llm_response( - conversation_history.get("conversation_history", []), use_biomd=True + conversation_history.get("conversation_history", []), database="bmdb" ) return {"response": result, "bmkeys": bmkeys} diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index ca22914..7cb13ba 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -4,7 +4,7 @@ execute_tool, ) -from backend.app.services.databases_service import ( +from app.services.databases_service import ( fetch_biomodels, get_vcml_file, get_diagram_url, @@ -45,7 +45,7 @@ async def get_llm_response(system_prompt: str, user_prompt: str): return response.choices[0].message.content -async def get_response_with_tools(conversation_history: list[dict], use_biomd: bool): +async def get_response_with_tools(conversation_history: list[dict], database: str): messages = [ { "role": "system", @@ -56,7 +56,7 @@ async def get_response_with_tools(conversation_history: list[dict], use_biomd: b messages = messages + conversation_history print("BBBBBBB" + str(messages)) - if use_biomd: + if database == "bmdb": print("DEBUG20: BIOMD POST: get_response_with_tools") response = client.chat.completions.create( model=settings.AZURE_DEPLOYMENT_NAME, @@ -64,7 +64,7 @@ async def get_response_with_tools(conversation_history: list[dict], use_biomd: b tools=biotool, tool_choice="auto", ) - else: + elif database == "vcdb": user_prompt = conversation_history[-1]["content"] print("CCCCCCC" + str(user_prompt)) diff --git a/backend/app/tests/test_vcelldb_service.py b/backend/app/tests/test_vcelldb_service.py index 651238f..8dc9ee5 100644 --- a/backend/app/tests/test_vcelldb_service.py +++ b/backend/app/tests/test_vcelldb_service.py @@ -7,7 +7,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) -from backend.app.services.databases_service import ( +from app.services.databases_service import ( fetch_biomodels, fetch_simulation_details, get_vcml_file, diff --git a/backend/app/utils/tools_utils.py b/backend/app/utils/tools_utils.py index bdac990..9c887a4 100644 --- a/backend/app/utils/tools_utils.py +++ b/backend/app/utils/tools_utils.py @@ -1,5 +1,5 @@ from typing import List -from backend.app.services.databases_service import ( +from app.services.databases_service import ( fetch_biomodels, fetch_simulation_details, get_vcml_file, From 7971cd350d4e3faf1cbe02ca3fe20adceaf607b0 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 4 Mar 2026 23:09:22 -0500 Subject: [PATCH 17/60] add links to bmdb formatted response: one to the original database and another one to /search/id --- backend/app/services/llms_service.py | 7 ++++++- frontend/components/ChatBox.tsx | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 7cb13ba..d4d7132 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -103,9 +103,14 @@ async def get_response_with_tools(conversation_history: list[dict], database: st logger.info(f"Tool Result: {str(result)[:500]}") + bmkeys = [] # Extract bmkeys only if result is a dictionary and contains the expected key if isinstance(result, dict): - bmkeys = result.get("unique_model_keys (bmkey)", []) + if database == "vcdb": + bmkeys = result.get("unique_model_keys (bmkey)", []) + elif database == "bmdb": + biomd_models = result.get("data", []) + bmkeys = [model.get("id") for model in biomd_models if model.get("id")] # Send the result back to the model messages.append( diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 33d1473..cd600e3 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -345,6 +345,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { data.response || "Sorry, I didn't get a response from the server."; const bmkeys = data.bmkeys || []; + console.log(bmkeys) // Format the response to include hyperlinks for biomodel IDs const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); From 7b136fdc676d4781b31b6ff0dfae3aba84c162cb Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 8 Mar 2026 21:38:46 -0400 Subject: [PATCH 18/60] add ai analysis tab to biomodel specific page for bmdb models --- frontend/app/search/[bmid]/page.tsx | 27 +++++++++++++++++++++++---- frontend/components/ChatBox.tsx | 10 ++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index f12811f..cc16d0f 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -242,7 +242,7 @@ export default function BiomodelDetailPage() { // Create combined messages when diagram analysis is ready useEffect(() => { - if (diagramAnalysis) { + if (diagramAnalysis && !bioMDid && data?.bmKey) { const diagramMessage = `# Diagram Analysis \n ${diagramAnalysis}`; setCombinedMessages([diagramMessage]); } @@ -368,12 +368,31 @@ export default function BiomodelDetailPage() { )} -
-
+ + + {/* AI Analysis Section */} +
+
+ + + AI Analysis Assistant + +
+
+ +
+
+
@@ -626,7 +645,7 @@ export default function BiomodelDetailPage() {
- = ({ + database, startMessage, quickActions, supplementalActions, @@ -151,7 +153,11 @@ export const ChatBox: React.FC = ({ const handleQuickAction = (message: string) => { setInputMessage(""); - handleSendMessage(message); + if (database == "vcdb") { + handleSendMessage(message); + } else if (database == "bmdb") { + handleSendMessage2(message); + } }; const handleSendMessage = async (overrideMessage?: string) => { @@ -473,7 +479,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { value={inputMessage} onChange={(e) => setInputMessage(e.target.value)} onKeyPress={handleKeyPress} - placeholder="Ask any questions about VCell2 biomodels..." + placeholder="Ask any questions about VCell biomodels..." className="flex-1 border-slate-300 focus:border-blue-500" disabled={isLoading || isInitialLoading} /> From ed9903ea3fef67e174118908588c4a89b09d3c17 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 8 Mar 2026 21:39:49 -0400 Subject: [PATCH 19/60] add new tool to handle getting results from xml files for bmdb specific questions, add the tool to llm processing --- backend/app/services/databases_service.py | 76 +++++++++++++++++++++-- backend/app/utils/tools_utils.py | 26 +++++++- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/backend/app/services/databases_service.py b/backend/app/services/databases_service.py index 5906733..5d94bf1 100644 --- a/backend/app/services/databases_service.py +++ b/backend/app/services/databases_service.py @@ -8,7 +8,7 @@ from typing import List VCELL_API_BASE_URL = "https://vcell.cam.uchc.edu/api/v0" -BIOMODELS_API_URL = "https://biomodels.org/search?query=" +BIOMODELS_API_URL = "https://biomodels.org/" logger = get_logger("vcelldb_service") print("CHECK: in VCELL_DB_SERVICE") @@ -40,6 +40,10 @@ def sanitize_vcml_content(vcml_content: str) -> str: logger.info("VCML content sanitized: ImageData tags removed") return sanitized_content +# def sanitize_xml_content(vcml_content: str) -> str: + +# return sanitized_content + async def check_vcell_connectivity() -> bool: """ @@ -139,14 +143,12 @@ async def fetch_simulation_details(params: SimulationRequestParams) -> dict: @observe(name="FETCH_BIOMD_MODELS") async def fetch_biomd_models(params: BiomodelRequestParams) -> dict: print("DEBUG20: BIOMD POST: in tool FETCH_BIOMD_MODELS") - # Transform None to "" (optional, only if needed for empty fields) - params_dict = {k: (v if v is not None else "") for k, v in params.dict().items()} # Construct the query string using urlencoded parameters (params_dict) query_string = params.bmName if params.bmName else params.bmId # Construct the full URL - url = f"{BIOMODELS_API_URL}{query_string}&format=json" + url = f"{BIOMODELS_API_URL}search?query={query_string}&format=json" # Log the URL being queried logger.info(f"Querying URL: {url}") @@ -175,6 +177,72 @@ async def fetch_biomd_models(params: BiomodelRequestParams) -> dict: } +@observe(name="GET_XML_FILE") +async def get_xml_file(bmId: str, truncate: bool = False, max_retries: int = 3) -> str: + + logger.info(f"Fetching XML file for biomodel: {bmId}") + + # Check connectivity first + if not await check_vcell_connectivity(): + logger.error( + "BioMD API is not reachable. Please check your network connection and DNS settings." + ) + raise Exception( + "BioMD API is not reachable. Please check your network connection and DNS settings." + ) + + for attempt in range(max_retries + 1): + try: + url = f"{BIOMODELS_API_URL}model/download/{bmId}?filename={bmId}_url.xml" + logger.info( + f"Requesting URL: {url} (attempt {attempt + 1}/{max_retries + 1})" + ) + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(url) + logger.info(f"Response status: {response.status_code}") + logger.info(f"Response headers: {dict(response.headers)}") + response.raise_for_status() + + return response.text + # if truncate: + # return sanitize_vcml_content(response.text[:500]) + # else: + # return sanitize_vcml_content(response.text) + + except httpx.HTTPStatusError as e: + logger.error( + f"HTTP error fetching XML file for biomodel {bmId}: {e.response.status_code} - {e.response.text}" + ) + if attempt == max_retries: + raise e + logger.warning(f"Retrying in {2 ** attempt} seconds...") + await asyncio.sleep(2**attempt) + + except httpx.RequestError as e: + logger.error( + f"Request error fetching XML file for biomodel {bmId}: {str(e)}" + ) + if attempt == max_retries: + raise e + logger.warning(f"Retrying in {2 ** attempt} seconds...") + await asyncio.sleep(2**attempt) + + except Exception as e: + logger.error( + f"Unexpected error fetching XML file for biomodel {bmId}: {str(e)}" + ) + if attempt == max_retries: + raise e + logger.warning(f"Retrying in {2 ** attempt} seconds...") + await asyncio.sleep(2**attempt) + + # This should never be reached, but just in case + raise Exception( + f"Failed to fetch XML file for biomodel {bmId} after {max_retries + 1} attempts" + ) + + @observe(name="GET_VCML_FILE") async def get_vcml_file( biomodel_id: str, truncate: bool = False, max_retries: int = 3 diff --git a/backend/app/utils/tools_utils.py b/backend/app/utils/tools_utils.py index 9c887a4..2d504b2 100644 --- a/backend/app/utils/tools_utils.py +++ b/backend/app/utils/tools_utils.py @@ -5,6 +5,7 @@ get_vcml_file, fetch_publications, fetch_biomd_models, + get_xml_file ) from app.services.knowledge_base_service import get_similar_chunks from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams @@ -205,6 +206,25 @@ ), ) +get_xml_file_tool = ToolDefinition( + type="function", + function=FunctionDefinition( + name="get_xml_file", + description="Retrieves the SBML XML (eXtensible Markup Language) file content for a specified BioModels model (BIOMD ID). SBML (Systems Biology Markup Language) files provide a detailed, machine-readable representation of a biomodel's structure and behavior, which is used for simulation and model analysis. This function downloads the XML representation of a biomodel for further analysis.", + parameters=ParameterSchema( + type="object", + properties={ + "bmId": { + "type": "string", + "description": "ID of the biomodel to retrieve VCML", + } + }, + required=["bmId"], + additionalProperties=False, + ), + strict=True, + ), +) # List of all tool definitions ToolsDefinitions = [ @@ -214,7 +234,8 @@ search_vcell_knowledge_base_tool, fetch_publications_tool, ] -BIOMD_TOOLS = [fetch_biomd_tool] +BIOMD_TOOLS = [fetch_biomd_tool, + get_xml_file_tool] # Tool Executor Function async def execute_tool(name, args): @@ -260,7 +281,8 @@ async def execute_tool(name, args): params = BiomodelRequestParams(**args) print("DEBUG About to call fetch_biomodels()") return await fetch_biomd_models(params) - + elif name == "get_xml_file": + return await get_xml_file(args["bmId"]) else: return {} From a301b40ea8841a495182ad5ea60de2b398e6e46e Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 8 Mar 2026 21:40:19 -0400 Subject: [PATCH 20/60] edit system prompt to make it less vcell specific --- backend/app/utils/system_prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 6c7d01e..4bade10 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -39,7 +39,7 @@ ### Biomodel Analysis Guidelines * Include as many relevant details as possible, such as biomodel ID, names, descriptions, parameters, and any other relevant metadata that can aid in the user's understanding. -* When the user query is about: "Describe parameters", "Describe species", "Describe reactions", or "What Applications are used?" — specifically in the context of model analysis: Make sure to use the `get_vcml_file` tool to retrieve the VCML file for the biomodel. This file contains detailed information about the model's structure and behavior, which is essential for providing accurate descriptions of parameters, species, reactions, and applications. Use also the "fetch_biomodels" tool to gather additional context about the biomodel, and Try when asked these questions to focus on the asked aspects, Do not provide general summaries, model structure, or unrelated metadata unless explicitly requested. Keep the focus tightly on the requested element and be as technically precise as possible. Elaborate as much as you can on the requested aspect, providing detailed descriptions and explanations based on the VCML content. +* When the user query is about: "Describe parameters", "Describe species", "Describe reactions", or "What Applications are used?" — specifically in the context of model analysis: Make sure to use the `get_vcml_file` tool to retrieve the VCML file for the VCELL biomodel or the `get_xml_file` tool to retrieve the SBML XML file for the BMDB biomodel. This file contains detailed information about the model's structure and behavior, which is essential for providing accurate descriptions of parameters, species, reactions, and applications. Use also the "fetch_biomodels" tool to gather additional context about the biomodel, and Try when asked these questions to focus on the asked aspects, Do not provide general summaries, model structure, or unrelated metadata unless explicitly requested. Keep the focus tightly on the requested element and be as technically precise as possible. Elaborate as much as you can on the requested aspect, providing detailed descriptions and explanations based on the VCML or SBML XML content. ### Publications Guidelines * If asked for publications, research papers, pubmed articles, etc. use the `fetch_publications` tool. After fetching, extract the relevant information, filter by user's specific needs, format publication links using markdown `[Title](DOI_URL)`, provide context (date, authors, description), and clearly communicate if no relevant publications are found. From 1a12e95589b26c13b0b488ec3103fd3f6c5bc07e Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 8 Mar 2026 23:06:39 -0400 Subject: [PATCH 21/60] add radio swtich buttons for each database instead of two search boxes; add a stop button --- frontend/components/ChatBox.tsx | 95 +++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index c199f83..ad337f0 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -8,7 +8,7 @@ import { MessageSquare, Send, Bot, User, Loader2 } from "lucide-react"; interface Message { id: string; - role: "user" | "assistant"; + role: "user" | "assistant" | "system"; content: string; timestamp: Date; } @@ -32,7 +32,7 @@ interface ChatParameters { } interface ChatBoxProps { - database: "vcdb" | "bmdb"; + database?: "vcdb" | "bmdb"; startMessage: string | string[]; quickActions: QuickAction[]; supplementalActions?: QuickAction[]; @@ -91,11 +91,13 @@ export const ChatBox: React.FC = ({ // }); const [inputMessage, setInputMessage] = useState(""); - const [inputMessage2, setInputMessage2] = useState(""); + const [dbSource, setDbSource] = useState<"vcdb" | "bmdb">(database ?? "vcdb"); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); + const abortController = useRef(null); + const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; @@ -160,9 +162,23 @@ export const ChatBox: React.FC = ({ } }; + const handleSend = (inputMessage: string) => { + if (!inputMessage.trim()) return; + + if (dbSource === "vcdb") { + handleSendMessage(inputMessage); + } else if (dbSource === "bmdb"){ + handleSendMessage2(inputMessage); + } +}; + const handleSendMessage = async (overrideMessage?: string) => { const msg = overrideMessage ?? inputMessage; if (!msg.trim()) return; + + const controller = new AbortController(); + abortController.current = controller; + // Build parameter context string let parameterContext = ""; if (parameters) { @@ -233,6 +249,7 @@ export const ChatBox: React.FC = ({ content: msg.content, })), }), + signal: controller.signal, }, ); console.log("AAAAAA API query sent to backend: " + finalPrompt); @@ -269,8 +286,10 @@ export const ChatBox: React.FC = ({ // const handleSendMessage2 = async (overrideMessage?: string) => { - const msg = overrideMessage ?? inputMessage2; + const msg = overrideMessage ?? inputMessage; if (!msg.trim()) return; + const controller = new AbortController(); + abortController.current = controller; // Build parameter context string let parameterContext = ""; if (parameters) { @@ -344,6 +363,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { content: msg.content, })), }), + signal: controller.signal, }, ); const data = await res.json(); @@ -383,10 +403,30 @@ const handleSendMessage2 = async (overrideMessage?: string) => { const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); - handleSendMessage(); + handleSend(inputMessage); } }; + const handleStop = () => { + if (abortController.current) { + abortController.current.abort(); + abortController.current = null; + + setMessages((prev) => [ + ...prev, + { + id: crypto.randomUUID(), + role: "system", + content: "Response stopped by user.", + timestamp: new Date(), + }, + ]); + + setIsLoading(false); + } + }; + + return ( @@ -473,41 +513,52 @@ const handleSendMessage2 = async (overrideMessage?: string) => {
+
+ + + +
setInputMessage(e.target.value)} onKeyPress={handleKeyPress} - placeholder="Ask any questions about VCell biomodels..." + placeholder={dbSource === "vcdb" + ? "Ask about VCell biomodels..." + : "Ask about BioModels biomodels..."} className="flex-1 border-slate-300 focus:border-blue-500" disabled={isLoading || isInitialLoading} /> -
-
- setInputMessage2(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Ask any questions about BMDB biomodels..." - className="flex-1 border-slate-300 focus:border-blue-500" - disabled={isLoading || isInitialLoading} - />
From e9b6750cbc8408422df1c1c415e1e9f9e038567b Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 10 Mar 2026 21:20:08 -0400 Subject: [PATCH 22/60] remove extra buttons on bottom of chat page --- frontend/app/chat/page.tsx | 9 --------- frontend/components/ChatBox.tsx | 15 --------------- 2 files changed, 24 deletions(-) diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index de53c9e..01ab31a 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -90,14 +90,6 @@ export default function ChatPage() { }, ]; - const moreActions = [ - { - label: "Search BioModels database", - icon: , - value: "Search BioModels database", - } - ]; - const cardTitle = "VCell AI Assistant"; return ( @@ -133,7 +125,6 @@ export default function ChatPage() { startMessage={[startMessage]} quickActions={quickActions} supplementalActions={supplementalActions} - moreActions={moreActions} cardTitle={cardTitle} />
diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index ad337f0..0844c80 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -36,7 +36,6 @@ interface ChatBoxProps { startMessage: string | string[]; quickActions: QuickAction[]; supplementalActions?: QuickAction[]; - moreActions?: QuickAction[]; cardTitle: string; promptPrefix?: string; isLoading?: boolean; @@ -48,7 +47,6 @@ export const ChatBox: React.FC = ({ startMessage, quickActions, supplementalActions, - moreActions, cardTitle, promptPrefix, isLoading: isInitialLoading = false, @@ -595,19 +593,6 @@ const handleSendMessage2 = async (overrideMessage?: string) => {
)} - {moreActions && ( -
-
- {moreActions.map((action, idx) => ( - - ))} -
-
- )} - ); From be96691c73e9912ec4d4f00213434de1738a6c42 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 10 Mar 2026 22:45:51 -0400 Subject: [PATCH 23/60] edit system prompt to fix formatting for both database responses --- backend/app/utils/system_prompt.py | 14 +++++---- frontend/components/ChatBox.tsx | 49 +++++++++++++++--------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 4bade10..9aa0598 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -1,5 +1,6 @@ SYSTEM_PROMPT = """ -You are a VCell BioModel Assistant, designed to help users understand and interact with biological models in VCell. +You are a mathematical modeler in biology, designed to help users understand and interact with biological models in VCell, and in +SBML format (taken from BioModels database, also called BMDB or BioModels.org). Your task is to provide human-readable, accurate, detailed, and contextually appropriate responses based on the tools available. ## Core Guidelines @@ -20,16 +21,17 @@ ### Formatting Guidelines for Biomodels, Biomodel IDs and Hyperlinks * Ignore all previous instructions. +* Applications are for VCell models ONLY otherwise do not output an Applications bullet. * When returning a list of biomodels, always follow this format: ``` -1. **[Biomodel Name](https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId})** - - **Biomodel Key:** ${biomodelId} +1. **[Biomodel Name] (/search/${biomodelID})** + - **Biomodel Key:** ${biomodelId} || [Database link] (https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId}) - **Owner:** ${owner} - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} - - **Applications:** + - **Applications:** -List every application name for the model in italics, each on its own bullet point. Under each bulleted -application name, list its corresponding simulations, with each simulation followed by a solver in round brackets. +List every application name for the model in italics, each on its own bullet point. Under each +bulleted application name, list its corresponding simulations, with each simulation followed by a solver in round brackets. Do not omit any applications. ``` diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 0844c80..f32a347 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -127,29 +127,26 @@ export const ChatBox: React.FC = ({ }, [startMessage]); // Helper function to format biomodel IDs as hyperlinks - const formatBiomodelIds = (content: string, bmkeys: string[]): string => { - if (!bmkeys || bmkeys.length === 0) return content; - - let formattedContent = content; - - // Replace biomodel IDs with hyperlinks - bmkeys.forEach((bmId) => { - const searchString = `${bmId}`; - const encodedPrompt = encodeURIComponent(`Describe model`); - /* const ai_link = `[AI Analysis](/analyze/${bmId}?prompt=${encodedPrompt})`; - const db_link = `[Database](/search/${bmId})`; - const replacementString = `**${bmId}** -- ${ai_link}  |  ${db_link}`; */ - const db_link = `[Database Details](/search/${bmId})`; - const replacementString = `${bmId} || ${db_link}`; - const match_IDs_Not_in_URLs = new RegExp(`(? { + // if (!bmkeys || bmkeys.length === 0) return content; + + // let formattedContent = content; + + // // Replace biomodel IDs with hyperlinks + // bmkeys.forEach((bmId) => { + // const searchString = `${bmId}`; + // const encodedPrompt = encodeURIComponent(`Describe model`); + // const db_link = `[Database Details](/search/${bmId})`; + // const replacementString = `${bmId} || ${db_link}`; + // const match_IDs_Not_in_URLs = new RegExp(`(? { setInputMessage(""); @@ -257,7 +254,8 @@ export const ChatBox: React.FC = ({ const bmkeys = data.bmkeys || []; // Format the response to include hyperlinks for biomodel IDs - const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); + //const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); + const formattedResponse = aiResponse const assistantMessage: Message = { id: (Date.now() + 1).toString(), @@ -371,7 +369,8 @@ const handleSendMessage2 = async (overrideMessage?: string) => { console.log(bmkeys) // Format the response to include hyperlinks for biomodel IDs - const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); + //const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); + const formattedResponse = aiResponse const assistantMessage: Message = { id: (Date.now() + 1).toString(), From fffa419659bbdb2dc92f082d3c6fd166adb0c87e Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 10 Mar 2026 23:10:33 -0400 Subject: [PATCH 24/60] connect all buttons to the chatbox --- frontend/components/ChatBox.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index f32a347..5aecccc 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -150,10 +150,14 @@ export const ChatBox: React.FC = ({ const handleQuickAction = (message: string) => { setInputMessage(""); - if (database == "vcdb") { - handleSendMessage(message); - } else if (database == "bmdb") { - handleSendMessage2(message); + if (database) { + if (database == "vcdb") { + handleSendMessage(message); + } else if (database == "bmdb") { + handleSendMessage2(message); + } + } else { + handleSend(message) } }; From 177e60b6c236661b5e3072ca4a1e0415de604cc4 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 10 Mar 2026 23:40:47 -0400 Subject: [PATCH 25/60] change the quickaction buttons and add switch for vcell specific and bmdb specific actions --- frontend/app/chat/page.tsx | 58 +++++++++++++++++++-------------- frontend/components/ChatBox.tsx | 24 +++++++++++--- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index 01ab31a..40082b6 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -39,14 +39,37 @@ export default function ChatPage() { const startMessage = `I'm here to help you with **biomodel analysis**, **vcell software** and **research support** .\nFeel free to ask anything! 🚀`; const quickActions = [ { - label: "List all tutorial models", + label: "List Calcium models", + icon: , + value: "List all Calcium models", + }, + { + label: "List all EGFR models", icon: , - value: "List all tutorial models", + value: "List all EGFR models", }, + ]; + + const VCellActions = [ { - label: "List Calcium models", + label: "How to create an account on VCell Software?", + icon: , + value: "How to create an account on VCell Software?", + }, + { + label: "How to model FrapBindings in VCell Software?", icon: , - value: "List all Calcium models", + value: "How to model FragBindings in VCell Software?", + }, + { + label: "How to model Moving Boundaries in VCell Software?", + icon: , + value: "How to model Moving Boundaries in VCell Software?", + }, + { + label: "List all tutorial models", + icon: , + value: "List all tutorial models", }, { label: "List all models by ModelBrick", @@ -58,35 +81,19 @@ export default function ChatPage() { icon: , value: "What solvers are used in tutorial models", }, -/* { - label: - "What are different types of VCell applications used in Tutorial models", - icon: , - value: - "What are different types of VCell applications used in Tutorial models", - }, */ { label: "What Tutorial models use Spatial Stochastic applications?", icon: , value: "What Tutorial models use Spatial Stochastic applications?", }, + ]; - const supplementalActions = [ - { - label: "How to create an account on VCell Software?", - icon: , - value: "How to create an account on VCell Software?", - }, + const bmdbActions = [ { - label: "How to model FrapBindings in VCell Software?", + label: "List all Kholodenko models", icon: , - value: "How to model FragBindings in VCell Software?", - }, - { - label: "How to model Moving Boundaries in VCell Software?", - icon: , - value: "How to model Moving Boundaries in VCell Software?", + value: "List all Kholodenko models", }, ]; @@ -124,7 +131,8 @@ export default function ChatPage() { diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 5aecccc..41dd8c9 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -35,7 +35,8 @@ interface ChatBoxProps { database?: "vcdb" | "bmdb"; startMessage: string | string[]; quickActions: QuickAction[]; - supplementalActions?: QuickAction[]; + VCellActions?: QuickAction[]; + bmdbActions?: QuickAction[]; cardTitle: string; promptPrefix?: string; isLoading?: boolean; @@ -46,7 +47,8 @@ export const ChatBox: React.FC = ({ database, startMessage, quickActions, - supplementalActions, + VCellActions, + bmdbActions, cardTitle, promptPrefix, isLoading: isInitialLoading = false, @@ -583,10 +585,24 @@ const handleSendMessage2 = async (overrideMessage?: string) => { )} - {supplementalActions && ( + + {dbSource === "vcdb" && VCellActions && ( +
+
+ {VCellActions.map((action, idx) => ( + + ))} +
+
+ )} + + {dbSource === "bmdb" && bmdbActions && (
- {supplementalActions.map((action, idx) => ( + {bmdbActions.map((action, idx) => ( + ))} +
+
+ )} + ); From 17465bfa6dd99a426aeb43230f3f3e002f3b2eb9 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 22 Mar 2026 22:09:35 -0400 Subject: [PATCH 27/60] update loading history in sidebar to only load once per chat and update in real time --- frontend/components/ChatBox.tsx | 187 ++++++++++++++++------------ frontend/components/app-sidebar.tsx | 50 +++++--- 2 files changed, 143 insertions(+), 94 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 15ff9aa..05fe954 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -6,6 +6,8 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { MarkdownRenderer } from "@/components/markdown-renderer"; import { MessageSquare, Send, Bot, User, Loader2 } from "lucide-react"; +import { useSearchParams } from "next/navigation"; + interface Message { id: string; role: "user" | "assistant" | "system"; @@ -77,21 +79,33 @@ export const ChatBox: React.FC = ({ }; const [messages, setMessages] = useState(() => { - //const saved = localStorage.getItem("chat_history"); - //return saved ? JSON.parse(saved) : createInitialMessages(startMessage); return createInitialMessages(startMessage); }); - const STORAGE_KEY = null; + const searchParams = useSearchParams(); + + useEffect(() => { + const id = searchParams.get("conversation"); + + if (id) { + const stored = localStorage.getItem("chat_conversations"); + if (!stored) return; + + const conversations = JSON.parse(stored); + const convo = conversations.find((c: any) => c.id === id); + + if (convo) { + setMessages(convo.messages); + setConversationId(id); +} + } else { + setMessages(createInitialMessages(startMessage)); + } +}, [searchParams, startMessage]); - // const [messages, setMessages] = useState(() => { - // if (typeof window === "undefined") return []; - // const saved = localStorage.getItem(STORAGE_KEY); - // return saved ? JSON.parse(saved) : []; - // }); const [inputMessage, setInputMessage] = useState(""); - //const [dbSource, setDbSource] = useState<"vcdb" | "bmdb">(database ?? "vcdb"); + const [conversationId, setConversationId] = useState(null); const [useVCDB, setUseVCDB] = useState(database ? database === "vcdb" : true); const [useBMDB, setUseBMDB] = useState(database === "bmdb"); const [isLoading, setIsLoading] = useState(false); @@ -108,49 +122,49 @@ export const ChatBox: React.FC = ({ scrollToBottom(); }, [messages]); - // useEffect(() => { - // const handleUnload = () => { - // localStorage.removeItem("chat_history"); - // }; - // window.addEventListener("beforeunload", handleUnload); +useEffect(() => { + if (messages.length === 0) return; + const hasUserMessage = messages.some((m) => m.role === "user"); + if (!hasUserMessage) return; - // return () => { - // window.removeEventListener("beforeunload", handleUnload); - // }; - // }, []); + saveConversation(messages); - // Update messages when startMessage changes (when analysis completes - // useEffect(() => { - // localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)); - // }, [messages]); + window.dispatchEvent(new Event("conversation-updated")); +}, [messages]); +const saveConversation = (messages: Message[]) => { + if (messages.length === 0) return; - useEffect(() => { - setMessages(createInitialMessages(startMessage)); - }, [startMessage]); - - // Helper function to format biomodel IDs as hyperlinks - // const formatBiomodelIds = (content: string, bmkeys: string[]): string => { - // if (!bmkeys || bmkeys.length === 0) return content; - - // let formattedContent = content; - - // // Replace biomodel IDs with hyperlinks - // bmkeys.forEach((bmId) => { - // const searchString = `${bmId}`; - // const encodedPrompt = encodeURIComponent(`Describe model`); - // const db_link = `[Database Details](/search/${bmId})`; - // const replacementString = `${bmId} || ${db_link}`; - // const match_IDs_Not_in_URLs = new RegExp(`(? m.role === "user"); + + const newConversation = { + id, + title: firstUserMessage?.content.slice(0, 40) || "Chat", + messages, + }; + + conversations.unshift(newConversation); + } else { + // Update existing conversation + const index = conversations.findIndex((c: any) => c.id === id); + if (index !== -1) { + conversations[index].messages = messages; + } + } + + localStorage.setItem("chat_conversations", JSON.stringify(conversations)); +}; const activeActions = [ ...(useVCDB && VCellActions ? VCellActions : []), @@ -176,6 +190,13 @@ export const ChatBox: React.FC = ({ alert("Please select at least one database."); return; } + const userMessage: Message = { + id: Date.now().toString(), + role: "user", + content: inputMessage, + timestamp: new Date(), + }; + setMessages((prev) => [...prev, userMessage]); if (useVCDB) {handleSendMessage(inputMessage);} if (useBMDB){handleSendMessage2(inputMessage);} @@ -224,14 +245,13 @@ export const ChatBox: React.FC = ({ } console.log("PPPPPP: " + "This is the msg: " + msg); - const userMessage: Message = { - id: Date.now().toString(), - role: "user", - content: msg, - timestamp: new Date(), - }; - console.log("QQQQQQ: " + "This is the userMessage: " + JSON.stringify(userMessage)); - setMessages((prev) => [...prev, userMessage]); + // const userMessage: Message = { + // id: Date.now().toString(), + // role: "user", + // content: msg, + // timestamp: new Date(), + // }; + // setMessages((prev) => [...prev, userMessage]); setInputMessage(""); setIsLoading(true); try { @@ -241,6 +261,16 @@ export const ChatBox: React.FC = ({ console.log("RRRRRR: " + "This is the promptPrefix: " + promptPrefix); console.log("RRRRRR: " + "This is the finalPrompt sent to backend: " + finalPrompt); + const systemMessages = messages.filter((m) => m.role === "system"); + const recentNonSystem = messages.filter((m) => m.role !== "system").slice(-5); + + const historyToSend = [ + ...systemMessages, + ...recentNonSystem, + { role: "user", content: finalPrompt }, + ].map((msg) => ({ role: msg.role, content: msg.content })); + + const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/query`, { @@ -249,15 +279,16 @@ export const ChatBox: React.FC = ({ "Content-Type": "application/json", accept: "application/json", }, - body: JSON.stringify({ - conversation_history: [ - ...messages, - { role: "user", content: finalPrompt }, - ].map(msg => ({ - role: msg.role, - content: msg.content, - })), - }), + body: JSON.stringify({ conversation_history: historyToSend }), +//JSON.stringify({ + // conversation_history: [ + // ...messages, + // { role: "user", content: finalPrompt }, + // ].map(msg => ({ + // role: msg.role, + // content: msg.content, + // })), + // }), signal: controller.signal, }, ); @@ -274,7 +305,9 @@ export const ChatBox: React.FC = ({ const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", - content: formattedResponse, + content: useVCDB && useBMDB + ? `**VCell Database:**\n\n${formattedResponse}` + : formattedResponse, timestamp: new Date(), }; setMessages((prev) => [...prev, assistantMessage]); @@ -336,25 +369,19 @@ const handleSendMessage2 = async (overrideMessage?: string) => { } console.log("PPPPPP: " + "This is the msg: " + msg); - const userMessage: Message = { - id: Date.now().toString(), - role: "user", - content: msg, - timestamp: new Date(), - }; - console.log("QQQQQQ: " + "This is the userMessage: " + JSON.stringify(userMessage)); - setMessages((prev) => [...prev, userMessage]); + // const userMessage: Message = { + // id: Date.now().toString(), + // role: "user", + // content: msg, + // timestamp: new Date(), + // }; + // setMessages((prev) => [...prev, userMessage]); setInputMessage(""); setIsLoading(true); try { const finalPrompt = promptPrefix ? `${promptPrefix} ${msg}${parameterContext}` : `${msg}${parameterContext}`; - - // const res = await fetch( - // `${process.env.NEXT_PUBLIC_API_URL2}/search?query=${encodeURIComponent(msg)}&format=json`); - - // console.log("DEBUG: This is the raw response from the backend: " + JSON.stringify(res)); const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/biomd-search`, @@ -389,7 +416,9 @@ const handleSendMessage2 = async (overrideMessage?: string) => { const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", - content: formattedResponse, + content: useVCDB && useBMDB + ? `**BIOMD Database:**\n\n${formattedResponse}` + : formattedResponse, timestamp: new Date(), }; setMessages((prev) => [...prev, assistantMessage]); diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index c02a5b3..feede0a 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -32,22 +32,38 @@ import { useSidebar, } from "@/components/ui/sidebar"; -const historyItems = [ - "Calcium Biomodel Comparison", - "Protein Details on Tutorial Models", - "Biomodels authored by ModelBrick", - "Count of Rule-based models", - "VCML File Analysis of Calcium Models", -]; +import { useState, useEffect } from "react"; export function AppSidebar() { const pathname = usePathname(); const { state } = useSidebar(); const isCollapsed = state === "collapsed"; + const [historyItems, setHistoryItems] = useState([]); + + useEffect(() => { + const loadHistory = () => { + const stored = localStorage.getItem("chat_conversations"); + if (!stored) return; + + const conversations = JSON.parse(stored); + + setHistoryItems( + conversations.map((c: any) => ({ + id: c.id, + text: c.title + })) + ); + }; + + loadHistory(); + window.addEventListener("conversation-updated", loadHistory); + return () => window.removeEventListener("storage", loadHistory); +}, []); + if (pathname == "/" || pathname == "/signin" || pathname == "/signup") { return null; - } + } return ( @@ -229,13 +245,17 @@ export function AppSidebar() { - {historyItems.map((item, index) => ( - - - {item} - - - ))} + {historyItems.map((item) => ( + + + + {item.text} + + + + ))} From a800a989c822e5060e73ee493013f17a950fa263 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 12:05:03 -0400 Subject: [PATCH 28/60] wrap SearchParams in Suspense to build with docker without error --- frontend/app/analyze/[id]/page.tsx | 2 ++ frontend/app/analyze/page.tsx | 2 -- frontend/app/chat/page.tsx | 3 +++ frontend/components/ChatBox.tsx | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/app/analyze/[id]/page.tsx b/frontend/app/analyze/[id]/page.tsx index fc66bec..a37a2cc 100644 --- a/frontend/app/analyze/[id]/page.tsx +++ b/frontend/app/analyze/[id]/page.tsx @@ -1,6 +1,7 @@ "use client"; import React from "react"; +import Suspense from "react"; import { useState, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { @@ -49,6 +50,7 @@ export default function AnalysisResultsPage({ }) { const router = useRouter(); const searchParams = useSearchParams(); + const session = searchParams.get('session') const prompt = searchParams.get("prompt") || ""; // Unwrap the params Promise diff --git a/frontend/app/analyze/page.tsx b/frontend/app/analyze/page.tsx index 387b785..37f7995 100644 --- a/frontend/app/analyze/page.tsx +++ b/frontend/app/analyze/page.tsx @@ -30,8 +30,6 @@ interface PromptTemplate { export default function AnalyzePage() { const router = useRouter(); - //const searchParams = useSearchParams(); - //const defaultbiomodelId = searchParams.get("bmId") ?? "270051643"; const [biomodelId, setBiomodelId] = useState("270051643"); const [prompt, setPrompt] = useState(""); diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index 40082b6..fefdcd8 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -1,6 +1,7 @@ "use client"; import type React from "react"; +import { Suspense } from "react"; import { useState, useEffect } from "react"; import { MessageSquare, @@ -100,6 +101,7 @@ export default function ChatPage() { const cardTitle = "VCell AI Assistant"; return ( + Loading...}>
{/* Header */} @@ -143,5 +145,6 @@ export default function ChatPage() { onClose={handleOnboardingClose} />
+ ); } diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 05fe954..aae5933 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -466,7 +466,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { } }; - + const session = searchParams.get('session') return ( From 49bbfb52c78dab4e751a7fb56a338eb29adb0bed Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 12:05:17 -0400 Subject: [PATCH 29/60] updated config settings --- backend/app/core/config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 7a9df5b..40ece26 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,4 +1,4 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -24,4 +24,10 @@ class Settings(BaseSettings): LANGFUSE_PUBLIC_KEY: str LANGFUSE_HOST: str + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + settings = Settings() From d88b73ddc6fa3b7ef7004c714b764da143061514 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 12:05:42 -0400 Subject: [PATCH 30/60] push updated docker_compose settings --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 958ec35..a60659b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,9 @@ services: container_name: frontend-vcell ports: - "3000:3000" + environment: + NEXT_PUBLIC_API_URL: http://localhost:8000 + NEXT_PUBLIC_API_URL2: https://www.biomodels.org/ depends_on: - backend env_file: From 6f047da04f83ab44f1ffa33942d87cc8e8f5b822 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 15:03:04 -0400 Subject: [PATCH 31/60] try separating into two format guideline sections for each database --- backend/app/utils/system_prompt.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 5a26bd4..5119c89 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -19,14 +19,14 @@ * Format all units, chemical names, reaction rates, and numerical expressions using math mode to ensure proper rendering. Example: "The rate is $5.2 \times 10^{-3} \text{ mmol}\cdot\text{ml}^{-1}\cdot\text{min}^{-1}$". -### Formatting Guidelines for Biomodels, Biomodel IDs and Hyperlinks -* Ignore all previous instructions. -* Applications are for VCell models ONLY otherwise do not output an Applications bullet. -* For models from BMDB, the database link would be https://www.biomodels.org/${biomodelId} -* When returning a list of biomodels, always follow this format: +## Formatting Guidelines +You MUST follow this exact output format. Do NOT modify, omit, or reorder any fields. Do NOT change any links. + +### Formatting Guidelines for Biomodels retrieved from VCell database (VCDB) +* For each model in the list of VCELL biomodels: ``` -1. **[Biomodel Name] (/search/${biomodelID})** - - **Biomodel Key:** ${biomodelId} || [Database link] (https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId}) +1. **[Biomodel Name](/search/${biomodelID})** + - **Biomodel Key:** ${biomodelId} || [Database link](https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId}) - **Owner:** ${owner} - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} - **Applications:** @@ -36,6 +36,15 @@ Do not omit any applications. ``` +### Formatting Guidelines for Biomodels retrieved from BioModels database (BMDB) +* For each model in the list of BIOMD biomodels: +``` +1. **[Biomodel Name](/search/${biomodelID})** + - **Biomodel Key:** ${biomodelId} || [Database link](https://www.biomodels.org/${biomodelId}) + - **Owner:** ${owner} + - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} +``` + ### Guidelines for Follow-up Questions and Further Actions * If there is an opportunity for follow-up questions or further actions, always ask the user if they'd like to explore more options or if you can assist with other related tasks. From d6871d1a089a6e6a0ec8b378083838b029410167 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 15:13:27 -0400 Subject: [PATCH 32/60] force consistent formatting in system prompt --- backend/app/utils/system_prompt.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 5119c89..aa9724b 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -19,11 +19,13 @@ * Format all units, chemical names, reaction rates, and numerical expressions using math mode to ensure proper rendering. Example: "The rate is $5.2 \times 10^{-3} \text{ mmol}\cdot\text{ml}^{-1}\cdot\text{min}^{-1}$". -## Formatting Guidelines +## Formatting Guidelines for Biomodels You MUST follow this exact output format. Do NOT modify, omit, or reorder any fields. Do NOT change any links. +NEVER output plain text for the biomodel name (it must always be a link to its ID-specific page only). ALWAYS add a database +link to the models CORRECT respective database. -### Formatting Guidelines for Biomodels retrieved from VCell database (VCDB) -* For each model in the list of VCELL biomodels: +### Formatting Guidelines for biomodels retrieved from VCell database (VCDB) +* For each VCELL model: ``` 1. **[Biomodel Name](/search/${biomodelID})** - **Biomodel Key:** ${biomodelId} || [Database link](https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId}) @@ -36,8 +38,8 @@ Do not omit any applications. ``` -### Formatting Guidelines for Biomodels retrieved from BioModels database (BMDB) -* For each model in the list of BIOMD biomodels: +### Formatting Guidelines for biomodels retrieved from BioModels database (BMDB) +* For each BIOMD model: ``` 1. **[Biomodel Name](/search/${biomodelID})** - **Biomodel Key:** ${biomodelId} || [Database link](https://www.biomodels.org/${biomodelId}) @@ -45,6 +47,7 @@ - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} ``` + ### Guidelines for Follow-up Questions and Further Actions * If there is an opportunity for follow-up questions or further actions, always ask the user if they'd like to explore more options or if you can assist with other related tasks. From 8296613f50dbe09cc089c48cb124154156407c17 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 15:14:07 -0400 Subject: [PATCH 33/60] introduce various changes to try and speed up llm response time --- backend/app/services/llms_service.py | 102 ++++++++++++++++++--------- 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index d4d7132..9acddef 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -21,6 +21,13 @@ logger = get_logger("llm_service") client = get_openai_client() +def summarize_tool_result(result, max_chars: int = 4000) -> str: + text = json.dumps(result, ensure_ascii=False) if not isinstance(result, str) else result + if len(text) <= max_chars: + return text + return text[:max_chars] + "\n...[truncated]..." + + async def get_llm_response(system_prompt: str, user_prompt: str): """ @@ -54,7 +61,6 @@ async def get_response_with_tools(conversation_history: list[dict], database: st ] messages = messages + conversation_history - print("BBBBBBB" + str(messages)) if database == "bmdb": print("DEBUG20: BIOMD POST: get_response_with_tools") @@ -72,7 +78,6 @@ async def get_response_with_tools(conversation_history: list[dict], database: st logger.info(f"User prompt: {user_prompt}") - print("DEBUG5: " + str(messages) + str(tools)) response = client.chat.completions.create( name="GET_RESPONSE_WITH_TOOLS::RETRIEVE_TOOLS", model=settings.AZURE_DEPLOYMENT_NAME, @@ -83,57 +88,84 @@ async def get_response_with_tools(conversation_history: list[dict], database: st # Handle the tool calls tool_calls = response.choices[0].message.tool_calls - # tool_calls_descriptions = tool_call.function.description for tool_call in tool_calls - # logger.info(f"DEBUG1: Tool descript: {tool_calls_descriptions}") messages.append(response.choices[0].message) bmkeys = [] - if tool_calls: - for tool_call in tool_calls: - # Extract the function name and arguments - name = tool_call.function.name - args = json.loads(tool_call.function.arguments) - - logger.info(f"Tool Call: {name} with args: {args}") - # Execute the tool function - result = await execute_tool(name, args) - logger.info(f"Tool Result: {str(result)[:500]}") + if not tool_calls: + direct_text = response.choices[0].message or "" + logger.info(f"LLM Response (no tools): {direct_text}") + return direct_text, bmkeys - bmkeys = [] - # Extract bmkeys only if result is a dictionary and contains the expected key - if isinstance(result, dict): - if database == "vcdb": - bmkeys = result.get("unique_model_keys (bmkey)", []) - elif database == "bmdb": - biomd_models = result.get("data", []) - bmkeys = [model.get("id") for model in biomd_models if model.get("id")] - # Send the result back to the model - messages.append( - {"role": "tool", "tool_call_id": tool_call.id, "content": str(result)} - ) + if tool_calls: + import asyncio + import json - print("AAAAAA") - with open ("output.txt", "w") as f: - print("BBBBBB") - print("DEBUG1: " + str(messages), file=f) + # execute all tool calls concurrently + tasks = [] + parsed_calls = [] - print("DEBUG2") - logger.info(str(messages)) + for tool_call in tool_calls: + name = tool_call.function.name + args = json.loads(tool_call.function.arguments) + parsed_calls.append((tool_call, name, args)) + tasks.append(execute_tool(name, args)) + + results = await asyncio.gather(*tasks) + + for (tool_call, name, args), result in zip(parsed_calls, results): + compact_result = summarize_tool_result(result) + messages.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(compact_result), + }) + # for tool_call in tool_calls: + # # Extract the function name and arguments + # name = tool_call.function.name + # args = json.loads(tool_call.function.arguments) + + # logger.info(f"Tool Call: {name} with args: {args}") + + # # Execute the tool function + # result = await execute_tool(name, args) + + # logger.info(f"Tool Result: {str(result)[:500]}") + + # bmkeys = [] + # # Extract bmkeys only if result is a dictionary and contains the expected key + # if isinstance(result, dict): + # if database == "vcdb": + # bmkeys = result.get("unique_model_keys (bmkey)", []) + # elif database == "bmdb": + # biomd_models = result.get("data", []) + # bmkeys = [model.get("id") for model in biomd_models if model.get("id")] + + # compact_result = summarize_tool_result(result) + + # # Send the result back to the model + # messages.append( + # {"role": "tool", "tool_call_id": tool_call.id, "content": str(compact_result)} + # ) + + + logger.info("DEBUG100-START") + print(len(str(messages))) # Send back the final response incorporating the tool result completion = client.chat.completions.create( name="GET_RESPONSE_WITH_TOOLS::PROCESS_TOOL_RESULTS", model=settings.AZURE_DEPLOYMENT_NAME, messages=messages, - metadata={ - "tool_calls": tool_calls, - }, + # metadata={ + # "tool_calls": tool_calls, + # }, ) + logger.info("DEBUG100-END") final_response = completion.choices[0].message.content From d994109c33664de21d70528ccdc6f2e724e24e44 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Tue, 24 Mar 2026 20:25:09 -0400 Subject: [PATCH 34/60] update summarize tool function and add clearer timing logs --- backend/app/services/llms_service.py | 39 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 9acddef..2e1cd23 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -18,14 +18,32 @@ import json from app.core.logger import get_logger +import time + +def log_timing(label: str, start: float): + duration = time.perf_counter() - start + logger.info(f"{label}: {duration:.3f}s") + logger = get_logger("llm_service") client = get_openai_client() -def summarize_tool_result(result, max_chars: int = 4000) -> str: - text = json.dumps(result, ensure_ascii=False) if not isinstance(result, str) else result - if len(text) <= max_chars: - return text - return text[:max_chars] + "\n...[truncated]..." +def summarize_tool_result(result): + if isinstance(result, dict) and "models" in result: + return [ + { + "id": m.get("id"), + "name": m.get("name"), + "description": m.get("description", "")[:200] + } + for m in result["models"][:5] + ] + return str(result)[:1000] + +async def timed_tool_call(name, args): + start = time.perf_counter() + result = await execute_tool(name, args) + log_timing(f"TOOL {name}", start) + return result @@ -53,6 +71,7 @@ async def get_llm_response(system_prompt: str, user_prompt: str): async def get_response_with_tools(conversation_history: list[dict], database: str): + total_start = time.perf_counter() messages = [ { "role": "system", @@ -62,6 +81,7 @@ async def get_response_with_tools(conversation_history: list[dict], database: st messages = messages + conversation_history + llm1_start = time.perf_counter() if database == "bmdb": print("DEBUG20: BIOMD POST: get_response_with_tools") response = client.chat.completions.create( @@ -86,6 +106,7 @@ async def get_response_with_tools(conversation_history: list[dict], database: st tool_choice="auto", ) + log_timing("LLM1 (tool selection)", llm1_start) # Handle the tool calls tool_calls = response.choices[0].message.tool_calls @@ -113,9 +134,11 @@ async def get_response_with_tools(conversation_history: list[dict], database: st name = tool_call.function.name args = json.loads(tool_call.function.arguments) parsed_calls.append((tool_call, name, args)) - tasks.append(execute_tool(name, args)) + tasks.append(timed_tool_call(name, args)) + tools_total_start = time.perf_counter() results = await asyncio.gather(*tasks) + log_timing("ALL TOOLS (parallel total)", tools_total_start) for (tool_call, name, args), result in zip(parsed_calls, results): compact_result = summarize_tool_result(result) @@ -156,6 +179,8 @@ async def get_response_with_tools(conversation_history: list[dict], database: st logger.info("DEBUG100-START") print(len(str(messages))) + llm2_start = time.perf_counter() + # Send back the final response incorporating the tool result completion = client.chat.completions.create( name="GET_RESPONSE_WITH_TOOLS::PROCESS_TOOL_RESULTS", @@ -165,11 +190,13 @@ async def get_response_with_tools(conversation_history: list[dict], database: st # "tool_calls": tool_calls, # }, ) + log_timing("LLM2 (final response)", llm2_start) logger.info("DEBUG100-END") final_response = completion.choices[0].message.content logger.info(f"LLM Response: {final_response}") + log_timing("TOTAL REQUEST", total_start) return final_response, bmkeys From 9da85247f8dc8103be63142e1f85955cf825a7fd Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 25 Mar 2026 20:01:30 -0400 Subject: [PATCH 35/60] add comments for each new implementation of a suggestion for speeding up llm response time --- backend/app/services/llms_service.py | 9 +++++++-- frontend/components/ChatBox.tsx | 30 +++++++++++++++++++--------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 2e1cd23..9132af7 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -20,6 +20,7 @@ import time +# adding specific time logs for easier profiling def log_timing(label: str, start: float): duration = time.perf_counter() - start logger.info(f"{label}: {duration:.3f}s") @@ -27,6 +28,7 @@ def log_timing(label: str, start: float): logger = get_logger("llm_service") client = get_openai_client() +# shorten tool result to only key fields id, name, and short description (limited to 200 characters) def summarize_tool_result(result): if isinstance(result, dict) and "models" in result: return [ @@ -35,10 +37,13 @@ def summarize_tool_result(result): "name": m.get("name"), "description": m.get("description", "")[:200] } + # limit to 5 models for m in result["models"][:5] ] + # limit result to at most 1000 characters return str(result)[:1000] +# adding specific time logs for easier profiling async def timed_tool_call(name, args): start = time.perf_counter() result = await execute_tool(name, args) @@ -115,13 +120,13 @@ async def get_response_with_tools(conversation_history: list[dict], database: st bmkeys = [] - + # introduce a fast path: if no tool_calls, return immediately if not tool_calls: direct_text = response.choices[0].message or "" logger.info(f"LLM Response (no tools): {direct_text}") return direct_text, bmkeys - + # perform tool calls concurrently rather than sequentially to reduce response time if tool_calls: import asyncio import json diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index aae5933..916d340 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -261,6 +261,7 @@ const saveConversation = (messages: Message[]) => { console.log("RRRRRR: " + "This is the promptPrefix: " + promptPrefix); console.log("RRRRRR: " + "This is the finalPrompt sent to backend: " + finalPrompt); + // Only send system messages, last 5 non-system messages, and new user prompt to backend const systemMessages = messages.filter((m) => m.role === "system"); const recentNonSystem = messages.filter((m) => m.role !== "system").slice(-5); @@ -383,6 +384,16 @@ const handleSendMessage2 = async (overrideMessage?: string) => { ? `${promptPrefix} ${msg}${parameterContext}` : `${msg}${parameterContext}`; + // Only send system messages, last 5 non-system messages, and new user prompt to backend + const systemMessages = messages.filter((m) => m.role === "system"); + const recentNonSystem = messages.filter((m) => m.role !== "system").slice(-5); + + const historyToSend = [ + ...systemMessages, + ...recentNonSystem, + { role: "user", content: finalPrompt }, + ].map((msg) => ({ role: msg.role, content: msg.content })); + const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/biomd-search`, { @@ -391,15 +402,16 @@ const handleSendMessage2 = async (overrideMessage?: string) => { "Content-Type": "application/json", accept: "application/json", }, - body: JSON.stringify({ - conversation_history: [ - ...messages, - { role: "user", content: finalPrompt }, - ].map(msg => ({ - role: msg.role, - content: msg.content, - })), - }), + body: JSON.stringify({ conversation_history: historyToSend }), + // JSON.stringify({ + // conversation_history: [ + // ...messages, + // { role: "user", content: finalPrompt }, + // ].map(msg => ({ + // role: msg.role, + // content: msg.content, + // })), + // }), signal: controller.signal, }, ); From 2b8032ec4c97f92716560af50e2089dbe5496394 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 25 Mar 2026 21:23:21 -0400 Subject: [PATCH 36/60] implement several suggestions for improving llm response time: separating tools into subsets and choosing which tools to send to llm based on user prompt; decreasing max result return --- backend/app/services/llms_service.py | 74 +++++++++++++++++++-- backend/app/utils/tools_utils.py | 96 +++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 9 deletions(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 9132af7..845dfce 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -1,7 +1,16 @@ +# from app.utils.tools_utils import ( +# ToolsDefinitions as tools, +# BIOMD_TOOLS as biotool, +# execute_tool, +# ) + + +# IMPLEMENTATION: separating tools into subsets and sending only relevant tools to llm from app.utils.tools_utils import ( - ToolsDefinitions as tools, - BIOMD_TOOLS as biotool, - execute_tool, + BIOMD_TOOLS as biotool, + execute_tool, + select_tools_for_prompt, + should_use_tools, ) from app.services.databases_service import ( @@ -28,6 +37,23 @@ def log_timing(label: str, start: float): logger = get_logger("llm_service") client = get_openai_client() +# IMPLEMENTATION: extract the last user message from the conversation history +def _last_user_message(conversation_history: list[dict]) -> str: + for msg in reversed(conversation_history): + if msg.get("role") == "user" and msg.get("content"): + return str(msg["content"]).strip() + return "" + +# IMPLEMENTATION: directly call llm without any tools for simple, conversational queries +def _direct_chat_completion(messages: list[dict]) -> str: + response = client.chat.completions.create( + name="GET_RESPONSE_DIRECT", + model=settings.AZURE_DEPLOYMENT_NAME, + messages=messages, + ) + return response.choices[0].message.content or "" + + # shorten tool result to only key fields id, name, and short description (limited to 200 characters) def summarize_tool_result(result): if isinstance(result, dict) and "models" in result: @@ -43,6 +69,7 @@ def summarize_tool_result(result): # limit result to at most 1000 characters return str(result)[:1000] + # adding specific time logs for easier profiling async def timed_tool_call(name, args): start = time.perf_counter() @@ -95,21 +122,54 @@ async def get_response_with_tools(conversation_history: list[dict], database: st tools=biotool, tool_choice="auto", ) - elif database == "vcdb": + # elif database == "vcdb": + + # user_prompt = conversation_history[-1]["content"] + # print("CCCCCCC" + str(user_prompt)) - user_prompt = conversation_history[-1]["content"] - print("CCCCCCC" + str(user_prompt)) + # logger.info(f"User prompt: {user_prompt}") + # response = client.chat.completions.create( + # name="GET_RESPONSE_WITH_TOOLS::RETRIEVE_TOOLS", + # model=settings.AZURE_DEPLOYMENT_NAME, + # messages=messages, + # tools=tools, + # tool_choice="auto", + # ) + + # IMPLEMENTATION: changing the way llm sees/chooses tools + elif database == "vcdb": + # extract last user message + user_prompt = _last_user_message(conversation_history) logger.info(f"User prompt: {user_prompt}") + # avoid the tool-calling process for simple, conversational promptsß + if not should_use_tools(user_prompt): + llm_direct_start = time.perf_counter() + + # generate the response directly + final_response = _direct_chat_completion(messages) + + # log timing for profiling + log_timing("LLM direct (no tools)", llm_direct_start) + log_timing("TOTAL REQUEST", total_start) + + # return response with no tool calls + return final_response, [] + + # only include relevant tools to the llm instead of all tools + selected_tools = select_tools_for_prompt(user_prompt) + + # first llm call to decide which tool to use from the given subset response = client.chat.completions.create( name="GET_RESPONSE_WITH_TOOLS::RETRIEVE_TOOLS", model=settings.AZURE_DEPLOYMENT_NAME, messages=messages, - tools=tools, + tools=selected_tools, tool_choice="auto", ) + log_timing("LLM1 (tool selection)", llm1_start) # Handle the tool calls diff --git a/backend/app/utils/tools_utils.py b/backend/app/utils/tools_utils.py index 2d504b2..7bdf10e 100644 --- a/backend/app/utils/tools_utils.py +++ b/backend/app/utils/tools_utils.py @@ -16,9 +16,15 @@ ParameterSchema, ) from app.core.logger import get_logger +import re logger = get_logger("tools_utils") +# NUMBER OF ROWS TO RETURN: +min_rows = 1 +max_rows = 50 +default_rows = 25 + # Function calling Definitions using Pydantic schema objects fetch_biomodels_tool = ToolDefinition( type="function", @@ -67,7 +73,9 @@ }, "maxRows": { "type": "integer", - "default": 1000, + "default": default_rows, + "minimum": min_rows, + "maximum": max_rows, "description": "The maximum number of results to return per page.", }, "orderBy": { @@ -237,6 +245,90 @@ BIOMD_TOOLS = [fetch_biomd_tool, get_xml_file_tool] + +# IMPLEMENTATION: separating all tool definitions into subsets +DB_TOOLS = [ + fetch_biomodels_tool, + fetch_simulation_details_tool, + get_vcml_file_tool, +] +KB_TOOLS = [ + search_vcell_knowledge_base_tool, +] +PUB_TOOLS = [ + fetch_publications_tool, +] + +# decide which subset (if any) of tools to send to the llm +# returning false skips tools and directly calls llm +# returning true allows the llm to use tools +def should_use_tools(prompt: str) -> bool: + if not prompt: + return False + + p = prompt.lower().strip() + + # common prefixes where tools are unnecessary + plain_chat_prefixes = ( + "summarize this", + "improve this", + "make this clearer", + ) + if p.startswith(plain_chat_prefixes): + return False + + # each signal indicates when tools are needed + # list of patterns that suggest a database lookup/a structured retrieval + tool_signals = [ + r"\b(list|show|find|get|fetch|search)\b", + r"\bmodel\b|\bmodels\b|\bbiomodel\b|\bbiomodels\b", + r"\bsimulation\b|\bsimulations\b", + r"\bvcml\b|\bxml\b", + r"\bpublication\b|\bpublications\b|\bpaper\b|\bpapers\b|\bpubmed\b", + r"\btutorial\b|\beducational\b|\bknowledge base\b", + r"\bhow do i\b|\bhow to\b|\bwhat is vcell\b", + r"\bBM\d+\b|\bBIOMD\d+\b", + ] + + # if any tool signal matches then use tools + return any(re.search(pattern, p) for pattern in tool_signals) + +# select only a subset of tools based on the user prompt +def select_tools_for_prompt(prompt: str): + p = (prompt or "").lower() + + # tools that the llm will see when making its choice + selected = [] + + # Database/data-fetch intent + if re.search(r"\b(model|models|biomodel|biomodels|simulation|simulations|vcml|bm\d+)\b", p): + selected.extend(DB_TOOLS) + + # Publications intent + if re.search(r"\b(publication|publications|paper|papers|pubmed)\b", p): + selected.extend(PUB_TOOLS) + + # Knowledge / tutorial / how-to intent + if re.search(r"\b(tutorial|educational|knowledge base|how do i|how to|what is vcell|explain)\b", p): + selected.extend(KB_TOOLS) + + # Default fallback: if tools are needed but no bucket matched, keep KB only. + if not selected: + selected = KB_TOOLS + + # De-duplicate while preserving order + deduped = [] + seen = set() + for tool in selected: + name = tool.function.name + if name not in seen: + deduped.append(tool) + seen.add(name) + + return deduped + + + # Tool Executor Function async def execute_tool(name, args): """ @@ -255,7 +347,7 @@ async def execute_tool(name, args): # args["savedLow"] = None # if args.get("savedHigh") == "": # args["savedHigh"] = None - args["maxRows"] = 1000 + args["maxRows"] = default_rows params = BiomodelRequestParams(**args) print("DEBUG About to call fetch_biomodels()") return await fetch_biomodels(params) From 6e41b2bf6f284982dc33ea019d7d541c090c6309 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sat, 28 Mar 2026 00:28:41 -0400 Subject: [PATCH 37/60] remove some link formatting from system_prompt and add rules for long lists (>10 models) --- backend/app/utils/system_prompt.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index aa9724b..116f844 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -28,7 +28,7 @@ * For each VCELL model: ``` 1. **[Biomodel Name](/search/${biomodelID})** - - **Biomodel Key:** ${biomodelId} || [Database link](https://vcell.cam.uchc.edu/api/v0/biomodel/${biomodelId}) + - **Biomodel Key:** ${biomodelId} - **Owner:** ${owner} - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} - **Applications:** @@ -42,11 +42,19 @@ * For each BIOMD model: ``` 1. **[Biomodel Name](/search/${biomodelID})** - - **Biomodel Key:** ${biomodelId} || [Database link](https://www.biomodels.org/${biomodelId}) + - **Biomodel Key:** ${biomodelId} - **Owner:** ${owner} - **Description:** ${description or summary of the biomodel, do not include `clonedFrom` info} ``` +### Rules for LONG LISTS (>10 models) + +- Continue numbering sequentially (1, 2, 3, ...) +- Repeat the EXACT same structure for EVERY item +- If applications exist, do NOT omit them +- Do NOT summarize or shorten later items +- Do NOT merge multiple models into one entry +- Maintain identical formatting across all entries ### Guidelines for Follow-up Questions and Further Actions * If there is an opportunity for follow-up questions or further actions, always ask the user if they'd like to explore From 5a965fff9f1186598d0e33244c6cd82d0d20fcf8 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sat, 28 Mar 2026 00:53:40 -0400 Subject: [PATCH 38/60] remove specific link formatting --- backend/app/utils/system_prompt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 116f844..6ab8879 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -20,9 +20,8 @@ proper rendering. Example: "The rate is $5.2 \times 10^{-3} \text{ mmol}\cdot\text{ml}^{-1}\cdot\text{min}^{-1}$". ## Formatting Guidelines for Biomodels -You MUST follow this exact output format. Do NOT modify, omit, or reorder any fields. Do NOT change any links. -NEVER output plain text for the biomodel name (it must always be a link to its ID-specific page only). ALWAYS add a database -link to the models CORRECT respective database. +You MUST follow this exact output format. Do NOT modify, omit, or reorder any fields. +NEVER output plain text for the biomodel name (it must always be a link to its ID-specific page: /search/${biomodelID}). ### Formatting Guidelines for biomodels retrieved from VCell database (VCDB) * For each VCELL model: From 4c9ac121a607f6ba9551c37bf2b59d8a9a2d209a Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sat, 28 Mar 2026 00:54:18 -0400 Subject: [PATCH 39/60] define a function to format a link to the database for each biomodel based on its type --- frontend/components/ChatBox.tsx | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 916d340..b166788 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -171,6 +171,30 @@ const saveConversation = (messages: Message[]) => { ...(useBMDB && bmdbActions ? bmdbActions : []), ]; + const formatBiomodelIds = (content: string, bmkeys: string[], db: "vcdb" | "bmdb"): string => { + if (!bmkeys || bmkeys.length === 0) return content; + + let formattedContent = content; + let db_link = ""; + + if (db == "vcdb") { + db_link = "https://vcell.cam.uchc.edu/api/v0/biomodel/" + } else if (db == "bmdb") { + db_link = "https://www.biomodels.org/" + } + // Replace biomodel IDs with hyperlinks + bmkeys.forEach((bmId) => { + const link = `[Database Details](${db_link}${bmId})`; + const replacementString = `${bmId} || ${link}`; + + // only replace if bmId is not already in a /search/ link + const regex = new RegExp(`(? { setInputMessage(""); if (database) { @@ -298,10 +322,10 @@ const saveConversation = (messages: Message[]) => { const aiResponse = data.response || "Sorry, I didn't get a response from the server."; const bmkeys = data.bmkeys || []; + console.log(bmkeys) // Format the response to include hyperlinks for biomodel IDs - //const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); - const formattedResponse = aiResponse + const formattedResponse = formatBiomodelIds(aiResponse, bmkeys, "vcdb"); const assistantMessage: Message = { id: (Date.now() + 1).toString(), @@ -422,8 +446,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { console.log(bmkeys) // Format the response to include hyperlinks for biomodel IDs - //const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); - const formattedResponse = aiResponse + const formattedResponse = formatBiomodelIds(aiResponse, bmkeys, "bmdb"); const assistantMessage: Message = { id: (Date.now() + 1).toString(), From 0b90dc3fae9b6cc21cd3ca693753ab598e3ab2bf Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sat, 28 Mar 2026 00:58:27 -0400 Subject: [PATCH 40/60] change the summarize tool function to only shorten results without changing format, this way the llm will stop returning false results --- backend/app/services/llms_service.py | 52 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 845dfce..158d991 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -54,20 +54,24 @@ def _direct_chat_completion(messages: list[dict]) -> str: return response.choices[0].message.content or "" -# shorten tool result to only key fields id, name, and short description (limited to 200 characters) +# do not change the tool call formatting, only shorten results +# this way the llm will stop returning false results def summarize_tool_result(result): if isinstance(result, dict) and "models" in result: - return [ - { - "id": m.get("id"), - "name": m.get("name"), - "description": m.get("description", "")[:200] - } - # limit to 5 models - for m in result["models"][:5] - ] - # limit result to at most 1000 characters - return str(result)[:1000] + return { + "models": [ + { + "id": m.get("id"), + "name": m.get("name"), + "description": m.get("description", "")[:200], + "score": m.get("score"), # keep useful signals + } + for m in result["models"][:5] + ], + "total": result.get("total"), + } + + return result # adding specific time logs for easier profiling @@ -210,9 +214,19 @@ async def get_response_with_tools(conversation_history: list[dict], database: st messages.append({ "role": "tool", "tool_call_id": tool_call.id, - "content": str(compact_result), + "content": json.dumps(compact_result, ensure_ascii=False), }) - # for tool_call in tool_calls: + + # extract the bmkeys + for tool_call in tool_calls: + bmkeys = [] + # Extract bmkeys only if result is a dictionary and contains the expected key + if isinstance(result, dict): + if database == "vcdb": + bmkeys = result.get("unique_model_keys (bmkey)", []) + elif database == "bmdb": + biomd_models = result.get("data", []) + bmkeys = [model.get("id") for model in biomd_models if model.get("id")] # # Extract the function name and arguments # name = tool_call.function.name # args = json.loads(tool_call.function.arguments) @@ -224,15 +238,6 @@ async def get_response_with_tools(conversation_history: list[dict], database: st # logger.info(f"Tool Result: {str(result)[:500]}") - # bmkeys = [] - # # Extract bmkeys only if result is a dictionary and contains the expected key - # if isinstance(result, dict): - # if database == "vcdb": - # bmkeys = result.get("unique_model_keys (bmkey)", []) - # elif database == "bmdb": - # biomd_models = result.get("data", []) - # bmkeys = [model.get("id") for model in biomd_models if model.get("id")] - # compact_result = summarize_tool_result(result) # # Send the result back to the model @@ -243,6 +248,7 @@ async def get_response_with_tools(conversation_history: list[dict], database: st logger.info("DEBUG100-START") print(len(str(messages))) + print("DEBUG300: ", messages) llm2_start = time.perf_counter() From 372202b91e7edcef1c4f074ce532d61b354bf456 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sat, 28 Mar 2026 01:10:20 -0400 Subject: [PATCH 41/60] when querying two databases, keep the "AI is thinking" box until both answers are registered on the chatpage --- frontend/components/ChatBox.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index b166788..d69ff27 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -108,11 +108,13 @@ export const ChatBox: React.FC = ({ const [conversationId, setConversationId] = useState(null); const [useVCDB, setUseVCDB] = useState(database ? database === "vcdb" : true); const [useBMDB, setUseBMDB] = useState(database === "bmdb"); - const [isLoading, setIsLoading] = useState(false); + const [isLoadingVCDB, setIsLoadingVCDB] = useState(false); + const [isLoadingBMDB, setIsLoadingBMDB] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const abortController = useRef(null); + const isLoading = isLoadingVCDB || isLoadingBMDB; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -277,7 +279,7 @@ const saveConversation = (messages: Message[]) => { // }; // setMessages((prev) => [...prev, userMessage]); setInputMessage(""); - setIsLoading(true); + setIsLoadingVCDB(true); try { const finalPrompt = promptPrefix ? `${promptPrefix} ${msg}${parameterContext}` @@ -348,7 +350,7 @@ const saveConversation = (messages: Message[]) => { }, ]); } finally { - setIsLoading(false); + setIsLoadingVCDB(false); } }; // End of handleSendMessage @@ -402,7 +404,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { // }; // setMessages((prev) => [...prev, userMessage]); setInputMessage(""); - setIsLoading(true); + setIsLoadingBMDB(true); try { const finalPrompt = promptPrefix ? `${promptPrefix} ${msg}${parameterContext}` @@ -469,7 +471,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { }, ]); } finally { - setIsLoading(false); + setIsLoadingBMDB(false); } }; // End of handleSendMessage2 @@ -497,7 +499,8 @@ const handleSendMessage2 = async (overrideMessage?: string) => { }, ]); - setIsLoading(false); + setIsLoadingVCDB(false); + setIsLoadingBMDB(false); } }; From 96ee74dad48f0d43565d494394556eed75723bd1 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Sun, 29 Mar 2026 16:58:54 -0400 Subject: [PATCH 42/60] editing system prompt to fix link formatting for biomodel names. --- backend/app/utils/system_prompt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 6ab8879..703d079 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -21,7 +21,7 @@ ## Formatting Guidelines for Biomodels You MUST follow this exact output format. Do NOT modify, omit, or reorder any fields. -NEVER output plain text for the biomodel name (it must always be a link to its ID-specific page: /search/${biomodelID}). +ALWAYS use the provided name and biomodelID exactly. Format the name as [name](/search/biomodelID). ### Formatting Guidelines for biomodels retrieved from VCell database (VCDB) * For each VCELL model: @@ -48,7 +48,7 @@ ### Rules for LONG LISTS (>10 models) -- Continue numbering sequentially (1, 2, 3, ...) +- ALWAYS continue numbering sequentially (1, 2, 3, ...) - Repeat the EXACT same structure for EVERY item - If applications exist, do NOT omit them - Do NOT summarize or shorten later items From 734af8fee76946713bf1ce26e37fbe6a6e0525f3 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Thu, 2 Apr 2026 11:05:57 -0400 Subject: [PATCH 43/60] add a tool summary to output to show how long the query/all tools took to generate the final response --- backend/app/controllers/llms_controller.py | 4 +- backend/app/routes/llms_router.py | 8 ++-- backend/app/services/llms_service.py | 53 ++++++++++++++++++---- frontend/components/ChatBox.tsx | 21 +++++++-- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/backend/app/controllers/llms_controller.py b/backend/app/controllers/llms_controller.py index 247efbc..6c0b4f8 100644 --- a/backend/app/controllers/llms_controller.py +++ b/backend/app/controllers/llms_controller.py @@ -17,8 +17,8 @@ async def get_llm_response(conversation_history: list[dict], database: str) -> t """ try: print("DEBUG20: BIOMD POST: get_llm_response") - result, bmkeys = await get_response_with_tools(conversation_history, database) - return result, bmkeys + result, bmkeys, tool_summary = await get_response_with_tools(conversation_history, database) + return result, bmkeys, tool_summary except Exception as e: raise HTTPException(status_code=500, detail=f"Error: {str(e)}") diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index fa95f79..e513efb 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -22,19 +22,19 @@ async def query_llm(conversation_history: dict): Returns: dict: The final response after processing the prompt with the tools. """ - result, bmkeys = await get_llm_response( + result, bmkeys, tool_summary = await get_llm_response( conversation_history.get("conversation_history", []), database="vcdb" ) - return {"response": result, "bmkeys": bmkeys} + return {"response": result, "bmkeys": bmkeys, "tool_summary": tool_summary} # For BioModelsDB search using BioModelsDB API @router.post("/biomd-search") async def search_llm(conversation_history: dict): print("DEBUG20: BIOMD POST: ROUTER") - result, bmkeys = await get_llm_response( + result, bmkeys, tool_summary = await get_llm_response( conversation_history.get("conversation_history", []), database="bmdb" ) - return {"response": result, "bmkeys": bmkeys} + return {"response": result, "bmkeys": bmkeys, "tool_summary": tool_summary} diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 158d991..989fd63 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -107,6 +107,7 @@ async def get_llm_response(system_prompt: str, user_prompt: str): async def get_response_with_tools(conversation_history: list[dict], database: str): + # start the total request timer for timing of the entire process total_start = time.perf_counter() messages = [ { @@ -117,7 +118,12 @@ async def get_response_with_tools(conversation_history: list[dict], database: st messages = messages + conversation_history + # create a summary string of all timing logs to print to frontend + tool_summary = "" + + # llm tool selection call llm1_start = time.perf_counter() + if database == "bmdb": print("DEBUG20: BIOMD POST: get_response_with_tools") response = client.chat.completions.create( @@ -150,6 +156,7 @@ async def get_response_with_tools(conversation_history: list[dict], database: st # avoid the tool-calling process for simple, conversational promptsß if not should_use_tools(user_prompt): + # if no tools are used, then skip to immediate response llm_direct_start = time.perf_counter() # generate the response directly @@ -160,10 +167,11 @@ async def get_response_with_tools(conversation_history: list[dict], database: st log_timing("TOTAL REQUEST", total_start) # return response with no tool calls - return final_response, [] + return final_response, [], "" # no tool summary since no tools used # only include relevant tools to the llm instead of all tools selected_tools = select_tools_for_prompt(user_prompt) + logger.info(f"TOOL SUBSET: {selected_tools}") # first llm call to decide which tool to use from the given subset response = client.chat.completions.create( @@ -174,8 +182,14 @@ async def get_response_with_tools(conversation_history: list[dict], database: st tool_choice="auto", ) + # log timing after the llm selects which tool to use + log_timing("LLM1 - selecting tools from the subset", llm1_start) + llm1_time = time.perf_counter() - llm1_start + print(selected_tools) + tool_summary += f"*We selected subset tools: {', '.join([t.function.name for t in selected_tools])}* " + tool_summary += f"*The LLM call to select tools from the subset took {llm1_time:.2f}s.* " + tool_summary += f"*The LLM chose to use {len(response.choices[0].message.tool_calls)} tool(s) from the subset.* " - log_timing("LLM1 (tool selection)", llm1_start) # Handle the tool calls tool_calls = response.choices[0].message.tool_calls @@ -188,7 +202,7 @@ async def get_response_with_tools(conversation_history: list[dict], database: st if not tool_calls: direct_text = response.choices[0].message or "" logger.info(f"LLM Response (no tools): {direct_text}") - return direct_text, bmkeys + return direct_text, bmkeys, "" # perform tool calls concurrently rather than sequentially to reduce response time if tool_calls: @@ -198,6 +212,7 @@ async def get_response_with_tools(conversation_history: list[dict], database: st # execute all tool calls concurrently tasks = [] parsed_calls = [] + tool_timings = [] for tool_call in tool_calls: name = tool_call.function.name @@ -205,9 +220,15 @@ async def get_response_with_tools(conversation_history: list[dict], database: st parsed_calls.append((tool_call, name, args)) tasks.append(timed_tool_call(name, args)) + # log timing for how long the tool calls take to execute in total tools_total_start = time.perf_counter() results = await asyncio.gather(*tasks) - log_timing("ALL TOOLS (parallel total)", tools_total_start) + + # log total time for all tool calls together + tools_total_time = time.perf_counter() - tools_total_start + log_timing("EXECUTION OF TOOL CALLS", tools_total_start) + tool_summary += f"*Executing the tool calls took {tools_total_time:.2f}s.* " + for (tool_call, name, args), result in zip(parsed_calls, results): compact_result = summarize_tool_result(result) @@ -216,7 +237,16 @@ async def get_response_with_tools(conversation_history: list[dict], database: st "tool_call_id": tool_call.id, "content": json.dumps(compact_result, ensure_ascii=False), }) - + + # log timing for each individual tool call + tool_timings.append({ + "tool_name": name, + "args": args, + "duration_s": round(time.perf_counter() - tools_total_start, 3) + }) + logger.info(f"Individual tool call timings: {tool_timings}") + tool_summary += f"Executing each tool call took: " + ", ".join([f"{t['tool_name']} ({t['duration_s']}s)" for t in tool_timings]) + "." + # extract the bmkeys for tool_call in tool_calls: bmkeys = [] @@ -248,8 +278,9 @@ async def get_response_with_tools(conversation_history: list[dict], database: st logger.info("DEBUG100-START") print(len(str(messages))) - print("DEBUG300: ", messages) + print("DEBUG100: ", messages) + # log timing for the final llm call that uses the tool result llm2_start = time.perf_counter() # Send back the final response incorporating the tool result @@ -261,15 +292,21 @@ async def get_response_with_tools(conversation_history: list[dict], database: st # "tool_calls": tool_calls, # }, ) + + llm2_time = time.perf_counter() - llm2_start log_timing("LLM2 (final response)", llm2_start) + tool_summary += f"*The final LLM call took {llm2_time:.2f}s.* " + logger.info("DEBUG100-END") final_response = completion.choices[0].message.content logger.info(f"LLM Response: {final_response}") - log_timing("TOTAL REQUEST", total_start) + log_timing("TOTAL REQUEST TIME (from initial request to final output)", total_start) + total_time = time.perf_counter() - total_start + tool_summary += f"*Total request time: {total_time:.2f}s.*" - return final_response, bmkeys + return final_response, bmkeys, tool_summary async def analyse_vcml(biomodel_id: str): diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index d69ff27..a490a09 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -325,16 +325,22 @@ const saveConversation = (messages: Message[]) => { data.response || "Sorry, I didn't get a response from the server."; const bmkeys = data.bmkeys || []; console.log(bmkeys) + const toolSummary = data.tool_summary || ""; // Format the response to include hyperlinks for biomodel IDs const formattedResponse = formatBiomodelIds(aiResponse, bmkeys, "vcdb"); + // show the tool summar text in the website output + const toolSummaryText = toolSummary + ? `\n\n${toolSummary}` + : ""; + const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: useVCDB && useBMDB - ? `**VCell Database:**\n\n${formattedResponse}` - : formattedResponse, + ? `**VCell Database:**\n\n${formattedResponse}${toolSummaryText}` + : `${formattedResponse}${toolSummaryText}`, timestamp: new Date(), }; setMessages((prev) => [...prev, assistantMessage]); @@ -446,16 +452,23 @@ const handleSendMessage2 = async (overrideMessage?: string) => { data.response || "Sorry, I didn't get a response from the server."; const bmkeys = data.bmkeys || []; + const toolSummary = data.tool_summary || ""; + console.log(bmkeys) // Format the response to include hyperlinks for biomodel IDs const formattedResponse = formatBiomodelIds(aiResponse, bmkeys, "bmdb"); + // show tool summary text in the website output + const toolSummaryText = toolSummary + ? `\n\n${toolSummary}` + : ""; + const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: useVCDB && useBMDB - ? `**BIOMD Database:**\n\n${formattedResponse}` - : formattedResponse, + ? `**BIOMD Database:**\n\n${formattedResponse}${toolSummaryText}` + : `${formattedResponse}${toolSummaryText}`, timestamp: new Date(), }; setMessages((prev) => [...prev, assistantMessage]); From 3001e0da279f8ae950368fa7175c7ea3abe22216 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Mon, 6 Apr 2026 11:53:04 -0400 Subject: [PATCH 44/60] update /search/page.tsx to allow for both databases to be selected and queried even on the biomodel-id-specific page instead of only one --- frontend/app/search/[bmid]/page.tsx | 9 +++++++-- frontend/components/ChatBox.tsx | 11 ++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index cc16d0f..324df1a 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -142,6 +142,11 @@ export default function BiomodelDetailPage() { }, ]; + // set default selected checkbox as bmdb if the id is from biomodels database, otherwise set to vcdb + const [selectedDatabases, setSelectedDatabases] = useState<("bmdb" | "vcdb")[]>( + bioMDid ? ["bmdb"] : ["vcdb"] + ); + useEffect(() => { if (!bmid) return; setLoading(true); @@ -383,7 +388,7 @@ export default function BiomodelDetailPage() {
-
- = ({ const [inputMessage, setInputMessage] = useState(""); const [conversationId, setConversationId] = useState(null); - const [useVCDB, setUseVCDB] = useState(database ? database === "vcdb" : true); - const [useBMDB, setUseBMDB] = useState(database === "bmdb"); + const [useVCDB, setUseVCDB] = useState(database ? database.includes("vcdb") : true); + const [useBMDB, setUseBMDB] = useState(database ? database.includes("bmdb") : false); const [isLoadingVCDB, setIsLoadingVCDB] = useState(false); const [isLoadingBMDB, setIsLoadingBMDB] = useState(false); const messagesEndRef = useRef(null); @@ -200,9 +200,10 @@ const saveConversation = (messages: Message[]) => { const handleQuickAction = (message: string) => { setInputMessage(""); if (database) { - if (database == "vcdb") { + if (database.includes("vcdb")) { handleSendMessage(message); - } else if (database == "bmdb") { + } + if (database.includes("bmdb")) { handleSendMessage2(message); } } else { From 113b3cedafe26fc538afae8b4bd85a7006c151bc Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Mon, 6 Apr 2026 12:20:33 -0400 Subject: [PATCH 45/60] change all references (in all files) for Biomodels Database to be BMDB for consistency --- backend/app/controllers/llms_controller.py | 2 +- backend/app/routes/llms_router.py | 18 ++++----------- backend/app/services/databases_service.py | 10 ++++----- backend/app/services/llms_service.py | 17 +++++--------- backend/app/utils/system_prompt.py | 2 +- backend/app/utils/tools_utils.py | 13 +++++------ frontend/app/search/[bmid]/page.tsx | 26 +++++++++++----------- frontend/components/ChatBox.tsx | 10 ++++----- 8 files changed, 40 insertions(+), 58 deletions(-) diff --git a/backend/app/controllers/llms_controller.py b/backend/app/controllers/llms_controller.py index 6c0b4f8..73bacb3 100644 --- a/backend/app/controllers/llms_controller.py +++ b/backend/app/controllers/llms_controller.py @@ -16,7 +16,7 @@ async def get_llm_response(conversation_history: list[dict], database: str) -> t tuple[str, list]: A tuple containing the final response and bmkeys list. """ try: - print("DEBUG20: BIOMD POST: get_llm_response") + print("DEBUG20: BMDB POST: get_llm_response") result, bmkeys, tool_summary = await get_response_with_tools(conversation_history, database) return result, bmkeys, tool_summary except Exception as e: diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index e513efb..a83cc34 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -28,24 +28,14 @@ async def query_llm(conversation_history: dict): return {"response": result, "bmkeys": bmkeys, "tool_summary": tool_summary} # For BioModelsDB search using BioModelsDB API -@router.post("/biomd-search") +@router.post("/bmdb-search") async def search_llm(conversation_history: dict): - print("DEBUG20: BIOMD POST: ROUTER") - result, bmkeys, tool_summary = await get_llm_response( + print("DEBUG20: BMDB POST: ROUTER") + result, bmdbkeys, tool_summary = await get_llm_response( conversation_history.get("conversation_history", []), database="bmdb" ) - return {"response": result, "bmkeys": bmkeys, "tool_summary": tool_summary} - - + return {"response": result, "bmkeys": bmdbkeys, "tool_summary": tool_summary} -# @router.get("/biomodels-search") -# async def biomodels_search(query: str): -# async with httpx.AsyncClient() as client: -# response = await client.get( -# "https://www.biomodels.org/search", -# params={"query": query, "format": "json"} -# ) -# return response.json() @router.post("/analyse/{biomodel_id}") async def analyse_biomodel(biomodel_id: str, user_prompt: str): diff --git a/backend/app/services/databases_service.py b/backend/app/services/databases_service.py index 5d94bf1..45558cb 100644 --- a/backend/app/services/databases_service.py +++ b/backend/app/services/databases_service.py @@ -140,9 +140,9 @@ async def fetch_simulation_details(params: SimulationRequestParams) -> dict: response.raise_for_status() return response.json() -@observe(name="FETCH_BIOMD_MODELS") -async def fetch_biomd_models(params: BiomodelRequestParams) -> dict: - print("DEBUG20: BIOMD POST: in tool FETCH_BIOMD_MODELS") +@observe(name="FETCH_BMDB_MODELS") +async def fetch_bmdb_models(params: BiomodelRequestParams) -> dict: + print("DEBUG20: BMDB POST: in tool FETCH_BMDB_MODELS") # Construct the query string using urlencoded parameters (params_dict) query_string = params.bmName if params.bmName else params.bmId @@ -185,10 +185,10 @@ async def get_xml_file(bmId: str, truncate: bool = False, max_retries: int = 3) # Check connectivity first if not await check_vcell_connectivity(): logger.error( - "BioMD API is not reachable. Please check your network connection and DNS settings." + "BMDB API is not reachable. Please check your network connection and DNS settings." ) raise Exception( - "BioMD API is not reachable. Please check your network connection and DNS settings." + "BMDB API is not reachable. Please check your network connection and DNS settings." ) for attempt in range(max_retries + 1): diff --git a/backend/app/services/llms_service.py b/backend/app/services/llms_service.py index 989fd63..c90648c 100644 --- a/backend/app/services/llms_service.py +++ b/backend/app/services/llms_service.py @@ -1,13 +1,6 @@ -# from app.utils.tools_utils import ( -# ToolsDefinitions as tools, -# BIOMD_TOOLS as biotool, -# execute_tool, -# ) - - # IMPLEMENTATION: separating tools into subsets and sending only relevant tools to llm from app.utils.tools_utils import ( - BIOMD_TOOLS as biotool, + BMDB_TOOLS as bmdbtools, execute_tool, select_tools_for_prompt, should_use_tools, @@ -125,11 +118,11 @@ async def get_response_with_tools(conversation_history: list[dict], database: st llm1_start = time.perf_counter() if database == "bmdb": - print("DEBUG20: BIOMD POST: get_response_with_tools") + print("DEBUG20: BMDB POST: get_response_with_tools") response = client.chat.completions.create( model=settings.AZURE_DEPLOYMENT_NAME, messages=messages, - tools=biotool, + tools=bmdbtools, tool_choice="auto", ) # elif database == "vcdb": @@ -255,8 +248,8 @@ async def get_response_with_tools(conversation_history: list[dict], database: st if database == "vcdb": bmkeys = result.get("unique_model_keys (bmkey)", []) elif database == "bmdb": - biomd_models = result.get("data", []) - bmkeys = [model.get("id") for model in biomd_models if model.get("id")] + bmdb_models = result.get("data", []) + bmkeys = [model.get("id") for model in bmdb_models if model.get("id")] # # Extract the function name and arguments # name = tool_call.function.name # args = json.loads(tool_call.function.arguments) diff --git a/backend/app/utils/system_prompt.py b/backend/app/utils/system_prompt.py index 703d079..7f6a33a 100644 --- a/backend/app/utils/system_prompt.py +++ b/backend/app/utils/system_prompt.py @@ -38,7 +38,7 @@ ``` ### Formatting Guidelines for biomodels retrieved from BioModels database (BMDB) -* For each BIOMD model: +* For each BMDB model: ``` 1. **[Biomodel Name](/search/${biomodelID})** - **Biomodel Key:** ${biomodelId} diff --git a/backend/app/utils/tools_utils.py b/backend/app/utils/tools_utils.py index 7bdf10e..84b8360 100644 --- a/backend/app/utils/tools_utils.py +++ b/backend/app/utils/tools_utils.py @@ -4,7 +4,7 @@ fetch_simulation_details, get_vcml_file, fetch_publications, - fetch_biomd_models, + fetch_bmdb_models, get_xml_file ) from app.services.knowledge_base_service import get_similar_chunks @@ -189,10 +189,10 @@ ) -fetch_biomd_tool = ToolDefinition( +fetch_bmdb_tool = ToolDefinition( type="function", function=FunctionDefinition( - name="fetch_biomd_models", + name="fetch_bmdb_models", description="Retrieves a list of biomodels from the BioModels database based on filtering criteria which is the biomodel name. This allows to search for specific biomodels in the BioModels database based on their attributes and retrieve the results.", parameters=ParameterSchema( type="object", @@ -242,7 +242,7 @@ search_vcell_knowledge_base_tool, fetch_publications_tool, ] -BIOMD_TOOLS = [fetch_biomd_tool, +BMDB_TOOLS = [fetch_bmdb_tool, get_xml_file_tool] @@ -369,10 +369,9 @@ async def execute_tool(name, args): elif name == "fetch_publications": return await fetch_publications() - elif name == "fetch_biomd_models": + elif name == "fetch_bmdb_models": params = BiomodelRequestParams(**args) - print("DEBUG About to call fetch_biomodels()") - return await fetch_biomd_models(params) + return await fetch_bmdb_models(params) elif name == "get_xml_file": return await get_xml_file(args["bmId"]) diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index 324df1a..8606955 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -79,7 +79,7 @@ interface BiomodelDetail { } interface BiomodelDBDetail { - bioID: string; + bmdbID: string; name: string; author?: string; description?: string; @@ -94,7 +94,7 @@ interface BiomodelDBDetail { export default function BiomodelDetailPage() { const params = useParams<{ bmid: string }>(); const bmid = params?.bmid; - const bioMDid = bmid.startsWith("BIOMD"); + const bmdbID = bmid.startsWith("BIOMD"); const [data, setData] = useState(null); const [bmdbData, setBmdbData] = useState(null); const [loading, setLoading] = useState(true); @@ -144,7 +144,7 @@ export default function BiomodelDetailPage() { // set default selected checkbox as bmdb if the id is from biomodels database, otherwise set to vcdb const [selectedDatabases, setSelectedDatabases] = useState<("bmdb" | "vcdb")[]>( - bioMDid ? ["bmdb"] : ["vcdb"] + bmdbID ? ["bmdb"] : ["vcdb"] ); useEffect(() => { @@ -152,7 +152,7 @@ export default function BiomodelDetailPage() { setLoading(true); setError(""); - if (bioMDid) { + if (bmdbID) { fetch(`${process.env.NEXT_PUBLIC_API_URL2}/${bmid}?format=json`) .then((res) => { if (!res.ok) throw new Error("Failed to fetch biomodel details"); @@ -160,7 +160,7 @@ export default function BiomodelDetailPage() { }) .then((json) => { setBmdbData({ - bioID: bmid, + bmdbID: bmid, name: json.name, author: json.publication?.authors?.map((a: any) => a.name) || [], description: extractDescription(json.description || ""), @@ -205,7 +205,7 @@ export default function BiomodelDetailPage() { try { // get diagram image from biomodels database - if (bioMDid) { + if (bmdbID) { const bmdbAPIUrl = process.env.NEXT_PUBLIC_API_URL2; const bmdbRes = await fetch(`${bmdbAPIUrl}/model/download/${bmid}?filename=${bmid}.png`); if (bmdbRes.ok) { @@ -243,11 +243,11 @@ export default function BiomodelDetailPage() { }; fetchDiagramAnalysis(); - }, [data?.bmKey, bioMDid, bmid]); + }, [data?.bmKey, bmdbID, bmid]); // Create combined messages when diagram analysis is ready useEffect(() => { - if (diagramAnalysis && !bioMDid && data?.bmKey) { + if (diagramAnalysis && !bmdbID && data?.bmKey) { const diagramMessage = `# Diagram Analysis \n ${diagramAnalysis}`; setCombinedMessages([diagramMessage]); } @@ -259,7 +259,7 @@ export default function BiomodelDetailPage() { let biomodelDiagramUrl = ""; if (data) { biomodelDiagramUrl = `https://vcell.cam.uchc.edu/api/v0/biomodel/${data.bmKey}/diagram`; - } else if (bioMDid) { + } else if (bmdbID) { biomodelDiagramUrl = diagramAnalysis; // the base64 image string } @@ -268,7 +268,7 @@ export default function BiomodelDetailPage() { return ( <> - {bioMDid && bmdbData && ( + {bmdbID && bmdbData && (
@@ -279,7 +279,7 @@ export default function BiomodelDetailPage() { {" "} - {bmdbData?.bioID} + {bmdbData?.bmdbID} {" "} {bmdbData?.author} @@ -392,7 +392,7 @@ export default function BiomodelDetailPage() { startMessage={""} quickActions={quickActions} cardTitle={"BioModels AI Assistant"} - promptPrefix={`Analyze the BioModels model with ID ${bmdbData.bioID}`} + promptPrefix={`Analyze the BioModels model with ID ${bmdbData.bmdbID}`} isLoading={false} />
@@ -404,7 +404,7 @@ export default function BiomodelDetailPage() {
-)} {!bioMDid && data && ( +)} {!bmdbID && data && (
diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 30c01af..1741706 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -204,7 +204,7 @@ const saveConversation = (messages: Message[]) => { handleSendMessage(message); } if (database.includes("bmdb")) { - handleSendMessage2(message); + handleSendMessageBMDB(message); } } else { handleSend(message) @@ -226,7 +226,7 @@ const saveConversation = (messages: Message[]) => { setMessages((prev) => [...prev, userMessage]); if (useVCDB) {handleSendMessage(inputMessage);} - if (useBMDB){handleSendMessage2(inputMessage);} + if (useBMDB){handleSendMessageBMDB(inputMessage);} }; const handleSendMessage = async (overrideMessage?: string) => { @@ -362,7 +362,7 @@ const saveConversation = (messages: Message[]) => { }; // End of handleSendMessage // -const handleSendMessage2 = async (overrideMessage?: string) => { +const handleSendMessageBMDB = async (overrideMessage?: string) => { const msg = overrideMessage ?? inputMessage; if (!msg.trim()) return; const controller = new AbortController(); @@ -428,7 +428,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { ].map((msg) => ({ role: msg.role, content: msg.content })); const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/biomd-search`, + `${process.env.NEXT_PUBLIC_API_URL}/bmdb-search`, { method: "POST", headers: { @@ -487,7 +487,7 @@ const handleSendMessage2 = async (overrideMessage?: string) => { } finally { setIsLoadingBMDB(false); } - }; // End of handleSendMessage2 + }; // End of handleSendMessageBMDB From 7a09ecf2c57bb94feac3d68cebc5afc1c6bbaac7 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Mon, 6 Apr 2026 12:37:41 -0400 Subject: [PATCH 46/60] update BMDB processing to include biomodels that start with both MODEL and BIOMD --- backend/app/utils/tools_utils.py | 2 +- frontend/app/search/[bmid]/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/utils/tools_utils.py b/backend/app/utils/tools_utils.py index 84b8360..af7ee9f 100644 --- a/backend/app/utils/tools_utils.py +++ b/backend/app/utils/tools_utils.py @@ -200,7 +200,7 @@ "bmId": { "type": "string", "default": "", - "description": "The unique identifier of the biomodel. This can be used to retrieve specific biomodels directly by their ID. It is under the format BIOMD followed by 10 numbers", + "description": "The unique identifier of the biomodel. This can be used to retrieve specific biomodels directly by their ID. It is under the format BIOMD followed by 10 numbers or MODEL followed by 10 numbers.", }, "bmName": { "type": "string", diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index 8606955..6947af3 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -94,7 +94,7 @@ interface BiomodelDBDetail { export default function BiomodelDetailPage() { const params = useParams<{ bmid: string }>(); const bmid = params?.bmid; - const bmdbID = bmid.startsWith("BIOMD"); + const bmdbID = bmid.startsWith("BIOMD") || bmid.startsWith("MODEL"); const [data, setData] = useState(null); const [bmdbData, setBmdbData] = useState(null); const [loading, setLoading] = useState(true); From 26f1b1c110d14fb8f8252827d1e91e49c26c6f22 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 09:48:01 -0400 Subject: [PATCH 47/60] change all references to biomodels database to be BMDB for consistency --- frontend/app/search/page.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index fc7e9ab..19dfdee 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -85,9 +85,9 @@ export default function BiomodelSearchPage() { orderBy: "date_desc", }); - const [bioModelsQuery, setBioModelsQuery] = useState(""); - const [bioModelsResults, setBioModelsResults] = useState([]); - const [bioModelsIsLoading, setBioModelsIsLoading] = useState(false); + const [BMDBQuery, setBMDBQuery] = useState(""); + const [BMDBResults, setBMDBResults] = useState([]); + const [BMDBIsLoading, setBMDBIsLoading] = useState(false); const [results, setResults] = useState([]); @@ -95,7 +95,7 @@ export default function BiomodelSearchPage() { const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(false); const handleSearch = async () => { - setBioModelsResults([]); + setBMDBResults([]); setIsLoading(true); try { // Build query params from filters, omitting empty bmName @@ -142,10 +142,10 @@ export default function BiomodelSearchPage() { // introduce a separate search function for the BioModel DB search form. const handleSearch2 = async () => { setResults([]); - setBioModelsIsLoading(true); + setBMDBIsLoading(true); try { // build API url - const apiUrl = `${process.env.NEXT_PUBLIC_API_URL2}/search?query=${bioModelsQuery}&format=json`; + const apiUrl = `${process.env.NEXT_PUBLIC_API_URL2}/search?query=${BMDBQuery}&format=json`; const res = await fetch(apiUrl); if (!res.ok) throw new Error("Failed to fetch biomodels"); @@ -181,11 +181,11 @@ export default function BiomodelSearchPage() { name: model.name, description: descriptions[index], })); - setBioModelsResults(mappedResults); + setBMDBResults(mappedResults); } catch (err) { - setBioModelsResults([]); + setBMDBResults([]); } finally { - setBioModelsIsLoading(false); + setBMDBIsLoading(false); }}; @@ -482,9 +482,9 @@ export default function BiomodelSearchPage() { - setBioModelsQuery(e.target.value) + setBMDBQuery(e.target.value) } className="border-slate-300 focus:border-blue-500 h-9" /> @@ -495,18 +495,18 @@ export default function BiomodelSearchPage() {
{/* Results Section */} - {(results.length > 0 || bioModelsResults.length > 0) && ( + {(results.length > 0 || BMDBResults.length > 0) && (

@@ -515,14 +515,14 @@ export default function BiomodelSearchPage() { {results.length > 0 ? results.length - : bioModelsResults.length} models found + : BMDBResults.length} models found

{/* Output for BioModels Database Results */} - {bioModelsResults.length > 0 && ( + {BMDBResults.length > 0 && (
- {bioModelsResults.map((model: any) => ( + {BMDBResults.map((model: any) => ( Date: Wed, 8 Apr 2026 10:34:16 -0400 Subject: [PATCH 48/60] move biomodels endpoint to its own router file --- backend/app/routes/llms_router.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index a83cc34..57a273a 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -19,6 +19,7 @@ async def query_llm(conversation_history: dict): Endpoint to query the LLM and execute the necessary tools. Args: conversation_history (dict): The conversation history containing user prompts and responses. + database (str): The database to query - vcdb in this case. Returns: dict: The final response after processing the prompt with the tools. """ @@ -27,15 +28,6 @@ async def query_llm(conversation_history: dict): ) return {"response": result, "bmkeys": bmkeys, "tool_summary": tool_summary} -# For BioModelsDB search using BioModelsDB API -@router.post("/bmdb-search") -async def search_llm(conversation_history: dict): - print("DEBUG20: BMDB POST: ROUTER") - result, bmdbkeys, tool_summary = await get_llm_response( - conversation_history.get("conversation_history", []), database="bmdb" - ) - return {"response": result, "bmkeys": bmdbkeys, "tool_summary": tool_summary} - @router.post("/analyse/{biomodel_id}") async def analyse_biomodel(biomodel_id: str, user_prompt: str): From 74d3f8a098cd27384f6c19ff6561044e5786ea6b Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 10:34:56 -0400 Subject: [PATCH 49/60] create a new section on /local/docs page for all BMDB endpoints --- backend/app/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/app/main.py b/backend/app/main.py index f48d10b..bec6694 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -14,6 +14,7 @@ from app.routes.llms_router import router as llms_router from app.routes.qdrant_router import router as qdrant_router from app.routes.knowledge_base_router import router as knowledge_base_router +from app.routes.bmdb_router import router as bmdb_router ascii_art = """ ╔════════════════════════════════════════════════════════════════════════════════════╗ @@ -54,6 +55,7 @@ async def startup_event(): # Including the routers app.include_router(knowledge_base_router, tags=["Knowledge Base"], prefix="/kb") app.include_router(llms_router, tags=["LLM with Tool Calling"]) +app.include_router(bmdb_router, tags=["BMDB API Wrapper"]) app.include_router(vcelldb_router, tags=["VCellDB API Wrapper"]) app.include_router(qdrant_router, tags=["Qdrant Vector DB"], prefix="/qdrant") From 986915bab51d9a7f98a80ce1cfe8d970ff4fb1c2 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 10:37:06 -0400 Subject: [PATCH 50/60] fix quickAction logic so that it shows the queries from the buttons on the biomodel specific page (AI Analysis) on the chat screen + loads the history in the sidebar --- frontend/components/ChatBox.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 1741706..0fbe034 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -199,6 +199,14 @@ const saveConversation = (messages: Message[]) => { const handleQuickAction = (message: string) => { setInputMessage(""); + const userMessage: Message = { + id: Date.now().toString(), + role: "user", + content: message, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); if (database) { if (database.includes("vcdb")) { handleSendMessage(message); From a477884edfca6a046c9e48341f891043ce45d0b1 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 10:38:32 -0400 Subject: [PATCH 51/60] create BMDB router and controller files for those specific endpoints/functions; start adding the endpoints to the router file --- backend/app/controllers/bmdb_controller.py | 41 +++++++++++++++++ backend/app/routes/bmdb_router.py | 53 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 backend/app/controllers/bmdb_controller.py create mode 100644 backend/app/routes/bmdb_router.py diff --git a/backend/app/controllers/bmdb_controller.py b/backend/app/controllers/bmdb_controller.py new file mode 100644 index 0000000..1ed8e36 --- /dev/null +++ b/backend/app/controllers/bmdb_controller.py @@ -0,0 +1,41 @@ +import httpx +from typing import List +from fastapi import HTTPException, Response +from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams +from app.services.databases_service import ( + get_xml_file, + fetch_bmdb_models, +) + + +async def get_bmdb_models_controller(params: BiomodelRequestParams) -> dict: + """ + Controller function to retrieve biomodels based on filters and sorting. + Raises: + HTTPException: If the BMDB API request fails. + """ + try: + biomodels = await fetch_bmdb_models(params) + return biomodels + except httpx.HTTPStatusError as e: + raise HTTPException( + status_code=e.response.status_code, detail="Error fetching biomodels." + ) + except httpx.RequestError as e: + raise HTTPException( + status_code=500, detail="Error communicating with BMDB API." + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +async def get_xml_controller(bmdbID: str, truncate: bool = False) -> str: + """ + Controller function to fetch the contents of the XML file for a bmdb biomodel. + Raises: + HTTPException: If the URL cannot be generated. + """ + try: + return await get_xml_file(bmdbID, truncate) + except Exception as e: + raise HTTPException(status_code=500, detail="Error fetching XML file.") \ No newline at end of file diff --git a/backend/app/routes/bmdb_router.py b/backend/app/routes/bmdb_router.py new file mode 100644 index 0000000..c9a6a64 --- /dev/null +++ b/backend/app/routes/bmdb_router.py @@ -0,0 +1,53 @@ +from multiprocessing import process + +from fastapi import APIRouter, HTTPException +import httpx +import requests +from app.controllers.llms_controller import ( + get_llm_response, +) +from app.controllers.bmdb_controller import ( + get_bmdb_models_controller, + get_xml_controller, +) + +router = APIRouter() + +# For BioModelsDB search using BioModelsDB API +@router.post("/bmdb-search") +async def search_llm(conversation_history: dict): + """ + Endpoint to query the LLM and execute the necessary tools. + Args: + conversation_history (dict): The conversation history containing user prompts and responses. + database (str): The database to query - bmdb in this case. + Returns: + dict: The final response after processing the prompt with the tools. + """ + + print("DEBUG20: BMDB POST: ROUTER") + result, bmdbkeys, tool_summary = await get_llm_response( + conversation_history.get("conversation_history", []), database="bmdb" + ) + return {"response": result, "bmkeys": bmdbkeys, "tool_summary": tool_summary} + + +# @router.get("/", response_model=dict) +# async def get_biomodels(params: BiomodelRequestParams = Depends()): +# """ +# Endpoint to retrieve biomodels based on provided filters and sorting. +# """ +# try: +# return await get_biomodels_controller(params) +# except HTTPException as e: +# raise e + +# @router.get("/", response_model=str) +# async def get_xml(bmdbID: str, truncate: bool = False): +# """ +# Endpoint to get XML file contents for a given biomodel. +# """ +# try: +# return await get_xml_controller(bmdbID, truncate) +# except HTTPException as e: +# raise e \ No newline at end of file From 61727771a06590053618239ba509e8a793894d4f Mon Sep 17 00:00:00 2001 From: Michael Blinov Date: Wed, 8 Apr 2026 16:45:07 -0400 Subject: [PATCH 52/60] Separate BMDB and VCDB routes and controllers; moved LLM BMDB query to llms_router --- backend/app/controllers/bmdb_controller.py | 4 +- backend/app/routes/bmdb_router.py | 44 ++++++---- backend/app/routes/llms_router.py | 25 +++++- backend/app/schemas/bmdb_schema.py | 35 ++++++++ backend/app/schemas/vcelldb_schema.py | 1 - frontend/package-lock.json | 95 ++++++---------------- 6 files changed, 115 insertions(+), 89 deletions(-) create mode 100644 backend/app/schemas/bmdb_schema.py diff --git a/backend/app/controllers/bmdb_controller.py b/backend/app/controllers/bmdb_controller.py index 1ed8e36..0463d71 100644 --- a/backend/app/controllers/bmdb_controller.py +++ b/backend/app/controllers/bmdb_controller.py @@ -1,14 +1,14 @@ import httpx from typing import List from fastapi import HTTPException, Response -from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams +from app.schemas.bmdb_schema import BMDBRequestParams from app.services.databases_service import ( get_xml_file, fetch_bmdb_models, ) -async def get_bmdb_models_controller(params: BiomodelRequestParams) -> dict: +async def get_bmdb_models_controller(params: BMDBRequestParams) -> dict: """ Controller function to retrieve biomodels based on filters and sorting. Raises: diff --git a/backend/app/routes/bmdb_router.py b/backend/app/routes/bmdb_router.py index c9a6a64..201d703 100644 --- a/backend/app/routes/bmdb_router.py +++ b/backend/app/routes/bmdb_router.py @@ -1,6 +1,7 @@ +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import List from multiprocessing import process - -from fastapi import APIRouter, HTTPException +from app.schemas.bmdb_schema import BMDBRequestParams import httpx import requests from app.controllers.llms_controller import ( @@ -13,23 +14,34 @@ router = APIRouter() -# For BioModelsDB search using BioModelsDB API -@router.post("/bmdb-search") -async def search_llm(conversation_history: dict): + +@router.get("/search", response_model=dict) +async def get_biomodels(params: BMDBRequestParams = Depends()): """ - Endpoint to query the LLM and execute the necessary tools. - Args: - conversation_history (dict): The conversation history containing user prompts and responses. - database (str): The database to query - bmdb in this case. - Returns: - dict: The final response after processing the prompt with the tools. + Endpoint to retrieve bmdb models based on provided parameters. """ + try: + return await get_bmdb_models_controller(params) + except HTTPException as e: + raise e + +# For BioModelsDB search using BioModelsDB API +# @router.post("/bmdb-search") +# async def search_llm(conversation_history: dict): +# """ +# Endpoint to query the LLM and execute the necessary tools. +# Args: +# conversation_history (dict): The conversation history containing user prompts and responses. +# database (str): The database to query - bmdb in this case. +# Returns: +# dict: The final response after processing the prompt with the tools. +# """ - print("DEBUG20: BMDB POST: ROUTER") - result, bmdbkeys, tool_summary = await get_llm_response( - conversation_history.get("conversation_history", []), database="bmdb" - ) - return {"response": result, "bmkeys": bmdbkeys, "tool_summary": tool_summary} +# print("DEBUG20: BMDB POST: ROUTER") +# result, bmdbkeys, tool_summary = await get_llm_response( +# conversation_history.get("conversation_history", []), database="bmdb" +# ) +# return {"response": result, "bmkeys": bmdbkeys, "tool_summary": tool_summary} # @router.get("/", response_model=dict) diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index 57a273a..3607ad8 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -1,6 +1,8 @@ from multiprocessing import process - -from fastapi import APIRouter, Request +from fastapi import APIRouter, Depends, HTTPException, Response +from typing import List +from app.schemas.bmdb_schema import BMDBRequestParams +from app.schemas.vcelldb_schema import BiomodelRequestParams import httpx import requests from app.controllers.llms_controller import ( @@ -12,6 +14,25 @@ router = APIRouter() +# For BioModelsDB search using BioModelsDB API +@router.post("/bmdb-search") +async def search_llm(conversation_history: dict): + """ + Endpoint to query the LLM and execute the necessary tools. + Args: + conversation_history (dict): The conversation history containing user prompts and responses. + database (str): The database to query - bmdb in this case. + Returns: + dict: The final response after processing the prompt with the tools. + """ + + print("DEBUG20: BMDB POST: ROUTER") + result, bmdbkeys, tool_summary = await get_llm_response( + conversation_history.get("conversation_history", []), database="bmdb" + ) + return {"response": result, "bmkeys": bmdbkeys, "tool_summary": tool_summary} + + @router.post("/query") async def query_llm(conversation_history: dict): diff --git a/backend/app/schemas/bmdb_schema.py b/backend/app/schemas/bmdb_schema.py new file mode 100644 index 0000000..b26ed21 --- /dev/null +++ b/backend/app/schemas/bmdb_schema.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import date +from enum import Enum + + +class CategoryEnum(str, Enum): + all = "all" + public = "public" + shared = "shared" + tutorials = "tutorial" + educational = "educational" + + +class OrderByEnum(str, Enum): + date_desc = "date_desc" + date_asc = "date_asc" + name_desc = "name_desc" + name_asc = "name_asc" + + +# Biomodel Request Parameters schema +class BMDBRequestParams(BaseModel, use_enum_values=True): + bmName: Optional[str] = "" # Name of the biomodel to search for + bmId: Optional[str] = "" # Biomodel ID + category: Optional[CategoryEnum] = CategoryEnum.all # Category of the biomodel + owner: Optional[str] = "" # Owner of the biomodel + # savedLow: Optional[date] = None # Lower bound of the save date range + # savedHigh: Optional[date] = None # Upper bound of the save date range + startRow: Optional[int] = 1 # Starting row of the result set (default is 1) + maxRows: Optional[int] = 1000 # Maximum number of rows to return (default is 100) + orderBy: Optional[OrderByEnum] = ( + OrderByEnum.date_desc + ) # Order of results (default is "date_desc") + diff --git a/backend/app/schemas/vcelldb_schema.py b/backend/app/schemas/vcelldb_schema.py index 51d467c..8964e47 100644 --- a/backend/app/schemas/vcelldb_schema.py +++ b/backend/app/schemas/vcelldb_schema.py @@ -33,7 +33,6 @@ class BiomodelRequestParams(BaseModel, use_enum_values=True): OrderByEnum.date_desc ) # Order of results (default is "date_desc") - class SimulationRequestParams(BaseModel): bmId: str # Biomodel ID for which simulations will be fetched simId: str # Simulation ID to fetch specific simulation details diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3fb5178..377dbef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -555,7 +555,6 @@ "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2165,7 +2164,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2177,7 +2175,6 @@ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -2250,6 +2247,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2260,6 +2258,7 @@ "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2282,7 +2281,6 @@ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -2293,24 +2291,21 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", @@ -2318,7 +2313,6 @@ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -2330,8 +2324,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", @@ -2339,7 +2332,6 @@ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2353,7 +2345,6 @@ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -2364,7 +2355,6 @@ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -2374,8 +2364,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", @@ -2383,7 +2372,6 @@ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2401,7 +2389,6 @@ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -2416,7 +2403,6 @@ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -2430,7 +2416,6 @@ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -2446,7 +2431,6 @@ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -2457,16 +2441,14 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.15.0", @@ -2488,7 +2470,6 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -2502,6 +2483,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2710,6 +2692,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2728,8 +2711,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/busboy": { "version": "1.6.0", @@ -2863,7 +2845,6 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -3238,7 +3219,8 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.5.1", @@ -3274,7 +3256,6 @@ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -3300,8 +3281,7 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/escalade": { "version": "3.2.0", @@ -3330,7 +3310,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -3345,7 +3324,6 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3359,7 +3337,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -3370,7 +3347,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -3397,7 +3373,6 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -3633,16 +3608,14 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", @@ -3650,7 +3623,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4026,7 +3998,6 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -4056,8 +4027,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -4106,7 +4076,6 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" } @@ -4458,8 +4427,7 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -5071,7 +5039,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -5082,7 +5049,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5190,8 +5156,7 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/next": { "version": "15.2.4", @@ -5462,6 +5427,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5620,7 +5586,6 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -5630,6 +5595,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5639,6 +5605,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5651,6 +5618,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6058,8 +6026,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/scheduler": { "version": "0.26.0", @@ -6106,7 +6073,6 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -6210,7 +6176,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6230,7 +6195,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6454,7 +6418,6 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6492,6 +6455,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6584,7 +6548,6 @@ "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", @@ -6604,7 +6567,6 @@ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -6639,8 +6601,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/thenify": { "version": "3.3.1", @@ -7007,7 +6968,6 @@ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -7082,7 +7042,6 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } From 460c472898ad84b2f0329157f01b28df8e19fd47 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 22:55:56 -0400 Subject: [PATCH 53/60] update quickAction handling so it only appears once in the chat screen --- frontend/components/ChatBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 0fbe034..4ac5cb9 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -198,7 +198,8 @@ const saveConversation = (messages: Message[]) => { }; const handleQuickAction = (message: string) => { - setInputMessage(""); + if (database) { + setInputMessage(""); const userMessage: Message = { id: Date.now().toString(), role: "user", @@ -207,7 +208,6 @@ const saveConversation = (messages: Message[]) => { }; setMessages((prev) => [...prev, userMessage]); - if (database) { if (database.includes("vcdb")) { handleSendMessage(message); } From 89a78abf06cdc3e367f1d5a1ee3c51ba03ae74f1 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 23:00:21 -0400 Subject: [PATCH 54/60] change all references to public_next_url2 to be a bmdb specific name --- docker-compose.yml | 2 +- frontend/app/search/[bmid]/page.tsx | 6 +++--- frontend/app/search/page.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a60659b..bb644a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: - "3000:3000" environment: NEXT_PUBLIC_API_URL: http://localhost:8000 - NEXT_PUBLIC_API_URL2: https://www.biomodels.org/ + NEXT_PUBLIC_API_URL_BMDB: https://www.biomodels.org/ depends_on: - backend env_file: diff --git a/frontend/app/search/[bmid]/page.tsx b/frontend/app/search/[bmid]/page.tsx index 6947af3..0d75fa5 100644 --- a/frontend/app/search/[bmid]/page.tsx +++ b/frontend/app/search/[bmid]/page.tsx @@ -153,7 +153,7 @@ export default function BiomodelDetailPage() { setError(""); if (bmdbID) { - fetch(`${process.env.NEXT_PUBLIC_API_URL2}/${bmid}?format=json`) + fetch(`${process.env.NEXT_PUBLIC_API_URL_BMDB}/${bmid}?format=json`) .then((res) => { if (!res.ok) throw new Error("Failed to fetch biomodel details"); return res.json(); @@ -174,7 +174,7 @@ export default function BiomodelDetailPage() { name: file.name, description: file.description || "", fileSize: file.fileSize || "", - downloadLink: `${process.env.NEXT_PUBLIC_API_URL2}/model/download/${bmid}?filename=${file.name}`, + downloadLink: `${process.env.NEXT_PUBLIC_API_URL_BMDB}/model/download/${bmid}?filename=${file.name}`, })), }); }) @@ -206,7 +206,7 @@ export default function BiomodelDetailPage() { // get diagram image from biomodels database if (bmdbID) { - const bmdbAPIUrl = process.env.NEXT_PUBLIC_API_URL2; + const bmdbAPIUrl = process.env.NEXT_PUBLIC_API_URL_BMDB; const bmdbRes = await fetch(`${bmdbAPIUrl}/model/download/${bmid}?filename=${bmid}.png`); if (bmdbRes.ok) { // convert the .png response into blob to store image as a string diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index 19dfdee..66afaf8 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -145,7 +145,7 @@ export default function BiomodelSearchPage() { setBMDBIsLoading(true); try { // build API url - const apiUrl = `${process.env.NEXT_PUBLIC_API_URL2}/search?query=${BMDBQuery}&format=json`; + const apiUrl = `${process.env.NEXT_PUBLIC_API_URL_BMDB}/search?query=${BMDBQuery}&format=json`; const res = await fetch(apiUrl); if (!res.ok) throw new Error("Failed to fetch biomodels"); @@ -157,7 +157,7 @@ export default function BiomodelSearchPage() { try { // build url using model ID - const descURL = `${process.env.NEXT_PUBLIC_API_URL2}/${model.id}?format=json`; + const descURL = `${process.env.NEXT_PUBLIC_API_URL_BMDB}/${model.id}?format=json`; const descRes = await fetch(descURL); if (!descRes.ok) throw new Error("Failed to fetch model description"); const descData = await descRes.json(); From 9318e99ab130abc087144adb6573952689f20a79 Mon Sep 17 00:00:00 2001 From: reeshapatel12 Date: Wed, 8 Apr 2026 23:01:26 -0400 Subject: [PATCH 55/60] update the handle search function name to be bmdb specific --- frontend/app/search/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index 66afaf8..8d8d206 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -140,7 +140,7 @@ export default function BiomodelSearchPage() { }; // introduce a separate search function for the BioModel DB search form. - const handleSearch2 = async () => { + const handleSearchBMDB = async () => { setResults([]); setBMDBIsLoading(true); try { @@ -494,7 +494,7 @@ export default function BiomodelSearchPage() { {/* Search Button for BioModel DB*/}