"""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}")