"""
{{ agent_name | to_pascal_case }} Agent - Pydantic AI Implementation

Auto-generated from SuperSpec playbook using SuperOptiX compiler.
Framework: Pydantic AI
Generated: {{ timestamp }}

SuperSpec Metadata:
  Name: {{ metadata.name }}
  Version: {{ metadata.version }}
  Description: {{ metadata.description }}
  Framework: Pydantic AI
"""

from typing import List, Dict, Any, Optional, Union
import asyncio
import os
import inspect

# Pydantic imports (required for BaseModel and Field)
try:
	from pydantic import BaseModel, Field
except ImportError as e:
	raise ImportError(
		"pydantic is required for Pydantic AI integration. "
		"Install with: pip install pydantic"
	) from e

# Pydantic AI imports
# Import ModelSettings separately for type hints (must be available at class definition time)
try:
	from pydantic_ai.settings import ModelSettings
except ImportError:
	# Use Any as fallback for type hints if pydantic_ai is not available
	from typing import Any
	ModelSettings = Any

try:
	from pydantic_ai import Agent
	from pydantic_ai.models import infer_model
	PYDANTIC_AI_AVAILABLE = True
except ImportError as e:
	PYDANTIC_AI_AVAILABLE = False
	print(f"⚠️  Pydantic AI not available: {e}")
	print("⚠️  Install with: pip install pydantic-ai")

# MCP (Model Context Protocol) imports for tool support
{% if spec.mcp and spec.mcp.enabled %}
try:
	from pydantic_ai.mcp import MCPServerStdio, MCPServerStreamableHTTP, MCPServerSSE
	MCP_AVAILABLE = True
except ImportError as e:
	MCP_AVAILABLE = False
	print(f"⚠️  MCP support not available: {e}")
	print("⚠️  Install with: pip install pydantic-ai (MCP support included)")
{% else %}
MCP_AVAILABLE = False
{% endif %}

from superoptix.core.base_component import BaseComponent
from superoptix.runners.pydantic_runtime_helpers import (
	build_stackone_tools as superoptix_build_stackone_tools,
	get_pydantic_rlm_config as superoptix_get_pydantic_rlm_config,
	run_agent_with_optional_rlm as superoptix_run_agent_with_optional_rlm,
)


# ======================================================================
# Structured Output Model (Generated from playbook output_fields)
# ======================================================================
{% if spec.output_fields %}
class {{ agent_name | to_pascal_case }}Output(BaseModel):
	"""
	Structured output model for {{ agent_name }} agent.
	
	Generated from playbook output_fields to ensure type-safe, validated responses.
	"""
{% for field in spec.output_fields %}
{%- set field_type_raw = field.type | default('string') -%}
{%- set field_type = field_type_raw | string | lower | trim -%}
{%- if field_type == 'string' or field_type == 'str' or field_type == 'text' -%}
{%- set python_type = 'str' -%}
{%- elif field_type == 'integer' or field_type == 'int' or field_type == 'number' -%}
{%- set python_type = 'int' -%}
{%- elif field_type == 'float' or field_type == 'double' -%}
{%- set python_type = 'float' -%}
{%- elif field_type == 'boolean' or field_type == 'bool' -%}
{%- set python_type = 'bool' -%}
{%- elif field_type == 'list' or field_type == 'array' -%}
{%- set python_type = 'List[str]' -%}
{%- elif field_type == 'dict' or field_type == 'object' or field_type == 'map' -%}
{%- set python_type = 'Dict[str, Any]' -%}
{%- else -%}
{%- set python_type = 'str' -%}
{%- endif %}
	{{ field.name | to_snake_case }}: {{ python_type }} = Field(
		description="{{ field.description | default(field.name) | replace('"', '\\"') }}"
	)
{% endfor %}
	# Field descriptions can be optimized with GEPA (when optimize_field_descriptions: true)
	# Optimized descriptions will be loaded from optimized/{{ agent_name }}_field_descriptions_optimized.json
{% else %}
# No output_fields defined - use plain string output
{{ agent_name | to_pascal_case }}Output = str
{% endif %}


