Skip to content

Commit 8134808

Browse files
authored
Add haiku explanation type with flexible prompt composition (#6)
## Summary Adds haiku explanation type and flexible prompt composition system that allows explanation types to override audience-specific guidance. ## Changes Made ### Core Implementation - Added `ExplanationType.HAIKU` enum value - Implemented explanation-type audience overrides in prompt system - Created haiku-specific prompt configuration that bypasses assembly guidance ### Testing & Evaluation - Added `prompt_testing/test_cases/haiku_tests.yaml` with test scenarios - Enhanced `claude_reviewer.py` with haiku-specific evaluation criteria - Updated `prompt_advisor.py` to handle nested audience configurations ### Code Quality & Reuse - Added structure-aware class methods to `Prompt` class: - `get_audience_metadata_from_dict()` - Works with raw prompt dictionaries - `has_audience_override()` - Checks for explanation-specific overrides - `get_all_audience_locations()` - Finds all audience guidance locations - Prevents duplication between runtime and testing framework ### Debug Support - Added configurable logging via `LOG_LEVEL` environment variable - Debug logging shows actual prompts sent to Claude ### Documentation - Updated `claude_explain.md` with flexible prompt system documentation ## Test Plan - All existing tests pass - Haiku test cases added covering simple to complex algorithms - Prompt testing framework handles new structure - Debug logging works for both assembly and haiku modes
2 parents c5f0a8e + a644feb commit 8134808

File tree

11 files changed

+622
-128
lines changed

11 files changed

+622
-128
lines changed

app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
1616
cache_s3_prefix: str = "explain-cache/"
1717
cache_ttl: str = "2d" # HTTP Cache-Control max-age (e.g., "2d", "48h", "172800s")
1818
cache_ttl_seconds: int = 172800 # Computed from cache_ttl for Cache-Control header
19+
log_level: str = "INFO" # Logging level (DEBUG, INFO, WARNING, ERROR)
1920
model_config = SettingsConfigDict(env_file=".env")
2021

2122
@field_validator("cache_ttl_seconds", mode="before")

app/explain.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,17 @@ async def _call_anthropic_api(
8080
# Generate messages using the Prompt instance
8181
prompt_data = prompt.generate_messages(body)
8282

83+
# Debug logging for prompts
84+
LOGGER.debug(f"=== PROMPT DEBUG FOR {body.explanation.value.upper()} (audience: {body.audience.value}) ===")
85+
LOGGER.debug("=== SYSTEM PROMPT ===")
86+
LOGGER.debug(prompt_data["system"])
87+
LOGGER.debug("=== MESSAGES ===")
88+
for message in prompt_data["messages"]:
89+
LOGGER.debug(message)
90+
LOGGER.debug("=== END PROMPT DEBUG ===")
91+
8392
# Call Claude API
84-
LOGGER.info(f"Using Anthropic client with model: {prompt_data['model']}")
93+
LOGGER.info("Using Anthropic client with model: %s", {prompt_data["model"]})
8594

8695
message = client.messages.create(
8796
model=prompt_data["model"],

app/explanation_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ class ExplanationType(str, Enum):
1919
"""Type of explanation to generate."""
2020

2121
ASSEMBLY = "assembly"
22+
HAIKU = "haiku"

app/main.py

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import logging
2+
from contextlib import asynccontextmanager
23
from pathlib import Path
34

45
from anthropic import Anthropic
56
from anthropic import __version__ as anthropic_version
6-
from fastapi import FastAPI
7+
from fastapi import FastAPI, Request
78
from fastapi.middleware.cors import CORSMiddleware
89
from mangum import Mangum
910

@@ -20,11 +21,47 @@
2021
from app.metrics import get_metrics_provider
2122
from app.prompt import Prompt
2223

23-
logging.basicConfig(level=logging.INFO)
24-
logger = logging.getLogger()
25-
logger.setLevel(logging.INFO)
2624

27-
app = FastAPI(root_path=get_settings().root_path)
25+
def configure_logging(log_level: str) -> None:
26+
"""Configure logging with the specified level."""
27+
level = getattr(logging, log_level.upper(), logging.INFO)
28+
logging.basicConfig(
29+
level=level,
30+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
31+
force=True, # Reconfigure if already configured
32+
)
33+
34+
35+
@asynccontextmanager
36+
async def lifespan(app: FastAPI):
37+
"""Configure app on startup, cleanup on shutdown."""
38+
# Startup
39+
settings = get_settings()
40+
configure_logging(settings.log_level)
41+
logger = logging.getLogger(__name__)
42+
43+
# Store shared resources in app.state
44+
app.state.settings = settings
45+
app.state.anthropic_client = Anthropic(api_key=settings.anthropic_api_key)
46+
47+
# Load the prompt configuration
48+
prompt_config_path = Path(__file__).parent / "prompt.yaml"
49+
app.state.prompt = Prompt(prompt_config_path)
50+
51+
logger.info(f"Application started with log level: {settings.log_level}")
52+
logger.info(f"Anthropic SDK version: {anthropic_version}")
53+
logger.info(f"Loaded prompt configuration from {prompt_config_path}")
54+
55+
yield
56+
57+
# Shutdown
58+
logger.info("Application shutting down")
59+
60+
61+
# Get settings once for app-level configuration
62+
# This is acceptable since these settings don't change during runtime
63+
_app_settings = get_settings()
64+
app = FastAPI(root_path=_app_settings.root_path, lifespan=lifespan)
2865

2966
# Configure CORS - allows all origins for public API
3067
app.add_middleware(
@@ -37,18 +74,10 @@
3774
)
3875
handler = Mangum(app)
3976

40-
anthropic_client = Anthropic(api_key=get_settings().anthropic_api_key)
41-
logger.info(f"Anthropic SDK version: {anthropic_version}")
42-
43-
# Load the prompt configuration
44-
prompt_config_path = Path(__file__).parent / "prompt.yaml"
45-
prompt = Prompt(prompt_config_path)
46-
logger.info(f"Loaded prompt configuration from {prompt_config_path}")
4777

48-
49-
def get_cache_provider():
78+
def get_cache_provider(settings) -> NoOpCacheProvider | S3CacheProvider:
5079
"""Get the configured cache provider."""
51-
settings = get_settings()
80+
logger = logging.getLogger(__name__)
5281

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

71100

72101
@app.get("/", response_model=AvailableOptions)
73-
async def get_options() -> AvailableOptions:
102+
async def get_options(request: Request) -> AvailableOptions:
74103
"""Get available options for the explain API."""
104+
prompt = request.app.state.prompt
75105
async with get_metrics_provider() as metrics_provider:
76106
metrics_provider.put_metric("ClaudeExplainOptionsRequest", 1)
77107
return AvailableOptions(
@@ -93,8 +123,14 @@ async def get_options() -> AvailableOptions:
93123

94124

95125
@app.post("/")
96-
async def explain(request: ExplainRequest) -> ExplainResponse:
126+
async def explain(explain_request: ExplainRequest, request: Request) -> ExplainResponse:
97127
"""Explain a Compiler Explorer compilation from its source and output assembly."""
98128
async with get_metrics_provider() as metrics_provider:
99-
cache_provider = get_cache_provider()
100-
return await process_request(request, anthropic_client, prompt, metrics_provider, cache_provider)
129+
cache_provider = get_cache_provider(request.app.state.settings)
130+
return await process_request(
131+
explain_request,
132+
request.app.state.anthropic_client,
133+
request.app.state.prompt,
134+
metrics_provider,
135+
cache_provider,
136+
)

app/prompt.py

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,52 @@ def __init__(self, config: dict[str, Any] | Path):
4646
self.audience_levels = self.config["audience_levels"]
4747
self.explanation_types = self.config["explanation_types"]
4848

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

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

57+
@classmethod
58+
def get_audience_metadata_from_dict(
59+
cls, prompt_dict: dict[str, Any], audience: str, for_explanation: str | None = None
60+
) -> dict[str, str]:
61+
"""Get audience metadata from a prompt dict structure (for use by prompt_advisor).
62+
63+
This is a class method version of get_audience_metadata that works with
64+
raw prompt dictionaries instead of Prompt instances.
65+
"""
66+
if "audience_levels" not in prompt_dict:
67+
return {}
68+
69+
audience_metadata = prompt_dict["audience_levels"].get(audience, {})
70+
71+
if for_explanation and "explanation_types" in prompt_dict:
72+
explanation_config = prompt_dict["explanation_types"].get(for_explanation, {})
73+
if "audience_levels" in explanation_config:
74+
explanation_audience = explanation_config["audience_levels"].get(audience, {})
75+
if explanation_audience:
76+
# Merge base audience metadata with explanation-specific overrides
77+
audience_metadata = {**audience_metadata, **explanation_audience}
78+
79+
return audience_metadata
80+
81+
@classmethod
82+
def has_audience_override(cls, prompt_dict: dict[str, Any], explanation: str, audience: str) -> bool:
83+
"""Check if an explanation type has audience-specific overrides."""
84+
return (
85+
"explanation_types" in prompt_dict
86+
and explanation in prompt_dict["explanation_types"]
87+
and "audience_levels" in prompt_dict["explanation_types"][explanation]
88+
and audience in prompt_dict["explanation_types"][explanation]["audience_levels"]
89+
)
90+
91+
# Note: In the future, prompt_advisor may need the ability to create new
92+
# explanation-specific audience overrides (like we did manually for haiku).
93+
# This would involve adding new audience_levels sections within explanation_types.
94+
5795
def select_important_assembly(
5896
self, asm_array: list[dict], label_definitions: dict, max_lines: int = MAX_ASSEMBLY_LINES
5997
) -> list[dict]:
@@ -192,34 +230,23 @@ def generate_messages(self, request: ExplainRequest) -> dict[str, Any]:
192230
- structured_data: The prepared data (for reference/debugging)
193231
"""
194232
# Get metadata
195-
audience_meta = self.get_audience_metadata(request.audience.value)
196233
explanation_meta = self.get_explanation_metadata(request.explanation.value)
197-
198-
# Prepare structured data
234+
audience_meta = self.get_audience_metadata(request.audience.value, request.explanation.value)
199235
structured_data = self.prepare_structured_data(request)
200236

201-
# Format the system prompt
202-
arch = request.instruction_set_with_default
203-
system_prompt = self.system_prompt_template.format(
204-
arch=arch,
205-
language=request.language,
206-
audience=request.audience.value,
207-
audience_guidance=audience_meta["guidance"],
208-
explanation_type=request.explanation.value,
209-
explanation_focus=explanation_meta["focus"],
210-
)
211-
212-
# Format the user prompt
213-
user_prompt = self.user_prompt_template.format(
214-
arch=arch,
215-
user_prompt_phrase=explanation_meta["user_prompt_phrase"],
216-
)
217-
218-
# Format the assistant prefill
219-
assistant_prefill = self.assistant_prefill.format(
220-
user_prompt_phrase=explanation_meta["user_prompt_phrase"],
221-
audience=request.audience.value,
222-
)
237+
prompt_dictionary = {
238+
"arch": request.instruction_set_with_default,
239+
"audience_guidance": audience_meta["guidance"],
240+
"audience": request.audience.value,
241+
"explanation_focus": explanation_meta["focus"],
242+
"explanation_type": request.explanation.value,
243+
"language": request.language,
244+
"user_prompt_phrase": explanation_meta["user_prompt_phrase"],
245+
}
246+
# Format the prompts and prefill
247+
system_prompt = self.system_prompt_template.format(**prompt_dictionary)
248+
user_prompt = self.user_prompt_template.format(**prompt_dictionary)
249+
assistant_prefill = self.assistant_prefill.format(**prompt_dictionary)
223250

224251
# Build messages array
225252
messages = [

0 commit comments

Comments
 (0)