File size: 6,479 Bytes
3551dcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
from __future__ import annotations

from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field, field_validator


class ActionType(str, Enum):
    """
    The four action types available to an agent in sql_quality_env.

    QUERY   β€” Read-only SELECT statement. Inspect schema or data.
              Does not modify the database. Can be called multiple times.
    PROFILE β€” Run null-rate and type distribution stats on a named column.
              Shortcut for common inspection patterns. Read-only.
    FIX     β€” Submit a write query intended to resolve the data issue.
              Executes on an isolated _fresh_test_db() copy for scoring.
    SUBMIT  β€” Declare final diagnosis and close the episode.
              Triggers composite score computation. Episode ends after this.
    """

    QUERY = "query"
    PROFILE = "profile"
    FIX = "fix"
    SUBMIT = "submit"


class SchemaInfo(BaseModel):
    """Schema information for a database table."""

    table_name: str
    columns: list[str]  # Format: "column_name (TYPE)"
    row_count: int


class SQLObservation(BaseModel):
    """What the agent sees at each step."""

    task_id: str = Field(
        description=(
            "One of: null_audit, type_repair, dedup_bug, fk_integrity, schema_drift"
        )
    )
    task_description: str = Field(
        description="Natural language description of the task"
    )
    schema_info: list[SchemaInfo] = Field(description="Schema of all relevant tables")
    sample_rows: dict[str, list[dict]] = Field(
        description="First 5 rows of each table as list of dicts"
    )
    episode_id: Optional[str] = Field(
        default=None, description="UUID4 identifying this episode"
    )
    attempt_number: int = Field(ge=1, description="Current attempt (1-indexed)")
    max_attempts: int = Field(
        ge=1, description="Maximum attempts allowed for this task"
    )
    previous_feedback: Optional[str] = Field(
        default=None, description="Feedback from previous attempt, if any"
    )
    # NEW FIELDS:
    trajectory_history: list[dict] = Field(
        default_factory=list,
        description=(
            "List of previous actions and their results in this episode. "
            "Each entry: {action_type, sql_or_answer, result_preview, step_reward}. "
            "Empty on first step after reset()."
        ),
    )
    available_actions: list[str] = Field(
        default_factory=lambda: ["query", "profile", "fix", "submit"],
        description="Action types available at this point in the episode.",
    )
    investigation_hint: str | None = Field(
        default=None,
        description=(
            "Hint provided after a failed attempt. None on first step. "
            "Populated with actionable feedback after each scored action."
        ),
    )


class SQLAction(BaseModel):
    """
    Agent action for sql_quality_env.

    Fields:
        task_id:       Which task this action targets.
        action_type:   One of QUERY / PROFILE / FIX / SUBMIT.
        sql:           For QUERY and FIX actions β€” the SQL statement to execute.
        column_name:   For PROFILE actions β€” the column to profile.
        answer:        For null_audit SUBMIT β€” the column name identified.
        fix_query:     Alias for sql in FIX context (kept for backward compat).
        root_cause_table:  For dedup_bug SUBMIT β€” the table with duplicates.
        offending_key:     For dedup_bug SUBMIT β€” the key column with duplicates.
    """

    task_id: str
    action_type: ActionType = ActionType.SUBMIT
    sql: str | None = None
    column_name: str | None = None
    answer: str | None = None
    fix_query: str | None = None
    root_cause_table: str | None = None
    offending_key: str | None = None

    @property
    def effective_sql(self) -> str | None:
        """Return sql or fix_query, whichever is set. fix_query takes precedence."""
        return self.fix_query or self.sql


class SQLReward(BaseModel):
    """
    Reward returned after each step.

    Fields:
        score:          Episode-level composite score 0.0–1.0.
        step_reward:    Immediate reward for this specific action (not cumulative).
                        Used for dense reward shaping in RL training.
        feedback:       Human-readable explanation of what was scored and why.
        dimensions:     Per-dimension breakdown. Present only after SUBMIT.
                        None for intermediate QUERY/PROFILE/FIX actions.
        partial_scores: Legacy field kept for backward compatibility.
    """

    score: float
    step_reward: float = 0.0
    feedback: str = ""
    dimensions: dict | None = None
    partial_scores: dict | None = None

    @field_validator("score", "step_reward")
    @classmethod
    def clamp_to_unit(cls, v: float) -> float:
        """Ensure scores stay in [0.001, 0.999] and round to 4 decimal places."""
        return round(max(0.001, min(0.999, v)), 4)


class TrajectoryStep(BaseModel):
    """A single step in the agent's investigation trajectory."""

    step_number: int
    action_type: ActionType
    sql_submitted: str | None = None
    answer_submitted: str | None = None
    result_preview: str = ""
    step_reward: float = 0.0
    timestamp_ms: int = 0


class SQLState(BaseModel):
    """Current episode state."""

    task_id: str
    episode_id: str = Field(description="UUID4 identifying this episode")
    attempt_number: int
    cumulative_reward: float = Field(ge=0.0)
    done: bool


class ResetRequest(BaseModel):
    """Request body for /reset endpoint."""

    task_id: str | None = Field(
        default=None,
        description=(
            "One of: null_audit, type_repair, dedup_bug, fk_integrity, schema_drift"
        )
    )
    seed: int | None = Field(
        default=None,
        description=(
            "Optional deterministic episode seed. If omitted, environment chooses a random seed."
        ),
    )


class StepResponse(BaseModel):
    """Response body for /step endpoint."""

    observation: SQLObservation
    reward: SQLReward
    done: bool = Field(default=False, description="Whether the episode is complete")
    info: dict = Field(default_factory=dict)


class HealthResponse(BaseModel):
    """Response body for health/ping endpoints."""

    status: str = Field(description="Service health status")
    llm_available: Optional[bool] = Field(default=None, description="LLM connection status")