class {{ agent_name | to_pascal_case }}Component(BaseComponent):
	"""
	BaseComponent wrapper for Pydantic AI - {{ metadata.description }}
	
	This component wraps a Pydantic AI Agent and makes it compatible with 
	SuperOptiX's Universal GEPA optimizer.
	
	Optimizable Variable: instructions (the agent's system instructions)
	
	Framework: Pydantic AI
	Input: Any field from input_fields
	Output: Any field from output_fields
	"""
	
	def __init__(
		self,
		instructions: Optional[str] = None,
		model_config: Optional[Dict] = None,
		playbook_path: Optional[str] = None,
		spec_data: Optional[Dict] = None,
		optimized_field_descriptions: Optional[Dict[str, str]] = None,
		**kwargs
	):
		"""
		Initialize Pydantic AI Agent component.
		
		Args:
			instructions: Agent instructions (system prompt) - optimizable by GEPA!
			model_config: Model configuration dict
			playbook_path: Path to playbook YAML (for MCP config access)
			spec_data: Playbook spec data (alternative to playbook_path)
			optimized_field_descriptions: Optional dict of optimized field descriptions
				from GEPA optimization (opt-in feature). Format: {field_name: optimized_description}
			**kwargs: Additional configuration
		"""
		# Default instructions from playbook
		default_instructions = self._build_default_instructions()
		
		# Load model config from playbook if not provided or if empty/incomplete
		# Check if model_config is None, empty dict, or missing required keys
		if not model_config or (isinstance(model_config, dict) and not model_config.get("model")):
			# Try to load from spec_data first (faster, no file I/O)
			if spec_data and "language_model" in spec_data:
				lm = spec_data["language_model"]
				model_config = {
					"model": lm.get("model"),
					"provider": lm.get("provider"),
					"api_base": lm.get("api_base"),
					"temperature": lm.get("temperature"),
					"max_tokens": lm.get("max_tokens"),
					"top_p": lm.get("top_p"),
				}
			# Fallback to loading from playbook file
			elif playbook_path:
				import yaml
				from pathlib import Path
				try:
					with open(playbook_path) as f:
						playbook = yaml.safe_load(f)
						spec = playbook.get("spec", {})
						if "language_model" in spec:
							lm = spec["language_model"]
							model_config = {
								"model": lm.get("model"),
								"provider": lm.get("provider"),
								"api_base": lm.get("api_base"),
								"temperature": lm.get("temperature"),
								"max_tokens": lm.get("max_tokens"),
								"top_p": lm.get("top_p"),
							}
				except Exception as e:
					print(f"⚠️  Failed to load model config from playbook: {e}")
			else:
				# Last resort: Try to auto-locate playbook file
				import yaml
				from pathlib import Path
				# Last resort: Try to auto-locate playbook file
				import yaml
				from pathlib import Path
				try:
					current_file = Path(__file__).resolve()
					# Try multiple naming conventions (handle both hyphen and underscore)
					agent_name_underscore = "{{ agent_name }}"
					agent_name_hyphen = agent_name_underscore.replace("_", "-")
					for playbook_name in [
						f"{agent_name_underscore}_playbook.yaml",
						f"{agent_name_hyphen}_playbook.yaml",
						f"{agent_name_underscore}-playbook.yaml",
						f"{agent_name_hyphen}-playbook.yaml"
					]:
						potential_playbook = current_file.parent.parent / "playbook" / playbook_name
						if potential_playbook.exists():
							with open(potential_playbook) as f:
								playbook = yaml.safe_load(f)
								spec = playbook.get("spec", {})
								if "language_model" in spec:
									lm = spec["language_model"]
									model_config = {
										"model": lm.get("model"),
										"provider": lm.get("provider"),
										"api_base": lm.get("api_base"),
										"temperature": lm.get("temperature"),
										"max_tokens": lm.get("max_tokens"),
										"top_p": lm.get("top_p"),
									}
									break
				except Exception as e:
					print(f"⚠️  Failed to auto-locate playbook: {e}")
		
		# Use empty dict if still no config (will use defaults)
		if not model_config:
			model_config = {}
		
		# Load playbook spec FIRST (needed for output_mode check and MCP configuration)
		# Priority: spec_data (passed directly) > playbook_path (load from file) > auto-locate
		# Always initialize self.spec to avoid AttributeError
		self.spec = {}
		
		if spec_data:
			self.spec = spec_data
			print(f"🔍 Component init: Loaded spec from spec_data, keys: {list(self.spec.keys())}")
		else:
			import yaml
			from pathlib import Path
			
			if playbook_path:
				playbook_file = Path(playbook_path)
				if playbook_file.exists():
					try:
						with open(playbook_file) as f:
							playbook = yaml.safe_load(f)
							self.spec = playbook.get("spec", {})
					except Exception as e:
						print(f"⚠️  Failed to load playbook from {playbook_path}: {e}")
						self.spec = {}
			else:
				# Try to auto-locate playbook
				try:
					current_file = Path(__file__).resolve()
					potential_playbook = current_file.parent.parent / "playbook" / "{{ agent_name }}_playbook.yaml"
					if potential_playbook.exists():
						try:
							with open(potential_playbook) as f:
								playbook = yaml.safe_load(f)
								self.spec = playbook.get("spec", {})
						except Exception as e:
							print(f"⚠️  Failed to load playbook from {potential_playbook}: {e}")
							self.spec = {}
				except (NameError, AttributeError, Exception) as e:
					# __file__ not available or other error (e.g., in some execution contexts)
					# self.spec remains {} as initialized above
					pass
		
		# Initialize BaseComponent
		super().__init__(
			name="{{ agent_name }}",
			description="""{{ metadata.description | replace('"', '\\"') }}""",
			input_fields={{ spec.input_fields | map(attribute='name') | list | tojson }},
			output_fields={{ spec.output_fields | map(attribute='name') | list | tojson }},
			variable=instructions or default_instructions,  # GEPA optimizes this!
			variable_type="instructions",
			framework="pydantic-ai",
			config=model_config,
		)
		
		# Store optimized field descriptions (for structured output when enabled)
		self.optimized_field_descriptions = optimized_field_descriptions
		if optimized_field_descriptions:
			print(f"✅ Using optimized field descriptions for {len(optimized_field_descriptions)} field(s)")
		
		# Store output field names for use in response extraction
		output_fields_spec = self.spec.get("output_fields", [])
		self.output_fields = [field.get("name") for field in output_fields_spec if field.get("name")]
		
		# Check if structured output is enabled
		self.output_mode = self.spec.get("output_mode", "plain")  # Default to plain text
		self._structured_output_model = None  # Will be set if structured output is enabled
		
		# Check LogFire configuration (defaults to enabled if LogFire is available)
		logfire_config = self.spec.get("logfire", {})
		self.logfire_enabled = logfire_config.get("enabled", True)  # Default to True (auto-detect)
		self.rlm_config = superoptix_get_pydantic_rlm_config(self.spec)
		if self.rlm_config.get("enabled", False):
			print(
				f"✅ RLM configured (mode={self.rlm_config.get('mode')}, "
				f"backend={self.rlm_config.get('backend')})"
			)
		
		if self.output_mode == "structured":
			if not output_fields_spec:
				print("⚠️  output_mode: structured requires output_fields to be defined. Falling back to plain text.")
				self.output_mode = "plain"
			else:
				print(f"✅ Using structured output mode (BaseModel)")
				# Create structured output model with optimized descriptions if available
				self._structured_output_model = self._create_structured_output_model(optimized_field_descriptions)
		
		# Lazy initialization
		self._agent = None
		self._mcp_servers = None  # Cache MCP servers
	
	def _build_default_instructions(self) -> str:
		"""Build default instructions from playbook."""
		instructions_parts = []
{% if spec.persona.role %}
		instructions_parts.append({{ (spec.persona.role | replace('\n', ' ')) | tojson }})
{% endif %}
{% if spec.persona.instructions %}
		instructions_parts.append("\\nInstructions: " + {{ (spec.persona.instructions | replace('\n', ' ')) | tojson }})
{% endif %}
{% if spec.persona.goal %}
		instructions_parts.append("\\nGoal: " + {{ (spec.persona.goal | replace('\n', ' ')) | tojson }})
{% endif %}
{% if spec.persona.backstory %}
		instructions_parts.append("\\nBackstory: " + {{ (spec.persona.backstory | replace('\n', ' ')) | tojson }})
{% endif %}
{% if spec.persona.traits %}
		instructions_parts.append("\\nTraits: " + {{ (spec.persona.traits | join(', ') | replace('\n', ' ')) | tojson }})
{% endif %}
{% if spec.reasoning and spec.reasoning.method %}
		instructions_parts.append("\\n\\nReasoning Method: " + {{ (spec.reasoning.method | replace('\n', ' ')) | tojson }})
{% endif %}
{% if spec.reasoning and spec.reasoning.steps %}
		instructions_parts.append("\\nSteps to follow:")
{% for step in spec.reasoning.steps %}
		instructions_parts.append("  {{ loop.index }}. " + {{ (step | replace('\n', ' ')) | tojson }})
{% endfor %}
{% endif %}
{% if spec.constraints %}
		instructions_parts.append("\\n\\nConstraints:")
{% for constraint in spec.constraints %}
		instructions_parts.append("  - " + {{ (constraint | replace('\n', ' ')) | tojson }})
{% endfor %}
{% endif %}
		
		if not instructions_parts:
			return "You are a helpful AI assistant."
		
		base_instructions = "\\n".join(instructions_parts)
		
		# Add explicit tool usage instructions if MCP is enabled
{% if spec.mcp and spec.mcp.enabled %}
		# Add explicit instructions to use tools when MCP is enabled
		# Check if spec is available (it might not be loaded yet)
		if hasattr(self, 'spec') and self.spec:
			mcp_config = self.spec.get("mcp", {})
			if mcp_config.get("enabled", False):
				tool_usage_note = (
					"\\n\\nIMPORTANT: You have access to tools through the Model Context Protocol (MCP). "
					"When the user's request involves file operations, directory listings, or other tool-based tasks, "
					"you MUST actually EXECUTE the appropriate tools - do not just describe what you would do. "
					"Call the tools directly and return their actual results. "
					"For example, if asked to list files, call the list_directory tool and show the actual file list. "
					"Do not output markdown code blocks describing commands - actually use the tools!"
				)
				base_instructions = base_instructions + tool_usage_note
{% endif %}
		
		# Add task-specific guidance for code generation tasks
		# Check if this is a code/implementation task based on output fields
{% if spec.output_fields %}
		{% set has_implementation = false %}
		{% for field in spec.output_fields %}
			{% set field_name_lower = (field.name | string | lower) %}
			{% if 'implementation' in field_name_lower or 'code' in field_name_lower %}
				{% set has_implementation = true %}
			{% endif %}
		{% endfor %}
		{% if has_implementation %}
		# This appears to be a code generation task
		# Add simple guidance about providing actual code
		task_guidance = (
			"\\n\\nWhen asked to implement code, write the actual executable code - "
			"not a description or metadata. Include imports, function definitions, "
			"and make it production-ready."
		)
		base_instructions = base_instructions + task_guidance
		{% endif %}
{% endif %}
		
		return base_instructions
	
	def _initialize_model(self):
		"""Initialize the model object from config.
		
		Pydantic AI uses infer_model() to automatically create the correct model instance
		based on the provider prefix (e.g., 'ollama:', 'openai:', 'anthropic:').
		
		This method handles cases where the playbook model string doesn't include the provider prefix.
		If provider is 'ollama' or api_base is set, it automatically adds 'ollama:' prefix.
		
		For Ollama models:
		- Model string format: 'ollama:model_name' (e.g., 'ollama:llama3.1:8b')
		- OLLAMA_BASE_URL environment variable should be set (e.g., 'http://localhost:11434/v1')
		- OLLAMA_API_KEY is optional (defaults to 'api-key-not-set' if not provided)
		"""
		# Convert config to dict if needed
		import types
		import os
		config = vars(self.config) if isinstance(self.config, types.SimpleNamespace) else self.config
		if not isinstance(config, dict):
			config = {}
		
		# Store config for later use (e.g., detecting small models for simpler instructions)
		self._model_config = config.copy()
		
		# Get model from config - use playbook model if specified
		# Note: Structured output works with 8b models when proper JSON instructions are provided
		model_str = config.get("model")
		if not model_str:
			# Default to llama3.1:8b for fast inference on local machines
			# Users can specify larger models in playbook for better quality
			model_str = "llama3.1:8b"
			print(f"⚠️  No model specified, using default: {model_str}")
		else:
			print(f"✅ Using model: {model_str}")
		provider = config.get("provider", "").lower()
		api_base = config.get("api_base")

		# Normalize provider aliases from CLI/runtime.
		if provider in {"google-genai", "google_generative_ai"}:
			provider = "google-gla"
		elif provider == "local":
			provider = "ollama"
		
		# Check if model string already has a provider prefix
		# Known provider prefixes: ollama:, openai:, anthropic:, google:, etc.
		known_providers = ["ollama", "openai", "anthropic", "google", "google-gla", "google-genai", "bedrock", "azure", "cohere", "mistral", "deepseek", "groq", "together", "fireworks", "litellm", "gateway"]
		has_provider_prefix = any(model_str.startswith(f"{p}:") for p in known_providers)
		
		# If no provider prefix, apply provider from config (runner/CLI source of truth).
		if not has_provider_prefix:
			if provider:
				model_str = f"{provider}:{model_str}"
			# If provider is empty, infer_model() may auto-detect by model name.
		
		# For Ollama models, set OLLAMA_BASE_URL if api_base is provided
		if model_str.startswith("ollama:") or provider == "ollama":
			if api_base:
				# Ensure OLLAMA_BASE_URL includes /v1 for OpenAI-compatible API
				ollama_base = api_base
				if not ollama_base.endswith("/v1"):
					if ollama_base.endswith("/"):
						ollama_base = ollama_base + "v1"
					else:
						ollama_base = ollama_base + "/v1"
				
				# Set environment variables for Ollama
				os.environ["OLLAMA_BASE_URL"] = ollama_base
			# OLLAMA_API_KEY is optional, but OllamaProvider expects it (uses placeholder if not set)
			if "OLLAMA_API_KEY" not in os.environ:
				os.environ["OLLAMA_API_KEY"] = "ollama"  # Placeholder key
		
		# Use Pydantic AI's infer_model() to automatically create the correct model instance
		# This handles all providers (ollama, openai, anthropic, etc.) automatically
		# Format: 'provider:model_name' (e.g., 'ollama:llama3.1:8b', 'ollama:gpt-oss:120b', 'openai:gpt-4o')
		# For cloud models, API keys should be set via environment variables:
		# - OPENAI_API_KEY for OpenAI models
		# - ANTHROPIC_API_KEY for Anthropic models
		# - etc. (see Pydantic AI docs for full list)
		try:
			return infer_model(model_str)
		except Exception as e:
			error_msg = str(e)
			# Provide helpful error message if model initialization fails
			if "Unknown provider" in error_msg or "not found" in error_msg.lower():
				print(f"❌ Model initialization failed: {error_msg}")
				print(f"💡 Check that Ollama is running and model '{config.get('model')}' is available")
			raise
	
	def _initialize_agent(self):
		"""Lazy initialization of Pydantic AI Agent."""
		# Always check if instructions changed (for GEPA optimization)
		current_instructions = self.variable
		if self._agent is not None:
			# Check if instructions have changed
			# Pydantic AI doesn't allow updating instructions directly, so we recreate
			# We can check by comparing with stored instructions
			if hasattr(self, '_current_instructions') and self._current_instructions == current_instructions:
				return  # No change, reuse agent
			# Instructions changed - recreate agent
		
		if not PYDANTIC_AI_AVAILABLE:
			raise ImportError("Pydantic AI not installed. Install with: pip install pydantic-ai")
		
		# Get model object using Pydantic AI's infer_model()
		# This automatically handles all providers (ollama, openai, anthropic, etc.)
		model = self._initialize_model()
		
		# Get model settings from config (excluding temperature as it's deprecated by OpenAI)
		model_settings = self._get_model_settings()
		
		# Create Pydantic AI Agent with current configuration
		# Using structured output type for type safety and validation (if output_fields defined)
		# Plain text output mode - no structured JSON output
		# This allows the model to return natural text responses (code, Dockerfiles, etc.)
		# instead of forcing JSON structure which can produce metadata-like responses
		enhanced_instructions = current_instructions
{% if spec.output_fields %}
		# Add guidance about what the response should contain
		output_field_names = {{ spec.output_fields | map(attribute='name') | list | tojson }}
		if output_field_names:
			field_guidance = []
{% for field in spec.output_fields %}
			{% if field.description %}
			field_guidance.append("{{ field.description }}")
			{% else %}
			field_guidance.append("Provide {{ field.name }}")
			{% endif %}
{% endfor %}
			guidance_str = '\\n'.join(['- ' + g for g in field_guidance])
			output_format_note = (
				"\\n\\nYour response should include:\\n"
				+ guidance_str + "\\n\\n"
				"Provide the actual content directly - write real code, configurations, or text as requested. "
				"Do NOT return JSON metadata or descriptions of what should be done."
			)
			enhanced_instructions = current_instructions + output_format_note
{% endif %}
		
		# Determine output type based on output_mode
		agent_kwargs = {
			"model": model,
			"instructions": enhanced_instructions,
			"name": "{{ metadata.name | replace('"', '\\"') }}",
		}
		
		# Add output_type for structured output mode (explicit opt-in)
		if self.output_mode == "structured" and self._structured_output_model:
			agent_kwargs["output_type"] = self._structured_output_model
			print(f"📋 Structured Output Mode: Enabled")
			print(f"   Output Model: {self._structured_output_model.__name__}")
			if self.optimized_field_descriptions:
				print(f"   ✅ Using optimized field descriptions")
			else:
				print(f"   📝 Using original field descriptions")
		else:
			print(f"📝 Plain Text Output Mode: Enabled (default)")
			print(f"   To enable structured output, set output_mode: structured in playbook")
		
		# Add model settings if available
		if model_settings:
			agent_kwargs["model_settings"] = model_settings
		try:
			retries = int(os.getenv("SUPEROPTIX_PYDANTIC_RETRIES", "3"))
		except Exception:
			retries = 3
		init_params = inspect.signature(Agent.__init__).parameters
		if "retries" in init_params:
			agent_kwargs["retries"] = retries
		if "output_retries" in init_params:
			agent_kwargs["output_retries"] = retries

		# Add StackOne SDK tools when configured in SuperSpec
		stackone_tools = superoptix_build_stackone_tools(self.spec, framework="pydantic_ai")
		if stackone_tools and len(stackone_tools) > 0:
			agent_kwargs["tools"] = stackone_tools
			print(f"✅ StackOne tools registered: {len(stackone_tools)} tool(s)")
		else:
			print("ℹ️  No StackOne tools configured")
		
		# Add MCP toolsets if available
		mcp_servers = self._initialize_mcp_servers()
		if mcp_servers and len(mcp_servers) > 0:
			agent_kwargs["toolsets"] = mcp_servers
			print(f"✅ MCP toolsets registered: {len(mcp_servers)} server(s)")
			for i, server in enumerate(mcp_servers):
				server_name = getattr(server, 'name', f"Server {i+1}")
				print(f"   - {server_name}")
		else:
			print("ℹ️  No MCP toolsets configured")
		
		self._agent = Agent(**agent_kwargs)
		
		# Optional LogFire instrumentation (user-controlled, graceful fallback)
		# Users can install and configure LogFire themselves if they want observability
		# Can be enabled/disabled via playbook spec.logfire.enabled
		if self.logfire_enabled:
			try:
				import logfire
				# LogFire must be configured before instrumentation
				# Configure LogFire if not already configured (no-op if already configured)
				# This allows traces to be captured if user has authenticated with 'logfire auth'
				try:
					logfire.configure()
				except Exception:
					# Configuration failed - that's fine, will skip instrumentation
					pass
				
				# Instrument this agent - requires LogFire to be configured
				# If not configured, instrumentation may not capture traces
				logfire.instrument_pydantic_ai(self._agent)
			except ImportError:
				# LogFire not installed - that's fine, silently skip
				pass
			except Exception:
				# LogFire installed but instrumentation failed - that's fine too, silently skip
				pass
		
		# Store current instructions for change detection
		self._current_instructions = current_instructions
	
	def _create_structured_output_model(self, optimized_field_descriptions: Optional[Dict[str, str]] = None):
		"""Create BaseModel for structured output, using optimized field descriptions if available.
		
		Args:
			optimized_field_descriptions: Optional dict of optimized field descriptions
				Format: {field_name: optimized_description}
		
		Returns:
			BaseModel class for structured output
		"""
{% if spec.output_fields %}
		# Get output_fields from spec
		output_fields = self.spec.get("output_fields", [])
		if not output_fields:
			return None
		
		# Type mapping
		type_mapping = {
			"string": str, "str": str, "text": str,
			"integer": int, "int": int, "number": int,
			"float": float, "double": float,
			"boolean": bool, "bool": bool,
			"list": List[str], "array": List[str],
			"dict": Dict[str, Any], "object": Dict[str, Any], "map": Dict[str, Any],
		}
		
		# Build field definitions
		field_definitions = {}
		for field in output_fields:
			field_name = field.get("name", "")
			if not field_name:
				continue
			
			field_name_snake = field_name.replace("-", "_").lower()
			field_type_str = (field.get("type", "string") or "string").lower()
			python_type = type_mapping.get(field_type_str, str)
			
			# Use optimized description if available, otherwise use original
			if optimized_field_descriptions and field_name in optimized_field_descriptions:
				description = optimized_field_descriptions[field_name]
			else:
				description = field.get("description", field_name)
			
			# Create Field with description
			field_definitions[field_name_snake] = (python_type, Field(description=description))
		
		# Create BaseModel dynamically
		if field_definitions:
			# Use pydantic's create_model to create BaseModel with optimized descriptions
			from pydantic import create_model
			OutputModel = create_model(
				"{{ agent_name | to_pascal_case }}Output",
				__base__=BaseModel,
				**field_definitions
			)
			return OutputModel
		
		return None
{% else %}
		# No output_fields defined - cannot create structured output model
		return None
{% endif %}
	
	def _get_model_settings(self) -> Optional[ModelSettings]:
		"""Get model settings from config (excluding temperature)."""
		import types
		config = vars(self.config) if isinstance(self.config, types.SimpleNamespace) else self.config
		if not isinstance(config, dict):
			config = {}
		
		# Extract settings (excluding temperature as it's deprecated by OpenAI)
		settings_kwargs = {}
		
		# max_tokens: Use config value or default to 4000 for detailed responses
		# Default is higher to allow for comprehensive responses (test plans, code, etc.)
		settings_kwargs["max_tokens"] = config.get("max_tokens", 4000)
		
		# top_p if specified
		if "top_p" in config:
			settings_kwargs["top_p"] = config["top_p"]
		
		# frequency_penalty if specified
		if "frequency_penalty" in config:
			settings_kwargs["frequency_penalty"] = config["frequency_penalty"]
		
		# presence_penalty if specified
		if "presence_penalty" in config:
			settings_kwargs["presence_penalty"] = config["presence_penalty"]
		
		# Only create ModelSettings if we have at least one setting
		if settings_kwargs:
			# Ensure ModelSettings is available (not just Any placeholder)
			if PYDANTIC_AI_AVAILABLE and ModelSettings is not None:
				return ModelSettings(**settings_kwargs)
			else:
				# If pydantic_ai not available, return None (model will use defaults)
				return None
		
		return None
	
	def _initialize_mcp_servers(self) -> Optional[List[Any]]:
		"""
		Initialize MCP servers from playbook configuration.
		
		Supports:
		- Local stdio servers (MCPServerStdio)
		- Remote HTTP servers (MCPServerStreamableHTTP)
		- Remote SSE servers (MCPServerSSE, deprecated)
		
		Returns:
			List of MCP server instances to register as toolsets, or None if not enabled
		"""
{% if spec.mcp and spec.mcp.enabled %}
		if not MCP_AVAILABLE:
			print("⚠️  MCP support not available. Install with: pip install pydantic-ai")
			return None
		
		# Ensure self.spec exists (should be initialized in __init__)
		print(f"🔍 MCP init check: hasattr(self, 'spec')={hasattr(self, 'spec')}, self.spec={self.spec if hasattr(self, 'spec') else 'N/A'}")
		if not hasattr(self, "spec") or not self.spec:
			# Try to load spec if not already loaded
			import yaml
			from pathlib import Path
			try:
				current_file = Path(__file__).resolve()
				# Try multiple possible playbook file names
				possible_names = [
					"{{ agent_name }}_playbook.yaml",
					"{{ agent_name | replace('-', '_') }}_playbook.yaml",
					"{{ agent_name | replace('_', '-') }}_playbook.yaml",
				]
				
				playbook_dir = current_file.parent.parent / "playbook"
				playbook_loaded = False
				
				for playbook_name in possible_names:
					potential_playbook = playbook_dir / playbook_name
					if potential_playbook.exists():
						print(f"🔍 MCP debug: Loading playbook from {potential_playbook}")
						with open(potential_playbook) as f:
							playbook = yaml.safe_load(f)
							self.spec = playbook.get("spec", {})
							playbook_loaded = True
							print(f"✅ MCP debug: Playbook loaded, spec keys: {list(self.spec.keys())}")
							break
				
				if not playbook_loaded:
					print(f"⚠️  MCP debug: Playbook not found in {playbook_dir}")
					print(f"   Tried: {possible_names}")
					self.spec = {}
			except Exception as e:
				print(f"⚠️  MCP debug: Failed to load playbook: {e}")
				import traceback
				traceback.print_exc()
				self.spec = {}
		
		# Debug: Check spec loading
		print(f"🔍 MCP debug: Checking spec, self.spec type={type(self.spec)}, keys={list(self.spec.keys()) if self.spec else 'EMPTY'}")
		
		if not self.spec:
			print("❌ MCP debug: self.spec is empty - cannot load MCP configuration")
			print("   This usually means the playbook was not loaded properly in Component.__init__")
			return None
		
		mcp_config = self.spec.get("mcp", {})
		print(f"🔍 MCP debug: mcp_config={mcp_config}")
		
		if not mcp_config:
			print("❌ MCP debug: No 'mcp' section found in playbook spec")
			print(f"   Available spec keys: {list(self.spec.keys())}")
			return None
		
		if not mcp_config.get("enabled", False):
			print("❌ MCP debug: mcp.enabled is False or missing in playbook")
			print(f"   mcp_config contents: {mcp_config}")
			return None
		
		print(f"✅ MCP debug: mcp.enabled=True, checking servers...")
		
		servers = []
		server_configs = mcp_config.get("servers", [])
		
		print(f"🔍 MCP debug: Found {len(server_configs)} server config(s)")
		
		if not server_configs:
			print("⚠️  MCP enabled but no servers configured in playbook")
			return None
		
		for server_config in server_configs:
			try:
				server_type = server_config.get("type", "stdio").lower()
				config = server_config.get("config", {})
				tool_prefix = server_config.get("tool_prefix")  # Optional prefix for tool names
				server_name = server_config.get("name", "unknown")
				
				if server_type == "stdio":
					# Local stdio server: run as subprocess
					command = config.get("command")
					args = config.get("args", [])
					env = config.get("env")  # Optional environment variables
					timeout = config.get("timeout", 30)  # Optional timeout
					
					if not command:
						print(f"⚠️  MCP server '{server_name}': 'command' required for stdio servers")
						continue
					
					server_kwargs = {
						"command": command,
						"timeout": timeout,
					}
					if args:
						server_kwargs["args"] = args
					if env:
						server_kwargs["env"] = env
					if tool_prefix:
						server_kwargs["tool_prefix"] = tool_prefix
					
					server = MCPServerStdio(**server_kwargs)
					servers.append(server)
					print(f"🛠️  Initialized MCP stdio server: {server_name}")
					
				elif server_type == "streamable_http":
					# Remote HTTP server (Streamable HTTP transport)
					url = config.get("url")
					headers = config.get("headers")  # Optional headers (e.g., auth)
					
					if not url:
						print(f"⚠️  MCP server '{server_name}': 'url' required for streamable_http servers")
						continue
					
					server_kwargs = {
						"url": url,
					}
					if tool_prefix:
						server_kwargs["tool_prefix"] = tool_prefix
					# Note: headers can be passed via http_client if needed (advanced)
					
					server = MCPServerStreamableHTTP(**server_kwargs)
					servers.append(server)
					print(f"🛠️  Initialized MCP streamable_http server: {server_name} ({url})")
					
				elif server_type == "sse":
					# Remote SSE server (deprecated, but still supported)
					url = config.get("url")
					
					if not url:
						print(f"⚠️  MCP server '{server_name}': 'url' required for sse servers")
						continue
					
					server_kwargs = {
						"url": url,
					}
					if tool_prefix:
						server_kwargs["tool_prefix"] = tool_prefix
					
					server = MCPServerSSE(**server_kwargs)
					servers.append(server)
					print(f"🛠️  Initialized MCP SSE server: {server_name} ({url}) [deprecated]")
					
				else:
					print(f"⚠️  MCP server '{server_name}': Unknown server type '{server_type}'. Supported: stdio, streamable_http, sse")
					continue
					
			except Exception as e:
				print(f"⚠️  Failed to initialize MCP server '{server_config.get('name', 'unknown')}': {e}")
				continue
		
		if servers:
			print(f"✅ Initialized {len(servers)} MCP server(s)")
			return servers
		
		return None
{% else %}
		# MCP not enabled in playbook
		return None
{% endif %}
	
	async def forward(self, **inputs: Any) -> Dict[str, Any]:
		"""
		Execute the Pydantic AI Agent.
		
		Args:
			**inputs: Input fields from playbook
		
		Returns:
			Dict with output fields
		"""
		# Ensure agent is initialized
		self._initialize_agent()
		
		# Get the input query/text
{% if spec.input_fields %}
{% set first_input = spec.input_fields[0] %}
		input_text = inputs.get("{{ first_input.name }}", "")
{% else %}
		input_text = inputs.get("query") or inputs.get("text") or inputs.get("input", "")
{% endif %}
		
		# Run agent asynchronously  
		# Note: Pydantic AI Agent.run() is async and returns AgentRunResult
		# The output is validated against the structured output type ({{ agent_name | to_pascal_case }}Output)
		try:
			input_preview = input_text[:80] + '...' if len(input_text) > 80 else input_text
			print(f"🚀 Running agent with input: {input_preview}")
			
			# Check if toolsets are available
			if hasattr(self._agent, 'toolsets') and self._agent.toolsets:
				print(f"🔧 Agent has {len(self._agent.toolsets)} toolset(s) configured")
			else:
				print("⚠️  No toolsets found on agent - tools may not be available")
			
			result = await superoptix_run_agent_with_optional_rlm(
				agent=self._agent,
				prompt=input_text,
				spec_data=self.spec,
				model_name=str(
					(getattr(self.config, "model", None) if hasattr(self.config, "model") else None)
					or (self.spec.get("language_model", {}) if isinstance(self.spec, dict) else {}).get("model")
					or "unknown"
				),
				logfire_enabled=bool(self.logfire_enabled),
			)
			print(f"✅ Model response received")
			
			# Debug: Check if tools were actually called
			if hasattr(result, 'messages'):
				tool_calls = [msg for msg in result.messages if hasattr(msg, 'tool_calls') and msg.tool_calls]
				if tool_calls:
					print(f"✅ Tool calls detected: {len(tool_calls)} tool invocation(s)")
					for i, call in enumerate(tool_calls, 1):
						print(f"   Tool call {i}: {call.tool_calls}")
				else:
					print("⚠️  No tool calls detected in response - agent may be hallucinating")
					print("   This can happen if:")
					print("   - Model is too small to handle tool calling (try llama3.1:70b)")
					print("   - Instructions don't encourage tool usage")
					print("   - Tool descriptions aren't clear enough")
			else:
				print("⚠️  Result has no 'messages' attribute - cannot check tool calls")
			
			# Debug: Check result structure
			print(f"📊 Result type: {type(result).__name__}")
			if hasattr(result, 'data'):
				print(f"   Has data attribute: {result.data is not None}")
			if hasattr(result, 'output'):
				print(f"   Has output attribute: {result.output is not None}")
			
			# Extract response based on output mode
			if isinstance(result, str):
				response_text = result
{% if spec.output_fields %}
{% set first_output = spec.output_fields[0] %}
				return {"{{ first_output.name }}": response_text}
{% else %}
				return {"response": response_text}
{% endif %}
			elif self.output_mode == "structured" and hasattr(result, 'data') and result.data:
				# Structured output mode - result.data is a BaseModel instance
				structured_data = result.data
				
				# Show verification that structured output was received
				print(f"✅ Structured Output Received!")
				print(f"   Type: {type(structured_data).__name__}")
				print(f"   Model: {getattr(structured_data, '__class__', type(structured_data)).__name__}")
				
				if hasattr(structured_data, 'model_dump'):
					# Pydantic v2
					response_dict = structured_data.model_dump()
					print(f"   📊 Pydantic v2 model validated successfully")
				elif hasattr(structured_data, 'dict'):
					# Pydantic v1
					response_dict = structured_data.dict()
					print(f"   📊 Pydantic v1 model validated successfully")
				else:
					# Fallback: convert to dict manually
					response_dict = {}
					if hasattr(self, 'output_fields') and self.output_fields:
						for field_name in self.output_fields:
							snake_name = field_name.replace("-", "_").lower()
							if hasattr(structured_data, snake_name):
								response_dict[field_name] = getattr(structured_data, snake_name)
				
				# Show the structured data as JSON for verification
				import json
				try:
					json_output = json.dumps(response_dict, indent=2, default=str)
					print(f"   📋 Structured Data (JSON):\n{json_output[:500]}..." if len(json_output) > 500 else f"   📋 Structured Data (JSON):\n{json_output}")
				except Exception:
					pass
				
				# Build output dict mapping field names
				outputs = {}
				if hasattr(self, 'output_fields') and self.output_fields:
					for output_field_name in self.output_fields:
						# Try exact name first, then snake_case
						snake_name = output_field_name.replace("-", "_").lower()
						value = response_dict.get(output_field_name) or response_dict.get(snake_name, "")
						outputs[output_field_name] = str(value) if value is not None else ""
				else:
					# Fallback: use all keys from response_dict
					outputs = {k: str(v) for k, v in response_dict.items()}
				
				# Debug output
				if outputs:
					preview = str(list(outputs.values())[0])[:100] + '...' if len(str(list(outputs.values())[0])) > 100 else str(list(outputs.values())[0])
					print(f"🔍 Extracted preview: {preview}")
				
				return outputs
			else:
				# Plain text output mode - extract the text response directly
				# result.output is a string (plain text from the model)
				response_text = ""
				
				# Try to get the response text
				if hasattr(result, 'output') and result.output:
					response_text = str(result.output)
				elif hasattr(result, 'data') and result.data:
					response_text = str(result.data)
				else:
					# Fallback: convert result to string
					response_text = str(result)
				
				# Debug output (truncated)
				preview = response_text[:100] + '...' if len(response_text) > 100 else response_text
				print(f"🔍 Response preview: {preview}")
				
