File size: 16,855 Bytes
b89c8aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
"""All Pydantic models and enums for TriageSieve-OpenEnv.



Implements:

- Issue taxonomy enums (§7.1)

- Queue taxonomy (§7.2)

- Impact / Urgency / Priority enums + derivation matrix (§7.3)

- Ticket status state machine (§7.4)

- NonActionableSubtype, CustomerTier, SourceChannel, CloseReason, TaskDifficulty (§7.5–7.9)

- ActionType + TriageSieveAction tagged union (§8)

- TriageSieveObservation (§9)

- TriageSieveState (§10)

- HiddenTicketTruth dataclass (§11) — internal, never serialized to observations



Python 3.11+, Pydantic v2, OpenEnv framework contract.

"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Literal

from openenv.core.env_server.types import Action, Observation, State
from pydantic import BaseModel, ConfigDict

__all__ = [
    # Enums
    "IssueFamily",
    "IssueSubtype",
    "QueueId",
    "Impact",
    "Urgency",
    "Priority",
    "TicketStatus",
    "NonActionableSubtype",
    "CustomerTier",
    "SourceChannel",
    "CloseReason",
    "TaskDifficulty",
    "ActionType",
    # Constants
    "VALID_FAMILY_SUBTYPES",
    "PRIORITY_MATRIX",
    "GATED_QUEUES",
    "PRIORITY_WEIGHTS",
    # Helper
    "derive_priority",
    # Standalone Pydantic models
    "InboxSummaryItem",
    "FocusedTicket",
    "RoutingPolicyCard",
    "SlaPolicyCard",
    # OpenEnv-inheriting models
    "TriageSieveAction",
    "TriageSieveObservation",
    "TriageSieveState",
    # Internal dataclass
    "HiddenTicketTruth",
]


# ---------------------------------------------------------------------------
# §7.1 Issue Taxonomy
# ---------------------------------------------------------------------------


class IssueFamily(str, Enum):
    """Top-level issue category."""

    BILLING = "billing"
    TECHNICAL = "technical"
    ACCOUNT = "account"
    SECURITY = "security"
    SHIPPING = "shipping"


class IssueSubtype(str, Enum):
    """Fine-grained issue type; always paired with a parent IssueFamily."""

    # billing
    REFUND = "refund"
    INVOICE_ERROR = "invoice_error"
    FAILED_CHARGE = "failed_charge"
    # technical
    BUG_REPORT = "bug_report"
    API_ERROR = "api_error"
    INTEGRATION_FAILURE = "integration_failure"
    # account
    PASSWORD_RESET = "password_reset"  # nosec B105
    SSO_ISSUE = "sso_issue"
    ACCOUNT_LOCKOUT = "account_lockout"
    # security
    SUSPICIOUS_LOGIN = "suspicious_login"
    EXPOSURE_RISK = "exposure_risk"
    ABUSE_REPORT = "abuse_report"
    # shipping
    DELAY = "delay"
    TRACKING_PROBLEM = "tracking_problem"
    LOST_PACKAGE = "lost_package"


# ---------------------------------------------------------------------------
# §7.2 Queue Taxonomy
# ---------------------------------------------------------------------------


class QueueId(str, Enum):
    """Available routing destinations."""

    BILLING_TEAM = "billing_team"
    TECH_SUPPORT_L1 = "tech_support_l1"
    TECH_SUPPORT_L2 = "tech_support_l2"
    ACCOUNT_TEAM = "account_team"
    SECURITY_TEAM = "security_team"
    SHIPPING_TEAM = "shipping_team"
    REFUND_TEAM = "refund_team"
    SPAM_FILTER = "spam_filter"
    SALES_OR_FEATURE_REQUESTS = "sales_or_feature_requests"


# ---------------------------------------------------------------------------
# §7.3 Impact / Urgency / Priority
# ---------------------------------------------------------------------------


class Impact(str, Enum):
    """Business scope of the issue."""

    SINGLE_USER = "single_user"
    TEAM = "team"
    ORG_WIDE = "org_wide"
    REVENUE_AFFECTING = "revenue_affecting"


class Urgency(str, Enum):
    """Time-to-business-effect of the issue."""

    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class Priority(str, Enum):
    """Computed priority; derived from impact × urgency. Never set directly by the agent."""

    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


# ---------------------------------------------------------------------------
# §7.4 Ticket Status
# ---------------------------------------------------------------------------


class TicketStatus(str, Enum):
    """State-machine status of a single ticket."""

    NEW = "new"
    OPENED = "opened"
    CLASSIFIED = "classified"
    WAITING_FOR_INFO = "waiting_for_info"
    ROUTED = "routed"
    ESCALATED = "escalated"
    MERGED = "merged"
    CLOSED = "closed"


# ---------------------------------------------------------------------------
# §7.5 Non-Actionable Subtypes
# ---------------------------------------------------------------------------


class NonActionableSubtype(str, Enum):
    """Reason a ticket requires no further action."""

    SPAM_MARKETING = "spam_marketing"
    BENIGN_EXPECTED = "benign_expected"
    AUTOMATION_FALSE_POSITIVE = "automation_false_positive"
    DATA_ERROR = "data_error"
    NO_RESPONSE_NEEDED = "no_response_needed"


# ---------------------------------------------------------------------------
# §7.6 Customer Tier
# ---------------------------------------------------------------------------


class CustomerTier(str, Enum):
    """SLA and support tier for the submitting customer."""

    FREE = "free"
    PRO = "pro"
    ENTERPRISE = "enterprise"
    INTERNAL = "internal"


# ---------------------------------------------------------------------------
# §7.7 Source Channel
# ---------------------------------------------------------------------------


class SourceChannel(str, Enum):
    """Origin channel of the ticket."""

    CUSTOMER_EMAIL = "customer_email"
    INTERNAL_REPORT = "internal_report"
    MONITORING_ALERT = "monitoring_alert"


# ---------------------------------------------------------------------------
# §7.8 Close Reason
# ---------------------------------------------------------------------------


class CloseReason(str, Enum):
    """Reason for closing a ticket."""

    RESOLVED = "resolved"
    DUPLICATE = "duplicate"
    NON_ACTIONABLE = "non_actionable"
    FEATURE_REQUEST = "feature_request"
    NO_RESPONSE = "no_response"


# ---------------------------------------------------------------------------
# §7.9 Task Difficulty
# ---------------------------------------------------------------------------


class TaskDifficulty(str, Enum):
    """Episode difficulty tier for the task ladder (§18)."""

    EASY = "easy"
    MEDIUM = "medium"
    HARD = "hard"


# ---------------------------------------------------------------------------
# §8 Action Type
# ---------------------------------------------------------------------------


class ActionType(str, Enum):
    """Discriminant tag for the TriageSieveAction tagged union."""

    OPEN_TICKET = "open_ticket"
    CLASSIFY_TICKET = "classify_ticket"
    SET_IMPACT_URGENCY = "set_impact_urgency"
    ROUTE_TICKET = "route_ticket"
    REQUEST_INFORMATION = "request_information"
    ESCALATE_TICKET = "escalate_ticket"
    MERGE_DUPLICATE = "merge_duplicate"
    CLOSE_TICKET = "close_ticket"
    SKIP_TURN = "skip_turn"
    FINISH_EPISODE = "finish_episode"


# ---------------------------------------------------------------------------
# Helper constants
# ---------------------------------------------------------------------------

VALID_FAMILY_SUBTYPES: dict[IssueFamily, frozenset[IssueSubtype]] = {
    IssueFamily.BILLING: frozenset(
        {
            IssueSubtype.REFUND,
            IssueSubtype.INVOICE_ERROR,
            IssueSubtype.FAILED_CHARGE,
        }
    ),
    IssueFamily.TECHNICAL: frozenset(
        {
            IssueSubtype.BUG_REPORT,
            IssueSubtype.API_ERROR,
            IssueSubtype.INTEGRATION_FAILURE,
        }
    ),
    IssueFamily.ACCOUNT: frozenset(
        {
            IssueSubtype.PASSWORD_RESET,
            IssueSubtype.SSO_ISSUE,
            IssueSubtype.ACCOUNT_LOCKOUT,
        }
    ),
    IssueFamily.SECURITY: frozenset(
        {
            IssueSubtype.SUSPICIOUS_LOGIN,
            IssueSubtype.EXPOSURE_RISK,
            IssueSubtype.ABUSE_REPORT,
        }
    ),
    IssueFamily.SHIPPING: frozenset(
        {
            IssueSubtype.DELAY,
            IssueSubtype.TRACKING_PROBLEM,
            IssueSubtype.LOST_PACKAGE,
        }
    ),
}
"""Maps each IssueFamily to the set of valid IssueSubtypes it may contain.



