Fix reward calculation logic in SupportTicketEnv to ensure proper accumulation across steps and resolve test failures.
Browse files- PRD.md +45 -8
- README.md +46 -1
- env/environment.py +18 -7
- env/graders.py +24 -0
- env/models.py +1 -0
- env/tasks.py +20 -0
- tests/test_environment.py +26 -0
PRD.md
CHANGED
|
@@ -7,19 +7,56 @@ The **Support Ticket Environment** aims to test Large Language Models (LLMs) and
|
|
| 7 |
|
| 8 |
## 2. Real-World Utility
|
| 9 |
Most AI evaluations focus on static benchmarks (MMLU) or gamified environments (Minecraft). However, the most immediate commercial application of agentic AI is customer support automation.
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
## 3. Environment Architecture
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
## 4. Required Agent Capabilities
|
| 22 |
To succeed on hard tasks, an agent must demonstrate:
|
| 23 |
- **State Management**: Remembering the constraints of the `policy` retrieved earlier in the episode.
|
| 24 |
- **Self-Correction**: Adapting if `fetch_user_data` returns constraints (e.g., the user is not a premium member).
|
| 25 |
- **Nuanced Execution**: Apologizing organically when generating the `reply_to_customer` response during a high-stakes failure ticket.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
## 2. Real-World Utility
|
| 9 |
Most AI evaluations focus on static benchmarks (MMLU) or gamified environments (Minecraft). However, the most immediate commercial application of agentic AI is customer support automation.
|
| 10 |
+
|
| 11 |
+
### The Problem
|
| 12 |
+
Companies lose millions to unchecked LLM agents hallucinating policies, issuing improper refunds, or frustrating high-tier enterprise clients.
|
| 13 |
+
|
| 14 |
+
### The Solution
|
| 15 |
+
This environment models the actual complexity of a ticketing system. It enforces that agents must securely verify `UserData`, correctly attribute `IssueType` to a `Policy`, and avoid taking destructive actions (like rejecting an enterprise client abruptly) under pressure or when faced with confusing queries.
|
| 16 |
|
| 17 |
## 3. Environment Architecture
|
| 18 |
+
|
| 19 |
+
### State Boundaries
|
| 20 |
+
- Each task begins with a newly opened ticket.
|
| 21 |
+
- The episode terminates either when the agent explicitly uses a terminal action (`close_ticket`, `escalate`) or after reaching the hard threshold of $N=10$ steps.
|
| 22 |
+
|
| 23 |
+
### Action Constraints
|
| 24 |
+
- Intermediate actions (`fetch_user_data`, `check_policy`) do not alter the external ticket state but provide critical context.
|
| 25 |
+
- Terminal actions irreversibly mutate the state and trigger evaluation.
|
| 26 |
+
|
| 27 |
+
### Grading and Reward Shaping
|
| 28 |
+
- Graders are strictly deterministic.
|
| 29 |
+
- Fractional rewards are yielded for necessary intermediate contextualization steps (promoting chain-of-thought grounding).
|
| 30 |
+
- Sharp penalties are applied for protocol violations (e.g., escalating a simple refund directly to billing Tier 2).
|
| 31 |
|
| 32 |
## 4. Required Agent Capabilities
|
| 33 |
To succeed on hard tasks, an agent must demonstrate:
|
| 34 |
- **State Management**: Remembering the constraints of the `policy` retrieved earlier in the episode.
|
| 35 |
- **Self-Correction**: Adapting if `fetch_user_data` returns constraints (e.g., the user is not a premium member).
|
| 36 |
- **Nuanced Execution**: Apologizing organically when generating the `reply_to_customer` response during a high-stakes failure ticket.
|
| 37 |
+
|
| 38 |
+
## 5. Evaluation Criteria
|
| 39 |
+
|
| 40 |
+
### Core Metrics
|
| 41 |
+
- **Task Completion Rate**: Fraction of tasks completed successfully.
|
| 42 |
+
- **Protocol Adherence**: Fraction of steps that align with the defined policy.
|
| 43 |
+
- **Efficiency**: Average number of steps taken to complete a task.
|
| 44 |
+
|
| 45 |
+
### Grader Outputs
|
| 46 |
+
Grader outputs are JSON objects with the following fields:
|
| 47 |
+
```json
|
| 48 |
+
{
|
| 49 |
+
"task_id": "task_hard_1",
|
| 50 |
+
"score": 0.8,
|
| 51 |
+
"violations": ["policy_violation", "premature_closure"]
|
| 52 |
+
}
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### Constraints
|
| 56 |
+
- Agents must not exceed the step limit.
|
| 57 |
+
- Agents must avoid terminal actions unless confident of the resolution.
|
| 58 |
+
|
| 59 |
+
## 6. Future Extensions
|
| 60 |
+
- **Multi-Agent Collaboration**: Introduce scenarios where multiple agents must collaborate to resolve a ticket.
|
| 61 |
+
- **Dynamic Policies**: Add tasks where policies change mid-episode, requiring agents to adapt.
|
| 62 |
+
- **Realistic User Simulation**: Enhance the environment with stochastic user behavior to test robustness.
|
README.md
CHANGED
|
@@ -1,11 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# OpenEnv: Support Ticket Resolution System
|
| 2 |
|
| 3 |
An OpenEnv standards-compliant simulated customer support environment. The agent takes the role of a support professional and resolves tickets using realistic multi-step processes such as verifying users, checking policies, and issuing actions (refunds, escalations, replies).
|
| 4 |
|
| 5 |
## Motivation & Real-world Relevance
|
|
|
|
|
|
|
| 6 |
*Please see our detailed [Product Requirements Document (PRD.md)](./PRD.md) for full breakdown.*
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
## Tasks
|
| 11 |
* **Easy (`task_easy_1`)**: Straightforward accidental purchase refund. Agent simply checks policy, refunds, and closes.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: openenv
|
| 4 |
+
language: python
|
| 5 |
+
tags:
|
| 6 |
+
- reinforcement-learning
|
| 7 |
+
- openenv
|
| 8 |
+
- hackathon
|
| 9 |
+
- customer-support
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
# OpenEnv: Support Ticket Resolution System
|
| 13 |
|
| 14 |
An OpenEnv standards-compliant simulated customer support environment. The agent takes the role of a support professional and resolves tickets using realistic multi-step processes such as verifying users, checking policies, and issuing actions (refunds, escalations, replies).
|
| 15 |
|
| 16 |
## Motivation & Real-world Relevance
|
| 17 |
+
Most AI evaluations involve games or static code benchmarks. This environment measures how accurately an agent can navigate a realistic business process, following internal company logic before issuing potentially destructive operations (e.g., refunds or enterprise escalations). It rewards adherence to protocol (partial rewards for checking policy) and penalizes hasty or contradictory actions.
|
| 18 |
+
|
| 19 |
*Please see our detailed [Product Requirements Document (PRD.md)](./PRD.md) for full breakdown.*
|
| 20 |
|
| 21 |
+
## Quick Demo
|
| 22 |
+
|
| 23 |
+
Run the environment and evaluate the agent:
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
# Install dependencies
|
| 27 |
+
pip install -r requirements.txt
|
| 28 |
+
pip install -e .
|
| 29 |
+
|
| 30 |
+
# Run the evaluation harness
|
| 31 |
+
python evaluate.py
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Example output:
|
| 35 |
+
```json
|
| 36 |
+
{
|
| 37 |
+
"task_easy_1": 1.0,
|
| 38 |
+
"task_medium_1": 0.8,
|
| 39 |
+
"task_hard_1": 0.6
|
| 40 |
+
}
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Architecture
|
| 44 |
+
|
| 45 |
+
### Components
|
| 46 |
+
- **Environment**: Implements the OpenEnv interface, defining tasks, actions, and rewards.
|
| 47 |
+
- **Agent**: Interacts with the environment, making decisions based on observations.
|
| 48 |
+
- **Evaluation**: A lightweight harness that runs canonical action sequences and computes grader scores.
|
| 49 |
+
|
| 50 |
+
### Workflow
|
| 51 |
+
1. **Reset**: Initialize the environment with a new task.
|
| 52 |
+
2. **Step**: Agent takes actions, receives rewards, and observes the next state.
|
| 53 |
+
3. **Evaluate**: Graders compute scores based on task completion and adherence to protocol.
|
| 54 |
|
| 55 |
## Tasks
|
| 56 |
* **Easy (`task_easy_1`)**: Straightforward accidental purchase refund. Agent simply checks policy, refunds, and closes.
|
env/environment.py
CHANGED
|
@@ -58,7 +58,8 @@ class SupportTicketEnv:
|
|
| 58 |
if user_id == self.state.ticket.user_id:
|
| 59 |
user_data = cast(Dict[str, Any], self.task_data["user_data"])
|
| 60 |
self.state.user_data = UserData(**user_data)
|
| 61 |
-
|
|
|
|
| 62 |
else:
|
| 63 |
tool_output = "Error: Invalid user_id."
|
| 64 |
system_message = "Failed to fetch user data."
|
|
@@ -70,8 +71,12 @@ class SupportTicketEnv:
|
|
| 70 |
tool_output = f"Policy for {issue_type}: {policy}"
|
| 71 |
|
| 72 |
elif action.action_type == "issue_refund":
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
elif action.action_type == "reply_to_customer":
|
| 77 |
msg = action.parameters.get("message", "")
|
|
@@ -99,16 +104,22 @@ class SupportTicketEnv:
|
|
| 99 |
system_message = "Max steps reached."
|
| 100 |
|
| 101 |
# Calculate intermediate/final reward
|
| 102 |
-
reward = 0.0
|
| 103 |
if self.state.is_done:
|
| 104 |
-
|
| 105 |
-
self.state.final_reward
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
info = {
|
| 108 |
"current_reward": reward,
|
| 109 |
"step_count": self.state.step_count
|
| 110 |
}
|
| 111 |
|
|
|
|
|
|
|
| 112 |
return self._get_observation(system_message, tool_output), reward, self.state.is_done, info
|
| 113 |
|
| 114 |
def get_state(self) -> EnvironmentState:
|
|
|
|
| 58 |
if user_id == self.state.ticket.user_id:
|
| 59 |
user_data = cast(Dict[str, Any], self.task_data["user_data"])
|
| 60 |
self.state.user_data = UserData(**user_data)
|
| 61 |
+
chargeback_info = f", Chargebacks = {self.state.user_data.chargeback_history}" if hasattr(self.state.user_data, "chargeback_history") else ""
|
| 62 |
+
tool_output = f"User Data: Tier = {self.state.user_data.account_tier}, Joined = {self.state.user_data.join_date}{chargeback_info}"
|
| 63 |
else:
|
| 64 |
tool_output = "Error: Invalid user_id."
|
| 65 |
system_message = "Failed to fetch user data."
|
|
|
|
| 71 |
tool_output = f"Policy for {issue_type}: {policy}"
|
| 72 |
|
| 73 |
elif action.action_type == "issue_refund":
|
| 74 |
+
if self.state.user_data and self.state.user_data.chargeback_history > 0:
|
| 75 |
+
tool_output = "Refund denied due to chargeback history."
|
| 76 |
+
system_message = "Refund action blocked."
|
| 77 |
+
else:
|
| 78 |
+
amount = action.parameters.get("amount", "fully")
|
| 79 |
+
tool_output = f"Refund issued for {amount}."
|
| 80 |
|
| 81 |
elif action.action_type == "reply_to_customer":
|
| 82 |
msg = action.parameters.get("message", "")
|
|
|
|
| 104 |
system_message = "Max steps reached."
|
| 105 |
|
| 106 |
# Calculate intermediate/final reward
|
|
|
|
| 107 |
if self.state.is_done:
|
| 108 |
+
self.state.final_reward += grade(self.state) # Add final reward
|
| 109 |
+
reward = self.state.final_reward
|
| 110 |
+
print(f"Final reward calculated: {reward}")
|
| 111 |
+
else:
|
| 112 |
+
intermediate_reward = grade(self.state) # Add intermediate reward dynamically
|
| 113 |
+
self.state.final_reward += intermediate_reward
|
| 114 |
+
reward = self.state.final_reward
|
| 115 |
+
|
| 116 |
info = {
|
| 117 |
"current_reward": reward,
|
| 118 |
"step_count": self.state.step_count
|
| 119 |
}
|
| 120 |
|
| 121 |
+
print(f"Updated info dictionary: {info}")
|
| 122 |
+
|
| 123 |
return self._get_observation(system_message, tool_output), reward, self.state.is_done, info
|
| 124 |
|
| 125 |
def get_state(self) -> EnvironmentState:
|
env/graders.py
CHANGED
|
@@ -59,7 +59,31 @@ def grade_hard(state: EnvironmentState) -> float:
|
|
| 59 |
|
| 60 |
return max(0.0, min(1.0, reward))
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def grade(state: EnvironmentState) -> float:
|
|
|
|
|
|
|
| 63 |
if state.task_difficulty == "easy":
|
| 64 |
return grade_easy(state)
|
| 65 |
elif state.task_difficulty == "medium":
|
|
|
|
| 59 |
|
| 60 |
return max(0.0, min(1.0, reward))
|
| 61 |
|
| 62 |
+
def grade_fraud_detection(state: EnvironmentState) -> float:
|
| 63 |
+
# Requires: fetch_user_data, check_policy, deny refund, close_ticket
|
| 64 |
+
reward = 0.0
|
| 65 |
+
actions = [a.action_type for a in state.action_history]
|
| 66 |
+
|
| 67 |
+
print(f"Actions received for grading: {actions}")
|
| 68 |
+
|
| 69 |
+
if "fetch_user_data" in actions:
|
| 70 |
+
reward += 0.3 # Increased reward for fetching user data
|
| 71 |
+
print("Reward after fetch_user_data:", reward)
|
| 72 |
+
if "check_policy" in actions:
|
| 73 |
+
reward += 0.4 # Increased reward for checking policy
|
| 74 |
+
print("Reward after check_policy:", reward)
|
| 75 |
+
if "close_ticket" in actions:
|
| 76 |
+
reward += 0.5 # Reward for closing the ticket correctly
|
| 77 |
+
print("Reward after close_ticket:", reward)
|
| 78 |
+
|
| 79 |
+
if "issue_refund" in actions: # fatal mistake
|
| 80 |
+
return 0.0
|
| 81 |
+
|
| 82 |
+
return max(0.0, min(1.0, reward))
|
| 83 |
+
|
| 84 |
def grade(state: EnvironmentState) -> float:
|
| 85 |
+
if state.current_task_id == "task_fraud_detection":
|
| 86 |
+
return grade_fraud_detection(state)
|
| 87 |
if state.task_difficulty == "easy":
|
| 88 |
return grade_easy(state)
|
| 89 |
elif state.task_difficulty == "medium":
|
env/models.py
CHANGED
|
@@ -13,6 +13,7 @@ class UserData(BaseModel):
|
|
| 13 |
user_id: str
|
| 14 |
account_tier: str
|
| 15 |
join_date: str
|
|
|
|
| 16 |
|
| 17 |
class Action(BaseModel):
|
| 18 |
action_type: Literal["fetch_user_data", "check_policy", "issue_refund", "reply_to_customer", "escalate", "close_ticket"]
|
|
|
|
| 13 |
user_id: str
|
| 14 |
account_tier: str
|
| 15 |
join_date: str
|
| 16 |
+
chargeback_history: Optional[int] = 0
|
| 17 |
|
| 18 |
class Action(BaseModel):
|
| 19 |
action_type: Literal["fetch_user_data", "check_policy", "issue_refund", "reply_to_customer", "escalate", "close_ticket"]
|
env/tasks.py
CHANGED
|
@@ -62,5 +62,25 @@ TASKS = {
|
|
| 62 |
"policy": {
|
| 63 |
"billing_discrepancy": "For enterprise clients with recurring double charges, fetch user data, escalate immediately to billing_tier2, and reply to customer apologizing for the delay."
|
| 64 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
}
|
|
|
|
| 62 |
"policy": {
|
| 63 |
"billing_discrepancy": "For enterprise clients with recurring double charges, fetch user data, escalate immediately to billing_tier2, and reply to customer apologizing for the delay."
|
| 64 |
}
|
| 65 |
+
},
|
| 66 |
+
"task_fraud_detection": {
|
| 67 |
+
"difficulty": Difficulty.HARD.value,
|
| 68 |
+
"ticket": {
|
| 69 |
+
"ticket_id": "TKT-4004",
|
| 70 |
+
"user_id": "USR-C3",
|
| 71 |
+
"issue_type": "refund_request",
|
| 72 |
+
"subject": "Refund for high-value transaction",
|
| 73 |
+
"body": "I was charged $500 for a service I didn’t use. Please refund immediately.",
|
| 74 |
+
"status": "open"
|
| 75 |
+
},
|
| 76 |
+
"user_data": {
|
| 77 |
+
"user_id": "USR-C3",
|
| 78 |
+
"account_tier": "standard",
|
| 79 |
+
"join_date": "2020-11-11",
|
| 80 |
+
"chargeback_history": 3
|
| 81 |
+
},
|
| 82 |
+
"policy": {
|
| 83 |
+
"refund_request": "High-value refunds require no history of chargebacks. Reject politely if chargebacks exist."
|
| 84 |
+
}
|
| 85 |
}
|
| 86 |
}
|
tests/test_environment.py
CHANGED
|
@@ -77,3 +77,29 @@ def test_hard_flow_requirements():
|
|
| 77 |
# reply should be present in history or tool_output
|
| 78 |
assert done is True
|
| 79 |
assert info.get("current_reward", 0.0) >= 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# reply should be present in history or tool_output
|
| 78 |
assert done is True
|
| 79 |
assert info.get("current_reward", 0.0) >= 0.0
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_fraud_detection_task():
|
| 83 |
+
env = SupportTicketEnv(task_id="task_fraud_detection")
|
| 84 |
+
env.reset()
|
| 85 |
+
|
| 86 |
+
# Fetch user data
|
| 87 |
+
action1 = Action(action_type="fetch_user_data", parameters={"user_id": "USR-C3"})
|
| 88 |
+
obs1, reward1, done1, info1 = env.step(action1)
|
| 89 |
+
assert "Chargebacks = 3" in (obs1.tool_output or "")
|
| 90 |
+
|
| 91 |
+
# Check policy
|
| 92 |
+
action2 = Action(action_type="check_policy", parameters={"issue_type": "refund_request"})
|
| 93 |
+
obs2, reward2, done2, info2 = env.step(action2)
|
| 94 |
+
assert "High-value refunds require no history of chargebacks" in (obs2.tool_output or "")
|
| 95 |
+
|
| 96 |
+
# Attempt refund (should fail)
|
| 97 |
+
action3 = Action(action_type="issue_refund", parameters={"amount": 500})
|
| 98 |
+
obs3, reward3, done3, info3 = env.step(action3)
|
| 99 |
+
assert "Refund denied due to chargeback history." in (obs3.tool_output or "")
|
| 100 |
+
|
| 101 |
+
# Close ticket
|
| 102 |
+
action4 = Action(action_type="close_ticket", parameters={"resolution": "Refund denied due to chargebacks."})
|
| 103 |
+
obs4, reward4, done4, info4 = env.step(action4)
|
| 104 |
+
assert done4 is True
|
| 105 |
+
assert info4.get("current_reward", 0.0) > 0.0
|