jeevan2717's picture
Phase 1: Multi-agent extension - REQUEST_REVIEW + REVISE_SECTION + skeptic agent
8395324
Raw
History Blame Contribute Delete
8.69 kB
"""
Pydantic models for the Incident Post-Mortem Writer OpenEnv environment.
Defines all typed Observation, Action, and Reward structures.
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class ActionType(str, Enum):
WRITE_SECTION = "WRITE_SECTION"
QUERY_LOGS = "QUERY_LOGS"
ASSIGN_ACTION_ITEM = "ASSIGN_ACTION_ITEM"
SUBMIT = "SUBMIT"
# Multi-agent extension β€” Phase 1
REQUEST_REVIEW = "REQUEST_REVIEW"
REVISE_SECTION = "REVISE_SECTION"
class SectionName(str, Enum):
SUMMARY = "summary"
TIMELINE = "timeline"
ROOT_CAUSE = "root_cause"
IMPACT = "impact"
ACTION_ITEMS = "action_items"
class SectionState(str, Enum):
UNWRITTEN = "unwritten"
WRITTEN_INVALID = "written_invalid"
WRITTEN_VALID = "written_valid"
class RootCauseCategory(str, Enum):
NULL_REF = "null_ref"
TIMEOUT = "timeout"
MEMORY_LEAK = "memory_leak"
CONFIG_ERROR = "config_error"
DEPENDENCY_FAILURE = "dependency_failure"
RESOURCE_EXHAUSTION = "resource_exhaustion"
DEPLOYMENT_BUG = "deployment_bug"
NETWORK_FAILURE = "network_failure"
# ---------------------------------------------------------------------------
# Sub-models used inside Observation
# ---------------------------------------------------------------------------
class AlertLog(BaseModel):
timestamp: str = Field(..., description="ISO-like time string e.g. '03:41:05'")
service: str = Field(..., description="Service that fired the alert")
severity: str = Field(..., description="INFO | WARN | ERROR | CRITICAL")
message: str = Field(..., description="Alert message text")
class SlackMessage(BaseModel):
timestamp: str
author: str
text: str
class ServiceDependency(BaseModel):
service: str = Field(..., description="Upstream service name")
depends_on: List[str] = Field(default_factory=list,
description="Services this one calls")
class SectionStatus(BaseModel):
name: SectionName
state: SectionState = SectionState.UNWRITTEN
content: Optional[str] = None
class QueryRecord(BaseModel):
service: str
from_time: str
to_time: str
was_correct: bool
step: int
class ActionItem(BaseModel):
description: str
owner: str
due_date: str
# ---------------------------------------------------------------------------
# Core Observation β€” what the agent sees each step
# ---------------------------------------------------------------------------
class Observation(BaseModel):
"""Everything the agent can see at each step."""
# Static incident data (always visible)
goal: str = Field(..., description="Natural language goal for this episode")
incident_id: str
incident_title: str
alerts: List[AlertLog] = Field(
default_factory=list,
description="Alert logs visible from the start (NOT the hidden evidence)"
)
slack_thread: List[SlackMessage] = Field(
default_factory=list,
description="Slack messages from the incident channel"
)
service_graph: List[ServiceDependency] = Field(
default_factory=list,
description="Dependency graph β€” which service calls which"
)
# Dynamic episode state
step: int = 0
max_steps: int = 25
queries_used: int = 0
max_queries: int = 8
sections: List[SectionStatus] = Field(
default_factory=list,
description="Current state of all 5 postmortem sections"
)
action_items_assigned: List[ActionItem] = Field(default_factory=list)
query_history: List[QueryRecord] = Field(default_factory=list)
# Last step feedback
last_action_result: Optional[str] = None
last_reward: float = 0.0
done: bool = False
# Retrieved log lines (populated after a QUERY_LOGS call)
retrieved_logs: Optional[List[AlertLog]] = Field(
default=None,
description="Log lines returned by the last QUERY_LOGS call. None if no query made yet."
)
# Multi-agent extension β€” Skeptic critiques (populated after REQUEST_REVIEW)
skeptic_critiques: List[str] = Field(
default_factory=list,
description="Critiques from the skeptic agent on current post-mortem draft. Addressing these via REVISE_SECTION earns reward."
)
critiques_addressed: int = Field(
default=0,
description="Count of skeptic critiques the agent has addressed via REVISE_SECTION."
)
reviews_requested: int = Field(
default=0,
description="Total REQUEST_REVIEW calls made this episode (soft-capped at 3)."
)
# ---------------------------------------------------------------------------
# Action β€” what the agent can do
# ---------------------------------------------------------------------------
class Action(BaseModel):
"""A single action the agent takes."""
action_type: ActionType
# For WRITE_SECTION
section_name: Optional[SectionName] = None
section_content: Optional[str] = Field(
default=None,
description="Full text content to write into the section"
)
# For QUERY_LOGS
query_service: Optional[str] = Field(
default=None,
description="Service name to query logs for"
)
query_from: Optional[str] = Field(
default=None,
description="Start of time window, e.g. '03:42'"
)
query_to: Optional[str] = Field(
default=None,
description="End of time window, e.g. '03:45'"
)
# For ASSIGN_ACTION_ITEM
action_item_description: Optional[str] = None
action_item_owner: Optional[str] = None
action_item_due_date: Optional[str] = None
# For REVISE_SECTION (multi-agent extension)
# Reuses section_name + section_content from WRITE_SECTION fields above.
critique_addressed_index: Optional[int] = Field(
default=None,
description="Index into skeptic_critiques list that this revision addresses (0-based)."
)
# ---------------------------------------------------------------------------
# Reward β€” returned alongside observation after each step
# ---------------------------------------------------------------------------
class RewardBreakdown(BaseModel):
"""Detailed breakdown so the agent (and judges) can see exactly why."""
section_written: float = 0.0
correct_query: float = 0.0
action_item_assigned: float = 0.0
overwrite_penalty: float = 0.0
bad_query_penalty: float = 0.0
missing_section_penalty: float = 0.0
no_submit_penalty: float = 0.0
# Multi-agent extension β€” Phase 1
review_requested: float = 0.0 # +0.04 for valid REQUEST_REVIEW
critique_addressed: float = 0.0 # +0.06 for REVISE_SECTION that addresses a critique
spurious_revision: float = 0.0 # -0.03 for REVISE_SECTION without an outstanding critique
class Reward(BaseModel):
total: float = Field(..., description="Net reward this step")
breakdown: RewardBreakdown
cumulative: float = Field(..., description="Total reward so far this episode")
# ---------------------------------------------------------------------------
# Step result β€” what env.step() returns
# ---------------------------------------------------------------------------
class StepResult(BaseModel):
observation: Observation
reward: Reward
done: bool
info: Dict[str, Any] = Field(default_factory=dict)
# ---------------------------------------------------------------------------
# Final grader output β€” what env.grade() returns at SUBMIT
# ---------------------------------------------------------------------------
class GradeResult(BaseModel):
total_score: float = Field(..., ge=0.0, le=1.0)
# Sub-scores (all 0.0–1.0)
root_cause_score: float = 0.0
timeline_score: float = 0.0
action_items_score: float = 0.0
impact_score: float = 0.0
completeness_score: float = 0.0
# Multi-agent extension β€” Phase 1
collaboration_score: float = 0.0 # 0.0 if no critiques addressed; 1.0 if all addressed
# Modifiers
timeline_root_cause_cap_applied: bool = False
correct_queries_made: int = 0
critiques_received: int = 0
critiques_addressed: int = 0
explanation: str = ""