611 lines
24 KiB
Python
611 lines
24 KiB
Python
"""API endpoints for template-driven analysis system."""
|
|
|
|
import logging
|
|
from typing import Dict, List, Optional, Any
|
|
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
from ..core.dependencies import get_current_user
|
|
from ..models.user import User
|
|
from ..models.analysis_templates import (
|
|
AnalysisTemplate,
|
|
TemplateSet,
|
|
TemplateRegistry,
|
|
TemplateType,
|
|
ComplexityLevel
|
|
)
|
|
from ..services.template_driven_agent import (
|
|
TemplateDrivenAgent,
|
|
TemplateAnalysisRequest,
|
|
TemplateAnalysisResult
|
|
)
|
|
from ..services.template_defaults import DEFAULT_REGISTRY
|
|
from ..services.enhanced_orchestrator import (
|
|
EnhancedMultiAgentOrchestrator,
|
|
OrchestrationConfig,
|
|
OrchestrationResult
|
|
)
|
|
from ..services.template_agent_factory import get_template_agent_factory
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/templates", tags=["Analysis Templates"])
|
|
|
|
# Response models (defined before endpoint decorators)
|
|
class MultiTemplateAnalysisResult(BaseModel):
|
|
"""Result from analyzing content with multiple templates."""
|
|
template_set_id: str
|
|
template_set_name: str
|
|
results: Dict[str, TemplateAnalysisResult]
|
|
synthesis_result: Optional[TemplateAnalysisResult] = None
|
|
total_processing_time_seconds: float
|
|
|
|
# Request/Response models (defined before endpoints that use them)
|
|
class TestEducationalRequest(BaseModel):
|
|
content: str = Field(..., min_length=50, description="Content to analyze")
|
|
|
|
class AnalyzeWithTemplateSetRequest(BaseModel):
|
|
"""Request to analyze content with a template set."""
|
|
content: str = Field(..., description="Content to analyze", min_length=10)
|
|
template_set_id: str = Field(..., description="Template set ID to use")
|
|
context: Dict[str, Any] = Field(default_factory=dict, description="Additional context variables")
|
|
include_synthesis: bool = Field(default=True, description="Whether to include synthesis of results")
|
|
video_id: Optional[str] = Field(None, description="Video ID if analyzing video content")
|
|
|
|
# Dependencies (defined before endpoints that use them)
|
|
async def get_template_agent() -> TemplateDrivenAgent:
|
|
"""Get template-driven agent instance."""
|
|
return TemplateDrivenAgent(template_registry=DEFAULT_REGISTRY)
|
|
|
|
# Test endpoint without auth for development
|
|
@router.post("/test-educational", summary="Test educational analysis (no auth)")
|
|
async def test_educational_analysis(
|
|
request: TestEducationalRequest
|
|
):
|
|
"""Test educational analysis without authentication - DEVELOPMENT ONLY."""
|
|
try:
|
|
# Use the educational template set
|
|
from ..services.template_driven_agent import TemplateDrivenAgent
|
|
|
|
# Create agent with registry (will automatically use multi-key services)
|
|
agent = TemplateDrivenAgent(template_registry=DEFAULT_REGISTRY)
|
|
|
|
# Process templates using analyze_with_template_set (will run in parallel with separate keys)
|
|
results = await agent.analyze_with_template_set(
|
|
content=request.content,
|
|
template_set_id="educational_perspectives", # The ID of the educational template set
|
|
context={
|
|
"content_type": "video content",
|
|
"topic": "the analyzed topic"
|
|
}
|
|
)
|
|
|
|
# Format results for response
|
|
formatted_results = {}
|
|
for template_id, result in results.items():
|
|
formatted_results[template_id] = {
|
|
"template_name": result.template_name,
|
|
"summary": result.analysis[:200] + "..." if len(result.analysis) > 200 else result.analysis,
|
|
"key_insights": result.key_insights[:3] if result.key_insights else [],
|
|
"confidence": result.confidence_score
|
|
}
|
|
|
|
# Try to synthesize results if we have them
|
|
synthesis_summary = None
|
|
if len(results) == 3: # All three educational perspectives
|
|
synthesis_summary = f"Successfully analyzed content from {len(results)} educational perspectives: Beginner's Lens, Expert's Lens, and Scholar's Lens."
|
|
|
|
return {
|
|
"status": "success",
|
|
"perspectives": formatted_results,
|
|
"synthesis": synthesis_summary,
|
|
"message": f"Educational orchestration is working! Processed {len(results)} templates successfully."
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Test analysis failed: {e}")
|
|
return {"status": "error", "message": str(e)}
|
|
|
|
|
|
# Educational analysis endpoint with authentication and full synthesis
|
|
@router.post("/analyze-educational", response_model=MultiTemplateAnalysisResult)
|
|
async def analyze_educational_content(
|
|
request: AnalyzeWithTemplateSetRequest,
|
|
background_tasks: BackgroundTasks,
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Analyze content with educational perspectives (Beginner, Expert, Scholar).
|
|
Uses multi-key parallel processing for optimal performance.
|
|
"""
|
|
try:
|
|
import time
|
|
start_time = time.time()
|
|
|
|
# Force educational template set
|
|
template_set_id = "educational_perspectives"
|
|
|
|
# Analyze with educational template set (parallel with 3 API keys)
|
|
results = await agent.analyze_with_template_set(
|
|
content=request.content,
|
|
template_set_id=template_set_id,
|
|
context={
|
|
**request.context,
|
|
"content_type": request.context.get("content_type", "video content"),
|
|
"topic": request.context.get("topic", "the subject matter")
|
|
},
|
|
video_id=request.video_id
|
|
)
|
|
|
|
# Always synthesize educational results with dedicated timeout
|
|
synthesis_result = None
|
|
if len(results) >= 2: # Synthesize even with partial results
|
|
try:
|
|
# Start synthesis immediately when we have results, with full 180s timeout
|
|
logger.info(f"Starting synthesis for {len(results)} perspectives - user {current_user.id}")
|
|
synthesis_result = await agent.synthesize_results(
|
|
results=results,
|
|
template_set_id=template_set_id,
|
|
context=request.context
|
|
)
|
|
logger.info(f"Educational synthesis completed successfully for user {current_user.id}")
|
|
except Exception as syn_err:
|
|
logger.warning(f"Synthesis failed but continuing: {syn_err}")
|
|
# Continue without synthesis rather than failing completely
|
|
|
|
total_processing_time = time.time() - start_time
|
|
|
|
# Get template set info
|
|
template_set = DEFAULT_REGISTRY.get_template_set(template_set_id)
|
|
template_set_name = template_set.name if template_set else "Educational Perspectives"
|
|
|
|
# Store analysis in database if requested
|
|
if request.context.get("store_results", False) and request.video_id:
|
|
# TODO: Store template analysis with video summary
|
|
pass
|
|
|
|
result = MultiTemplateAnalysisResult(
|
|
template_set_id=template_set_id,
|
|
template_set_name=template_set_name,
|
|
results=results,
|
|
synthesis_result=synthesis_result,
|
|
total_processing_time_seconds=total_processing_time
|
|
)
|
|
|
|
logger.info(f"Educational analysis completed in {total_processing_time:.2f}s for user {current_user.id}")
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Educational analysis failed for user {current_user.id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# Request/Response Models
|
|
class AnalyzeWithTemplateRequest(BaseModel):
|
|
"""Request to analyze content with a specific template."""
|
|
content: str = Field(..., description="Content to analyze", min_length=10)
|
|
template_id: str = Field(..., description="Template ID to use for analysis")
|
|
context: Dict[str, Any] = Field(default_factory=dict, description="Additional context variables")
|
|
video_id: Optional[str] = Field(None, description="Video ID if analyzing video content")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateTemplateRequest(BaseModel):
|
|
"""Request to create a custom template."""
|
|
name: str = Field(..., min_length=1, max_length=100)
|
|
description: str = Field(..., min_length=10, max_length=500)
|
|
template_type: TemplateType
|
|
system_prompt: str = Field(..., min_length=50)
|
|
analysis_focus: List[str] = Field(..., min_items=1, max_items=10)
|
|
output_format: str = Field(..., min_length=20)
|
|
complexity_level: Optional[ComplexityLevel] = None
|
|
target_audience: str = Field(default="general")
|
|
tone: str = Field(default="professional")
|
|
depth: str = Field(default="standard")
|
|
variables: Dict[str, Any] = Field(default_factory=dict)
|
|
tags: List[str] = Field(default_factory=list, max_items=10)
|
|
|
|
|
|
class UpdateTemplateRequest(BaseModel):
|
|
"""Request to update an existing template."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
description: Optional[str] = Field(None, min_length=10, max_length=500)
|
|
system_prompt: Optional[str] = Field(None, min_length=50)
|
|
analysis_focus: Optional[List[str]] = Field(None, min_items=1, max_items=10)
|
|
output_format: Optional[str] = Field(None, min_length=20)
|
|
target_audience: Optional[str] = None
|
|
tone: Optional[str] = None
|
|
depth: Optional[str] = None
|
|
variables: Optional[Dict[str, Any]] = None
|
|
tags: Optional[List[str]] = Field(None, max_items=10)
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
# Enhanced Unified System Request/Response Models
|
|
|
|
class UnifiedAnalysisRequest(BaseModel):
|
|
"""Request for unified multi-agent analysis."""
|
|
content: str = Field(..., description="Content to analyze", min_length=10)
|
|
template_set_id: str = Field(..., description="Template set ID for orchestrated analysis")
|
|
context: Dict[str, Any] = Field(default_factory=dict, description="Additional context variables")
|
|
video_id: Optional[str] = Field(None, description="Video ID if analyzing video content")
|
|
enable_synthesis: bool = Field(default=True, description="Whether to synthesize results")
|
|
parallel_execution: bool = Field(default=True, description="Execute agents in parallel")
|
|
save_to_database: bool = Field(default=True, description="Save results to database")
|
|
|
|
|
|
class MixedPerspectiveRequest(BaseModel):
|
|
"""Request for mixed perspective analysis (Educational + Domain)."""
|
|
content: str = Field(..., description="Content to analyze", min_length=10)
|
|
template_ids: List[str] = Field(..., description="List of template IDs to use", min_items=1, max_items=10)
|
|
context: Dict[str, Any] = Field(default_factory=dict, description="Additional context variables")
|
|
video_id: Optional[str] = Field(None, description="Video ID if analyzing video content")
|
|
enable_synthesis: bool = Field(default=True, description="Whether to synthesize mixed results")
|
|
|
|
|
|
class OrchestrationResultResponse(BaseModel):
|
|
"""Response from unified orchestration."""
|
|
job_id: str
|
|
template_set_id: str
|
|
results: Dict[str, TemplateAnalysisResult]
|
|
synthesis_result: Optional[TemplateAnalysisResult] = None
|
|
processing_time_seconds: float
|
|
success: bool
|
|
error: Optional[str] = None
|
|
metadata: Dict[str, Any]
|
|
timestamp: str
|
|
|
|
|
|
# Dependencies
|
|
|
|
|
|
|
|
async def get_enhanced_orchestrator() -> EnhancedMultiAgentOrchestrator:
|
|
"""Get enhanced multi-agent orchestrator instance."""
|
|
agent_factory = get_template_agent_factory(template_registry=DEFAULT_REGISTRY)
|
|
config = OrchestrationConfig(
|
|
parallel_execution=True,
|
|
synthesis_enabled=True,
|
|
max_concurrent_agents=4,
|
|
timeout_seconds=300,
|
|
enable_database_persistence=True
|
|
)
|
|
return EnhancedMultiAgentOrchestrator(
|
|
template_registry=DEFAULT_REGISTRY,
|
|
agent_factory=agent_factory,
|
|
config=config
|
|
)
|
|
|
|
|
|
# Analysis Endpoints
|
|
@router.post("/analyze", response_model=TemplateAnalysisResult)
|
|
async def analyze_with_template(
|
|
request: AnalyzeWithTemplateRequest,
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Analyze content using a specific template."""
|
|
try:
|
|
analysis_request = TemplateAnalysisRequest(
|
|
content=request.content,
|
|
template_id=request.template_id,
|
|
context=request.context,
|
|
video_id=request.video_id
|
|
)
|
|
|
|
result = await agent.analyze_with_template(analysis_request)
|
|
|
|
logger.info(f"Template analysis completed: {request.template_id} for user {current_user.id}")
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Template analysis failed: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/analyze-set", response_model=MultiTemplateAnalysisResult)
|
|
async def analyze_with_template_set(
|
|
request: AnalyzeWithTemplateSetRequest,
|
|
background_tasks: BackgroundTasks,
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Analyze content using all templates in a template set."""
|
|
try:
|
|
import time
|
|
start_time = time.time()
|
|
|
|
# Analyze with template set
|
|
results = await agent.analyze_with_template_set(
|
|
content=request.content,
|
|
template_set_id=request.template_set_id,
|
|
context=request.context,
|
|
video_id=request.video_id
|
|
)
|
|
|
|
synthesis_result = None
|
|
if request.include_synthesis:
|
|
synthesis_result = await agent.synthesize_results(
|
|
results=results,
|
|
template_set_id=request.template_set_id,
|
|
context=request.context
|
|
)
|
|
|
|
total_processing_time = time.time() - start_time
|
|
|
|
# Get template set info
|
|
template_set = DEFAULT_REGISTRY.get_template_set(request.template_set_id)
|
|
template_set_name = template_set.name if template_set else "Unknown"
|
|
|
|
result = MultiTemplateAnalysisResult(
|
|
template_set_id=request.template_set_id,
|
|
template_set_name=template_set_name,
|
|
results=results,
|
|
synthesis_result=synthesis_result,
|
|
total_processing_time_seconds=total_processing_time
|
|
)
|
|
|
|
logger.info(f"Template set analysis completed: {request.template_set_id} for user {current_user.id}")
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Template set analysis failed: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# Template Management Endpoints
|
|
@router.get("/list", response_model=List[AnalysisTemplate])
|
|
async def list_templates(
|
|
template_type: Optional[TemplateType] = Query(None, description="Filter by template type"),
|
|
active_only: bool = Query(True, description="Only return active templates"),
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent)
|
|
):
|
|
"""List all available templates."""
|
|
try:
|
|
templates = agent.template_registry.list_templates(template_type)
|
|
|
|
if active_only:
|
|
templates = [t for t in templates if t.is_active]
|
|
|
|
return templates
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to list templates: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to list templates")
|
|
|
|
|
|
@router.get("/sets", response_model=List[TemplateSet])
|
|
async def list_template_sets(
|
|
template_type: Optional[TemplateType] = Query(None, description="Filter by template type"),
|
|
active_only: bool = Query(True, description="Only return active template sets"),
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent)
|
|
):
|
|
"""List all available template sets."""
|
|
try:
|
|
template_sets = agent.template_registry.list_template_sets(template_type)
|
|
|
|
if active_only:
|
|
template_sets = [ts for ts in template_sets if ts.is_active]
|
|
|
|
return template_sets
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to list template sets: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to list template sets")
|
|
|
|
|
|
@router.get("/template/{template_id}", response_model=AnalysisTemplate)
|
|
async def get_template(
|
|
template_id: str,
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent)
|
|
):
|
|
"""Get a specific template by ID."""
|
|
template = agent.template_registry.get_template(template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
|
|
|
return template
|
|
|
|
|
|
@router.get("/set/{set_id}", response_model=TemplateSet)
|
|
async def get_template_set(
|
|
set_id: str,
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent)
|
|
):
|
|
"""Get a specific template set by ID."""
|
|
template_set = agent.template_registry.get_template_set(set_id)
|
|
if not template_set:
|
|
raise HTTPException(status_code=404, detail="Template set not found")
|
|
|
|
return template_set
|
|
|
|
|
|
# Custom Template Creation (Future Enhancement)
|
|
@router.post("/create", response_model=AnalysisTemplate)
|
|
async def create_custom_template(
|
|
request: CreateTemplateRequest,
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create a custom template (placeholder for future implementation)."""
|
|
# This is a placeholder for custom template creation
|
|
# In a full implementation, this would:
|
|
# 1. Validate the template configuration
|
|
# 2. Save to database
|
|
# 3. Register with template registry
|
|
# 4. Handle template versioning and permissions
|
|
|
|
raise HTTPException(
|
|
status_code=501,
|
|
detail="Custom template creation not yet implemented. Use default templates."
|
|
)
|
|
|
|
|
|
# Unified Multi-Agent Analysis Endpoints
|
|
|
|
@router.post("/unified-analyze", response_model=OrchestrationResultResponse)
|
|
async def unified_analysis(
|
|
request: UnifiedAnalysisRequest,
|
|
background_tasks: BackgroundTasks,
|
|
orchestrator: EnhancedMultiAgentOrchestrator = Depends(get_enhanced_orchestrator),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Perform unified multi-agent analysis using a template set."""
|
|
import uuid
|
|
|
|
try:
|
|
job_id = str(uuid.uuid4())
|
|
|
|
# Perform orchestrated analysis
|
|
result = await orchestrator.orchestrate_template_set(
|
|
job_id=job_id,
|
|
template_set_id=request.template_set_id,
|
|
content=request.content,
|
|
context=request.context,
|
|
video_id=request.video_id
|
|
)
|
|
|
|
# Convert OrchestrationResult to response format
|
|
response = OrchestrationResultResponse(
|
|
job_id=result.job_id,
|
|
template_set_id=result.template_set_id,
|
|
results=result.results,
|
|
synthesis_result=result.synthesis_result,
|
|
processing_time_seconds=result.processing_time_seconds,
|
|
success=result.success,
|
|
error=result.error,
|
|
metadata=result.metadata,
|
|
timestamp=result.timestamp.isoformat()
|
|
)
|
|
|
|
logger.info(f"Unified analysis completed: {job_id} for user {current_user.id}")
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unified analysis failed: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/mixed-perspective", response_model=OrchestrationResultResponse)
|
|
async def mixed_perspective_analysis(
|
|
request: MixedPerspectiveRequest,
|
|
background_tasks: BackgroundTasks,
|
|
orchestrator: EnhancedMultiAgentOrchestrator = Depends(get_enhanced_orchestrator),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Perform analysis using mixed perspectives (Educational + Domain)."""
|
|
import uuid
|
|
|
|
try:
|
|
job_id = str(uuid.uuid4())
|
|
|
|
# Perform mixed perspective analysis
|
|
result = await orchestrator.orchestrate_mixed_perspectives(
|
|
job_id=job_id,
|
|
template_ids=request.template_ids,
|
|
content=request.content,
|
|
context=request.context,
|
|
video_id=request.video_id,
|
|
enable_synthesis=request.enable_synthesis
|
|
)
|
|
|
|
# Convert OrchestrationResult to response format
|
|
response = OrchestrationResultResponse(
|
|
job_id=result.job_id,
|
|
template_set_id=result.template_set_id,
|
|
results=result.results,
|
|
synthesis_result=result.synthesis_result,
|
|
processing_time_seconds=result.processing_time_seconds,
|
|
success=result.success,
|
|
error=result.error,
|
|
metadata=result.metadata,
|
|
timestamp=result.timestamp.isoformat()
|
|
)
|
|
|
|
logger.info(f"Mixed perspective analysis completed: {job_id} for user {current_user.id}")
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Mixed perspective analysis failed: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/orchestrator/stats")
|
|
async def get_orchestrator_statistics(
|
|
orchestrator: EnhancedMultiAgentOrchestrator = Depends(get_enhanced_orchestrator),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get comprehensive orchestrator and factory statistics."""
|
|
try:
|
|
stats = orchestrator.get_orchestration_statistics()
|
|
active_jobs = orchestrator.get_active_orchestrations()
|
|
|
|
return {
|
|
"orchestrator_stats": stats,
|
|
"active_orchestrations": active_jobs,
|
|
"system_status": "operational"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get orchestrator statistics: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to get orchestrator statistics")
|
|
|
|
|
|
# Statistics and Information Endpoints
|
|
@router.get("/stats")
|
|
async def get_template_statistics(
|
|
agent: TemplateDrivenAgent = Depends(get_template_agent),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get template usage statistics."""
|
|
try:
|
|
usage_stats = agent.get_usage_stats()
|
|
available_templates = len(agent.get_available_templates())
|
|
available_sets = len(agent.get_available_template_sets())
|
|
|
|
return {
|
|
"available_templates": available_templates,
|
|
"available_template_sets": available_sets,
|
|
"usage_statistics": usage_stats,
|
|
"total_uses": sum(usage_stats.values())
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get template statistics: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to get statistics")
|
|
|
|
|
|
@router.get("/types", response_model=List[str])
|
|
async def get_template_types():
|
|
"""Get list of available template types."""
|
|
return [template_type.value for template_type in TemplateType]
|
|
|
|
|
|
@router.get("/complexity-levels", response_model=List[str])
|
|
async def get_complexity_levels():
|
|
"""Get list of available complexity levels."""
|
|
return [level.value for level in ComplexityLevel]
|
|
|
|
|
|
# Health check
|
|
@router.get("/health")
|
|
async def template_service_health():
|
|
"""Health check for template service."""
|
|
try:
|
|
agent = await get_template_agent()
|
|
template_count = len(agent.get_available_templates())
|
|
set_count = len(agent.get_available_template_sets())
|
|
|
|
return {
|
|
"status": "healthy",
|
|
"available_templates": template_count,
|
|
"available_template_sets": set_count,
|
|
"timestamp": "2024-01-01T00:00:00Z" # Would use actual timestamp
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Template service health check failed: {e}")
|
|
raise HTTPException(status_code=503, detail="Template service unhealthy") |