📦 EqualifyEverything / equalify-reflow

📄 auto_correction.py · 136 lines
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"""AutoCorrection model for safe automatic edits.

AutoCorrections replace the old Proposal model. They represent edits that are
safe to apply automatically (high confidence) without human review.

Key differences from Proposal:
- No status lifecycle (applied immediately or converted to ReviewItem)
- Requires high confidence (>0.95) for auto-application
- Links to single observation (not a list)
- Simpler search/replace fields (no nested SearchReplaceDiff)
"""

from datetime import UTC, datetime
from uuid import uuid4

from pydantic import BaseModel, ConfigDict, Field


class AutoCorrection(BaseModel):
    """A correction safe to apply automatically.

    AutoCorrections are generated by specialized agents when they have
    high confidence in a fix. They contain a search-replace edit that
    can be applied without human review.

    For lower-confidence fixes, agents generate ReviewItems instead.

    Attributes:
        id: UUID for this correction
        observation_id: Links to the observation this fixes
        search: Exact text to find in markdown
        replace: Text to replace with (can be empty for deletion)
        justification: Why this is safe to auto-apply (glass box)
        confidence: Should be high (>0.95) for auto-corrections
        applied: Whether this correction has been applied
        applied_at: When the correction was applied
        agent: Which agent generated this correction
        page_num: Page number where the correction applies

    Example:
        >>> correction = AutoCorrection(
        ...     observation_id="obs-123",
        ...     search="## Introduction",
        ...     replace="## 1. Introduction",
        ...     justification="Adding section number to match document structure",
        ...     confidence=0.98,
        ...     agent="structure"
        ... )
    """

    id: str = Field(
        default_factory=lambda: str(uuid4()),
        description="UUID for this correction"
    )
    observation_id: str = Field(
        ...,
        description="Links to the observation this fixes"
    )

    # The edit operation
    search: str = Field(
        ...,
        min_length=1,
        description="Exact text to find in markdown"
    )
    replace: str = Field(
        ...,
        description="Text to replace with (can be empty for deletion)"
    )

    # Glass box transparency
    justification: str = Field(
        ...,
        min_length=1,
        description="Why this is safe to auto-apply"
    )
    confidence: float = Field(
        ...,
        ge=0.0,
        le=1.0,
        description="Should be high (>0.95) for auto-corrections"
    )

    # Application tracking
    applied: bool = Field(
        default=False,
        description="Whether this correction has been applied"
    )
    applied_at: datetime | None = Field(
        default=None,
        description="When the correction was applied"
    )

    # Source tracking
    agent: str = Field(
        ...,
        description="Which agent generated this correction"
    )
    page_num: int | None = Field(
        default=None,
        description="Page number where the correction applies"
    )

    def mark_applied(self) -> None:
        """Mark this correction as applied.

        Sets applied=True and records the current timestamp.

        Raises:
            ValueError: If correction is already applied
        """
        if self.applied:
            raise ValueError("Correction already applied")
        self.applied = True
        self.applied_at = datetime.now(UTC)

    model_config = ConfigDict(
        json_schema_extra={
            "example": {
                "id": "ac-550e8400-e29b-41d4-a716-446655440000",
                "observation_id": "obs-550e8400-e29b-41d4-a716-446655440001",
                "search": "## Introduction",
                "replace": "## 1. Introduction",
                "justification": "Adding section number to match document's numbered heading structure",
                "confidence": 0.98,
                "applied": False,
                "applied_at": None,
                "agent": "structure",
                "page_num": 1
            }
        }
    )


__all__ = ["AutoCorrection"]