File size: 6,164 Bytes
29edd33
 
 
 
 
 
 
c35438f
 
 
 
 
 
 
 
 
094509b
 
 
 
c35438f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29edd33
 
 
 
 
 
c35438f
29edd33
 
 
76c31b2
c35438f
29edd33
 
 
 
c35438f
29edd33
 
c35438f
29edd33
 
 
c35438f
29edd33
 
c35438f
29edd33
c35438f
29edd33
 
 
 
 
 
 
 
c35438f
29edd33
c35438f
 
 
 
 
 
 
 
29edd33
c35438f
 
 
29edd33
c35438f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29edd33
 
c35438f
29edd33
c35438f
29edd33
 
 
 
c35438f
29edd33
c35438f
29edd33
 
 
 
 
 
 
 
 
 
c35438f
29edd33
76c31b2
c35438f
 
 
 
 
 
 
 
29edd33
 
 
c35438f
 
 
 
29edd33
 
c35438f
29edd33
 
c35438f
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
import random
from typing import Dict, Any, Optional

from openenv.models import Observation, Action, Reward, EnvState, StepResult, ResetResult
from openenv.tasks import TASK_REGISTRY
from openenv.reward import compute_reward

# Phase labels for logging and prompting
PHASE_LABELS = {
    1: "triage",
    2: "route",
    3: "resolve",
}

PHASE_INSTRUCTIONS = {
    1: (
        "PHASE 1 β€” TRIAGE: Read the ticket carefully and classify it.\n"
        "Identify the issue type and include it in the response field.\n"
        "Issue types: shipping, billing, technical, returns, safety, cancellation, complaint, orders, sales\n"
        "Required action: {\"action_type\": \"classify\", \"response\": \"<issue_type>\"}"
    ),
    2: (
        "PHASE 2 β€” ROUTE: Assign the ticket to the correct specialist team.\n"
        "Required action: {\"action_type\": \"assign\", \"team\": \"<team_name>\"}\n"
        "Teams: logistics_team | tech_support_team | safety_team | finance_team | orders_team | management_team"
    ),
    3: (
        "PHASE 3 β€” RESOLVE: Take the final resolution action.\n"
        "Choose ONE of:\n"
        "  escalate β€” angry/manager request/safety emergency (include team + response)\n"
        "  refund   β€” provably company fault/wrong product (include response)\n"
        "  respond  β€” customer needs information (write detailed response)"
    ),
}


class SupportEnv:
    def __init__(self, task_name: str = "easy"):
        self.task_name = task_name
        self.scenarios = TASK_REGISTRY.get(task_name, TASK_REGISTRY["easy"])
        self.current_scenario = None
        self.step_count = 0
        self.phase = 1
        self.history = []
        self.done = False
        self.total_reward = 0.0
        self.reset(task_name)

    def reset(self, task_name: Optional[str] = None) -> ResetResult:
        if task_name:
            self.task_name = task_name
            self.scenarios = TASK_REGISTRY.get(task_name, TASK_REGISTRY["easy"])

        self.current_scenario = random.choice(self.scenarios)
        self.step_count = 0
        self.phase = 1
        self.history = []
        self.done = False
        self.total_reward = 0.0

        obs = self.current_scenario.ticket.copy(deep=True)
        obs.history = self.history.copy()

        return ResetResult(observation=obs, done=False, info={})

    def step(self, action: Action) -> StepResult:
        if self.done:
            return StepResult(
                observation=self.get_current_observation(),
                reward=0.0,
                done=True,
                info={"error": "Episode already done."}
            )

        self.step_count += 1
        gt = self.current_scenario.ground_truth

        # Log action to history
        act_str = f"[Phase {self.phase}/{PHASE_LABELS[self.phase]}] Agent: {action.action_type}"
        if action.team:
            act_str += f" β†’ {action.team}"
        if action.response:
            act_str += f" | \"{action.response[:80]}{'...' if len(action.response) > 80 else ''}\""
        self.history.append(act_str)

        # Grade this step against its phase
        reward = compute_reward(action, gt, self.step_count, phase=self.phase)
        self.total_reward += reward.score

        # Advance phase and update observation state
        if self.phase == 1:
            # After triage: reveal the true issue_type so phase 2 routing is informed
            self.current_scenario.ticket.issue_type = gt.issue_type
            self.history.append(
                f"System: Issue classified as '{gt.issue_type}'. Proceed to route the ticket."
            )
            self.phase = 2

        elif self.phase == 2:
            # After routing: record which team was assigned; prepare for resolution
            assigned = action.team or "(no team)"
            self.history.append(
                f"System: Ticket routed to '{assigned}'. Proceed to resolve the customer's issue."
            )
            self.phase = 3

        elif self.phase == 3:
            # Resolution phase β€” episode terminates here
            if action.action_type == "respond" and reward.score < 0.5:
                # Poor response: customer is still unhappy, allow one retry (up to max_steps)
                self.history.append(
                    "Customer: I'm still not satisfied. Could you be more specific?"
                )
                # Stay in phase 3 for retry
            else:
                self.done = True
                if action.action_type in ("refund", "escalate"):
                    self.history.append("System: Ticket escalated/refunded and closed.")
                else:
                    self.history.append("Customer: Thank you, that answers my question!")

        # Hard cap at 5 steps
        if self.step_count >= 5:
            self.done = True

        obs = self.get_current_observation()

        return StepResult(
            observation=obs,
            reward=reward.score,
            done=self.done,
            info={"feedback": reward.feedback, "phase": self.phase}
        )

    def state(self) -> EnvState:
        return EnvState(
            observation=self.get_current_observation(),
            step_count=self.step_count,
            done=self.done,
            episode_id="local_run",
            total_reward=self.total_reward,
            task_name=self.task_name,
            max_steps=5
        )

    def get_current_observation(self) -> Observation:
        if not self.current_scenario:
            return Observation(
                ticket_id="None",
                issue_type="None",
                sentiment="None",
                priority="None",
                message="No active scenario. Please reset.",
                history=self.history.copy()
            )
        obs = self.current_scenario.ticket.copy(deep=True)
        obs.history = self.history.copy()
        return obs

    def get_phase_instruction(self) -> str:
        return PHASE_INSTRUCTIONS.get(self.phase, PHASE_INSTRUCTIONS[3])

    def close(self):
        pass

    @classmethod
    def from_docker_image(cls, image_name: str = None):
        return cls()