{% if spec.output_fields %}
				# Return response in the expected output field format
{% set first_output = spec.output_fields[0] %}
				return {"{{ first_output.name }}": response_text}
{% else %}
				return {"response": response_text}
{% endif %}
		
		except Exception as e:
			import traceback
			error_str = str(e)
			print(f"⚠️  Error executing agent: {error_str}")
			traceback.print_exc()
			
			# Return error in expected output format
{% if spec.output_fields %}
{% set first_output = spec.output_fields[0] %}
			return {"{{ first_output.name }}": f"Error: {error_str}"}
{% else %}
			return {"response": f"Error: {error_str}"}
{% endif %}
	
	def update(self, new_variable: Any) -> None:
		"""
		Update instructions (called by GEPA optimizer during optimization).
		
		This method is called by Universal GEPA when testing new instruction variants.
		We recreate the agent with new instructions since Pydantic AI doesn't allow
		updating instructions on an existing agent instance.
		
		Args:
			new_variable: New instructions text
		"""
		super().update(new_variable)
		# Force re-initialization with new instructions
		# _initialize_agent() will detect the change and recreate
		self._agent = None
		# Clear cached instructions so agent gets recreated
		if hasattr(self, '_current_instructions'):
			delattr(self, '_current_instructions')
	
	def __repr__(self) -> str:
		return (
			f"{{ agent_name | to_pascal_case }}Component("
			f"framework=pydantic-ai, "
			f"optimizable={self.optimizable})"
		)


