Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions backend/app/controllers/llms_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,25 @@ async def get_llm_response(
conversation_history: list[dict],
model: str,
payload: dict,
access_token: str,
) -> tuple[str, list, str]:
"""
Controller function to interact with the LLM service.
Args:
conversation_history (list[dict]): The conversation history containing user prompts and responses.
model (str): The LiteLLM model alias to use.
payload (dict): The verified Auth0 token payload for the caller.
access_token (str): The caller's raw Auth0 access token, forwarded
to tool calls that need it (e.g. to include the user's
private/shared biomodels in fetch_biomodels results).
Returns:
tuple[str, list, str]: The final response, bmkeys list, and model actually used.
"""
try:
supabase = get_supabase_client()
virtual_key = await _get_virtual_key(payload, supabase)
result, bmkeys, model_used = await get_response_with_tools(
conversation_history, virtual_key, model
conversation_history, virtual_key, model, access_token
)
return result, bmkeys, model_used
except Exception as e:
Expand Down Expand Up @@ -74,20 +78,24 @@ async def analyse_vcml_controller(biomodel_id: str, model: str, payload: dict) -
)


async def analyse_diagram_controller(biomodel_id: str, model: str, payload: dict) -> str:
async def analyse_diagram_controller(
biomodel_id: str, model: str, payload: dict, access_token: str
) -> str:
"""
Controller function to analyze diagram for a given biomodel.
Args:
biomodel_id (str): The ID of the biomodel to analyze.
model (str): The LiteLLM model alias to use.
payload (dict): The verified Auth0 token payload for the caller.
access_token (str): The caller's raw Auth0 access token, needed to
fetch a private or shared biomodel's diagram.
Returns:
str: The diagram analysis response.
"""
try:
supabase = get_supabase_client()
virtual_key = await _get_virtual_key(payload, supabase)
result = await analyse_diagram(biomodel_id, virtual_key, model)
result = await analyse_diagram(biomodel_id, virtual_key, model, access_token)
return result
except Exception as e:
if isinstance(e, HTTPException):
Expand Down
14 changes: 9 additions & 5 deletions backend/app/controllers/vcelldb_controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import httpx
from typing import List
from typing import List, Optional
from fastapi import HTTPException, Response
from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams
from app.services.vcelldb_service import (
Expand All @@ -15,14 +15,16 @@
)


