Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Settings(BaseSettings):
cache_s3_prefix: str = "explain-cache/"
cache_ttl: str = "2d" # HTTP Cache-Control max-age (e.g., "2d", "48h", "172800s")
cache_ttl_seconds: int = 172800 # Computed from cache_ttl for Cache-Control header
log_level: str = "INFO" # Logging level (DEBUG, INFO, WARNING, ERROR)
model_config = SettingsConfigDict(env_file=".env")

@field_validator("cache_ttl_seconds", mode="before")
Expand Down
11 changes: 10 additions & 1 deletion app/explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,17 @@ async def _call_anthropic_api(
# Generate messages using the Prompt instance
prompt_data = prompt.generate_messages(body)

# Debug logging for prompts
LOGGER.debug(f"=== PROMPT DEBUG FOR {body.explanation.value.upper()} (audience: {body.audience.value}) ===")
LOGGER.debug("=== SYSTEM PROMPT ===")
LOGGER.debug(prompt_data["system"])
LOGGER.debug("=== MESSAGES ===")
for message in prompt_data["messages"]:
LOGGER.debug(message)
LOGGER.debug("=== END PROMPT DEBUG ===")

# Call Claude API
LOGGER.info(f"Using Anthropic client with model: {prompt_data['model']}")
LOGGER.info("Using Anthropic client with model: %s", {prompt_data["model"]})

message = client.messages.create(
model=prompt_data["model"],
Expand Down
1 change: 1 addition & 0 deletions app/explanation_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ class ExplanationType(str, Enum):
"""Type of explanation to generate."""

ASSEMBLY = "assembly"
HAIKU = "haiku"
74 changes: 55 additions & 19 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
from contextlib import asynccontextmanager
from pathlib import Path

from anthropic import Anthropic
from anthropic import __version__ as anthropic_version
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from mangum import Mangum

Expand All @@ -20,11 +21,47 @@
from app.metrics import get_metrics_provider
from app.prompt import Prompt

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
logger.setLevel(logging.INFO)

app = FastAPI(root_path=get_settings().root_path)
def configure_logging(log_level: str) -> None:
"""Configure logging with the specified level."""
level = getattr(logging, log_level.upper(), logging.INFO)
logging.basicConfig(
level=level,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
force=True, # Reconfigure if already configured
)


@asynccontextmanager
async def lifespan(app: FastAPI):
"""Configure app on startup, cleanup on shutdown."""
# Startup
settings = get_settings()
configure_logging(settings.log_level)
logger = logging.getLogger(__name__)

# Store shared resources in app.state
app.state.settings = settings
app.state.anthropic_client = Anthropic(api_key=settings.anthropic_api_key)

# Load the prompt configuration
prompt_config_path = Path(__file__).parent / "prompt.yaml"
app.state.prompt = Prompt(prompt_config_path)

logger.info(f"Application started with log level: {settings.log_level}")
logger.info(f"Anthropic SDK version: {anthropic_version}")
logger.info(f"Loaded prompt configuration from {prompt_config_path}")

yield

# Shutdown
logger.info("Application shutting down")


# Get settings once for app-level configuration
# This is acceptable since these settings don't change during runtime
_app_settings = get_settings()
app = FastAPI(root_path=_app_settings.root_path, lifespan=lifespan)

# Configure CORS - allows all origins for public API
app.add_middleware(
Expand All @@ -37,18 +74,10 @@
)
handler = Mangum(app)

anthropic_client = Anthropic(api_key=get_settings().anthropic_api_key)
logger.info(f"Anthropic SDK version: {anthropic_version}")

# Load the prompt configuration
prompt_config_path = Path(__file__).parent / "prompt.yaml"
prompt = Prompt(prompt_config_path)
logger.info(f"Loaded prompt configuration from {prompt_config_path}")


def get_cache_provider():
def get_cache_provider(settings) -> NoOpCacheProvider | S3CacheProvider:
"""Get the configured cache provider."""
settings = get_settings()
logger = logging.getLogger(__name__)

if not settings.cache_enabled:
logger.info("Caching disabled by configuration")
Expand All @@ -70,8 +99,9 @@ def get_cache_provider():


@app.get("/", response_model=AvailableOptions)
async def get_options() -> AvailableOptions:
async def get_options(request: Request) -> AvailableOptions:
"""Get available options for the explain API."""
prompt = request.app.state.prompt
async with get_metrics_provider() as metrics_provider:
metrics_provider.put_metric("ClaudeExplainOptionsRequest", 1)
return AvailableOptions(
Expand All @@ -93,8 +123,14 @@ async def get_options() -> AvailableOptions:


@app.post("/")
async def explain(request: ExplainRequest) -> ExplainResponse:
async def explain(explain_request: ExplainRequest, request: Request) -> ExplainResponse:
"""Explain a Compiler Explorer compilation from its source and output assembly."""
async with get_metrics_provider() as metrics_provider:
cache_provider = get_cache_provider()
return await process_request(request, anthropic_client, prompt, metrics_provider, cache_provider)
cache_provider = get_cache_provider(request.app.state.settings)
return await process_request(
explain_request,
request.app.state.anthropic_client,
request.app.state.prompt,
metrics_provider,
cache_provider,
)
83 changes: 55 additions & 28 deletions app/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,52 @@ def __init__(self, config: dict[str, Any] | Path):
self.audience_levels = self.config["audience_levels"]
self.explanation_types = self.config["explanation_types"]

def get_audience_metadata(self, audience: str) -> dict[str, str]:
"""Get metadata for an audience level."""
return self.audience_levels[audience]
def get_audience_metadata(self, audience: str, for_explanation: str | None = None) -> dict[str, str]:
"""Get metadata for an audience level (and optionally an explanation type)."""
return self.get_audience_metadata_from_dict(self.config, audience, for_explanation)

def get_explanation_metadata(self, explanation: str) -> dict[str, str]:
"""Get metadata for an explanation type."""
return self.explanation_types[explanation]

@classmethod
def get_audience_metadata_from_dict(
cls, prompt_dict: dict[str, Any], audience: str, for_explanation: str | None = None
) -> dict[str, str]:
"""Get audience metadata from a prompt dict structure (for use by prompt_advisor).

This is a class method version of get_audience_metadata that works with
raw prompt dictionaries instead of Prompt instances.
"""
if "audience_levels" not in prompt_dict:
return {}

audience_metadata = prompt_dict["audience_levels"].get(audience, {})

if for_explanation and "explanation_types" in prompt_dict:
explanation_config = prompt_dict["explanation_types"].get(for_explanation, {})
if "audience_levels" in explanation_config:
explanation_audience = explanation_config["audience_levels"].get(audience, {})
if explanation_audience:
# Merge base audience metadata with explanation-specific overrides
audience_metadata = {**audience_metadata, **explanation_audience}

return audience_metadata

@classmethod
def has_audience_override(cls, prompt_dict: dict[str, Any], explanation: str, audience: str) -> bool:
"""Check if an explanation type has audience-specific overrides."""
return (
"explanation_types" in prompt_dict
and explanation in prompt_dict["explanation_types"]
and "audience_levels" in prompt_dict["explanation_types"][explanation]
and audience in prompt_dict["explanation_types"][explanation]["audience_levels"]
)

# Note: In the future, prompt_advisor may need the ability to create new
# explanation-specific audience overrides (like we did manually for haiku).
# This would involve adding new audience_levels sections within explanation_types.

def select_important_assembly(
self, asm_array: list[dict], label_definitions: dict, max_lines: int = MAX_ASSEMBLY_LINES
) -> list[dict]:
Expand Down Expand Up @@ -192,34 +230,23 @@ def generate_messages(self, request: ExplainRequest) -> dict[str, Any]:
- structured_data: The prepared data (for reference/debugging)
"""
# Get metadata
audience_meta = self.get_audience_metadata(request.audience.value)
explanation_meta = self.get_explanation_metadata(request.explanation.value)

# Prepare structured data
audience_meta = self.get_audience_metadata(request.audience.value, request.explanation.value)
structured_data = self.prepare_structured_data(request)

# Format the system prompt
arch = request.instruction_set_with_default
system_prompt = self.system_prompt_template.format(
arch=arch,
language=request.language,
audience=request.audience.value,
audience_guidance=audience_meta["guidance"],
explanation_type=request.explanation.value,
explanation_focus=explanation_meta["focus"],
)

# Format the user prompt
user_prompt = self.user_prompt_template.format(
arch=arch,
user_prompt_phrase=explanation_meta["user_prompt_phrase"],
)

# Format the assistant prefill
assistant_prefill = self.assistant_prefill.format(
user_prompt_phrase=explanation_meta["user_prompt_phrase"],
audience=request.audience.value,
)
prompt_dictionary = {
"arch": request.instruction_set_with_default,
"audience_guidance": audience_meta["guidance"],
"audience": request.audience.value,
"explanation_focus": explanation_meta["focus"],
"explanation_type": request.explanation.value,
"language": request.language,
"user_prompt_phrase": explanation_meta["user_prompt_phrase"],
}
# Format the prompts and prefill
system_prompt = self.system_prompt_template.format(**prompt_dictionary)
user_prompt = self.user_prompt_template.format(**prompt_dictionary)
assistant_prefill = self.assistant_prefill.format(**prompt_dictionary)

# Build messages array
messages = [
Expand Down
Loading