File size: 4,496 Bytes
34dc4d2
b20ecce
 
aa75886
b20ecce
aa75886
571cfca
 
 
 
 
 
 
 
 
 
34dc4d2
 
b20ecce
 
 
c89e310
 
34dc4d2
 
b20ecce
 
 
 
 
34dc4d2
b20ecce
 
 
 
 
 
34dc4d2
c89e310
571cfca
 
 
c89e310
 
 
 
 
 
 
 
 
ff5de8a
c89e310
 
 
34dc4d2
 
b20ecce
571cfca
b20ecce
 
 
34dc4d2
b20ecce
 
 
 
c89e310
571cfca
 
 
34dc4d2
c89e310
 
 
 
 
 
 
 
 
571cfca
c89e310
571cfca
c89e310
 
 
 
 
 
 
b20ecce
 
 
 
c89e310
 
 
 
571cfca
c89e310
 
 
 
ff5de8a
c89e310
 
 
571cfca
ff5de8a
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
"""
models.py – PhishGuard-Env Pydantic Models
==========================================

Typed request / response schemas used by env.py (FastAPI).

PhishAction   : Body schema for POST /step
StepResponse  : Response schema for POST /step  (OpenEnv grader compliance)
ResetResponse : Response schema for POST /reset

BUG FIX (v1.0.2 β†’ v1.0.3)
────────────────────────────────────────────────────────────────────────────
  StepResponse.task_id was typed as `str` but the episode-already-over guard
  branch in env.py returns task_id=None.  Pydantic would raise a validation
  error on every post-episode /step call.
  Fix: task_id is now Optional[str] with a default of None.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field


# ─────────────────────────────────────────────────────────────────────────────
# REQUEST MODELS
# ─────────────────────────────────────────────────────────────────────────────

class PhishAction(BaseModel):
    """
    Action submitted by the agent to POST /step.

    Fields
    ------
    action    : One of MARK_SAFE | MOVE_TO_SPAM | QUARANTINE | BLOCK_DOMAIN
    reasoning : Optional one-sentence technical justification (for logging).
    """
    action: str = Field(
        max_length=64,
        description="Triage decision. Must be exactly one of: "
                    "MARK_SAFE | MOVE_TO_SPAM | QUARANTINE | BLOCK_DOMAIN"
    )
    reasoning: Optional[str] = Field(
        default=None,
        description="One-sentence technical justification for the triage decision",
    )


class ResetRequest(BaseModel):
    """Body schema for POST /reset."""
    difficulty: str = Field(
        default="easy",
        description="Difficulty level: easy | medium | hard",
    )


# ─────────────────────────────────────────────────────────────────────────────
# RESPONSE MODELS  (OpenEnv spec β€” all fields required by validator)
# ─────────────────────────────────────────────────────────────────────────────

class StepResponse(BaseModel):
    """
    Full response for POST /step.

    The OpenEnv validator inspects `task_id` and `is_correct` on every step
    to count how many distinct tasks have been graded.

    task_id is Optional[str] (not str) because the episode-already-over guard
    branch returns None β€” a non-optional field would cause a Pydantic
    ValidationError on every post-episode call.
    """
    observation: Optional[Dict[str, Any]] = Field(
        description="Next email dict, or null when the episode is done"
    )
    reward: float = Field(
        description="Step reward strictly in (0.0, 1.0)"
    )
    done: bool = Field(
        description="True when all scenarios are complete or health reaches 0"
    )
    task_id: Optional[str] = Field(          # BUG FIX: was `str`, must be Optional
        default=None,
        description="Scenario ID e.g. 'lv3' β€” required by OpenEnv validator"
    )
    is_correct: bool = Field(
        description="True when reward >= R_PERFECT (0.95)"
    )
    info: Dict[str, Any] = Field(
        description="Full grader info payload"
    )


class ResetResponse(BaseModel):
    """Response for POST /reset."""
    observation: Dict[str, Any] = Field(
        description="First email observation for this episode"
    )
    task_id: str = Field(
        description="ID of the first scenario in this episode"
    )
    task_group: str = Field(
        description="Difficulty level of the first scenario: easy | medium | hard"
    )
    difficulty: str = Field(
        description="Active difficulty level for this episode"
    )
    total_tasks: int = Field(
        description="Total number of scenarios in this level"
    )