async def get_biomodels_controller(params: BiomodelRequestParams) -> dict:
async def get_biomodels_controller(
params: BiomodelRequestParams, auth0_token: Optional[str] = None
) -> dict:
"""
Controller function to retrieve biomodels based on filters and sorting.
Raises:
HTTPException: If the VCell API request fails.
"""
try:
biomodels = await fetch_biomodels(params)
biomodels = await fetch_biomodels(params, auth0_token)
return biomodels
except httpx.HTTPStatusError as e:
raise HTTPException(
Expand Down Expand Up @@ -110,14 +112,16 @@ async def get_diagram_url_controller(biomodel_id: str) -> str:
raise HTTPException(status_code=500, detail="Error fetching diagram URL.")


async def get_diagram_image_controller(biomodel_id: str) -> Response:
async def get_diagram_image_controller(
biomodel_id: str, auth0_token: Optional[str] = None
) -> Response:
"""
Controller function to fetch the diagram image for a biomodel and return it as a PNG response.
Raises:
HTTPException: If the image cannot be fetched.
"""
try:
image_bytes = await get_diagram_image(biomodel_id)
image_bytes = await get_diagram_image(biomodel_id, auth0_token)
return Response(content=image_bytes, media_type="image/png")
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
Expand Down
40 changes: 30 additions & 10 deletions backend/app/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,21 @@ async def get_bearer_token(
return credentials.credentials


async def verify_auth0_token(
access_token: str = Depends(get_bearer_token),
) -> dict[str, Any]:
"""
Verify Auth0 JWT access token and return decoded payload.
"""

def _decode_and_verify(access_token: str) -> dict[str, Any]:
try:
issuer, audience, jwks_client = _get_auth0_config()
signing_key = jwks_client.get_signing_key_from_jwt(
access_token
).key

payload = jwt.decode(
return jwt.decode(
access_token,
signing_key,
algorithms=["RS256"],
audience=audience,
issuer=issuer,
)

return payload

except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -81,3 +73,31 @@ async def verify_auth0_token(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token",
)


async def verify_auth0_token(
access_token: str = Depends(get_bearer_token),
) -> dict[str, Any]:
"""
Verify Auth0 JWT access token and return decoded payload.
"""
return _decode_and_verify(access_token)


async def get_optional_auth0_token(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> str | None:
"""
Extract and verify an Auth0 access token if one was sent, without
requiring one.

Returns None when no Authorization header is present at all, so routes
can serve logged-out users public-only results. Still raises 401 if a
token IS present but invalid/expired, rather than silently treating a
bad token the same as being logged out.
"""
if credentials is None:
return None

_decode_and_verify(credentials.credentials)
return credentials.credentials
7 changes: 5 additions & 2 deletions backend/app/routes/llms_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
analyse_vcml_controller,
analyse_diagram_controller,
)
from app.core.auth import verify_auth0_token
from app.core.auth import verify_auth0_token, get_bearer_token
from app.schemas.llms_schema import AnalysisResponse, ChatRequest, ChatResponse, LLMModel

router = APIRouter()
Expand All @@ -16,6 +16,7 @@
async def query_llm(
request: ChatRequest,
payload: dict = Depends(verify_auth0_token),
access_token: str = Depends(get_bearer_token),
):
"""
Endpoint to query the LLM and execute the necessary tools.
Expand All @@ -28,6 +29,7 @@ async def query_llm(
request.conversation_history,
request.model,
payload,
access_token,
)
return {"response": result, "bmkeys": bmkeys, "model_used": model_used}

Expand Down Expand Up @@ -73,6 +75,7 @@ async def analyse_diagram(
biomodel_id: str,
model: LLMModel = "openai-model",
payload: dict = Depends(verify_auth0_token),
access_token: str = Depends(get_bearer_token),
):
"""
Endpoint to analyze diagram for a given biomodel.
Expand All @@ -81,5 +84,5 @@ async def analyse_diagram(
Returns:
dict: The diagram analysis response.
"""
result = await analyse_diagram_controller(biomodel_id, model, payload)
result = await analyse_diagram_controller(biomodel_id, model, payload, access_token)
return {"response": result}
23 changes: 17 additions & 6 deletions backend/app/routes/vcelldb_router.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Response
from typing import List
from typing import List, Optional
from app.core.auth import get_optional_auth0_token
from app.schemas.vcelldb_schema import BiomodelRequestParams, SimulationRequestParams
from app.controllers.vcelldb_controller import (
get_biomodels_controller,
Expand All @@ -17,12 +18,17 @@


@router.get("/biomodel", response_model=dict)
async def get_biomodels(params: BiomodelRequestParams = Depends()):
async def get_biomodels(
params: BiomodelRequestParams = Depends(),
auth0_token: Optional[str] = Depends(get_optional_auth0_token),
):
"""
Endpoint to retrieve biomodels based on provided filters and sorting.
If a valid Authorization bearer token is sent, results also include
the logged-in user's private and shared biomodels.
"""
try:
return await get_biomodels_controller(params)
return await get_biomodels_controller(params, auth0_token)
except HTTPException as e:
raise e

Expand Down Expand Up @@ -87,11 +93,16 @@ async def get_diagram_url(biomodel_id: str):


@router.get("/biomodel/{biomodel_id}/diagram/image")
async def get_diagram_image(biomodel_id: str):
async def get_diagram_image(
biomodel_id: str,
auth0_token: Optional[str] = Depends(get_optional_auth0_token),
):
"""
Endpoint to get the diagram image (PNG) for a given biomodel.
Endpoint to get the diagram image (PNG) for a given biomodel. If a
valid Authorization bearer token is sent, this also works for private
and shared biomodels.
"""
return await get_diagram_image_controller(biomodel_id)
return await get_diagram_image_controller(biomodel_id, auth0_token)


@router.get("/biomodel/{biomodel_id}/applications/files", response_model=dict)
Expand Down
25 changes: 18 additions & 7 deletions backend/app/services/llms_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
from app.services.vcelldb_service import (
fetch_biomodels,
get_vcml_file,
get_diagram_url,
get_diagram_image,
)

from app.utils.system_prompt import SYSTEM_PROMPT

from app.schemas.vcelldb_schema import BiomodelRequestParams
from app.core.litellm import get_litellm_client
from app.core.config import settings
import base64
import json
from app.core.logger import get_logger

Expand Down Expand Up @@ -95,6 +96,7 @@ async def get_response_with_tools(
conversation_history: list[dict],
virtual_key: str,
model: str,
auth0_token: str | None = None,
) -> tuple[str, list, str]:
messages = [
{
Expand Down Expand Up @@ -138,7 +140,7 @@ async def get_response_with_tools(
logger.info(f"Tool Call: {name} with args: {args}")

# Execute the tool function
result = await execute_tool(name, args)
result = await execute_tool(name, args, auth0_token)

logger.info(f"Tool Result: {str(result)[:500]}")

Expand Down Expand Up @@ -241,14 +243,19 @@ async def analyse_biomodel(
return f"An error occurred during AI analysis: {str(e)}"


async def analyse_diagram(biomodel_id: str, virtual_key: str, model: str):
async def analyse_diagram(
biomodel_id: str, virtual_key: str, model: str, auth0_token: str | None = None
):
"""
Analyze diagram for a given biomodel.

args:
biomodel_id (str): The ID of the biomodel to analyze.
virtual_key (str): The caller's LiteLLM virtual key.
model (str): The LiteLLM model alias to use.
auth0_token (str | None): Verified Auth0 access token for the
logged-in user, if any. Required to analyze a private or
shared biomodel's diagram.
returns:
str: The diagram analysis response.
"""
Expand All @@ -266,11 +273,15 @@ async def analyse_diagram(biomodel_id: str, virtual_key: str, model: str):
"orderBy": "date_desc",
}
biomodel_params = BiomodelRequestParams(**params_dict)
biomodels_info = await fetch_biomodels(biomodel_params)
biomodels_info = await fetch_biomodels(biomodel_params, auth0_token)
biomodel_info = f"Here is some information about Biomodel {biomodel_id}: {str(biomodels_info)}"

# Fetch Diagram URL
diagram_url = await get_diagram_url(biomodel_id)
# Fetch the diagram image ourselves and inline it as base64: the
# LLM provider fetches image_url URLs from its own infrastructure,
# with no way for it to send our Authorization header, so a plain
# VCell URL can never work for a private/shared biomodel.
image_bytes = await get_diagram_image(biomodel_id, auth0_token)
image_data_uri = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('utf-8')}"
# Diagram Analysis
diagram_analysis_prompt = (
"You are a VCell BioModel Assistant, designed to help users understand and interact with biological models in VCell. "
Expand All @@ -279,7 +290,7 @@ async def analyse_diagram(biomodel_id: str, virtual_key: str, model: str):
)
diagram_analysis_prompt = [
{"type": "text", "text": diagram_analysis_prompt},
{"type": "image_url", "image_url": {"url": diagram_url}},
{"type": "image_url", "image_url": {"url": image_data_uri}},
]
response = await _create_chat_completion(
virtual_key,
Expand Down
Loading
Loading