"""Planner input models — CatalogSummary and Constraints. `CatalogSummary` is a condensed, PII-safe view of the user's `Catalog`, built for the planner prompt. It carries every table + column id/type/PII flag + row counts + low-cardinality top_values, with `sample_values` nulled on PII columns (INV: no PII sample values into the prompt, see doc §13). It also lists the available unstructured sources so the planner can plan `retrieve_documents`. The planner *validator* still checks inline `query_structured` IRs against the full `Catalog` via the existing IRValidator — the summary is a prompt input, not the validation source of truth. See AGENT_ARCHITECTURE_CONTEXT_new.md §7.3. """ from __future__ import annotations from typing import Any from pydantic import BaseModel, Field from ...catalog.models import Catalog, DataType class ColumnSummary(BaseModel): column_id: str name: str data_type: DataType pii_flag: bool = False sample_values: list[Any] | None = None # nulled when pii_flag is True top_values: list[Any] | None = None class TableSummary(BaseModel): table_id: str name: str row_count: int | None = None columns: list[ColumnSummary] = Field(default_factory=list) class StructuredSourceSummary(BaseModel): source_id: str name: str source_type: str # "schema" | "tabular" tables: list[TableSummary] = Field(default_factory=list) class UnstructuredSourceSummary(BaseModel): source_id: str name: str class CatalogSummary(BaseModel): structured_sources: list[StructuredSourceSummary] = Field(default_factory=list) unstructured_sources: list[UnstructuredSourceSummary] = Field(default_factory=list) @classmethod def from_catalog(cls, catalog: Catalog) -> CatalogSummary: structured: list[StructuredSourceSummary] = [] unstructured: list[UnstructuredSourceSummary] = [] for source in catalog.sources: if source.source_type == "unstructured": unstructured.append( UnstructuredSourceSummary(source_id=source.source_id, name=source.name) ) continue tables = [ TableSummary( table_id=table.table_id, name=table.name, row_count=table.row_count, columns=[ ColumnSummary( column_id=col.column_id, name=col.name, data_type=col.data_type, pii_flag=col.pii_flag, # PII columns leak nothing into the prompt: both # sample_values and (low-cardinality) top_values are # suppressed — top_values are the same class of data. sample_values=None if col.pii_flag else col.sample_values, top_values=( None if col.pii_flag or col.stats is None else col.stats.top_values ), ) for col in table.columns ], ) for table in source.tables ] structured.append( StructuredSourceSummary( source_id=source.source_id, name=source.name, source_type=source.source_type, tables=tables, ) ) return cls(structured_sources=structured, unstructured_sources=unstructured) def render(self) -> str: """Render the summary as compact text for the planner prompt.""" if not self.structured_sources and not self.unstructured_sources: return "(catalog is empty — the user has not registered any data yet)" lines: list[str] = [] for source in self.structured_sources: lines.append(f"Source: {source.name} ({source.source_type}) — id={source.source_id}") for table in source.tables: rc = f" ({table.row_count:,} rows)" if table.row_count is not None else "" lines.append(f" Table: {table.name}{rc} — id={table.table_id}") for col in table.columns: samples = "PII (suppressed)" if col.pii_flag else (col.sample_values or []) top = f", top={col.top_values}" if col.top_values else "" lines.append( f" - {col.name} [{col.data_type}]: " f"samples={samples}{top} — id={col.column_id}" ) lines.append("") if self.unstructured_sources: lines.append("Unstructured sources (for retrieve_documents):") for src in self.unstructured_sources: lines.append(f" - {src.name} — id={src.source_id}") return "\n".join(lines).rstrip() class Constraints(BaseModel): max_tasks: int = 5 modeling_allowed: bool = False # no modeling tools in v1 token_budget: int | None = None time_budget_seconds: int | None = None row_budget: int = 10_000