SwapnilPatil28's picture
Major Update 1 - Add server, domain, client, models, and tests
4058302 verified
"""Pydantic schemas for the Incident Command Center environment.
These are the wire types shared by the HTTP server and the client. They are
designed to be:
- **Forwards-compatible**: new observation fields have default values so old
clients keep working.
- **Strict on the server**: every action field has a validator that ensures
the server never receives malformed data.
- **Self-documenting**: every field has a `description` that renders into
the OpenAPI schema at `/docs`.
"""
from __future__ import annotations
from typing import Dict, List, Literal, Optional
from openenv.core.env_server import Action, Observation, State
from pydantic import ConfigDict, Field, field_validator
# ----- Constants shared with server code -----------------------------------
ActionType = Literal[
"inspect_logs",
"inspect_metrics",
"consult_kb",
"negotiate_handoff",
"apply_fix",
"close_incident",
"escalate",
"rollback",
"submit_postmortem",
]
RoleName = Literal[
"triage_agent",
"investigator_agent",
"ops_manager_agent",
]
CustomerTier = Literal["free", "standard", "premium", "enterprise"]
# ---------------------------------------------------------------------------
# Action
# ---------------------------------------------------------------------------
class IncidentAction(Action):
"""Structured action payload accepted by the environment.
Validators reject obviously malformed input (empty targets, invalid roles)
and trim whitespace so training-time and inference-time JSON is normalised
identically.
"""
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
action_type: ActionType = Field(
..., description="Selected action from the supported action space."
)
actor: RoleName = Field(
"triage_agent",
description="Specialist role acting in the environment during this turn.",
)
target: Optional[str] = Field(
None,
description=(
"Service id for inspect_logs/inspect_metrics, KB id for consult_kb, "
"team name for negotiate_handoff/escalate."
),
)
root_cause: Optional[str] = Field(
None, description="Predicted root cause for close_incident."
)
resolution_summary: Optional[str] = Field(
None,
description="Human-readable fix summary for apply_fix, rollback and close_incident.",
)
postmortem_note: Optional[str] = Field(
None,
description="Postmortem text for submit_postmortem actions.",
)
confidence: Optional[float] = Field(
None,
ge=0.0,
le=1.0,
description="Optional self-reported confidence of the agent in this action.",
)
reason: Optional[str] = Field(
None,
description="Optional free-text rationale for audit logs and traceability.",
)
@field_validator("target", "root_cause", "resolution_summary", "postmortem_note", "reason")
@classmethod
def _empty_string_to_none(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
value = value.strip()
return value or None
# ---------------------------------------------------------------------------
# Observation
# ---------------------------------------------------------------------------
class IncidentObservation(Observation):
"""Observation returned to the agent after each action.
All newly added fields carry defaults so older clients continue to
deserialize this type correctly.
"""
model_config = ConfigDict(extra="ignore")
incident_id: str = ""
incident_title: str = ""
incident_description: str = ""
incident_category: str = ""
incident_difficulty: str = "easy"
customer_tier: CustomerTier = "standard"
affected_users_estimate: int = 0
revenue_impact_usd_per_min: int = 0
postmortem_required: bool = False
available_actions: List[str] = Field(default_factory=list)
available_teams: List[str] = Field(default_factory=list)
allowed_actors_by_action: Dict[str, List[str]] = Field(default_factory=dict)
visible_signals: List[str] = Field(default_factory=list)
investigation_targets: Dict[str, List[str]] = Field(
default_factory=dict,
description="Per-tool list of known investigation ids (logs/metrics/kb).",
)
playbook_hints: List[str] = Field(default_factory=list)
terminal_output: str = ""
budget_remaining: int = 0
sla_minutes_remaining: int = 0
incidents_remaining: int = 0
episode_step: int = 0
incident_step: int = 0
clues_found: int = 0
mitigation_applied: bool = False
postmortem_submitted: bool = False
reward_components: Dict[str, float] = Field(default_factory=dict)
last_action_notes: List[str] = Field(default_factory=list)
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
class IncidentState(State):
"""Full environment state exposed at `/state` for observability."""
model_config = ConfigDict(extra="ignore")
task_id: str = "easy"
seed: int = 0
version: str = "3.0.0"
current_incident_index: int = 0
incidents_resolved: int = 0
incidents_failed: int = 0
budget_remaining: int = 0
sla_minutes_remaining: int = 0
cumulative_reward: float = 0.0
mitigation_applied: bool = False
postmortem_submitted: bool = False
clue_keywords_used: List[str] = Field(default_factory=list)
investigation_keys_used: List[str] = Field(default_factory=list)
handoff_history: List[str] = Field(default_factory=list)
action_trace: List[str] = Field(default_factory=list)
per_incident_steps: Dict[str, int] = Field(default_factory=dict)
reward_trace: List[Dict[str, float]] = Field(default_factory=list)
terminated_reason: Optional[str] = None