sentinel / apps /api /main.py
jeuko's picture
Sync from GitHub (main)
cc034ee verified
"""FastAPI application exposing cancer risk assessment endpoints."""
from pathlib import Path
from fastapi import FastAPI, HTTPException
from sentinel.config import AppConfig, ModelConfig, ResourcePaths
from sentinel.factory import SentinelFactory
from sentinel.models import InitialAssessment
from sentinel.user_input import UserInput
app = FastAPI(
title="Cancer Risk Assessment Assistant",
description="API for assessing cancer risks using LLMs.",
)
# Define base paths relative to the project root
BASE_DIR = Path(__file__).resolve().parents[2] # Go up to project root
CONFIGS_DIR = BASE_DIR / "configs"
PROMPTS_DIR = BASE_DIR / "prompts"
def create_knowledge_base_paths() -> ResourcePaths:
"""Build resource path configuration resolved from the repository root.
Returns:
ResourcePaths: Paths pointing to persona, prompt, and configuration
assets required by the API routes.
"""
return ResourcePaths(
persona=PROMPTS_DIR / "persona" / "default.md",
instruction_assessment=PROMPTS_DIR / "instruction" / "assessment.md",
instruction_conversation=PROMPTS_DIR / "instruction" / "conversation.md",
output_format_assessment=CONFIGS_DIR / "output_format" / "assessment.yaml",
output_format_conversation=CONFIGS_DIR / "output_format" / "conversation.yaml",
cancer_modules_dir=CONFIGS_DIR / "knowledge_base" / "cancer_modules",
dx_protocols_dir=CONFIGS_DIR / "knowledge_base" / "dx_protocols",
)
@app.get("/")
async def read_root() -> dict:
"""Return a simple greeting message.
Returns:
dict: A dictionary containing a greeting message.
"""
return {"message": "Hello, world!"}
@app.post("/assess/{provider}", response_model=InitialAssessment)
async def assess(
provider: str,
user_input: UserInput,
model: str | None = None,
cancer_modules: list[str] | None = None,
dx_protocols: list[str] | None = None,
) -> InitialAssessment:
"""Assess cancer risk for a user.
Args:
provider (str): LLM provider identifier (for example ``"openai"`` or
``"anthropic"``).
user_input (UserInput): Structured demographics and clinical
information supplied by the client.
model (str | None): Optional model name overriding the provider
default.
cancer_modules (list[str] | None): Optional list of cancer module slugs
to include in the knowledge base.
dx_protocols (list[str] | None): Optional list of diagnostic protocol
slugs to include.
Returns:
InitialAssessment: Parsed model output describing the initial
assessment.
Raises:
HTTPException: 400 for invalid input, 500 for unexpected errors.
"""
try:
# Create knowledge base paths
knowledge_base_paths = create_knowledge_base_paths()
# Set default model name if not provided
if model is None:
model_defaults = {
"openai": "gpt-4o-mini",
"anthropic": "claude-3-5-sonnet-20241022",
"google": "gemini-1.5-pro",
}
model = model_defaults.get(provider, "gpt-4o-mini")
# Set default modules if not provided
if cancer_modules is None:
cancer_modules_dir = knowledge_base_paths.cancer_modules_dir
cancer_modules = [p.stem for p in cancer_modules_dir.glob("*.yaml")]
if dx_protocols is None:
dx_protocols_dir = knowledge_base_paths.dx_protocols_dir
dx_protocols = [p.stem for p in dx_protocols_dir.glob("*.yaml")]
# Create AppConfig
app_config = AppConfig(
model=ModelConfig(provider=provider, model_name=model),
knowledge_base_paths=knowledge_base_paths,
selected_cancer_modules=cancer_modules,
selected_dx_protocols=dx_protocols,
)
# Create factory and conversation manager
factory = SentinelFactory(app_config)
conversation_manager = factory.create_conversation_manager()
# Run assessment
response = conversation_manager.initial_assessment(user_input)
return response
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Internal Server Error: {e!s}")