CoolFace
Apppublic

Raje19112003/Invoice_Dispute_Resolution_Environment

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
environment.py335 linesDownload Raw Back to server
1"""2Invoice Dispute Resolution Environment — Core Logic3Implements reset(), step(), and state() for the OpenEnv framework.4 5An AI agent receives a billing dispute scenario and must:6  1. Analyse the invoice, customer complaint, and company policy7  2. Choose the correct resolution (full refund, partial refund, reject, escalate, request_info)8  3. Draft a professional, policy-compliant customer response9 10DIFFICULTY LEVELS (3 Tasks):11  EASY       → Obvious disputes (duplicate charges, clear service failures)12  MEDIUM     → Ambiguous cases (wrong amount, customer history needed)13  HARD       → Complex cases (unauthorized charges, policy edge cases)14 15Reward is shaped by:16  - Correctness of the decision          (+0.5)17  - Policy compliance                    (+0.2)18  - Response quality (tone & clarity)    (+0.2)19  - Efficiency (fewer steps = bonus)     (+0.1)20  - Wrong decisions                      (-0.3)21  - Policy violations                    (-0.2)22"""23 24import random25import uuid26from datetime import datetime, timedelta27from typing import Optional, Literal28 29from models import DisputeAction, DisputeObservation, DisputeState30 31 32# ─────────────────────────────────────────────────────────────33# TASK DEFINITIONS - 3 difficulty levels34# ─────────────────────────────────────────────────────────────35 36# EASY: Obvious, clear-cut disputes37EASY_SCENARIOS = [38    {39        "dispute_type": "duplicate_charge",40        "invoice_amount": 99.99,41        "line_items": [42            {"item": "Pro Plan Subscription", "amount": 99.99},43            {"item": "Pro Plan Subscription", "amount": 99.99},44        ],45        "customer_message": (46            "I was charged twice for my Pro Plan this month. "47            "Please refund the duplicate charge of $99.99."48        ),49        "correct_decision": "full_refund",50        "expected_refund": 99.99,51        "difficulty": "easy",52    },53    {54        "dispute_type": "service_not_received",55        "invoice_amount": 59.99,56        "line_items": [57            {"item": "Premium Support (Monthly)", "amount": 59.99},58        ],59        "customer_message": (60            "I was billed for Premium Support but nobody responded to my tickets. "61            "This service was never provided."62        ),63        "correct_decision": "full_refund",64        "expected_refund": 59.99,65        "difficulty": "easy",66    },67]68 69# MEDIUM: Requires understanding customer history and policy70MEDIUM_SCENARIOS = [71    {72        "dispute_type": "wrong_amount",73        "invoice_amount": 249.99,74        "line_items": [75            {"item": "Enterprise Storage (500 GB)", "amount": 149.99},76            {"item": "API Add-on", "amount": 100.00},77        ],78        "customer_message": (79            "My invoice shows $249.99 but I was quoted $199.99 during sign-up. "80            "I have the email confirmation. This is incorrect billing."81        ),82        "correct_decision": "partial_refund",83        "expected_refund": 50.00,84        "difficulty": "medium",85    },86    {87        "dispute_type": "sla_breach",88        "invoice_amount": 199.99,89        "line_items": [90            {"item": "SLA Support Plan", "amount": 199.99},91        ],92        "customer_message": (93            "Your SLA guarantees 4-hour response time. I submitted a critical issue "94            "and got no response for 48 hours. This is a breach of contract."95        ),96        "correct_decision": "partial_refund",97        "expected_refund": 100.00,98        "difficulty": "medium",99    },100]101 102# HARD: Complex, ambiguous, requires policy knowledge103HARD_SCENARIOS = [104    {105        "dispute_type": "unauthorized_charge",106        "invoice_amount": 399.99,107        "line_items": [108            {"item": "Enterprise Upgrade", "amount": 399.99},109        ],110        "customer_message": (111            "I never authorised an upgrade to Enterprise. Someone on my team must "112            "have done this by mistake. Please reverse this charge."113        ),114        "correct_decision": "escalate",115        "expected_refund": None,116        "difficulty": "hard",117    },118    {119        "dispute_type": "policy_ambiguity",120        "invoice_amount": 1299.00,121        "line_items": [122            {"item": "Annual Enterprise Plan", "amount": 1299.00},123        ],124        "customer_message": (125            "Your pricing page showed $99/month for Enterprise, which is $1188/year. "126            "You charged $1299. Is this a mistake or a different plan?"127        ),128        "correct_decision": "escalate",129        "expected_refund": None,130        "difficulty": "hard",131    },132]133 134ALL_SCENARIOS = {135    "easy": EASY_SCENARIOS,136    "medium": MEDIUM_SCENARIOS,137    "hard": HARD_SCENARIOS,138}139 140CUSTOMER_TIERS = ["standard", "premium", "enterprise"]141 142POLICIES = {143    "standard":   {"max_auto_refund": 100, "escalate_above": 300, "response_sla_hours": 48},144    "premium":    {"max_auto_refund": 250, "escalate_above": 500, "response_sla_hours": 24},145    "enterprise": {"max_auto_refund": 500, "escalate_above": 1000, "response_sla_hours": 4},146}147 148 149# ─────────────────────────────────────────────────────────────150# ENVIRONMENT CLASS151# ─────────────────────────────────────────────────────────────152 153class InvoiceDisputeEnv:154    """155    OpenEnv-compatible Invoice Dispute Resolution Environment.156    Supports 3 difficulty levels: easy, medium, hard.157    """158 159    MAX_STEPS = 3160 161    def __init__(self, difficulty: Literal["easy", "medium", "hard"] = "medium"):162        self.difficulty = difficulty163        self._state: Optional[DisputeState] = None164        self._scenario = None165 166    # ── reset ─────────────────────────────────────────────────167    def reset(self) -> DisputeObservation:168        """Start a new dispute episode with a scenario from the selected difficulty."""169        scenario = random.choice(ALL_SCENARIOS[self.difficulty])170        tier = random.choice(CUSTOMER_TIERS)171        policy = POLICIES[tier]172 173        invoice_date = (datetime.today() - timedelta(days=random.randint(1, 14))).strftime("%Y-%m-%d")174 175        self._state = DisputeState(176            invoice_id=f"INV-{uuid.uuid4().hex[:8].upper()}",177            invoice_amount=scenario["invoice_amount"],178            invoice_date=invoice_date,179            line_items=scenario["line_items"],180            dispute_type=scenario["dispute_type"],181            customer_message=scenario["customer_message"],182            customer_tier=tier,183            customer_history={184                "total_orders": random.randint(1, 50),185                "disputes_filed": random.randint(0, 3),186                "churn_risk": random.choice(["low", "medium", "high"]),187            },188            policy=policy,189            step_count=0,190            max_steps=self.MAX_STEPS,191            is_done=False,192            total_reward=0.0,193            correct_decision=scenario["correct_decision"],194        )195 196        self._scenario = scenario197 198        return DisputeObservation(199            step_result="New dispute received. Analyse the invoice and customer complaint, then resolve.",200            reward=0.0,201            done=False,202            feedback="Episode started. No action taken yet.",203            customer_reaction=None,204        )205 206    # ── step ──────────────────────────────────────────────────207    def step(self, action: DisputeAction) -> DisputeObservation:208        """Process the agent's resolution decision and return reward + feedback."""209        if self._state is None:210            raise RuntimeError("Call reset() before step().")211        if self._state.is_done:212            raise RuntimeError("Episode already finished. Call reset() to start a new one.")213 214        self._state.step_count += 1215        reward = 0.0216        feedback_parts = []217 218        correct = self._scenario["correct_decision"]219        expected_refund = self._scenario.get("expected_refund")220        policy = self._state.policy221 222        # ── Decision correctness ───────────────────────────────223        if action.decision == correct:224            reward += 0.5225            feedback_parts.append("✅ Correct decision.")226        else:227            reward -= 0.3228            feedback_parts.append(f"❌ Wrong (expected {correct}, got {action.decision}).")229 230        # ── Policy compliance ──────────────────────────────────231        policy_ok = True232        if action.decision == "full_refund":233            if self._state.invoice_amount > policy["max_auto_refund"]:234                reward -= 0.2235                feedback_parts.append(236                    f"⚠️ Refund ${self._state.invoice_amount} exceeds limit ${policy['max_auto_refund']}."237                )238                policy_ok = False239 240        if action.decision == "partial_refund":241            if action.refund_amount is None:242                reward -= 0.1243                feedback_parts.append("⚠️ partial_refund chosen but refund_amount missing.")244                policy_ok = False245            elif expected_refund and abs(action.refund_amount - expected_refund) < 10:246                reward += 0.1247                feedback_parts.append(f"✅ Refund ${action.refund_amount} within range.")248 249        if action.decision == "escalate" and self._state.invoice_amount < policy["escalate_above"]:250            reward -= 0.1251            feedback_parts.append(f"⚠️ Escalation unnecessary (${self._state.invoice_amount} < ${policy['escalate_above']}).")252            policy_ok = False253 254        if policy_ok and action.decision == correct:255            reward += 0.2256            feedback_parts.append("✅ Policy compliant.")257 258        # ── Response quality ───────────────────────────────────259        response_score = self._grade_response(action.response_text, action.decision)260        reward += response_score * 0.2261        if response_score >= 0.8:262            feedback_parts.append("✅ Professional response.")263        elif response_score >= 0.5:264            feedback_parts.append("ℹ️ Response acceptable but could be better.")265        else:266            feedback_parts.append("❌ Response too brief or unprofessional.")267 268        # ── Efficiency bonus ───────────────────────────────────269        if self._state.step_count == 1 and action.decision == correct:270            reward += 0.1271            feedback_parts.append("🚀 Efficiency bonus (1-step resolution).")272 273        # ── Termination ────────────────────────────────────────274        done = False275        if action.decision in ("full_refund", "partial_refund", "reject", "escalate"):276            done = True277        elif self._state.step_count >= self._state.max_steps:278            done = True279            reward -= 0.2280            feedback_parts.append("⏰ Max steps exceeded.")281 282        reward = max(-1.0, min(1.0, reward))283 284        self._state.is_done = done285        self._state.total_reward += reward286 287        if done:288            self._state.correct_decision = correct289            feedback_parts.append(f"\n📋 Ground truth: {correct}")290 291        customer_reaction = self._simulate_customer_reaction(action.decision, reward)292 293        return DisputeObservation(294            step_result=f"Step {self._state.step_count}: {action.decision}",295            reward=round(reward, 4),296            done=done,297            feedback=" | ".join(feedback_parts),298            customer_reaction=customer_reaction,299        )300 301    # ── state ─────────────────────────────────────────────────302    @property303    def state(self) -> DisputeState:304        if self._state is None:305            raise RuntimeError("Call reset() first.")306        return self._state307 308    # ── helpers ───────────────────────────────────────────────309    def _grade_response(self, text: str, decision: str) -> float:310        if not text or len(text) < 20:311            return 0.0312        score = 0.5313        text_lower = text.lower()314        empathy = ["apologise", "sorry", "understand", "inconvenience", "appreciate"]315        if any(w in text_lower for w in empathy):316            score += 0.15317        action_words = ["refund", "credit", "escalate", "investigate", "resolve"]318        if any(w in text_lower for w in action_words):319            score += 0.15320        if text == text.upper():321            score -= 0.2322        if len(text) > 60:323            score += 0.1324        if len(text) > 150:325            score += 0.1326        return min(1.0, max(0.0, score))327 328    def _simulate_customer_reaction(self, decision: str, reward: float) -> str:329        if reward >= 0.6:330            return "Thank you! I appreciate the quick resolution."331        elif reward >= 0.2:332            return "OK, I understand. Let's proceed."333        else:334            return "I'm disappointed with this response."335