Used by step() to validate classify actions (§8 validation rule).

"""

PRIORITY_MATRIX: dict[tuple[Impact, Urgency], Priority] = {
    # single_user row
    (Impact.SINGLE_USER, Urgency.LOW): Priority.LOW,
    (Impact.SINGLE_USER, Urgency.MEDIUM): Priority.LOW,
    (Impact.SINGLE_USER, Urgency.HIGH): Priority.MEDIUM,
    (Impact.SINGLE_USER, Urgency.CRITICAL): Priority.HIGH,
    # team row
    (Impact.TEAM, Urgency.LOW): Priority.LOW,
    (Impact.TEAM, Urgency.MEDIUM): Priority.MEDIUM,
    (Impact.TEAM, Urgency.HIGH): Priority.HIGH,
    (Impact.TEAM, Urgency.CRITICAL): Priority.HIGH,
    # org_wide row
    (Impact.ORG_WIDE, Urgency.LOW): Priority.MEDIUM,
    (Impact.ORG_WIDE, Urgency.MEDIUM): Priority.HIGH,
    (Impact.ORG_WIDE, Urgency.HIGH): Priority.HIGH,
    (Impact.ORG_WIDE, Urgency.CRITICAL): Priority.CRITICAL,
    # revenue_affecting row
    (Impact.REVENUE_AFFECTING, Urgency.LOW): Priority.HIGH,
    (Impact.REVENUE_AFFECTING, Urgency.MEDIUM): Priority.HIGH,
    (Impact.REVENUE_AFFECTING, Urgency.HIGH): Priority.CRITICAL,
    (Impact.REVENUE_AFFECTING, Urgency.CRITICAL): Priority.CRITICAL,
}
"""Full 4×4 impact × urgency → priority derivation table (§7.3)."""

GATED_QUEUES: frozenset[QueueId] = frozenset(
    {
        QueueId.TECH_SUPPORT_L2,
        QueueId.SECURITY_TEAM,
    }
)
"""Queues that require prerequisites before routing is permitted (§7.2, §15)."""

PRIORITY_WEIGHTS: dict[Priority, float] = {
    Priority.LOW: 0.5,
    Priority.MEDIUM: 1.0,
    Priority.HIGH: 1.5,
    Priority.CRITICAL: 2.0,
}
"""Per-priority weights used in the priority-weighted terminal business score (§17.3)."""


def derive_priority(impact: Impact, urgency: Urgency) -> Priority:
    """Compute ticket priority from impact and urgency using the §7.3 matrix.



    Args:

        impact: Business scope of the issue.

        urgency: Time-to-business-effect of the issue.



    Returns:

        Derived Priority enum value.

    """
    key = (impact, urgency)
    if key not in PRIORITY_MATRIX:
        raise ValueError(f"No priority mapping for impact={impact!r}, urgency={urgency!r}")
    return PRIORITY_MATRIX[key]


# ---------------------------------------------------------------------------
# §9 Standalone observation sub-models
# ---------------------------------------------------------------------------


class InboxSummaryItem(BaseModel):
    """One row in the inbox list shown to the agent."""

    model_config = ConfigDict(extra="forbid")

    ticket_id: str
    subject: str
    sender_email: str
    received_at: str  # ISO 8601
    status: TicketStatus
    customer_tier: CustomerTier
    has_attachment: bool
    sla_remaining_minutes: int | None
    short_preview: str  # first ~80 chars of body


class FocusedTicket(BaseModel):
    """Full ticket detail revealed when the agent opens a ticket."""

    model_config = ConfigDict(extra="forbid")

    ticket_id: str
    subject: str
    latest_message: str
    thread_history: list[dict[str, Any]]  # [{role, content, timestamp}]
    attachments: list[str]  # filenames
    visible_internal_notes: list[str]
    prior_actions_taken: list[str]  # human-readable log of agent's actions on this ticket


class RoutingPolicyCard(BaseModel):
    """Policy card shown to the agent describing a routing queue."""

    model_config = ConfigDict(extra="forbid")

    queue_id: QueueId
    description: str
    prerequisites: list[str]
    handles_families: list[IssueFamily]


class SlaPolicyCard(BaseModel):
    """SLA policy for a given customer tier."""

    model_config = ConfigDict(extra="forbid")

    tier: CustomerTier
    response_deadline_minutes: int
    resolution_deadline_minutes: int


# ---------------------------------------------------------------------------
# §8 TriageSieveAction (OpenEnv Action subclass)
# ---------------------------------------------------------------------------


class TriageSieveAction(Action):
    """Tagged-union action model for all agent operations (§8).



    Discriminated by action_type. Field presence requirements per action_type

    are validated at step() time, not at model construction time, to allow

    the environment to return precise error messages.

    """

    action_type: ActionType
    ticket_id: str | None = None
    # classify fields
    issue_family: IssueFamily | None = None
    issue_subtype: IssueSubtype | None = None
    # impact / urgency fields
    impact: Impact | None = None
    urgency: Urgency | None = None
    # route / escalate fields
    queue_id: QueueId | None = None
    reason_code: str | None = None
    # request_information fields
    template_id: str | None = None
    requested_fields: list[str] | None = None
    # merge field
    target_ticket_id: str | None = None
    # close field
    close_reason: CloseReason | None = None


# ---------------------------------------------------------------------------
# §9 TriageSieveObservation (OpenEnv Observation subclass)
# ---------------------------------------------------------------------------


class TriageSieveObservation(Observation):
    """Full observation returned to the agent on every step (§9).



    Inherits done, reward, metadata from Observation base.

    """

    inbox_summaries: list[InboxSummaryItem]
    focused_ticket: FocusedTicket | None = None
    available_templates: list[dict[str, Any]]  # [{template_id, name, description, applies_to}]
    allowed_queues: list[QueueId]
    routing_policy_cards: list[RoutingPolicyCard]
    sla_policy_cards: list[SlaPolicyCard]
    legal_actions: list[ActionType]
    action_budget_remaining: int
    step_count: int
    current_time: str  # ISO 8601
    last_action_result: str  # "ok", error message, or pushback message
    task_difficulty: TaskDifficulty
    hint: str | None = None  # only populated when mode="train_guided"


# ---------------------------------------------------------------------------
# §10 TriageSieveState (OpenEnv State subclass)
# ---------------------------------------------------------------------------


class TriageSieveState(State):
    """Internal episode state exposed for debugging and TRL integration (§10).



    Inherits episode_id, step_count from State base (extra='allow').

    """

    task_difficulty: TaskDifficulty
    seed: int
    total_tickets: int
    action_budget: int
    action_budget_remaining: int
    mode: Literal["eval_strict", "train_guided"]
    tickets_summary: list[dict[str, Any]]  # [{ticket_id, status, gold_priority}]


# ---------------------------------------------------------------------------
# §11 HiddenTicketTruth (internal dataclass — never serialized to observations)
# ---------------------------------------------------------------------------


@dataclass
class HiddenTicketTruth:
    """Ground-truth metadata for a single ticket, hidden from the agent.



    Used exclusively by the scorer and episode engine. Never included in any

    Observation or State object. Plain (mutable) dataclass to allow the episode

    engine to update fields during runtime (e.g., follow-up generation).

    """

    ticket_id: str
    customer_tier: CustomerTier
    source_channel: SourceChannel
    issue_family: IssueFamily
    issue_subtype: IssueSubtype
    product_area: str
    impact: Impact
    urgency: Urgency
    priority: Priority  # DERIVED from impact × urgency matrix; set by episode engine
    required_queue: QueueId
    required_missing_fields: list[str] = field(default_factory=list)
    escalation_required: bool = False
    escalation_target: QueueId | None = None
    is_duplicate: bool = False
    duplicate_of: str | None = None  # ticket_id of original
    sla_response_deadline: int = 0  # minutes
    sla_resolution_deadline: int = 0  # minutes
    policy_graph_id: str = ""  # references SOP DAG in data/archetypes.json
    correct_template_ids: list[str] = field(default_factory=list)
    gold_terminal_status: TicketStatus = TicketStatus.CLOSED
    non_actionable_subtype: NonActionableSubtype | None = None