File size: 4,497 Bytes
20cd78e
312907b
 
20cd78e
312907b
20cd78e
312907b
 
 
 
 
 
 
 
 
 
20cd78e
 
 
 
 
 
312907b
20cd78e
 
 
312907b
20cd78e
 
312907b
20cd78e
312907b
20cd78e
312907b
 
 
 
20cd78e
312907b
 
 
 
 
 
 
 
 
20cd78e
 
312907b
 
5370a84
312907b
 
 
20cd78e
 
 
312907b
20cd78e
 
312907b
20cd78e
312907b
20cd78e
312907b
 
20cd78e
312907b
 
 
20cd78e
312907b
 
20cd78e
312907b
 
 
 
 
 
 
20cd78e
312907b
 
 
 
 
 
 
20cd78e
 
 
312907b
 
 
 
 
 
 
 
 
 
 
5370a84
312907b
 
 
 
 
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
"""
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"
    )