# ======================================================================
# Factory Function
# ======================================================================

def create_{{ agent_name }}_agent(
	instructions: Optional[str] = None,
	model_config: Optional[Dict] = None,
	playbook_path: Optional[str] = None,
	spec_data: Optional[Dict] = None,
	optimized_field_descriptions: Optional[Dict[str, str]] = None,
	**kwargs
) -> {{ agent_name | to_pascal_case }}Component:
	"""
	Factory function to create {{ agent_name }} agent.
	
	Args:
		instructions: Optional custom instructions (overrides default)
		model_config: Model configuration
		playbook_path: Path to playbook YAML (for MCP config access)
		spec_data: Playbook spec data (alternative to playbook_path)
		optimized_field_descriptions: Optional dict of optimized field descriptions
			from GEPA optimization (opt-in feature). Format: {field_name: optimized_description}
		**kwargs: Additional configuration
	
	Returns:
		Initialized {{ agent_name | to_pascal_case }}Component
	"""
	return {{ agent_name | to_pascal_case }}Component(
		instructions=instructions,
		model_config=model_config,
		playbook_path=playbook_path,
		spec_data=spec_data,
		optimized_field_descriptions=optimized_field_descriptions,
		**kwargs
	)


# ======================================================================
# {{ agent_name | to_pascal_case }}Pipeline - SuperOptiX Workflow Support
# ======================================================================

