Spaces:
Sleeping
Sleeping
File size: 6,230 Bytes
daf58ef 16d2e95 daf58ef 16d2e95 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | """Pydantic models for multi-file column classification and join logic.
The AI analyzes metadata from multiple SAS files and produces:
1. How files relate to each other (join keys)
2. How each column should be classified (sample_id, subject_id, metadata, data, exclude)
"""
from pydantic import BaseModel, Field, field_validator
class FileJoin(BaseModel):
"""How two files should be joined."""
left_file: str # filename
right_file: str # filename
join_columns: list[str] # columns to join on (must exist in both files)
join_type: str = "left" # "left", "inner", "outer"
reasoning: str
class ColumnClassification(BaseModel):
"""How a single source column should be used in the output."""
source_file: str # which file this column comes from
source_column: str
classification: str # "sample_id", "subject_id", "metadata", "data", "join_key", "exclude"
output_name: str | None = None
confidence: float = Field(ge=0.0, le=1.0)
reasoning: str
transform: str | None = None
class MappingResult(BaseModel):
"""Full analysis result from the AI agent — joins + classifications."""
session_id: str
joins: list[FileJoin]
columns: list[ColumnClassification]
primary_file: str # the main file other files join onto
class MappingRequest(BaseModel):
"""User request to analyze and classify columns across multiple files."""
session_id: str
user_prompt: str
template_name: str = "aseesa_standard_v1"
class MappingConfirmation(BaseModel):
"""User-confirmed joins and column classifications."""
session_id: str
joins: list[FileJoin]
columns: list[ColumnClassification]
primary_file: str
class GenerateRequest(BaseModel):
"""Request to generate output from confirmed classification."""
session_id: str
output_format: str = "csv" # "csv", "xlsx", "xpt"
# ---------------------------------------------------------------------------
# Reshape plan — for arbitrary-layout files (the raw-grid / Stars reshape flow).
#
# Instead of classifying clean columns, the agent looks at the literal cell grid
# of each sheet (see ``core/layout_extractor.py``) and proposes how to turn it
# into the Stars data matrix (variables as rows, samples as columns) plus a
# derived metadata table.
# ---------------------------------------------------------------------------
class GroupAssignment(BaseModel):
"""The cohort a sheet/block belongs to, parsed from the sheet name or a banner.
Any field may be null if it can't be determined. These become the Sample_ID
prefix and the derived metadata columns (Genotype/Sex/Diet).
"""
genotype: str | None = None # e.g. "KO", "WT", "HT"
sex: str | None = None # canonical "Male" / "Female"
diet: str | None = None # canonical "Chow" / "HFD"
class SheetReshapePlan(BaseModel):
"""How to extract one sheet/block into (sample_id, variable, value) records.
Coordinates are 0-based indices into the rendered grid. The executor is
tolerant: it clamps ranges and re-derives the sample columns from the header
row, so small index errors self-heal — the semantic fields (which row holds
sample IDs, which column holds variable labels, the group) are what matter.
"""
logical_name: str # which sheet, e.g. "ITT.xlsx::KO M HFD"
include: bool = True # set False to skip this sheet
samples_axis: str = "columns" # "columns" (samples across a row) or "rows"
# --- columns-axis (e.g. ITT): sample IDs along a row, variables down a column ---
sample_id_row: int | None = None # row index holding sample IDs
# --- rows-axis (e.g. insulin): sample labels down a column, variables along a row ---
sample_id_col: int | None = None # col index holding sample labels ("KO F Chow 1")
# variable_label_index: for columns-axis this is the COLUMN of variable labels;
# for rows-axis it is the ROW index of the measurement-name header.
variable_label_index: int | None = None
# value_first/value_last: for columns-axis, first/last value ROW; for rows-axis,
# first/last sample ROW (both optional — the executor re-derives them).
value_first: int | None = None
value_last: int | None = None
# For rows-axis side-by-side blocks (e.g. a Chow block and an HFD block on one
# sheet): emit ONE plan entry per block, each with its own sample_id_col and its
# own diet in `group`. The executor bounds each block's variable columns
# automatically from sample_id_col up to the next blank header cell.
group: GroupAssignment = GroupAssignment()
variable_kind: str = "generic" # "timepoint" → emit T{v} + %Basal rows; else "generic"
notes: str = ""
# LLMs sometimes emit null for these on excluded/edge sheets. Coerce to
# sensible defaults so a single stray null doesn't fail the whole plan.
@field_validator("samples_axis", mode="before")
@classmethod
def _default_axis(cls, v):
return v or "columns"
@field_validator("variable_kind", mode="before")
@classmethod
def _default_kind(cls, v):
return v or "generic"
@field_validator("group", mode="before")
@classmethod
def _default_group(cls, v):
return {} if v is None else v
@field_validator("include", mode="before")
@classmethod
def _default_include(cls, v):
return True if v is None else v
class ReshapePlan(BaseModel):
"""Full agent proposal for reshaping a set of grids into Stars output."""
sheets: list[SheetReshapePlan]
# Canonical Sample_ID template. Tokens: {genotype} {sex} {diet} {n}
id_format: str = "{genotype}-{sex}-{diet}-{n}"
# Maps for shortening derived values when building the ID (full forms kept in metadata).
sex_map: dict[str, str] = {"Male": "M", "Female": "F"}
diet_map: dict[str, str] = {"Chow": "C", "HFD": "HFD"}
compute_percent_basal: bool = True # for timepoint sheets, add %Basal T{v} rows
reasoning: str = ""
class ReshapeConfirmation(BaseModel):
"""User-confirmed reshape plan sent back from the classify page."""
session_id: str
plan: ReshapePlan
|