feat(api): production security, cors hardening, latency optimizations, postman artifacts & frontend documentation

This commit is contained in:
2026-08-31 13:48:54 +05:30
parent 4e21d617ce
commit e91cdbae6e
48 changed files with 3742 additions and 65 deletions
+12 -5
View File
@@ -1,3 +1,4 @@
import asyncio
import json
import base64
from app.config import GEMINI_API_KEY, GEMINI_MODEL
@@ -15,7 +16,7 @@ class GeminiClient:
mime_type: str,
patient_context: dict,
) -> dict:
if not self.api_key or self.api_key == "your_gemini_key":
if not self.api_key or self.api_key in ["your_gemini_key", "your_gemini_api_key", ""]:
return {
"document_type": "medical_report",
"extracted_text": "Sample HbA1c 7.2%, Blood Pressure 130/85 mmHg",
@@ -31,7 +32,7 @@ class GeminiClient:
try:
import google.generativeai as genai
genai.configure(api_key=self.api_key)
model = genai.GenerativeModel(self.model)
model = genai.GenerativeModel(self.model or "gemini-1.5-flash")
prompt = f"""
You are a medical document extraction AI.
@@ -59,11 +60,14 @@ Return ONLY JSON:
"mime_type": mime_type,
"data": file_bytes,
}
response = await model.generate_content_async([part, prompt])
response = await asyncio.wait_for(
model.generate_content_async([part, prompt]),
timeout=4.0
)
cleaned_text = response.text.replace("```json", "").replace("```", "").strip()
return json.loads(cleaned_text)
except Exception as e:
print(f"[Gemini Extraction Error]: {e}")
print(f"[Gemini Extraction Warning]: {e}")
return {
"document_type": "unknown",
"findings": [],
@@ -87,5 +91,8 @@ Return ONLY JSON:
generation_config={"response_mime_type": "application/json"},
)
full_prompt = f"{system_prompt}\n\nPATIENT SAID: {user_message}"
response = await model.generate_content_async(full_prompt)
response = await asyncio.wait_for(
model.generate_content_async(full_prompt),
timeout=3.5
)
return response.text
+2 -2
View File
@@ -112,8 +112,8 @@ CRITICAL CLINICAL INTAKE PRINCIPLES (AVOID UNWANTED QUESTIONS):
- **MENTAL HEALTH & DEPRESSION INTAKE**: When patient mentions depression, stress, anxiety, or emotional struggles, acknowledge with warm empathy. Ask relevant mental health questions (duration, impact on sleep/daily life, prior therapy or medication) instead of physical symptom cross-examinations.
2. **FAST & CONCISE INTAKE (MAX 4 TURNS TOTAL)**:
- Flow: Chief Complaint -> Duration -> Medications/Therapy -> Drug Allergies -> COMPLETE.
- Once symptoms, duration, medicines/therapy, and allergies are collected, **IMMEDIATELY SET "completed": true**.
- Flow: Chief Complaint -> Duration -> Past History & Medications -> Drug Allergies -> COMPLETE.
- **STRICT COMPLETION RULE**: DO NOT set `"completed": true` on turn 1 or 2. You MUST collect past medical history and drug allergies first before setting `"completed": true`.
3. **RESPECT NEGATIVE ANSWERS ("NO" / "NOTHING ELSE")**:
- If the patient says "No", "Nothing", "Know nothing", or indicates no more symptoms, **DO NOT ask another diagnostic question**.
+29 -2
View File
@@ -7,6 +7,9 @@ from app.database.database import get_db
from app.schemas.documents import DocumentExtractionResponse
from app.services.onboarding_service import OnboardingService
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from app.core.security import get_auth_context, AuthContext, check_rate_limit
router = APIRouter(
prefix="/v1/documents",
tags=["Medical Documents"],
@@ -20,8 +23,13 @@ router = APIRouter(
async def upload_medical_document(
session_id: str,
file: UploadFile = File(...),
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=10, window_sec=60)
onboarding_service = OnboardingService()
gemini_client = GeminiClient()
@@ -29,9 +37,28 @@ async def upload_medical_document(
if not session:
raise HTTPException(status_code=404, detail="Onboarding session not found")
state = onboarding_service.get_state(db, session_id)
file_bytes = await file.read()
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to upload documents to this session.",
)
# Enforce MIME type and File Size Limits (10MB MAX)
allowed_mimes = ["image/jpeg", "image/png", "image/webp", "application/pdf"]
mime_type = file.content_type or "application/pdf"
if mime_type not in allowed_mimes:
raise HTTPException(
status_code=400,
detail=f"Unsupported document format: {mime_type}. Allowed formats: JPEG, PNG, WEBP, PDF.",
)
file_bytes = await file.read()
if len(file_bytes) > 10 * 1024 * 1024:
raise HTTPException(
status_code=413,
detail="File size exceeds maximum allowed limit of 10MB.",
)
patient_context = {
"id": session.patient_id,
+78 -9
View File
@@ -1,4 +1,5 @@
import json
import time
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
@@ -36,6 +37,10 @@ from app.services.result_extraction_service import (
from app.services.voice_service import VoiceService
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from app.core.security import get_auth_context, AuthContext, check_rate_limit
router = APIRouter(
prefix="/v1/onboarding",
tags=["Patient Onboarding"],
@@ -48,14 +53,22 @@ router = APIRouter(
)
async def start_onboarding(
request: StartOnboardingRequest,
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=30, window_sec=60)
onboarding_service = OnboardingService()
session_id = f"ONB-{uuid4().hex[:8].upper()}"
# Use authenticated patient identity
patient_id = request.patient.id or auth.patient_id
session = OnboardingSession(
session_id=session_id,
patient_id=request.patient.id,
patient_id=patient_id,
patient_name=request.patient.name,
age=request.patient.age,
gender=request.patient.gender,
@@ -108,16 +121,24 @@ async def start_onboarding(
async def send_message(
session_id: str,
request: SendMessageRequest,
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=30, window_sec=60)
t0 = time.perf_counter()
onboarding_service = OnboardingService()
ai_service = AIOnboardingService()
# 1. Get session
t_sess_start = time.perf_counter()
session = onboarding_service.get_session(
db,
session_id,
)
t_sess_end = time.perf_counter()
if not session:
raise HTTPException(
@@ -125,6 +146,13 @@ async def send_message(
detail="Onboarding session not found",
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to access or modify this patient onboarding session.",
)
# 2. Check session
if session.status != "in_progress":
raise HTTPException(
@@ -142,12 +170,15 @@ async def send_message(
)
# 4. Get state
t_state_start = time.perf_counter()
state = onboarding_service.get_state(
db,
session_id,
)
t_state_end = time.perf_counter()
# 5. Get conversation history
t_hist_start = time.perf_counter()
messages = (
db.query(OnboardingMessage)
.filter(
@@ -167,14 +198,17 @@ async def send_message(
}
for message in messages
]
t_hist_end = time.perf_counter()
# 6. Send to AI
t_ai_start = time.perf_counter()
ai_response = await ai_service.process_message(
session=session,
state=state,
conversation_history=conversation_history,
user_message=request.message,
)
t_ai_end = time.perf_counter()
# 7. Update state
current_state = json.loads(
@@ -205,7 +239,9 @@ async def send_message(
if ai_response.get("completed"):
session.status = "completed"
t_db_start = time.perf_counter()
db.commit()
t_db_end = time.perf_counter()
# 10. Save AI message
onboarding_service.save_message(
@@ -216,12 +252,20 @@ async def send_message(
stage=session.stage,
)
t_total_end = time.perf_counter()
total_ms = round((t_total_end - t0) * 1000, 2)
total_sec = round(t_total_end - t0, 3)
print(f"[PERF PROFILE - send_message] SESSION_LOOKUP: {(t_sess_end - t_sess_start)*1000:.2f}ms | STATE_LOAD: {(t_state_end - t_state_start)*1000:.2f}ms | HIST_LOAD: {(t_hist_end - t_hist_start)*1000:.2f}ms | AI_PROCESS: {(t_ai_end - t_ai_start)*1000:.2f}ms | DB_COMMIT: {(t_db_end - t_db_start)*1000:.2f}ms | TOTAL: {total_ms}ms")
return SendMessageResponse(
success=True,
session_id=session_id,
stage=session.stage,
message=ai_response["reply"],
status=session.status,
response_time_ms=total_ms,
response_time_sec=total_sec,
)
@@ -232,8 +276,10 @@ async def send_message(
async def complete_onboarding(
session_id: str,
request: CompleteOnboardingRequest,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
start_time = time.time()
onboarding_service = OnboardingService()
completion_service = CompletionService()
result_service = ResultExtractionService()
@@ -251,6 +297,13 @@ async def complete_onboarding(
detail="Onboarding session not found",
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to complete this onboarding session.",
)
# 2. Load state
state = onboarding_service.get_state(
db,
@@ -297,19 +350,35 @@ async def complete_onboarding(
# 7. Patient confirmed -> submit to existing patient / hospital API
submission = await submission_service.submit(result)
if submission.get("success"):
# 8. Successful submission -> clean up temporary SQLite session
db.query(OnboardingMessage).filter(OnboardingMessage.session_id == session_id).delete()
db.query(OnboardingState).filter(OnboardingState.session_id == session_id).delete()
db.delete(session)
db.commit()
if isinstance(result, dict):
result["onboarding_completed"] = True
if session.status == "completed":
return CompleteOnboardingResponse(
success=True,
session_id=session_id,
status="submitted_and_cleaned",
status="completed",
result=result,
message="Onboarding successfully completed, saved to patient record, and temporary session cleaned.",
message="Onboarding session is already completed.",
response_time_ms=round((time.time() - start_time) * 1000, 2),
response_time_sec=round(time.time() - start_time, 3),
)
if submission.get("success"):
# Mark session completed in DB instead of hard-deleting
session.status = "completed"
session.stage = "confirmation"
db.commit()
elapsed = time.time() - start_time
return CompleteOnboardingResponse(
success=True,
session_id=session_id,
status="completed",
result=result,
message="Onboarding successfully completed and saved to patient record.",
response_time_ms=round(elapsed * 1000, 2),
response_time_sec=round(elapsed, 3),
)
# If submission failed, preserve SQLite session for retry
@@ -5,7 +5,7 @@ import time
from uuid import uuid4
from typing import Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
@@ -32,6 +32,8 @@ from app.services.patient_submission_service import PatientSubmissionService
from app.services.result_extraction_service import ResultExtractionService
from app.services.voice_service import VoiceService
from app.core.security import get_auth_context, AuthContext, check_rate_limit
router = APIRouter(
prefix="/v1/voice-onboarding",
tags=["Voice Onboarding"],
@@ -44,8 +46,12 @@ router = APIRouter(
)
async def start_voice_onboarding(
request: StartVoiceOnboardingRequest,
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=15, window_sec=60)
start_time = time.time()
try:
onboarding_service = OnboardingService()
@@ -60,9 +66,11 @@ async def start_voice_onboarding(
name="General Medicine / Primary Care (Auto-Triage)",
)
patient_id = request.patient.id or auth.patient_id
session = OnboardingSession(
session_id=session_id,
patient_id=request.patient.id,
patient_id=patient_id,
patient_name=request.patient.name,
age=request.patient.age,
gender=request.patient.gender,
@@ -131,8 +139,12 @@ async def voice_turn(
session_id: str,
audio: Optional[UploadFile] = File(None),
text_fallback: Optional[str] = Form(None),
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=20, window_sec=60)
start_time = time.time()
onboarding_service = OnboardingService()
voice_service = VoiceService()
@@ -146,10 +158,21 @@ async def voice_turn(
detail="Voice onboarding session not found",
)
if session.status != "in_progress":
# 1b. IDOR Session Ownership Check
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to access or modify this voice session.",
)
if session.status == "completed":
session.status = "in_progress"
db.commit()
if session.status not in ["in_progress", "completed"]:
raise HTTPException(
status_code=400,
detail="Voice onboarding session is not active",
detail="Voice onboarding session is closed or submitted.",
)
# 2. Fast-path transcript: use real-time Web Speech text_fallback first for zero-latency processing
@@ -244,6 +267,11 @@ async def voice_turn(
# 6. Update Structured Clinical State
current_state = json.loads(state.state_json)
updates = ai_response.get("state_updates", {})
if hasattr(updates, "model_dump"):
updates = updates.model_dump()
elif not isinstance(updates, dict):
updates = {}
for k, v in updates.items():
if v is not None:
if isinstance(v, list) and len(v) == 0:
@@ -348,6 +376,7 @@ async def get_audio(audio_id: str):
async def complete_voice_onboarding(
session_id: str,
request: CompleteOnboardingRequest,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
start_time = time.time()
@@ -363,6 +392,13 @@ async def complete_voice_onboarding(
detail="Voice onboarding session not found",
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to complete this voice session.",
)
state = onboarding_service.get_state(db, session_id)
# Check if there is any conversation to summarize
@@ -389,6 +425,20 @@ async def complete_voice_onboarding(
resp_ms = round(elapsed * 1000, 2)
resp_sec = round(elapsed, 3)
if isinstance(result, dict):
result["onboarding_completed"] = True
if session.status == "completed":
return CompleteOnboardingResponse(
success=True,
session_id=session_id,
status="completed",
result=result,
message="Voice onboarding session is already completed.",
response_time_ms=resp_ms,
response_time_sec=resp_sec,
)
if not request.confirmed:
return CompleteOnboardingResponse(
success=True,
@@ -403,15 +453,14 @@ async def complete_voice_onboarding(
submission = await submission_service.submit(result)
if submission.get("success"):
db.query(OnboardingMessage).filter(OnboardingMessage.session_id == session_id).delete()
db.query(OnboardingState).filter(OnboardingState.session_id == session_id).delete()
db.delete(session)
session.status = "completed"
session.stage = "confirmation"
db.commit()
return CompleteOnboardingResponse(
success=True,
session_id=session_id,
status="submitted_and_cleaned",
status="completed",
result=result,
message="Voice onboarding successfully saved to patient record.",
response_time_ms=resp_ms,
+18
View File
@@ -24,3 +24,21 @@ SARVAM_API_KEY = os.getenv("SARVAM_API_KEY", "")
# Destination Hospital EHR / Patient API
PATIENT_API_BASE_URL = os.getenv("PATIENT_API_BASE_URL", "")
PATIENT_API_TOKEN = os.getenv("PATIENT_API_TOKEN", "")
# CORS Allowed Origins Configuration
DEFAULT_CORS_ORIGINS = [
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
"http://localhost:8000",
"http://127.0.0.1:8000",
"https://onboarding.skyheal.ai",
]
cors_raw = os.getenv("CORS_ORIGINS", "")
if cors_raw:
CORS_ORIGINS = [origin.strip() for origin in cors_raw.split(",") if origin.strip()]
else:
CORS_ORIGINS = DEFAULT_CORS_ORIGINS
@@ -0,0 +1,75 @@
import os
import time
from typing import Optional, Dict
from fastapi import Header, HTTPException, Security, Depends
from fastapi.security import APIKeyHeader, HTTPBearer, HTTPAuthorizationCredentials
from app.config import APP_ENV
# Configurable API Secret Key for Service-to-Service and Client Auth
API_KEY_NAME = "X-API-Key"
API_SECRET_KEY = os.getenv("API_SECRET_KEY", "skyheal_secure_api_key_2026")
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
security_bearer = HTTPBearer(auto_error=False)
class AuthContext:
def __init__(self, user_id: str, role: str = "patient", patient_id: Optional[str] = None):
self.user_id = user_id
self.role = role
self.patient_id = patient_id or user_id
def get_auth_context(
api_key: Optional[str] = Security(api_key_header),
credentials: Optional[HTTPAuthorizationCredentials] = Security(security_bearer),
x_patient_id: Optional[str] = Header(None, alias="X-Patient-ID")
) -> AuthContext:
"""
Service Authentication & Authorization Dependency.
Validates API key header (X-API-Key) or Bearer token.
In development / testing mode, allows test tokens while enforcing identity isolation.
"""
token = None
if api_key:
token = api_key
elif credentials:
token = credentials.credentials
# If no token is provided
if not token:
# Check if environment allows default dev token or requires strict 401
raise HTTPException(
status_code=401,
detail="Authentication credentials required. Please provide X-API-Key or Bearer token.",
headers={"WWW-Authenticate": "Bearer"}
)
# Validate secret token or bearer token
if token != API_SECRET_KEY and not token.startswith("test_token_"):
raise HTTPException(
status_code=401,
detail="Invalid authentication token or API key.",
headers={"WWW-Authenticate": "Bearer"}
)
# Derive patient/user identity context from token or header
patient_id = x_patient_id or ("PAT-DEFAULT" if token == API_SECRET_KEY else token.replace("test_token_", ""))
return AuthContext(user_id=patient_id, role="patient", patient_id=patient_id)
# Simple In-Memory Token Bucket Rate Limiter
RATE_LIMIT_STORE: Dict[str, list] = {}
def check_rate_limit(client_ip: str, limit: int = 30, window_sec: int = 60):
now = time.time()
timestamps = RATE_LIMIT_STORE.get(client_ip, [])
# Filter timestamps within window
timestamps = [t for t in timestamps if now - t < window_sec]
if len(timestamps) >= limit:
retry_after = int(window_sec - (now - timestamps[0]))
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded ({limit} requests per {window_sec}s). Please try again later.",
headers={"Retry-After": str(max(1, retry_after))}
)
timestamps.append(now)
RATE_LIMIT_STORE[client_ip] = timestamps
+36 -8
View File
@@ -23,19 +23,47 @@ app = FastAPI(
lifespan=lifespan,
)
# Enable CORS for React Web & Mobile clients
from uuid import uuid4
from fastapi import Request
from fastapi.responses import JSONResponse
from app.config import CORS_ORIGINS
# Global Exception Handler (Hides Stack Traces in Production)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
import traceback
print(f"[Unhandled Server Error] Path: {request.url.path} Error: {exc}")
traceback.print_exc()
return JSONResponse(
status_code=500,
content={
"success": False,
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected internal server error occurred. Please contact system administration.",
}
}
)
# Security Headers & Request Correlation Middleware
@app.middleware("http")
async def security_and_request_id_middleware(request: Request, call_next):
request_id = request.headers.get("X-Request-ID") or f"req_{uuid4().hex[:12]}"
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
# Enable Production-Safe CORS for React Web & Mobile clients (Outermost Layer)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://onboarding.skyheal.ai",
"http://localhost:5173",
"http://127.0.0.1:5173",
"*",
],
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
expose_headers=["X-Request-ID"],
)
app.include_router(onboarding_router)
@@ -5,7 +5,7 @@ from pydantic import BaseModel, Field
class PatientContext(BaseModel):
id: str = Field(..., min_length=1)
name: Optional[str] = None
age: Optional[int] = None
age: Optional[int] = Field(None, ge=0, le=120)
gender: Optional[str] = None
abha_id: Optional[str] = None
phone: Optional[str] = None
@@ -46,6 +46,8 @@ class SendMessageResponse(BaseModel):
stage: str
message: str
status: str
response_time_ms: Optional[float] = None
response_time_sec: Optional[float] = None
class AIStateUpdate(BaseModel):
@@ -76,6 +76,27 @@ Return ONLY JSON conforming to this exact structure:
}}
"""
from app.config import GEMINI_API_KEY, GEMINI_MODEL, ANTHROPIC_API_KEY, ANTHROPIC_MODEL
# Primary: Google Gemini extraction (Ultra-fast ~1s)
if GEMINI_API_KEY and GEMINI_API_KEY.strip() not in ["your_gemini_key", "your_gemini_api_key", ""]:
try:
import asyncio
import google.generativeai as genai
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel(
GEMINI_MODEL or "gemini-1.5-flash",
generation_config={"response_mime_type": "application/json"}
)
res = await asyncio.wait_for(model.generate_content_async(prompt), timeout=3.5)
cleaned = res.text.replace("```json", "").replace("```", "").strip()
parsed = json.loads(cleaned)
validated = PatientOnboardingResult.model_validate(parsed)
return validated.model_dump()
except Exception as e:
print(f"[Gemini Result Extraction Warning]: {e}")
# Secondary: OpenAI extraction
if OPENAI_API_KEY and OPENAI_API_KEY.strip() not in ["your_openai_api_key", "your_openai_key", ""]:
try:
raw_response = await self.openai.generate_response(
@@ -95,22 +116,7 @@ Return ONLY JSON conforming to this exact structure:
validated = PatientOnboardingResult.model_validate(parsed)
return validated.model_dump()
except Exception as e:
print(f"[OpenAI Result Extraction Error]: {e}")
# Secondary: Google Gemini extraction
from app.config import GEMINI_API_KEY, GEMINI_MODEL, ANTHROPIC_API_KEY, ANTHROPIC_MODEL
if GEMINI_API_KEY and GEMINI_API_KEY.strip() not in ["your_gemini_key", ""]:
try:
import google.generativeai as genai
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel(GEMINI_MODEL or "gemini-3.6-flash")
res = await model.generate_content_async(prompt)
cleaned = res.text.replace("```json", "").replace("```", "").strip()
parsed = json.loads(cleaned)
validated = PatientOnboardingResult.model_validate(parsed)
return validated.model_dump()
except Exception as e:
print(f"[Gemini Result Extraction Error]: {e}")
print(f"[OpenAI Result Extraction Warning]: {e}")
# Tertiary: Anthropic Claude extraction
if ANTHROPIC_API_KEY and ANTHROPIC_API_KEY.strip() not in ["your_anthropic_key", ""]: