Raje19112003/Invoice_Dispute_Resolution_Environment
0
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 