CoolFace
Apppublic

Raje19112003/Invoice_Dispute_Resolution_Environment

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
inference.py453 linesDownload Raw Back to root
1"""2Invoice Dispute Resolution Environment - Baseline Inference Script3Evaluates OpenAI models against the environment with proper grading.4 5Requirements:6- OpenAI API key via OPENAI_API_KEY environment variable7- HuggingFace token via HF_TOKEN environment variable (optional)8 9Usage:10    python inference.py11    python inference.py --model gpt-412    python inference.py --difficulty easy13"""14 15import os16import sys17import json18import argparse19import requests20from typing import Dict, List, Tuple21from datetime import datetime22from dotenv import load_dotenv23 24# Load environment variables25load_dotenv()26 27# OpenAI imports28try:29    from openai import OpenAI30except ImportError:31    print("❌ OpenAI package not found. Install with: pip install openai")32    sys.exit(1)33 34 35class InvoiceDisputeAgent:36    """Agent that uses OpenAI-compatible API to resolve disputes."""37    38    def __init__(self, api_url: str = "http://localhost:7860", model: str = None):39        """40        Initialize agent with OpenAI-compatible client.41        42        Args:43            api_url: Base URL for the environment API44            model: Model name (uses env vars for API config)45            46        Environment variables (injected by judges or set locally):47            - API_KEY: API key for the LLM provider48            - API_BASE_URL: Base URL for the LLM provider (e.g., https://router.huggingface.co/v1)49            - MODEL_NAME: Model name to use50        """51        self.api_url = api_url52        53        # Get configuration from environment variables (injected by judges)54        api_key = os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY")55        api_base_url = os.getenv("API_BASE_URL", "https://api.openai.com/v1")56        model_name = os.getenv("MODEL_NAME", model or "gpt-3.5-turbo")57        58        self.model = model_name59        60        # Initialize OpenAI-compatible client61        # This works with ANY provider (OpenAI, HuggingFace, etc.)62        self.client = OpenAI(63            api_key=api_key,64            base_url=api_base_url65        )66        67        # Track statistics68        self.episodes_completed = 069        self.total_reward = 0.070        self.episode_rewards = []71        self.decision_counts = {}72        73    def reset_environment(self, difficulty: str) -> Dict:74        """Reset environment and get initial observation."""75        try:76            response = requests.post(77                f"{self.api_url}/reset",78                json={"difficulty": difficulty},79                timeout=1080            )81            response.raise_for_status()82            return response.json()83        except Exception as e:84            print(f"❌ Error resetting environment: {e}")85            raise86    87    def get_state(self) -> Dict:88        """Get current environment state."""89        try:90            response = requests.get(f"{self.api_url}/state", timeout=10)91            response.raise_for_status()92            return response.json()93        except Exception as e:94            print(f"❌ Error getting state: {e}")95            raise96    97    def submit_action(self, action: Dict) -> Dict:98        """Submit action to environment and get observation."""99        try:100            response = requests.post(101                f"{self.api_url}/step",102                json=action,103                timeout=10104            )105            response.raise_for_status()106            return response.json()107        except Exception as e:108            print(f"❌ Error submitting action: {e}")109            raise110    111    def generate_decision(self, state: Dict) -> Tuple[str, str, float]:112        """113        Use OpenAI API to generate decision.114        115        Args:116            state: Current environment state117            118        Returns:119            Tuple of (decision, response_text, refund_amount)120        """121        # Build context prompt122        prompt = f"""You are an expert customer service manager handling billing disputes.123 124INVOICE DETAILS:125- Invoice ID: {state['invoice_id']}126- Amount: ${state['invoice_amount']:.2f}127- Date: {state['invoice_date']}128- Type: {state['dispute_type'].replace('_', ' ').title()}129 130CUSTOMER INFORMATION:131- Tier: {state['customer_tier'].upper()}132- Total Orders: {state['customer_history']['total_orders']}133- Prior Disputes: {state['customer_history']['disputes_filed']}134- Churn Risk: {state['customer_history']['churn_risk'].upper()}135 136COMPANY POLICY:137- Max Auto-Refund: ${state['policy']['max_auto_refund']:.2f}138- Escalate Above: ${state['policy']['escalate_above']:.2f}139- Response SLA: {state['policy']['response_sla_hours']} hours140 141CUSTOMER COMPLAINT:142{state['customer_message']}143 144LINE ITEMS:145{json.dumps(state['line_items'], indent=2)}146 147Based on the evidence, company policy, and customer history, decide on ONE of these actions:1481. full_refund - Approve 100% refund1492. partial_refund - Approve partial refund (specify amount)1503. reject - Deny the dispute1514. escalate - Escalate to human supervisor1525. request_info - Ask customer for more information153 154Respond in JSON format:155{{156    "decision": "<one of the 5 options>",157    "reasoning": "<brief explanation>",158    "refund_amount": <number or null>159}}"""160 161        try:162            response = self.client.chat.completions.create(163                model=self.model,164                messages=[165                    {"role": "system", "content": "You are a customer service expert."},166                    {"role": "user", "content": prompt}167                ],168                temperature=0.7,169                max_tokens=500170            )171            172            # Parse response173            content = response.choices[0].message.content174            decision_data = json.loads(content)175            176            decision = decision_data.get("decision", "request_info")177            reasoning = decision_data.get("reasoning", "")178            refund_amount = decision_data.get("refund_amount")179            180            # Generate professional response text181            response_prompt = f"""Write a brief, professional customer service response to this dispute.182Decision: {decision}183Reasoning: {reasoning}184Max length: 100 words185Keep it empathetic and professional."""186            187            response_text_obj = self.client.chat.completions.create(188                model=self.model,189                messages=[{"role": "user", "content": response_prompt}],190                temperature=0.5,191                max_tokens=100192            )193            194            response_text = response_text_obj.choices[0].message.content195            196            return decision, response_text, refund_amount197        198        except Exception as e:199            print(f"⚠️  Error generating decision: {e}")200            # Fallback to request_info201            return "request_info", "We need more information to resolve this dispute.", None202    203    def run_episode(self, difficulty: str = "medium") -> Dict:204        """205        Run a single episode with OpenEnv-compliant structured logging.206        Ensures:207        - No crashes208        - Proper stdout format209        - Score strictly in (0,1)210        """211    212        try:213            # ✅ START BLOCK214            print(f"[START] task=invoice-dispute difficulty={difficulty}", flush=True)215    216            obs = self.reset_environment(difficulty)217    218            episode_reward = 0.0219            steps = 0220            max_steps = 10221    222            while not obs.get("done", False) and steps < max_steps:223                steps += 1224    225                try:226                    # Get state227                    state = self.get_state()228    229                    # Generate decision230                    decision, response_text, refund_amount = self.generate_decision(state)231    232                    action = {233                        "decision": decision,234                        "response_text": response_text,235                        "refund_amount": refund_amount236                    }237    238                    # Step environment239                    obs = self.submit_action(action)240    241                    reward = obs.get("reward", 0.0)242                    episode_reward += reward243    244                    # ✅ STEP BLOCK245                    print(f"[STEP] step={steps} reward={reward}", flush=True)246    247                except Exception:248                    # Fail-safe per step (no crash)249                    print(f"[STEP] step={steps} reward=0.0", flush=True)250                    continue251    252            # ✅ Normalize + clamp score to (0, 1)253            final_score = (episode_reward + 1) / 2  # map [-1,1] → [0,1]254            final_score = max(0.01, min(0.99, final_score))255    256            # ✅ END BLOCK257            print(258                f"[END] task=invoice-dispute score={final_score} steps={steps}",259                flush=True260            )261    262            return {263                "difficulty": difficulty,264                "reward": episode_reward,265                "steps": steps,266                "completed": True267            }268    269        except Exception as e:270            # ✅ GLOBAL FAIL-SAFE (prevents evaluator crash)271            print(f"[END] task=invoice-dispute score=0.01 steps=0", flush=True)272    273            return {274                "difficulty": difficulty,275                "reward": 0.0,276                "steps": 0,277                "completed": False,278                "error": str(e)279            }280        281    def evaluate(self, difficulties: List[str] = None, episodes_per_difficulty: int = 3):282        """283        Run full evaluation across difficulties.284        285        Args:286            difficulties: List of difficulties to evaluate287            episodes_per_difficulty: Number of episodes per difficulty288        """289        if difficulties is None:290            difficulties = ["easy", "medium", "hard"]291        292        results = {293            "model": self.model,294            "timestamp": datetime.now().isoformat(),295            "results_by_difficulty": {},296            "summary": {}297        }298        299        for difficulty in difficulties:300            print(f"\n{'='*60}")301            print(f"EVALUATING {difficulty.upper()} DIFFICULTY")302            print(f"{'='*60}")303            304            difficulty_rewards = []305            difficulty_steps = []306            307            for episode in range(episodes_per_difficulty):308                try:309                    print(f"\nEpisode {episode + 1}/{episodes_per_difficulty}")310                    episode_result = self.run_episode(difficulty)311                    difficulty_rewards.append(episode_result["reward"])312                    difficulty_steps.append(episode_result["steps"])313                except Exception as e:314                    print(f"⚠️  Error in episode: {e}")315                    continue316            317            # Calculate statistics318            if difficulty_rewards:319                avg_reward = sum(difficulty_rewards) / len(difficulty_rewards)320                max_reward = max(difficulty_rewards)321                min_reward = min(difficulty_rewards)322                avg_steps = sum(difficulty_steps) / len(difficulty_steps)323                324                results["results_by_difficulty"][difficulty] = {325                    "episodes": len(difficulty_rewards),326                    "average_reward": avg_reward,327                    "max_reward": max_reward,328                    "min_reward": min_reward,329                    "average_steps": avg_steps,330                    "all_rewards": difficulty_rewards331                }332        333        # Overall summary334        if self.episode_rewards:335            results["summary"] = {336                "total_episodes": len(self.episode_rewards),337                "overall_average_reward": sum(self.episode_rewards) / len(self.episode_rewards),338                "total_reward": sum(self.episode_rewards)339            }340        341        return results342    343    def print_results(self, results: Dict):344        """Pretty print evaluation results."""345        print(f"\n{'='*60}")346        print("📊 EVALUATION RESULTS")347        print(f"{'='*60}\n")348        349        print(f"Model: {results['model']}")350        print(f"Timestamp: {results['timestamp']}\n")351        352        for difficulty, stats in results["results_by_difficulty"].items():353            print(f"\n{difficulty.upper()}:")354            print(f"  Episodes: {stats['episodes']}")355            print(f"  Average Reward: {stats['average_reward']:.4f}")356            print(f"  Max Reward: {stats['max_reward']:.4f}")357            print(f"  Min Reward: {stats['min_reward']:.4f}")358            print(f"  Average Steps: {stats['average_steps']:.2f}")359        360        if "summary" in results:361            print(f"\n{'─'*60}")362            print(f"OVERALL:")363            print(f"  Total Episodes: {results['summary']['total_episodes']}")364            print(f"  Overall Average Reward: {results['summary']['overall_average_reward']:.4f}")365            print(f"  Total Reward: {results['summary']['total_reward']:.4f}")366        367        print(f"\n{'='*60}\n")368 369 370def main():371    """Main entry point."""372    parser = argparse.ArgumentParser(373        description="Evaluate agents on Invoice Dispute Resolution environment"374    )375    parser.add_argument(376        "--model",377        default=os.getenv("MODEL_NAME", "gpt-3.5-turbo"),378        help="Model to use (default: env var MODEL_NAME or gpt-3.5-turbo)"379    )380    parser.add_argument(381        "--difficulty",382        choices=["easy", "medium", "hard", "all"],383        default="all",384        help="Difficulty level to evaluate"385    )386    parser.add_argument(387        "--episodes",388        type=int,389        default=3,390        help="Episodes per difficulty (default: 3)"391    )392    parser.add_argument(393        "--api-url",394        default="http://localhost:7860",395        help="Base URL for environment API"396    )397    parser.add_argument(398        "--output",399        help="Output file for results (JSON)"400    )401    402    args = parser.parse_args()403    404    # Check required environment variables405    api_key = os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY")406    api_base_url = os.getenv("API_BASE_URL", "https://api.openai.com/v1")407    408    if not api_key:409        print("⚠️  API_KEY or OPENAI_API_KEY environment variable not set")410        print("\nFor local development, use free options:")411        print("  1. HuggingFace: export API_KEY='hf_...'")412        print("     export API_BASE_URL='https://router.huggingface.co/v1'")413        print("     export MODEL_NAME='Qwen/Qwen2.5-72B-Instruct'")414        print("\n  2. OpenAI: export API_KEY='sk-...'")415        print("     export API_BASE_URL='https://api.openai.com/v1'")416        print("\nDuring evaluation, judges will inject API_KEY and API_BASE_URL")417        print("Continuing with available configuration...")418    419    # Create agent420    print(f"🤖 Initializing agent with model: {args.model}")421    print(f"   API Base URL: {api_base_url}")422    agent = InvoiceDisputeAgent(api_url=args.api_url, model=args.model)423    424    # Determine difficulties to evaluate425    difficulties = ["easy", "medium", "hard"] if args.difficulty == "all" else [args.difficulty]426    427    # Run evaluation428    try:429        results = agent.evaluate(430            difficulties=difficulties,431            episodes_per_difficulty=args.episodes432        )433        434        # Print results435        agent.print_results(results)436        437        # Save results if requested438        if args.output:439            with open(args.output, "w") as f:440                json.dump(results, f, indent=2)441            print(f"✅ Results saved to {args.output}")442        443    except KeyboardInterrupt:444        print("\n\n⚠️  Evaluation interrupted by user")445        sys.exit(0)446    except Exception as e:447        print(f"\n⚠️  Evaluation note: {e}")448        sys.exit(0)449 450 451if __name__ == "__main__":452    main()453