""" Claude-based researcher implementation. Uses output_config.format for native structured outputs. Ref: https://platform.claude.com/docs/en/build-with-claude/structured-outputs """ import json from anthropic import Anthropic from backend.pydantic_schema import ImageAdEssentialsOutput from backend.prompt import get_system_prompt, get_user_prompt from dotenv import load_dotenv import os load_dotenv() ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") def _add_additional_properties_false(schema: dict) -> dict: """ Recursively add 'additionalProperties': false to all object types. Required by Claude's structured outputs. """ if isinstance(schema, dict): if schema.get("type") == "object": schema["additionalProperties"] = False for value in schema.values(): if isinstance(value, dict): _add_additional_properties_false(value) elif isinstance(value, list): for item in value: if isinstance(item, dict): _add_additional_properties_false(item) return schema def researcher_claude(target_audience: str, product_category: str, product_description: str): """ Claude-based researcher function using native structured outputs. Args: target_audience: Target audience from the predefined list product_category: Product category (e.g., "ring", "bangles") product_description: Description of the product Returns: list[ImageAdEssentials]: List of psychology triggers, angles, and concepts """ # Initialize Claude client claude_client = Anthropic(api_key=ANTHROPIC_API_KEY) # Get prompts system_prompt = get_system_prompt() user_prompt = get_user_prompt(target_audience, product_category, product_description) # Build JSON schema from Pydantic model and add required additionalProperties: false json_schema = ImageAdEssentialsOutput.model_json_schema() json_schema = _add_additional_properties_false(json_schema) # Use Claude's native structured outputs via output_config.format message = claude_client.messages.create( model="claude-opus-4-6", max_tokens=1024, system=system_prompt, messages=[ { "role": "user", "content": user_prompt } ], output_config={ "format": { "type": "json_schema", "schema": json_schema } } ) # Check for safety refusal if message.stop_reason == "refusal": raise ValueError("Claude refused the request.") # Check if response was truncated if message.stop_reason == "max_tokens": raise ValueError("Claude response was truncated — increase max_tokens.") # Parse the JSON response and validate with Pydantic response_data = json.loads(message.content[0].text) output = ImageAdEssentialsOutput(**response_data) return output.output