diff --git a/backend/app/controllers/knowledge_base_controller.py b/backend/app/controllers/knowledge_base_controller.py index e8da3f7..97dd702 100644 --- a/backend/app/controllers/knowledge_base_controller.py +++ b/backend/app/controllers/knowledge_base_controller.py @@ -12,39 +12,61 @@ delete_knowledge_base_file, get_file_chunks, ) +from app.utils.response_formatter import ResponseFormatter -async def create_collection_controller(): +async def create_collection_controller() -> JSONResponse: """ Create the knowledge base collection if it doesn't exist. + Returns: + JSONResponse: Standardized response for collection creation. """ try: result = create_knowledge_base_collection_if_not_exists() if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + return ResponseFormatter.success_response( + data=None, + message=result["message"] + ) else: - return JSONResponse(content=result, status_code=500) + return ResponseFormatter.error_response( + message=result["message"], + status_code=500 + ) except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error creating collection: {str(e)}" + return ResponseFormatter.error_response( + message=f"Failed to create knowledge base collection: {str(e)}", + status_code=500 ) -async def get_files_controller(): +async def get_files_controller() -> JSONResponse: """ Get all files in the knowledge base. + Returns: + JSONResponse: Standardized response with knowledge base files. """ try: result = get_knowledge_base_files() if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + files = result.get("files", []) + return ResponseFormatter.success_response( + data=files, + message=f"Retrieved {len(files)} files from knowledge base" + ) else: - return JSONResponse(content=result, status_code=500) + return ResponseFormatter.error_response( + message="Failed to retrieve knowledge base files", + status_code=500 + ) except Exception as e: - raise HTTPException(status_code=500, detail=f"Error getting files: {str(e)}") + return ResponseFormatter.error_response( + message=f"Failed to get knowledge base files: {str(e)}", + status_code=500 + ) -async def upload_pdf_controller(file: UploadFile = File(...)): +async def upload_pdf_controller(file: UploadFile = File(...)) -> JSONResponse: """ Upload a PDF file to the knowledge base. """ @@ -66,14 +88,26 @@ async def upload_pdf_controller(file: UploadFile = File(...)): result = upload_pdf_file(temp_file_path, file_name) if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + return ResponseFormatter.success_response( + data={"file_name": file_name}, + message=result["message"] + ) else: - return JSONResponse(content=result, status_code=500) + return ResponseFormatter.error_response( + message=result["message"], + status_code=500 + ) - except HTTPException: - raise + except HTTPException as e: + return ResponseFormatter.error_response( + message=str(e.detail), + status_code=e.status_code + ) except Exception as e: - raise HTTPException(status_code=500, detail=f"Error uploading PDF: {str(e)}") + return ResponseFormatter.error_response( + message="Failed to upload PDF file", + status_code=500 + ) finally: # Clean up temporary file if temp_file_path and os.path.exists(temp_file_path): @@ -83,9 +117,11 @@ async def upload_pdf_controller(file: UploadFile = File(...)): pass # Ignore cleanup errors -async def upload_text_controller(file: UploadFile = File(...)): +async def upload_text_controller(file: UploadFile = File(...)) -> JSONResponse: """ Upload a text file to the knowledge base. + Returns: + JSONResponse: Standardized response for text file upload. """ temp_file_path = None try: @@ -105,15 +141,25 @@ async def upload_text_controller(file: UploadFile = File(...)): result = upload_text_file(temp_file_path, file_name) if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + return ResponseFormatter.success_response( + data={"file_name": file_name}, + message=result["message"] + ) else: - return JSONResponse(content=result, status_code=500) + return ResponseFormatter.error_response( + message=result["message"], + status_code=500 + ) - except HTTPException: - raise + except HTTPException as e: + return ResponseFormatter.error_response( + message=str(e.detail), + status_code=e.status_code + ) except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error uploading text file: {str(e)}" + return ResponseFormatter.error_response( + message="Failed to upload text file", + status_code=500 ) finally: # Clean up temporary file @@ -124,67 +170,102 @@ async def upload_text_controller(file: UploadFile = File(...)): pass # Ignore cleanup errors -async def delete_file_controller(file_name: str): +async def delete_file_controller(file_name: str) -> JSONResponse: """ Delete a file from the knowledge base. + Returns: + JSONResponse: Standardized response for file deletion. """ try: if not file_name: - raise HTTPException(status_code=400, detail="File name is required") + return ResponseFormatter.error_response( + message="File name is required", + status_code=400 + ) result = delete_knowledge_base_file(file_name) if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + return ResponseFormatter.success_response( + data={"file_name": file_name}, + message=result["message"] + ) else: - return JSONResponse(content=result, status_code=500) - except HTTPException: - raise + return ResponseFormatter.error_response( + message=result["message"], + status_code=500 + ) except Exception as e: - raise HTTPException(status_code=500, detail=f"Error deleting file: {str(e)}") + return ResponseFormatter.error_response( + message="Failed to delete file from knowledge base", + status_code=500 + ) -async def get_similar_controller(query: str, limit: int = 10): +async def get_similar_controller(query: str, limit: int = 10) -> JSONResponse: """ Get similar chunks from the knowledge base. + Returns: + JSONResponse: Standardized response with similar chunks. """ try: if not query: - raise HTTPException(status_code=400, detail="Query is required") + return ResponseFormatter.error_response( + message="Query is required", + status_code=400 + ) if limit <= 0 or limit > 100: - raise HTTPException( - status_code=400, detail="Limit must be between 1 and 100" + return ResponseFormatter.error_response( + message="Limit must be between 1 and 100", + status_code=400 ) result = get_similar_chunks(query=query, limit=limit) if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + results = result.get("results", []) + return ResponseFormatter.success_response( + data=results, + message=f"Found {len(results)} similar chunks for query" + ) else: - return JSONResponse(content=result, status_code=500) - except HTTPException: - raise + return ResponseFormatter.error_response( + message=result["message"], + status_code=500 + ) except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error getting similar chunks: {str(e)}" + return ResponseFormatter.error_response( + message="Failed to perform similarity search", + status_code=500 ) -async def get_file_chunks_controller(file_name: str): +async def get_file_chunks_controller(file_name: str) -> JSONResponse: """ Get all chunks for a specific file. + Returns: + JSONResponse: Standardized response with file chunks. """ try: if not file_name: - raise HTTPException(status_code=400, detail="File name is required") + return ResponseFormatter.error_response( + message="File name is required", + status_code=400 + ) result = get_file_chunks(file_name) if result["status"] == "success": - return JSONResponse(content=result, status_code=200) + chunks = result.get("chunks", []) + return ResponseFormatter.success_response( + data=chunks, + message=f"Retrieved {len(chunks)} chunks for file {file_name}" + ) else: - return JSONResponse(content=result, status_code=500) - except HTTPException: - raise + return ResponseFormatter.error_response( + message=result["message"], + status_code=500 + ) except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error getting file chunks: {str(e)}" + return ResponseFormatter.error_response( + message="Failed to get file chunks", + status_code=500 ) diff --git a/backend/app/controllers/llms_controller.py b/backend/app/controllers/llms_controller.py index fe271f5..4df3a51 100644 --- a/backend/app/controllers/llms_controller.py +++ b/backend/app/controllers/llms_controller.py @@ -1,76 +1,101 @@ -from fastapi import HTTPException +from fastapi.responses import JSONResponse from app.services.llms_service import ( get_response_with_tools, analyse_biomodel, analyse_vcml, analyse_diagram, ) +from app.utils.response_formatter import ResponseFormatter -async def get_llm_response(conversation_history: list[dict]) -> tuple[str, list]: +async def get_llm_response(conversation_history: list[dict]) -> JSONResponse: """ Controller function to interact with the LLM service. Args: conversation_history (list[dict]): The conversation history containing user prompts and responses. Returns: - tuple[str, list]: A tuple containing the final response and bmkeys list. + JSONResponse: Standardized response with LLM output and biomodel keys. """ try: result, bmkeys = await get_response_with_tools(conversation_history) - return result, bmkeys + + # Simple data structure for backward compatibility + data = {"response": result} + + # Simple metadata - only bmkeys if present + meta = {"bmkeys": bmkeys} if bmkeys else None + + return ResponseFormatter.success_response( + data=data, + message="LLM query processed successfully", + meta=meta + ) except Exception as e: - raise HTTPException(status_code=500, detail=f"Error: {str(e)}") + return ResponseFormatter.error_response( + message=f"Failed to process LLM query: {str(e)}", + status_code=500 + ) -async def analyse_vcml_controller(biomodel_id: str) -> str: +async def analyse_vcml_controller(biomodel_id: str) -> JSONResponse: """ Controller function to analyze VCML content for a given biomodel. Args: biomodel_id (str): The ID of the biomodel to analyze. Returns: - str: The VCML analysis response. + JSONResponse: Standardized response with VCML analysis. """ try: result = await analyse_vcml(biomodel_id) - return result + return ResponseFormatter.success_response( + data={"response": result}, + message=f"VCML analysis completed for biomodel {biomodel_id}" + ) except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Error analyzing VCML for biomodel {biomodel_id}: {str(e)}", + return ResponseFormatter.error_response( + message=f"Failed to analyze VCML for biomodel {biomodel_id}: {str(e)}", + status_code=500 ) -async def analyse_diagram_controller(biomodel_id: str) -> str: +async def analyse_diagram_controller(biomodel_id: str) -> JSONResponse: """ Controller function to analyze diagram for a given biomodel. Args: biomodel_id (str): The ID of the biomodel to analyze. Returns: - str: The diagram analysis response. + JSONResponse: Standardized response with diagram analysis. """ try: result = await analyse_diagram(biomodel_id) - return result + return ResponseFormatter.success_response( + data={"response": result}, + message=f"Diagram analysis completed for biomodel {biomodel_id}" + ) except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Error analyzing diagram for biomodel {biomodel_id}: {str(e)}", + return ResponseFormatter.error_response( + message=f"Failed to analyze diagram for biomodel {biomodel_id}: {str(e)}", + status_code=500 ) -async def analyse_biomodel_controller(biomodel_id: str, user_prompt: str) -> str: +async def analyse_biomodel_controller(biomodel_id: str, user_prompt: str) -> JSONResponse: """ Controller function to analyze a biomodel using the LLM service. Args: biomodel_id (str): The ID of the biomodel to be analyzed. user_prompt (str): The query or input provided by the user. Returns: - str: The analysis result from the LLM service. + JSONResponse: Standardized response with biomodel analysis. """ try: result = await analyse_biomodel(biomodel_id, user_prompt) - return result + return ResponseFormatter.success_response( + data={"response": result}, + message=f"Biomodel analysis completed for {biomodel_id}" + ) except Exception as e: - raise HTTPException( - status_code=500, detail=f"Error analyzing biomodel {biomodel_id}: {str(e)}" + return ResponseFormatter.error_response( + message=f"Failed to analyze biomodel {biomodel_id}: {str(e)}", + status_code=500 ) diff --git a/backend/app/controllers/vcelldb_controller.py b/backend/app/controllers/vcelldb_controller.py index 93371ff..c770b7f 100644 --- a/backend/app/controllers/vcelldb_controller.py +++ b/backend/app/controllers/vcelldb_controller.py @@ -1,6 +1,7 @@ import httpx from typing import List from fastapi import HTTPException, Response +from fastapi.responses import JSONResponse from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams from app.services.vcelldb_service import ( fetch_biomodels, @@ -12,85 +13,131 @@ fetch_biomodel_applications_files, fetch_publications, ) +from app.utils.response_formatter import ResponseFormatter -async def get_biomodels_controller(params: BiomodelRequestParams) -> dict: +async def get_biomodels_controller(params: BiomodelRequestParams) -> JSONResponse: """ Controller function to retrieve biomodels based on filters and sorting. - Raises: - HTTPException: If the VCell API request fails. + Returns: + JSONResponse: Standardized response with biomodel data and metadata. """ try: biomodels = await fetch_biomodels(params) - return biomodels + + # Extract data and metadata + bmkeys = biomodels.get("unique_model_keys (bmkey)", []) + models_data = biomodels.get("data", []) + models_count = biomodels.get("models_count", 0) + + meta = {"bmkeys": bmkeys} if bmkeys else None + + return ResponseFormatter.success_response( + data=models_data, + message=f"Retrieved {models_count} biomodels successfully", + meta=meta + ) except httpx.HTTPStatusError as e: - raise HTTPException( - status_code=e.response.status_code, detail="Error fetching biomodels." + return ResponseFormatter.error_response( + message="Failed to fetch biomodels from VCell API", + status_code=e.response.status_code ) except httpx.RequestError as e: - raise HTTPException( - status_code=500, detail="Error communicating with VCell API." + return ResponseFormatter.error_response( + message=f"Error communicating with VCell API: {str(e)}", + status_code=500 ) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + return ResponseFormatter.error_response( + message=f"Error fetching biomodels: {str(e)}", + status_code=500 + ) -async def get_simulation_details_controller(params: SimulationRequestParams) -> dict: +async def get_simulation_details_controller(params: SimulationRequestParams) -> JSONResponse: """ Controller function to fetch detailed simulation data for a biomodel. - Raises: - HTTPException: If the VCell API request fails. + Returns: + JSONResponse: Standardized response with simulation details. """ try: simulation = await fetch_simulation_details(params) - return simulation + return ResponseFormatter.success_response( + data=simulation, + message=f"Simulation details retrieved for biomodel {params.bmId}" + ) except httpx.HTTPStatusError as e: - raise HTTPException( - status_code=e.response.status_code, - detail="Error fetching simulation details.", + return ResponseFormatter.error_response( + message="Failed to fetch simulation details from VCell API", + status_code=e.response.status_code ) except httpx.RequestError as e: - raise HTTPException( - status_code=500, detail="Error communicating with VCell API." + return ResponseFormatter.error_response( + message=f"Error communicating with VCell API: {str(e)}", + status_code=500 ) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + return ResponseFormatter.error_response( + message=f"Error fetching simulation details: {str(e)}", + status_code=500 + ) -async def get_vcml_controller(biomodel_id: str, truncate: bool = False) -> str: +async def get_vcml_controller(biomodel_id: str, truncate: bool = False) -> JSONResponse: """ Controller function to fetch the contents of the VCML file for a biomodel. - Raises: - HTTPException: If the URL cannot be generated. + Returns: + JSONResponse: Standardized response with VCML content. """ try: - return await get_vcml_file(biomodel_id, truncate) + vcml_content = await get_vcml_file(biomodel_id, truncate) + return ResponseFormatter.success_response( + data={"content": vcml_content}, + message=f"VCML file retrieved for biomodel {biomodel_id}" + ) except Exception as e: - raise HTTPException(status_code=500, detail="Error fetching VCML URL.") + return ResponseFormatter.error_response( + message=f"Failed to fetch VCML file for biomodel {biomodel_id}: {str(e)}", + status_code=500 + ) -async def get_sbml_controller(biomodel_id: str) -> str: +async def get_sbml_controller(biomodel_id: str) -> JSONResponse: """ Controller function to fetch the contents of the SBML file for a biomodel. - Raises: - HTTPException: If the URL cannot be generated. + Returns: + JSONResponse: Standardized response with SBML content. """ try: - return await get_sbml_file(biomodel_id) + sbml_content = await get_sbml_file(biomodel_id) + return ResponseFormatter.success_response( + data={"content": sbml_content}, + message=f"SBML file retrieved for biomodel {biomodel_id}" + ) except Exception as e: - raise HTTPException(status_code=500, detail="Error fetching SBML URL.") + return ResponseFormatter.error_response( + message=f"Failed to fetch SBML file for biomodel {biomodel_id}: {str(e)}", + status_code=500 + ) -async def get_diagram_url_controller(biomodel_id: str) -> str: +async def get_diagram_url_controller(biomodel_id: str) -> JSONResponse: """ Controller function to fetch the URL of the diagram image for a biomodel. - Raises: - HTTPException: If the URL cannot be generated. + Returns: + JSONResponse: Standardized response with diagram URL. """ try: - return await get_diagram_url(biomodel_id) + diagram_url = await get_diagram_url(biomodel_id) + return ResponseFormatter.success_response( + data={"url": diagram_url}, + message=f"Diagram URL retrieved for biomodel {biomodel_id}" + ) except Exception as e: - raise HTTPException(status_code=500, detail="Error fetching diagram URL.") + return ResponseFormatter.error_response( + message=f"Failed to fetch diagram URL for biomodel {biomodel_id}: {str(e)}", + status_code=500 + ) async def get_diagram_image_controller(biomodel_id: str) -> Response: @@ -112,45 +159,60 @@ async def get_diagram_image_controller(biomodel_id: str) -> Response: raise HTTPException(status_code=500, detail="Error fetching diagram image.") -async def get_biomodel_applications_files_controller(biomodel_id: str) -> dict: +async def get_biomodel_applications_files_controller(biomodel_id: str) -> JSONResponse: """ Controller function to fetch applications data along with SBML and BNGL file URLs for a biomodel. - Raises: - HTTPException: If the VCell API request fails. + Returns: + JSONResponse: Standardized response with biomodel applications and file URLs. """ try: - return await fetch_biomodel_applications_files(biomodel_id) + applications_data = await fetch_biomodel_applications_files(biomodel_id) + return ResponseFormatter.success_response( + data=applications_data, + message=f"Applications and file URLs retrieved for biomodel {biomodel_id}" + ) except httpx.HTTPStatusError as e: - if e.response.status_code == 404: - raise HTTPException(status_code=404, detail="Biomodel not found.") - raise HTTPException( - status_code=e.response.status_code, - detail="Error fetching biomodel applications.", + error_message = "Biomodel not found" if e.response.status_code == 404 else "Error fetching biomodel applications" + return ResponseFormatter.error_response( + message=error_message, + status_code=e.response.status_code ) except httpx.RequestError as e: - raise HTTPException( - status_code=500, detail="Error communicating with VCell API. " + str(e) + return ResponseFormatter.error_response( + message=f"Error communicating with VCell API: {str(e)}", + status_code=500 ) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + return ResponseFormatter.error_response( + message=f"Error fetching applications for biomodel {biomodel_id}: {str(e)}", + status_code=500 + ) -async def get_publications_controller() -> List[dict]: +async def get_publications_controller() -> JSONResponse: """ Controller function to fetch publications from the VCell API. - Raises: - HTTPException: If the VCell API request fails. + Returns: + JSONResponse: Standardized response with publications data. """ try: publications = await fetch_publications() - return publications + return ResponseFormatter.success_response( + data=publications, + message=f"Retrieved {len(publications)} publications successfully" + ) except httpx.HTTPStatusError as e: - raise HTTPException( - status_code=e.response.status_code, detail="Error fetching publications." + return ResponseFormatter.error_response( + message="Failed to fetch publications from VCell API", + status_code=e.response.status_code ) except httpx.RequestError as e: - raise HTTPException( - status_code=500, detail="Error communicating with VCell API." + return ResponseFormatter.error_response( + message=f"Error communicating with VCell API: {str(e)}", + status_code=500 ) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + return ResponseFormatter.error_response( + message=f"Error fetching publications: {str(e)}", + status_code=500 + ) diff --git a/backend/app/routes/llms_router.py b/backend/app/routes/llms_router.py index 35ed27a..d32f1c3 100644 --- a/backend/app/routes/llms_router.py +++ b/backend/app/routes/llms_router.py @@ -16,12 +16,11 @@ async def query_llm(conversation_history: dict): Args: conversation_history (dict): The conversation history containing user prompts and responses. Returns: - dict: The final response after processing the prompt with the tools. + JSONResponse: Standardized response with LLM output and metadata. """ - result, bmkeys = await get_llm_response( + return await get_llm_response( conversation_history.get("conversation_history", []) ) - return {"response": result, "bmkeys": bmkeys} @router.post("/analyse/{biomodel_id}") @@ -32,10 +31,9 @@ async def analyse_biomodel(biomodel_id: str, user_prompt: str): biomodel_id (str): The ID of the biomodel to be analyzed. user_prompt (str): The prompt entered by the user. Returns: - dict: The analysis result from the LLM service. + JSONResponse: Standardized response with biomodel analysis. """ - result = await analyse_biomodel_controller(biomodel_id, user_prompt) - return {"response": result} + return await analyse_biomodel_controller(biomodel_id, user_prompt) @router.post("/analyse/{biomodel_id}/vcml") @@ -45,10 +43,9 @@ async def analyse_vcml(biomodel_id: str): Args: biomodel_id (str): The ID of the biomodel to analyze. Returns: - dict: The VCML analysis response. + JSONResponse: Standardized response with VCML analysis. """ - result = await analyse_vcml_controller(biomodel_id) - return {"response": result} + return await analyse_vcml_controller(biomodel_id) @router.post("/analyse/{biomodel_id}/diagram") @@ -58,7 +55,6 @@ async def analyse_diagram(biomodel_id: str): Args: biomodel_id (str): The ID of the biomodel to analyze. Returns: - dict: The diagram analysis response. + JSONResponse: Standardized response with diagram analysis. """ - result = await analyse_diagram_controller(biomodel_id) - return {"response": result} + return await analyse_diagram_controller(biomodel_id) diff --git a/backend/app/routes/vcelldb_router.py b/backend/app/routes/vcelldb_router.py index a4ad835..4d9060e 100644 --- a/backend/app/routes/vcelldb_router.py +++ b/backend/app/routes/vcelldb_router.py @@ -15,62 +15,57 @@ router = APIRouter() -@router.get("/biomodel", response_model=dict) +@router.get("/biomodel") async def get_biomodels(params: BiomodelRequestParams = Depends()): """ Endpoint to retrieve biomodels based on provided filters and sorting. + Returns: + JSONResponse: Standardized response with biomodel data. """ - try: - return await get_biomodels_controller(params) - except HTTPException as e: - raise e + return await get_biomodels_controller(params) -@router.get("/biomodel/{biomodel_id}/simulations", response_model=dict) +@router.get("/biomodel/{biomodel_id}/simulations") async def get_simulations( biomodel_id: str, params: SimulationRequestParams = Depends() ): """ Endpoint to retrieve simulations for a specific biomodel by biomodel ID. + Returns: + JSONResponse: Standardized response with simulation data. """ params.bmId = biomodel_id - try: - return await get_simulation_details_controller(params) - except HTTPException as e: - raise e + return await get_simulation_details_controller(params) -@router.get("/biomodel/{biomodel_id}/biomodel.vcml", response_model=str) +@router.get("/biomodel/{biomodel_id}/biomodel.vcml") async def get_vcml(biomodel_id: str, truncate: bool = False): """ Endpoint to get VCML file contents for a given biomodel. + Returns: + JSONResponse: Standardized response with VCML content. """ - try: - return await get_vcml_controller(biomodel_id, truncate) - except HTTPException as e: - raise e + return await get_vcml_controller(biomodel_id, truncate) -@router.get("/biomodel/{biomodel_id}/biomodel.sbml", response_model=str) +@router.get("/biomodel/{biomodel_id}/biomodel.sbml") async def get_sbml(biomodel_id: str): """ Endpoint to get SBML file contents for a given biomodel. + Returns: + JSONResponse: Standardized response with SBML content. """ - try: - return await get_sbml_controller(biomodel_id) - except HTTPException as e: - raise e + return await get_sbml_controller(biomodel_id) -@router.get("/biomodel/{biomodel_id}/diagram", response_model=str) +@router.get("/biomodel/{biomodel_id}/diagram") async def get_diagram_url(biomodel_id: str): """ Endpoint to get the diagram image URL for a given biomodel. + Returns: + JSONResponse: Standardized response with diagram URL. """ - try: - return await get_diagram_url_controller(biomodel_id) - except HTTPException as e: - raise e + return await get_diagram_url_controller(biomodel_id) @router.get("/biomodel/{biomodel_id}/diagram/image") @@ -81,23 +76,21 @@ async def get_diagram_image(biomodel_id: str): return await get_diagram_image_controller(biomodel_id) -@router.get("/biomodel/{biomodel_id}/applications/files", response_model=dict) +@router.get("/biomodel/{biomodel_id}/applications/files") async def get_biomodel_applications_files(biomodel_id: str): """ Endpoint to get applications data along with SBML and BNGL file URLs for a given biomodel. + Returns: + JSONResponse: Standardized response with applications and file URLs. """ - try: - return await get_biomodel_applications_files_controller(biomodel_id) - except HTTPException as e: - raise e + return await get_biomodel_applications_files_controller(biomodel_id) -@router.get("/publications", response_model=List[dict]) +@router.get("/publications") async def get_publications(): """ Endpoint to retrieve publications from the VCell API. + Returns: + JSONResponse: Standardized response with publications data. """ - try: - return await get_publications_controller() - except HTTPException as e: - raise e + return await get_publications_controller() diff --git a/backend/app/utils/response_formatter.py b/backend/app/utils/response_formatter.py new file mode 100644 index 0000000..afc3fd5 --- /dev/null +++ b/backend/app/utils/response_formatter.py @@ -0,0 +1,85 @@ +from datetime import datetime +from typing import Any, Dict, Optional, Union +from fastapi import Response +from fastapi.responses import JSONResponse + + +class ResponseFormatter: + """ + Utility class for standardizing API response formats across the VCell-AI backend. + + Provides consistent response structure with success indicators, messages, timestamps, + and metadata for better API usability and debugging. + """ + + @staticmethod + def _generate_timestamp() -> str: + """Generate ISO format timestamp for response tracking.""" + return datetime.utcnow().isoformat() + "Z" + + @staticmethod + def success_response( + data: Any = None, + message: str = "Request completed successfully", + meta: Optional[Dict[str, Any]] = None, + status_code: int = 200 + ) -> JSONResponse: + """ + Create a standardized success response. + + Args: + data: The response data payload + message: Human-readable success message + meta: Additional metadata (pagination, counts, etc.) + status_code: HTTP status code (default: 200) + + Returns: + JSONResponse with standardized format + """ + response_body = { + "success": True, + "data": data, + "message": message, + "timestamp": ResponseFormatter._generate_timestamp() + } + + if meta is not None: + response_body["meta"] = meta + + return JSONResponse(content=response_body, status_code=status_code) + + @staticmethod + def error_response( + message: str, + error_code: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + status_code: int = 500 + ) -> JSONResponse: + """ + Create a standardized error response. + + Args: + message: Human-readable error message + error_code: Specific error code for categorization + details: Additional error details and context + status_code: HTTP status code (default: 500) + + Returns: + JSONResponse with standardized error format + """ + response_body = { + "success": False, + "data": None, + "message": message, + "timestamp": ResponseFormatter._generate_timestamp() + } + + if error_code is not None or details is not None: + response_body["error"] = {} + if error_code: + response_body["error"]["code"] = error_code + if details: + response_body["error"]["details"] = details + + return JSONResponse(content=response_body, status_code=status_code) + diff --git a/frontend/components/ChatBox.tsx b/frontend/components/ChatBox.tsx index 15c89fd..c42eddb 100644 --- a/frontend/components/ChatBox.tsx +++ b/frontend/components/ChatBox.tsx @@ -195,9 +195,15 @@ export const ChatBox: React.FC = ({ }, ); const data = await res.json(); - const aiResponse = - data.response || "Sorry, I didn't get a response from the server."; - const bmkeys = data.bmkeys || []; + + // Handle new standardized response format + if (!data.success) { + throw new Error(data.message || "API request failed"); + } + + // Extract response text (check both new and legacy formats for backward compatibility) + const aiResponse = data.data?.response || data.data?.text || data.response || "Sorry, I didn't get a response from the server."; + const bmkeys = data.meta?.bmkeys || data.bmkeys || []; // Format the response to include hyperlinks for biomodel IDs const formattedResponse = formatBiomodelIds(aiResponse, bmkeys); @@ -210,13 +216,21 @@ export const ChatBox: React.FC = ({ }; setMessages((prev) => [...prev, assistantMessage]); } catch (error) { + console.error("Chat error:", error); + + // Extract error message from standardized error response or use default + let errorMessage = "There was an error connecting to the backend. Please try again."; + + if (error instanceof Error) { + errorMessage = error.message; + } + setMessages((prev) => [ ...prev, { id: (Date.now() + 2).toString(), role: "assistant", - content: - "There was an error connecting to the backend. Please try again.", + content: errorMessage, timestamp: new Date(), }, ]);