import yaml
from pathlib import Path
from typing import List, Dict, Any, Optional


class {{ agent_name | to_pascal_case }}Pipeline:
	"""
	Pydantic AI pipeline with full SuperOptiX workflow support.
	
	Supports:
	- compile: Generate this code from playbook
	- evaluate: Run BDD scenarios
	- optimize: GEPA optimization  
	- run: Execute agent
	
	This makes Pydantic AI work with standard SuperOptiX commands!
	"""
	
	def __init__(self, playbook_path: str = None, model_config: Optional[Dict[str, Any]] = None):
		"""Initialize pipeline from playbook."""
		# Auto-locate playbook if not provided
		if not playbook_path:
			# Try to find playbook relative to this file
			import os
			current_file = Path(__file__).resolve()
			# Go up: pipelines -> {agent_name} -> agents -> {project_name} -> {project_name}
			potential_playbook = current_file.parent.parent / "playbook" / "{{ agent_name }}_playbook.yaml"
			if potential_playbook.exists():
				playbook_path = str(potential_playbook)
		
		# Load playbook
		if playbook_path:
			with open(playbook_path) as f:
				playbook = yaml.safe_load(f)
				self.spec = playbook.get("spec", {})
				self.metadata = playbook.get("metadata", {})
		else:
			self.spec = {}
			self.metadata = {}

		# Get model config from spec - default to llama3.1:8b for local machines
		spec_model_config = {}
		if "language_model" in self.spec:
			lm = self.spec["language_model"]
			model_from_spec = lm.get("model")
			# Default to llama3.1:8b for fast inference on local machines
			if not model_from_spec:
				model_from_spec = "llama3.1:8b"
				print(f"⚠️  No model specified in playbook, using default: {model_from_spec}")
			
			spec_model_config = {
				"model": model_from_spec,
				"provider": lm.get("provider", "ollama"),
				"api_base": lm.get("api_base"),
				"temperature": lm.get("temperature"),
				"max_tokens": lm.get("max_tokens", 4000),  # Default to 4000 for detailed responses
				"top_p": lm.get("top_p"),
			}

		# Merge runtime overrides (CLI --provider/--model) on top of playbook config.
		effective_model_config: Dict[str, Any] = dict(spec_model_config)
		if isinstance(model_config, dict) and model_config:
			for key, value in model_config.items():
				if key == "gateway" and isinstance(value, dict):
					gw = effective_model_config.get("gateway", {})
					if not isinstance(gw, dict):
						gw = {}
					gw.update(value)
					effective_model_config["gateway"] = gw
				else:
					effective_model_config[key] = value

		# Check for optimized weights
		optimized_instructions = None
		optimized_field_descriptions = None
		self.is_trained = False

		if playbook_path:
			# Try to load optimized weights from GEPA
			import os
			import json

			# Build path to optimized file
			playbook_dir = Path(playbook_path).parent.parent
			optimized_dir = playbook_dir / "optimized"
			optimized_file = optimized_dir / "{{ agent_name }}_pydantic_ai_optimized.json"

			if optimized_file.exists():
				try:
					with open(optimized_file) as f:
						opt_data = json.load(f)
						optimized_instructions = opt_data.get("best_variable")
						best_score = opt_data.get("best_score", 0.0)

					if optimized_instructions:
						print(f"✅ Loaded optimized instructions (score: {best_score:.2%})")
						self.is_trained = True
				except Exception as e:
					print(f"⚠️  Failed to load optimization: {e}")
			
			# Try to load optimized field descriptions (opt-in feature)
			field_descriptions_file = optimized_dir / "{{ agent_name }}_field_descriptions_optimized.json"
			if field_descriptions_file.exists():
				try:
					with open(field_descriptions_file) as f:
						field_data = json.load(f)
						optimized_field_descriptions = field_data.get("optimized_field_descriptions")
						field_score = field_data.get("best_score", 0.0)
					
					if optimized_field_descriptions:
						print(f"✅ Loaded optimized field descriptions (score: {field_score:.2%})")
				except Exception as e:
					print(f"⚠️  Failed to load optimized field descriptions: {e}")

		# Create component (with optimized instructions and field descriptions if available)
		# Pass spec_data to component so it has access to MCP config without needing to reload
		self.component = create_{{ agent_name }}_agent(
			instructions=optimized_instructions,
			model_config=effective_model_config,
			playbook_path=playbook_path,  # Pass playbook path for MCP config access
			spec_data=self.spec,  # Also pass spec_data directly for immediate access
			optimized_field_descriptions=optimized_field_descriptions  # Pass optimized field descriptions
		)

		# Load BDD scenarios
		self.test_scenarios = self._load_bdd_scenarios()

		# Alias for CLI compatibility
		self.test_examples = self.test_scenarios
	
	def _check_keyword_variations(self, keyword: str, text: str) -> bool:
		"""Check for common variations of a keyword in text.
		
		Handles:
		- Plural forms (validate -> validates, validation)
		- Verb forms (validate -> validating, validated)
		- Common suffixes (-tion, -ing, -ed, -s)
		"""
		# Common variations mapping
		variations = {
			"validate": ["validates", "validating", "validated", "validation", "validator"],
			"function": ["functions", "functional"],
			"implement": ["implements", "implementing", "implemented", "implementation"],
			"test": ["tests", "testing", "tested"],
			"optimize": ["optimizes", "optimizing", "optimized", "optimization"],
			"endpoint": ["endpoints"],
			"error": ["errors"],
			"response": ["responses"],
		}
		
		# Check if keyword or its variations are in text
		if keyword in text:
			return True
		
		# Check known variations
		if keyword in variations:
			for variant in variations[keyword]:
				if variant in text:
					return True
		
		# Check common suffixes
		for suffix in ["s", "ing", "ed", "tion", "er", "or"]:
			if keyword + suffix in text or (keyword.endswith("e") and keyword[:-1] + suffix in text):
				return True
		
		return False
	
	def _load_bdd_scenarios(self) -> List[Dict]:
		"""Load BDD test scenarios from playbook.
		
		Supports both formats:
		- feature_specifications.scenarios (standard SuperSpec format)
		- bdd.scenarios (alternative format)
		"""
		if not hasattr(self, "spec"):
			return []
		
		scenarios = []
		
		# Try feature_specifications first (standard format)
		feature_specs = self.spec.get("feature_specifications", {})
		scenario_list = feature_specs.get("scenarios", [])
		
		if scenario_list:
			# Standard format: feature_specifications.scenarios
			for scenario_spec in scenario_list:
				scenario = {
					"scenario": scenario_spec.get("name", "Unnamed"),
					"description": scenario_spec.get("description", ""),
					"input": scenario_spec.get("input", {}),
					"expected_output": scenario_spec.get("expected_output", {}),
				}
				scenarios.append(scenario)
		else:
			# Fallback to bdd.scenarios format
			bdd_specs = self.spec.get("bdd", {}).get("scenarios", [])
			for scenario_spec in bdd_specs:
				scenario = {
					"scenario": scenario_spec.get("name", "Unnamed"),
					"description": scenario_spec.get("description", ""),
					"input": scenario_spec.get("given", {}),
					"expected_output": scenario_spec.get("then", {}),
				}
				scenarios.append(scenario)
		
		return scenarios
	
	async def run(self, query: str = None, **inputs: Any) -> Dict[str, Any]:
		"""
		Run the agent with a query.
		
		Args:
			query: Input query/text (from --goal flag, maps to first input field)
			**inputs: Additional input fields
		
		Returns:
			Agent response
		"""
		# Map query parameter to the first input field from playbook
{% if spec.input_fields %}
{% set first_input = spec.input_fields[0] %}
		if query:
			inputs["{{ first_input.name }}"] = query
		elif "{{ first_input.name }}" not in inputs:
			inputs["{{ first_input.name }}"] = query or ""
{% else %}
		# Fallback to "query" if no input_fields defined
		if query:
			inputs["query"] = query
		elif "query" not in inputs:
			inputs["query"] = query or ""
{% endif %}
		
		# Execute component
		return await self.component.forward(**inputs)
	
	def evaluate(self, threshold: float = 0.7, ignore_checks: bool = False) -> Dict[str, Any]:
		"""
		Evaluate agent against BDD scenarios.
		
		Args:
			threshold: Confidence threshold for passing
			ignore_checks: Skip validation checks
		
		Returns:
			Evaluation results
		"""
		if not self.test_scenarios:
			return {
				"success": False,
				"summary": {"total": 0, "passed": 0, "failed": 0, "pass_rate": "0%"},
				"bdd_results": {"detailed_results": []},
				"model_analysis": {},
				"recommendations": [],
			}
		
		# Run evaluation
		results = []
		for scenario in self.test_scenarios:
			# Run agent
			try:
				output = asyncio.run(self.run(**scenario["input"]))
				
				# Get output text - use first output field from playbook
{% if spec.output_fields %}
{% set first_output = spec.output_fields[0] %}
				output_text = str(output.get("{{ first_output.name }}", "")).lower()
{% else %}
				output_text = str(output.get("response", "")).lower()
{% endif %}
				expected = scenario["expected_output"]
				
				# Extract expected keywords - handle multiple formats:
				# 1. expected_keywords as list
				# 2. implementation (or any output field) as space-separated string
				expected_keywords = []
				if "expected_keywords" in expected:
					expected_keywords = expected["expected_keywords"] if isinstance(expected["expected_keywords"], list) else []
{% if spec.output_fields %}
{% for output_field in spec.output_fields %}
				elif "{{ output_field.name }}" in expected:
					# Split space-separated keywords from string
					keyword_str = str(expected["{{ output_field.name }}"]).strip()
					expected_keywords = [kw.strip() for kw in keyword_str.split() if kw.strip()]
{% endfor %}
{% else %}
				elif "implementation" in expected:
					# Split space-separated keywords from string
					keyword_str = str(expected["implementation"]).strip()
					expected_keywords = [kw.strip() for kw in keyword_str.split() if kw.strip()]
{% endif %}
				
				# Evaluate based on keywords with improved matching
				if expected_keywords:
					# Improved keyword matching:
					# 1. Exact matches
					# 2. Partial matches (keyword appears as part of a word)
					# 3. Handle common variations (e.g., "validate" vs "validation")
					matches = 0
					output_words = set(output_text.split())  # Split into words for better matching
					
					for kw in expected_keywords:
						kw_lower = kw.lower().strip()
						if not kw_lower:
							continue
						
						# Exact match in text
						if kw_lower in output_text:
							matches += 1
						# Partial match (keyword is part of a word)
						elif any(kw_lower in word for word in output_words if len(word) >= len(kw_lower)):
							matches += 0.8  # Partial credit for partial matches
						# Handle common variations
						elif self._check_keyword_variations(kw_lower, output_text):
							matches += 0.9  # High credit for variations
					
					# Calculate score (normalize by number of keywords)
					score = matches / len(expected_keywords) if expected_keywords else 0.5
					# Use a slightly more lenient threshold for better results
					effective_threshold = max(0.6, threshold - 0.1)  # Lower threshold by 0.1, min 0.6
					passed = score >= effective_threshold
				else:
					# No keywords defined - pass if ignore_checks, otherwise fail
					passed = True if ignore_checks else False
					score = 1.0 if passed else 0.0
				
				results.append({
					"scenario": scenario["scenario"],
					"description": scenario["description"],
					"passed": passed,
					"output": output,
					"expected": expected,
					"score": score,
				})
			except Exception as e:
				results.append({
					"scenario": scenario["scenario"],
					"description": scenario["description"],
					"passed": False,
					"output": {"error": str(e)},
					"expected": scenario["expected_output"],
					"score": 0.0,
				})
		
		# Calculate metrics
		total = len(results)
		passed = sum(1 for r in results if r["passed"])
		failed = total - passed
		pass_rate = (passed / total * 100) if total > 0 else 0.0
		
		# Convert to CLI format
		detailed_results = []
		for result in results:
			detailed_results.append({
				"scenario_name": result.get("scenario", "Unnamed"),
				"description": result.get("description", ""),
				"passed": result.get("passed", False),
				"confidence_score": result.get("score", 0.0),
				"actual_output": result.get("output", {}),
			})
		
		return {
			"success": True,
			"summary": {
				"total": total,
				"passed": passed,
				"failed": failed,
				"pass_rate": f"{pass_rate:.1f}%",
			},
			"bdd_results": {
				"detailed_results": detailed_results,
				"total_scenarios": total,
				"scenarios_passed": passed,
				"scenarios_failed": failed,
				"pass_rate": f"{pass_rate:.1f}%",
				"bdd_score": pass_rate / 100,
			},
			"model_analysis": {
				"framework": "Pydantic AI",
				"model": "{{ spec.language_model.model | default('not specified') }}",
			},
			"recommendations": [],
		}

	# Compatibility shim for CLI BDD runner
	def run_bdd_test_suite(self, auto_tune: bool = False, ignore_checks: bool = False):
		"""Run BDD/feature specs (CLI expects this method)."""
		return self.evaluate(ignore_checks=ignore_checks)

	def _optimize_with_pydantic_native(
		self,
		trainset: List[Dict[str, Any]],
		valset: List[Dict[str, Any]],
		auto: str,
		metric: str,
		opt_params: Dict[str, Any],
		task_model: Optional[str],
		reflection_model: Optional[str],
	) -> Dict[str, Any]:
		"""
		Attempt native Pydantic-style GEPA optimization when available.

		Current behavior:
		- Detects whether local environment exposes a stable native optimization API.
		- If unavailable, returns an error payload so caller can fall back to UniversalGEPA.
		"""
		try:
			import pydantic_evals  # noqa: F401
		except Exception as e:
			return {"error": f"pydantic_evals unavailable: {e}"}

		# pydantic_evals presence alone is not enough; API differs by release.
		# Keep this path explicit but safe until a stable native GEPA API is guaranteed.
		return {
			"error": (
				"Native Pydantic GEPA API not detected in current environment. "
				"Falling back to UniversalGEPA."
			)
		}
	
	def optimize_with_gepa(
		self,
		auto: str = "medium",
		metric: str = "response_accuracy",
	) -> Dict[str, Any]:
		"""
		Optimize agent with GEPA.
		
		Args:
			auto: Optimization budget ("light", "medium", "heavy")
			metric: Metric to optimize
		
		Returns:
			Optimization results
		"""
		from superoptix.optimizers.universal_gepa import UniversalGEPA
		
		opt_params = {}
		if "optimization" in self.spec and "optimizer" in self.spec["optimization"]:
			opt_params = self.spec["optimization"]["optimizer"].get("params", {}) or {}

		backend = str(opt_params.get("backend", "universal")).strip().lower()

		# Support both old and new naming:
		# - task_model (preferred) == task/refinement model
		# - reflection_model (preferred) == reflection/critic model
		task_model = (
			opt_params.get("task_model")
			or opt_params.get("task_lm")
			or self.spec.get("language_model", {}).get("model")
		)
		reflection_model = (
			opt_params.get("reflection_model")
			or opt_params.get("reflection_lm")
			or task_model
		)

		print(f"\n🔄 Optimizing {{ metadata.name }} with GEPA...\n")
		print(f"Framework: Pydantic AI")
		print(f"GEPA backend: {backend}")
		print(f"Optimization level: {auto}")
		print(f"Task model: {task_model}")
		print(f"Reflection model: {reflection_model}")
		print(f"Optimizing: instructions\n")
		
		# Define metric function
		def eval_metric(inputs, outputs, gold, component_name=None):
			"""Metric function for GEPA - evaluates output against expected keywords."""
			# Get output text - use first output field from playbook
{% if spec.output_fields %}
{% set first_output = spec.output_fields[0] %}
			output_text = str(outputs.get("{{ first_output.name }}", "")).lower()
{% else %}
			output_text = str(outputs.get("response", "")).lower()
{% endif %}
			
			# Extract expected keywords - handle multiple formats:
			# 1. expected_keywords as list
			# 2. implementation (or any output field) as space-separated string
			expected_keywords = []
			if "expected_keywords" in gold:
				expected_keywords = gold["expected_keywords"] if isinstance(gold["expected_keywords"], list) else []
{% if spec.output_fields %}
{% for output_field in spec.output_fields %}
			elif "{{ output_field.name }}" in gold:
				# Split space-separated keywords from string
				keyword_str = str(gold["{{ output_field.name }}"]).strip()
				expected_keywords = [kw.strip() for kw in keyword_str.split() if kw.strip()]
{% endfor %}
{% else %}
			elif "implementation" in gold:
				# Split space-separated keywords from string
				keyword_str = str(gold["implementation"]).strip()
				expected_keywords = [kw.strip() for kw in keyword_str.split() if kw.strip()]
{% endif %}
			
			# Calculate score based on keyword matches with improved matching
			if expected_keywords:
				# Improved keyword matching (same logic as evaluate method)
				matches = 0
				output_words = set(output_text.split())
				
				for kw in expected_keywords:
					kw_lower = kw.lower().strip()
					if not kw_lower:
						continue
					
					# Exact match
					if kw_lower in output_text:
						matches += 1
					# Partial match
					elif any(kw_lower in word for word in output_words if len(word) >= len(kw_lower)):
						matches += 0.8
					# Variations (using same helper if available, otherwise simple check)
					elif self._check_keyword_variations(kw_lower, output_text):
						matches += 0.9
				
				score = matches / len(expected_keywords) if expected_keywords else 0.5
			else:
				score = 0.5  # Default if no keywords defined
			
			return score
		
		# Prepare training data from BDD scenarios
		# UniversalGEPA expects format: [{"inputs": {...}, "outputs": {...}}, ...]
		trainset = []
		for scenario in self.test_scenarios[:len(self.test_scenarios)//2]:  # First half for training
			trainset.append({
				"inputs": scenario["input"],
				"outputs": scenario["expected_output"],
			})
		
		# Validation set
		valset = []
		for scenario in self.test_scenarios[len(self.test_scenarios)//2:]:  # Second half for validation
			valset.append({
				"inputs": scenario["input"],
				"outputs": scenario["expected_output"],
			})
		
		if not trainset:
			print("⚠️  No training data available. Need BDD scenarios in playbook.")
			return {"error": "No training data"}

		# For optimization runs, allow task model override from optimization params.
		# This mirrors task/reflection model split terminology used by GEPA docs.
		try:
			if task_model:
				if isinstance(self.component.config, dict):
					self.component.config["model"] = task_model
				else:
					setattr(self.component.config, "model", task_model)
				# Force model re-init with updated model config during optimization.
				self.component._agent = None
		except Exception:
			pass

		# Try native backend first if requested.
		if backend in {"pydantic_native", "native", "pydantic"}:
			native_result = self._optimize_with_pydantic_native(
				trainset=trainset,
				valset=valset if valset else trainset,
				auto=auto,
				metric=metric,
				opt_params=opt_params,
				task_model=task_model,
				reflection_model=reflection_model,
			)
			if native_result and not native_result.get("error"):
				return native_result
			if native_result and native_result.get("error"):
				print(f"⚠️  {native_result.get('error')}")
				print("↪️  Continuing with UniversalGEPA backend...")
		
		# Run GEPA optimization
		try:
			optimizer = UniversalGEPA(
				metric=eval_metric,
				auto=auto,
				reflection_lm=reflection_model,
				skip_perfect_score=self.spec.get("optimization", {}).get("optimizer", {}).get("params", {}).get("skip_perfect_score", True),
			)
			
			result = optimizer.compile(
				component=self.component,
				trainset=trainset,
				valset=valset if valset else trainset,
			)
			
			print(f"\n✅ Optimization complete!")
			print(f"Best score: {result.best_score:.2f}")
			print(f"Optimized instructions:\n{result.best_variable[:200]}...\n")
			
			# Save optimized instructions
			import json
			import os
			from pathlib import Path
			
			# Get playbook directory (same structure as pipeline initialization)
			current_file = Path(__file__).resolve()
			# Go up: pipelines -> {agent_name} -> agents -> {project_name} -> {project_name}
			playbook_dir = current_file.parent.parent
			optimized_dir = playbook_dir / "optimized"
			optimized_dir.mkdir(exist_ok=True)
			
			optimized_file = optimized_dir / "{{ agent_name }}_pydantic_ai_optimized.json"
			with open(optimized_file, "w") as f:
				json.dump({
					"best_variable": result.best_variable,
					"best_score": result.best_score,
					"all_scores": result.all_scores,
					"num_iterations": result.num_iterations,
					"framework": result.framework,
				}, f, indent=2)
			
			print(f"💾 Saved optimized instructions to: {optimized_file}")
			
			# Update component with optimized prompt
			self.component.update(result.best_variable)
			self.is_trained = True
			
			return {
				"best_score": result.best_score,
				"optimized_prompt": result.best_variable,
				"improvement": result.best_score - result.all_scores[0] if result.all_scores else 0.0,
				"optimized_file": str(optimized_file),
			}
		
		except Exception as e:
			print(f"❌ Optimization failed: {e}")
			import traceback
			traceback.print_exc()
			return {"error": str(e)}


# Example usage
if __name__ == "__main__":
	# Option 1: Use component directly (for Universal GEPA)
	agent_component = create_{{ agent_name }}_agent()
	print(f"Agent: {agent_component.name}")
	print(f"Framework: {agent_component.framework}")
	result = asyncio.run(agent_component.forward(query="Hello!"))
	print(f"Response: {result}\n")
	
	# Option 2: Use pipeline (for full SuperOptiX workflow)
	# pipeline = {{ agent_name | to_pascal_case }}Pipeline(playbook_path="playbook/{{ agent_name }}_playbook.yaml")
	# asyncio.run(pipeline.run(query="Hello!"))  # Execute
	# pipeline.evaluate()  # Run BDD tests
	# pipeline.optimize_with_gepa(auto="medium")  # GEPA optimization
