"""
Model Wiki Generator Step Configuration with Self-Contained Derivation Logic
This module implements the configuration class for the model wiki generator step
using a self-contained design where derived fields are private with read-only properties.
Fields are organized into three tiers:
1. Tier 1: Essential User Inputs - fields that users must explicitly provide
2. Tier 2: System Inputs with Defaults - fields with reasonable defaults that can be overridden
3. Tier 3: Derived Fields - fields calculated from other fields (private with properties)
"""
from pydantic import Field, model_validator, field_validator, PrivateAttr
from typing import Optional, Dict, List, Any, TYPE_CHECKING
import logging
from .config_processing_step_base import ProcessingStepConfigBase
# Import for type hints only
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
[docs]
class ModelWikiGeneratorConfig(ProcessingStepConfigBase):
"""
Configuration for model wiki generator step with self-contained derivation logic.
This class defines the configuration parameters for the model wiki generator step,
which loads metrics data and visualizations, generates comprehensive wiki documentation,
and creates multi-format model documentation. Supports automated documentation creation
for model registries and compliance requirements.
Fields are organized into three tiers:
1. Tier 1: Essential User Inputs - fields that users must explicitly provide
2. Tier 2: System Inputs with Defaults - fields with reasonable defaults that can be overridden
3. Tier 3: Derived Fields - fields calculated from other fields (private with properties)
"""
# ===== Essential User Inputs (Tier 1) =====
# These are fields that users must explicitly provide
model_name: str = Field(
...,
description="Name of the model for documentation (required for wiki generation).",
)
# ===== System Inputs with Defaults (Tier 2) =====
# These are fields with reasonable defaults that users can override
# Note: processing_entry_point is inherited from ProcessingStepConfigBase with default=None
# We override it here to provide a specific default for wiki generation
processing_entry_point: str = Field(
default="model_wiki_generator.py",
description="Entry point script for model wiki generation.",
)
# Model metadata with defaults
model_use_case: str = Field(
default="Machine Learning Model",
description="Description of model use case for documentation.",
)
team_alias: str = Field(
default="ml-team@",
description="Team email alias for documentation.",
)
contact_email: str = Field(
default="ml-team@company.com",
description="Point of contact email for documentation.",
)
cti_classification: str = Field(
default="Internal",
description="CTI classification for the model documentation.",
)
# Documentation generation options
output_formats: str = Field(
default="wiki,html,markdown",
description="Comma-separated list of output formats (wiki,html,markdown).",
)
include_technical_details: bool = Field(
default=True,
description="Include technical details section in documentation.",
)
# Optional custom content
model_description: Optional[str] = Field(
default=None,
description="Custom model description text (auto-generated if not provided).",
)
model_purpose: str = Field(
default="perform classification tasks",
description="Custom model purpose description for documentation.",
)
# For wiki generation, we typically use smaller instances as it's mostly text processing
use_large_processing_instance: bool = Field(
default=False,
description="Whether to use large instance type for processing (wiki generation typically needs minimal resources)",
)
model_config = ProcessingStepConfigBase.model_config
# ===== Derived Fields (Tier 3) =====
# These are fields calculated from other fields, stored in private attributes
# with public read-only properties for access
_model_display_name: Optional[str] = PrivateAttr(default=None)
_output_formats_list: Optional[List[str]] = PrivateAttr(default=None)
_effective_model_description: Optional[str] = PrivateAttr(default=None)
# Public properties for derived fields
# Note: pipeline_name is inherited from BasePipelineConfig
@property
def model_display_name(self) -> str:
"""Get display name for the model in documentation."""
if self._model_display_name is None:
self._model_display_name = self.model_name.replace("_", " ").title()
return self._model_display_name
@property
def output_formats_list(self) -> List[str]:
"""Get list of output formats from comma-separated string."""
if self._output_formats_list is None:
formats = [fmt.strip().lower() for fmt in self.output_formats.split(",")]
# Validate formats
valid_formats = {"wiki", "html", "markdown"}
self._output_formats_list = [fmt for fmt in formats if fmt in valid_formats]
if not self._output_formats_list:
self._output_formats_list = ["wiki"] # Default fallback
return self._output_formats_list
@property
def effective_model_description(self) -> str:
"""Get effective model description (custom or auto-generated)."""
if self._effective_model_description is None:
if self.model_description:
self._effective_model_description = self.model_description
else:
self._effective_model_description = f"This is a machine learning model for {self.model_use_case.lower()}."
return self._effective_model_description
# Field validators
[docs]
@field_validator("cti_classification")
@classmethod
def validate_cti_classification(cls, v: str) -> str:
"""Validate CTI classification values."""
valid_classifications = {
"public",
"internal",
"confidential",
"restricted",
"Public",
"Internal",
"Confidential",
"Restricted",
}
if v not in valid_classifications:
logger.warning(
f"CTI classification '{v}' is not in standard classifications: {valid_classifications}"
)
return v
[docs]
@field_validator("model_name")
@classmethod
def validate_model_name(cls, v: str) -> str:
"""Validate model name is not empty and contains valid characters."""
if not v or not v.strip():
raise ValueError("model_name cannot be empty")
# Check for potentially problematic characters for file naming
import re
if re.search(r'[<>:"/\\|?*]', v):
logger.warning(
f"Model name '{v}' contains characters that may cause issues in file names"
)
return v.strip()
# Initialize derived fields at creation time to avoid potential validation loops
[docs]
@model_validator(mode="after")
def initialize_derived_fields(self) -> "ModelWikiGeneratorConfig":
"""Initialize all derived fields once after validation."""
# Call parent validator first
super().initialize_derived_fields()
# Initialize wiki generator specific derived fields
# Access properties to trigger initialization
_ = self.pipeline_name
_ = self.model_display_name
_ = self.output_formats_list
_ = self.effective_model_description
return self
[docs]
@model_validator(mode="after")
def validate_wiki_generator_config(self) -> "ModelWikiGeneratorConfig":
"""Additional validation specific to wiki generator configuration"""
# Basic validation
if not self.processing_entry_point:
raise ValueError("wiki generator step requires a processing_entry_point")
# Validate required fields from script contract
if not self.model_name:
raise ValueError(
"model_name must be provided (required by model wiki generator contract)"
)
# Validate output formats
if not self.output_formats_list:
raise ValueError("At least one valid output format must be specified")
# Validate email format if provided
if self.contact_email and "@" not in self.contact_email:
logger.warning(
f"contact_email '{self.contact_email}' may not be a valid email address"
)
logger.debug(
f"Model '{self.model_name}' will generate documentation in formats: {self.output_formats_list}"
)
return self
[docs]
def get_environment_variables(self) -> Dict[str, str]:
"""
Get environment variables for the model wiki generator script.
Returns:
Dict[str, str]: Dictionary mapping environment variable names to values
"""
# Get base environment variables from parent class if available
env_vars = (
super().get_environment_variables()
if hasattr(super(), "get_environment_variables")
else {}
)
# Add model wiki generator specific environment variables
env_vars.update(
{
"MODEL_NAME": self.model_name,
"MODEL_USE_CASE": self.model_use_case,
"MODEL_VERSION": self.pipeline_version, # Use pipeline_version from base config
"PIPELINE_NAME": self.pipeline_name,
"AUTHOR": self.author, # From base config
"TEAM_ALIAS": self.team_alias,
"CONTACT_EMAIL": self.contact_email,
"CTI_CLASSIFICATION": self.cti_classification,
"REGION": self.region, # From base config
"OUTPUT_FORMATS": self.output_formats,
"INCLUDE_TECHNICAL_DETAILS": str(
self.include_technical_details
).lower(),
"MODEL_PURPOSE": self.model_purpose,
}
)
# Add optional fields if specified
if self.model_description:
env_vars["MODEL_DESCRIPTION"] = self.model_description
return env_vars
# Custom model_dump method to include derived properties
[docs]
def model_dump(self, **kwargs) -> Dict[str, Any]:
"""Override model_dump to include derived properties."""
data = super().model_dump(**kwargs)
# Add derived properties to output
data["pipeline_name"] = self.pipeline_name
data["model_display_name"] = self.model_display_name
data["output_formats_list"] = self.output_formats_list
data["effective_model_description"] = self.effective_model_description
return data
[docs]
def get_public_init_fields(self) -> Dict[str, Any]:
"""
Override get_public_init_fields to include wiki generator specific fields.
Gets a dictionary of public fields suitable for initializing a child config.
Includes both base fields (from parent) and wiki generator specific fields.
Returns:
Dict[str, Any]: Dictionary of field names to values for child initialization
"""
# Get fields from parent class (ProcessingStepConfigBase)
base_fields = super().get_public_init_fields()
# Add model wiki generator specific fields (only fields not in base classes)
wiki_fields = {
# Tier 1 - Essential User Inputs
"model_name": self.model_name,
# Tier 2 - System Inputs with Defaults
"processing_entry_point": self.processing_entry_point,
"model_use_case": self.model_use_case,
"team_alias": self.team_alias,
"contact_email": self.contact_email,
"cti_classification": self.cti_classification,
"output_formats": self.output_formats,
"include_technical_details": self.include_technical_details,
"model_purpose": self.model_purpose,
"use_large_processing_instance": self.use_large_processing_instance,
}
# Only include optional fields if they're set
if self.model_description is not None:
wiki_fields["model_description"] = self.model_description
# Combine base fields and wiki fields (wiki fields take precedence if overlap)
init_fields = {**base_fields, **wiki_fields}
return init_fields