""" Report configuration data models for the universal data report generator. Defines ReportConfig, MetricDef, PageDef, ThemeConfig, and related enums. """ from dataclasses import dataclass, field from enum import Enum from datetime import date from typing import Optional class PeriodType(str, Enum): DAILY = 'daily' WEEKLY = 'weekly' MONTHLY = 'monthly' QUARTERLY = 'quarterly' CUSTOM = 'custom' class AudienceType(str, Enum): MANAGEMENT = 'management' OPERATION = 'operation' CLIENT = 'client' CUSTOM = 'custom' class ComparisonType(str, Enum): PREV_PERIOD = 'prev_period' YOY = 'yoy' NONE = 'none' class ColumnRole(str, Enum): TIME = 'time' NUMERIC = 'numeric' CATEGORY = 'category' TEXT = 'text' ID = 'id' BOOLEAN = 'boolean' UNKNOWN = 'unknown' class AggregationType(str, Enum): SUM = 'sum' COUNT = 'count' AVG = 'avg' MAX = 'max' MIN = 'min' DISTINCT_COUNT = 'distinct_count' class MetricType(str, Enum): KPI = 'kpi' TREND = 'trend' DISTRIBUTION = 'distribution' RANKING = 'ranking' FUNNEL = 'funnel' ALERT = 'alert' class ChartType(str, Enum): COLUMN = 'column' BAR = 'bar' LINE = 'line' DOUGHNUT = 'doughnut' PIE = 'pie' FUNNEL = 'funnel' TABLE = 'table' GROUPED_BAR = 'grouped_bar' class ThemePreset(str, Enum): BUSINESS_CLASSIC = 'business_classic' FRESH_SIMPLE = 'fresh_simple' DARK_PROFESSIONAL = 'dark_professional' WARM_BRAND = 'warm_brand' CUSTOM = 'custom' FROM_TEMPLATE = 'from_template' @dataclass class ColumnProfile: column_name: str dtype: str role: ColumnRole null_count: int null_rate: float unique_count: int sample_values: list = field(default_factory=list) numeric_stats: Optional[dict] = None inferred_label: str = '' @dataclass class MetricDef: name: str label: str column: str aggregation: AggregationType metric_type: MetricType = MetricType.KPI unit: str = '' format_spec: str = ',.0f' selected: bool = True is_primary: bool = False @dataclass class PageDef: page_id: str title: str page_type: str order: int selected: bool = True elements: list[dict] = field(default_factory=list) conclusion_title: str = '' @dataclass class ConfirmationSpec: """Six user confirmations required before building a report.""" period_and_page_range: bool = False core_metrics: bool = False audience_and_decision: bool = False visual_style_and_palette: bool = False page_structure_and_template: bool = False data_scope_and_field_mapping: bool = False def missing_items(self) -> list[str]: labels = { 'period_and_page_range': '报告周期与页数范围', 'core_metrics': '核心指标集', 'audience_and_decision': '受众与决策场景', 'visual_style_and_palette': '视觉风格与配色方向', 'page_structure_and_template': '页面结构与模板方案', 'data_scope_and_field_mapping': '数据范围与字段映射', } return [ label for field_name, label in labels.items() if not getattr(self, field_name) ] def is_complete(self) -> bool: return not self.missing_items() @dataclass class ThemeConfig: preset: ThemePreset = ThemePreset.BUSINESS_CLASSIC name: str = '商务经典' primary: str = '#1E3A5F' accent: str = '#10B981' accent_neg: str = '#EF4444' secondary: str = '#64748B' dark: str = '#1F3A5C' white: str = '#FFFFFF' gray_bg: str = '#F2F2F2' card_bg: str = '#E7F0F7' text: str = '#333333' text_gray: str = '#666666' line: str = '#D9D9D9' chart_series: list[str] = field(default_factory=lambda: [ '#1E3A5F', '#10B981', '#ED7D31', '#64748B', '#EF4444', '#707070', '#4472C4', '#10B981' ]) title_font: str = '微软雅黑' body_font: str = '微软雅黑' number_font: str = 'Arial' @dataclass class ReportConfig: title: str period_type: PeriodType date_range: tuple[date, date] period_str: str = '' metrics: list[MetricDef] = field(default_factory=list) pages: list[PageDef] = field(default_factory=list) audience: AudienceType = AudienceType.MANAGEMENT decision_scenario: str = '' custom_audience: str = '' theme: ThemeConfig = field(default_factory=ThemeConfig) template_path: str = '' template_profile: Optional[object] = None # TemplateProfile from template_parser use_template_theme: bool = True visual_style_direction: str = '' page_structure_template: str = '' filters: dict = field(default_factory=dict) comparison: ComparisonType = ComparisonType.PREV_PERIOD page_count_range: tuple[int, int] = (6, 15) source_label: str = '数据报告系统' data_scope: str = '' data_field_mapping: dict = field(default_factory=dict) data_profiling: Optional[dict] = None agent_recommendations: Optional[dict] = None user_confirmation: ConfirmationSpec = field(default_factory=ConfirmationSpec) require_six_confirmations: bool = True quality_threshold: int = 85 max_fix_iterations: int = 5 def to_dict(self) -> dict: return { 'title': self.title, 'period_type': self.period_type.value, 'period_str': self.period_str, 'page_count_range': list(self.page_count_range), 'audience': self.audience.value, 'theme_preset': self.theme.preset.value, 'metrics_count': len(self.metrics), 'pages_count': len(self.pages), 'six_confirmations_complete': self.user_confirmation.is_complete(), } def validate_six_confirmations(config: ReportConfig, data_columns: Optional[list[str]] = None) -> list[str]: """Return validation gaps for the six confirmation contract.""" issues = [] missing = config.user_confirmation.missing_items() if missing: issues.append('六项确认未完成:' + '、'.join(missing)) if not config.period_str and not config.date_range: issues.append('缺少报告周期。') if not config.page_count_range or len(config.page_count_range) != 2: issues.append('缺少页数范围。') if not [m for m in config.metrics if m.selected]: issues.append('缺少已确认的核心指标集。') if not config.decision_scenario: issues.append('缺少受众与决策场景说明。') if not config.visual_style_direction and not config.theme: issues.append('缺少视觉风格与配色方向。') if not config.pages: issues.append('缺少页面结构与模板方案。') if not config.data_field_mapping: issues.append('缺少数据范围与字段映射。') if data_columns: missing_cols = [] for metric in config.metrics: if metric.selected and metric.column and metric.column not in data_columns: missing_cols.append(f'{metric.label} -> {metric.column}') if missing_cols: issues.append('核心指标字段映射不存在:' + '、'.join(missing_cols[:8])) selected_pages = [p for p in config.pages if p.selected] if config.page_count_range and selected_pages: low, high = config.page_count_range if len(selected_pages) < low - 1 or len(selected_pages) > high + 1: issues.append(f'页面数量 {len(selected_pages)} 不在确认范围 {low}-{high} 页附近。') return issues