Spaces:
Running
Running
Download judge_simulator.py from IDKHowToCodeFr/vera-engine: direct link, hf CLI and curl.
- Browser
- Download file 35.9 kB
-
https://huggingface.co/spaces/IDKHowToCodeFr/vera-engine/resolve/main/judge_simulator.py
- Command line
-
hf download hf://spaces/IDKHowToCodeFr/vera-engine/judge_simulator.py
-
curl -L -o judge_simulator.py https://huggingface.co/spaces/IDKHowToCodeFr/vera-engine/resolve/main/judge_simulator.py
35.9 kB
| #!/usr/bin/env python3 | |
| """ | |
| magicpin AI Challenge — LLM-Powered Judge Simulator | |
| ==================================================== | |
| A strict but fair judge that scores your bot and explains WHY. | |
| HOW TO USE: | |
| 1. Edit the CONFIGURATION section below (lines 25-45) | |
| 2. Set your LLM provider and API key | |
| 3. Set your bot URL | |
| 4. Run: python judge_simulator.py | |
| That's it! | |
| Author: magicpin AI Challenge Team | |
| """ | |
| # ============================================================================= | |
| # ██████ CONFIGURATION - EDIT THIS SECTION ██████ | |
| # ============================================================================= | |
| # Your bot's URL (where your bot is running) | |
| BOT_URL = "http://localhost:8080" | |
| # Choose your LLM provider: "openai", "anthropic", "gemini", "deepseek", "groq", "ollama", "openrouter" | |
| LLM_PROVIDER = "ollama" | |
| # Your API key (paste your key here) | |
| LLM_API_KEY = "" # <-- PUT YOUR API KEY HERE | |
| # Model to use (leave empty for default, or specify like "gpt-4o", "claude-3-5-sonnet-20241022", etc.) | |
| LLM_MODEL = "llama3.2:latest" # <-- Optional: specify model or leave empty for default | |
| # For Ollama only: local server URL | |
| OLLAMA_URL = "http://localhost:11434" | |
| # Which test to run by default | |
| TEST_SCENARIO = "full_evaluation" | |
| # ============================================================================= | |
| # ██████ END OF CONFIGURATION - DON'T EDIT BELOW THIS LINE ██████ | |
| # ============================================================================= | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import re | |
| import socket | |
| from datetime import datetime | |
| from dataclasses import dataclass, field | |
| from typing import Optional, List, Dict, Any, Tuple | |
| from pathlib import Path | |
| from urllib import request as urlrequest, error as urlerror | |
| from abc import ABC, abstractmethod | |
| # Constants | |
| TIMEOUT_LLM = 45 | |
| DATASET_DIR = Path(__file__).parent / "dataset" | |
| # ============================================================================= | |
| # TERMINAL OUTPUT | |
| # ============================================================================= | |
| class Colors: | |
| HEADER = '\033[95m' | |
| BLUE = '\033[94m' | |
| CYAN = '\033[96m' | |
| GREEN = '\033[92m' | |
| YELLOW = '\033[93m' | |
| RED = '\033[91m' | |
| MAGENTA = '\033[35m' | |
| BOLD = '\033[1m' | |
| DIM = '\033[2m' | |
| RESET = '\033[0m' | |
| def print_header(text: str): | |
| print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*70}{Colors.RESET}") | |
| print(f"{Colors.HEADER}{Colors.BOLD}{text.center(70)}{Colors.RESET}") | |
| print(f"{Colors.HEADER}{Colors.BOLD}{'='*70}{Colors.RESET}\n") | |
| def print_section(text: str): | |
| print(f"\n{Colors.CYAN}{Colors.BOLD}--- {text} ---{Colors.RESET}\n") | |
| def print_success(text: str): | |
| print(f"{Colors.GREEN}[PASS]{Colors.RESET} {text}") | |
| def print_fail(text: str): | |
| print(f"{Colors.RED}[FAIL]{Colors.RESET} {text}") | |
| def print_warn(text: str): | |
| print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {text}") | |
| def print_info(text: str): | |
| print(f"{Colors.BLUE}[INFO]{Colors.RESET} {text}") | |
| def print_llm(text: str): | |
| print(f"{Colors.MAGENTA}[LLM]{Colors.RESET} {text}") | |
| def print_score_bar(dimension: str, score: int, max_score: int = 10): | |
| bar_filled = int((score / max_score) * 20) | |
| bar_empty = 20 - bar_filled | |
| color = Colors.GREEN if score >= 7 else Colors.YELLOW if score >= 4 else Colors.RED | |
| print(f" {dimension:22} [{color}{'█' * bar_filled}{Colors.DIM}{'░' * bar_empty}{Colors.RESET}] {color}{score:2}/{max_score}{Colors.RESET}") | |
| def print_reason(text: str): | |
| wrapped = text[:200] + "..." if len(text) > 200 else text | |
| print(f" {Colors.DIM}{wrapped}{Colors.RESET}") | |
| def print_hint(hint: str): | |
| print(f"\n {Colors.YELLOW}Hint:{Colors.RESET} {hint}") | |
| # ============================================================================= | |
| # DATA CLASSES | |
| # ============================================================================= | |
| class ScoreResult: | |
| specificity: int = 0 | |
| specificity_reason: str = "" | |
| category_fit: int = 0 | |
| category_fit_reason: str = "" | |
| merchant_fit: int = 0 | |
| merchant_fit_reason: str = "" | |
| decision_quality: int = 0 | |
| decision_quality_reason: str = "" | |
| engagement_compulsion: int = 0 | |
| engagement_reason: str = "" | |
| penalties: int = 0 | |
| penalty_reasons: List[str] = field(default_factory=list) | |
| hint: str = "" | |
| def total(self) -> int: | |
| return max(0, self.specificity + self.category_fit + self.merchant_fit + | |
| self.decision_quality + self.engagement_compulsion - self.penalties) | |
| # ============================================================================= | |
| # LLM PROVIDERS | |
| # ============================================================================= | |
| class LLMProvider(ABC): | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| pass | |
| def name(self) -> str: | |
| pass | |
| class OpenAIProvider(LLMProvider): | |
| def __init__(self, api_key: str, model: str = ""): | |
| self.api_key = api_key | |
| self.model = model or "gpt-4o-mini" | |
| def name(self) -> str: | |
| return f"OpenAI ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| messages = [] | |
| if system: | |
| messages.append({"role": "system", "content": system}) | |
| messages.append({"role": "user", "content": prompt}) | |
| body = json.dumps({ | |
| "model": self.model, | |
| "messages": messages, | |
| "temperature": 0.2, | |
| "max_tokens": 1500 | |
| }).encode("utf-8") | |
| req = urlrequest.Request( | |
| "https://api.openai.com/v1/chat/completions", | |
| data=body, | |
| headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} | |
| ) | |
| resp = urlrequest.urlopen(req, timeout=TIMEOUT_LLM) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["choices"][0]["message"]["content"] | |
| class AnthropicProvider(LLMProvider): | |
| def __init__(self, api_key: str, model: str = ""): | |
| self.api_key = api_key | |
| self.model = model or "claude-3-5-sonnet-20241022" | |
| def name(self) -> str: | |
| return f"Anthropic ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| body_dict = {"model": self.model, "max_tokens": 1500, | |
| "messages": [{"role": "user", "content": prompt}]} | |
| if system: | |
| body_dict["system"] = system | |
| req = urlrequest.Request( | |
| "https://api.anthropic.com/v1/messages", | |
| data=json.dumps(body_dict).encode("utf-8"), | |
| headers={"x-api-key": self.api_key, "Content-Type": "application/json", | |
| "anthropic-version": "2023-06-01"} | |
| ) | |
| resp = urlrequest.urlopen(req, timeout=TIMEOUT_LLM) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["content"][0]["text"] | |
| class GeminiProvider(LLMProvider): | |
| def __init__(self, api_key: str, model: str = ""): | |
| self.api_key = api_key | |
| self.model = model or "gemini-1.5-flash" | |
| def name(self) -> str: | |
| return f"Gemini ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| full_prompt = f"{system}\n\n{prompt}" if system else prompt | |
| body = json.dumps({ | |
| "contents": [{"parts": [{"text": full_prompt}]}], | |
| "generationConfig": {"temperature": 0.2, "maxOutputTokens": 1500} | |
| }).encode("utf-8") | |
| url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model}:generateContent?key={self.api_key}" | |
| req = urlrequest.Request(url, data=body, headers={"Content-Type": "application/json"}) | |
| resp = urlrequest.urlopen(req, timeout=TIMEOUT_LLM) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["candidates"][0]["content"]["parts"][0]["text"] | |
| class DeepSeekProvider(LLMProvider): | |
| def __init__(self, api_key: str, model: str = ""): | |
| self.api_key = api_key | |
| self.model = model or "deepseek-chat" | |
| def name(self) -> str: | |
| return f"DeepSeek ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| messages = [] | |
| if system: | |
| messages.append({"role": "system", "content": system}) | |
| messages.append({"role": "user", "content": prompt}) | |
| req = urlrequest.Request( | |
| "https://api.deepseek.com/v1/chat/completions", | |
| data=json.dumps({"model": self.model, "messages": messages, | |
| "temperature": 0.2, "max_tokens": 1500}).encode("utf-8"), | |
| headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} | |
| ) | |
| resp = urlrequest.urlopen(req, timeout=TIMEOUT_LLM) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["choices"][0]["message"]["content"] | |
| class GroqProvider(LLMProvider): | |
| def __init__(self, api_key: str, model: str = ""): | |
| self.api_key = api_key | |
| self.model = model or "llama-3.1-70b-versatile" | |
| def name(self) -> str: | |
| return f"Groq ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| messages = [] | |
| if system: | |
| messages.append({"role": "system", "content": system}) | |
| messages.append({"role": "user", "content": prompt}) | |
| req = urlrequest.Request( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| data=json.dumps({"model": self.model, "messages": messages, | |
| "temperature": 0.2, "max_tokens": 1500}).encode("utf-8"), | |
| headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"} | |
| ) | |
| resp = urlrequest.urlopen(req, timeout=TIMEOUT_LLM) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["choices"][0]["message"]["content"] | |
| class OllamaProvider(LLMProvider): | |
| def __init__(self, model: str = "", api_url: str = ""): | |
| self.model = model or "llama3" | |
| self.api_url = api_url or "http://localhost:11434" | |
| def name(self) -> str: | |
| return f"Ollama ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| full_prompt = f"{system}\n\n{prompt}" if system else prompt | |
| req = urlrequest.Request( | |
| f"{self.api_url}/api/generate", | |
| data=json.dumps({"model": self.model, "prompt": full_prompt, | |
| "stream": False, "options": {"temperature": 0.2}}).encode("utf-8"), | |
| headers={"Content-Type": "application/json"} | |
| ) | |
| resp = urlrequest.urlopen(req, timeout=90) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["response"] | |
| class OpenRouterProvider(LLMProvider): | |
| def __init__(self, api_key: str, model: str = ""): | |
| self.api_key = api_key | |
| self.model = model or "anthropic/claude-3-haiku" | |
| def name(self) -> str: | |
| return f"OpenRouter ({self.model})" | |
| def complete(self, prompt: str, system: str = None) -> str: | |
| messages = [] | |
| if system: | |
| messages.append({"role": "system", "content": system}) | |
| messages.append({"role": "user", "content": prompt}) | |
| req = urlrequest.Request( | |
| "https://openrouter.ai/api/v1/chat/completions", | |
| data=json.dumps({"model": self.model, "messages": messages, | |
| "temperature": 0.2, "max_tokens": 1500}).encode("utf-8"), | |
| headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", | |
| "HTTP-Referer": "https://magicpin.com"} | |
| ) | |
| resp = urlrequest.urlopen(req, timeout=TIMEOUT_LLM) | |
| data = json.loads(resp.read().decode("utf-8")) | |
| return data["choices"][0]["message"]["content"] | |
| def create_provider() -> LLMProvider: | |
| """Create LLM provider from configuration.""" | |
| providers = { | |
| "openai": lambda: OpenAIProvider(LLM_API_KEY, LLM_MODEL), | |
| "anthropic": lambda: AnthropicProvider(LLM_API_KEY, LLM_MODEL), | |
| "gemini": lambda: GeminiProvider(LLM_API_KEY, LLM_MODEL), | |
| "deepseek": lambda: DeepSeekProvider(LLM_API_KEY, LLM_MODEL), | |
| "groq": lambda: GroqProvider(LLM_API_KEY, LLM_MODEL), | |
| "ollama": lambda: OllamaProvider(LLM_MODEL, OLLAMA_URL), | |
| "openrouter": lambda: OpenRouterProvider(LLM_API_KEY, LLM_MODEL), | |
| } | |
| if LLM_PROVIDER not in providers: | |
| print_fail(f"Unknown provider: {LLM_PROVIDER}") | |
| print_info(f"Available: {', '.join(providers.keys())}") | |
| sys.exit(1) | |
| return providers[LLM_PROVIDER]() | |
| # ============================================================================= | |
| # DATASET & BOT CLIENT | |
| # ============================================================================= | |
| class DatasetLoader: | |
| def __init__(self, dataset_dir: Path): | |
| self.dataset_dir = dataset_dir | |
| self.categories = {} | |
| self.merchants = {} | |
| self.customers = {} | |
| self.triggers = {} | |
| def load(self) -> bool: | |
| try: | |
| cat_dir = self.dataset_dir / "categories" | |
| if cat_dir.exists(): | |
| for f in cat_dir.glob("*.json"): | |
| data = json.load(open(f)) | |
| self.categories[data.get("slug", f.stem)] = data | |
| for name, container, key in [ | |
| ("merchants_seed.json", "merchants", "merchant_id"), | |
| ("customers_seed.json", "customers", "customer_id"), | |
| ("triggers_seed.json", "triggers", "id") | |
| ]: | |
| path = self.dataset_dir / name | |
| if path.exists(): | |
| data = json.load(open(path)) | |
| items = data.get(container, data.get(container.rstrip("s"), [])) | |
| storage = getattr(self, container) | |
| for item in items: | |
| if key in item: | |
| storage[item[key]] = item | |
| return True | |
| except Exception as e: | |
| print_fail(f"Dataset load error: {e}") | |
| return False | |
| class BotClient: | |
| def __init__(self, base_url: str): | |
| self.base_url = base_url.rstrip("/") | |
| def _request(self, method: str, path: str, timeout: int = 30, | |
| body_dict: Dict = None) -> Tuple[Optional[Dict], Optional[str], float]: | |
| url = f"{self.base_url}{path}" | |
| start = time.time() | |
| body = json.dumps(body_dict).encode("utf-8") if body_dict else None | |
| headers = {"Content-Type": "application/json"} | |
| req = urlrequest.Request(url, data=body, method=method, headers=headers) | |
| try: | |
| resp = urlrequest.urlopen(req, timeout=timeout) | |
| return json.loads(resp.read().decode("utf-8")), None, (time.time() - start) * 1000 | |
| except urlerror.HTTPError as e: | |
| latency = (time.time() - start) * 1000 | |
| if e.code == 401: | |
| return None, "Unauthorized", latency | |
| try: | |
| return json.loads(e.read().decode("utf-8")), None, latency | |
| except: | |
| return None, f"HTTP {e.code}", latency | |
| except Exception as e: | |
| return None, str(e), (time.time() - start) * 1000 | |
| def healthz(self): | |
| return self._request("GET", "/v1/healthz", 5) | |
| def metadata(self): | |
| return self._request("GET", "/v1/metadata", 5) | |
| def push_context(self, scope, cid, version, payload): | |
| return self._request("POST", "/v1/context", 10, { | |
| "scope": scope, "context_id": cid, "version": version, | |
| "payload": payload, "delivered_at": datetime.utcnow().isoformat() + "Z" | |
| }) | |
| def tick(self, triggers): | |
| return self._request("POST", "/v1/tick", 15, { | |
| "now": datetime.utcnow().isoformat() + "Z", "available_triggers": triggers | |
| }) | |
| def reply(self, conv_id, merchant_id, message, turn): | |
| return self._request("POST", "/v1/reply", 15, { | |
| "conversation_id": conv_id, "merchant_id": merchant_id, "customer_id": None, | |
| "from_role": "merchant", "message": message, | |
| "received_at": datetime.utcnow().isoformat() + "Z", "turn_number": turn | |
| }) | |
| # ============================================================================= | |
| # LLM SCORING ENGINE | |
| # ============================================================================= | |
| class LLMScorer: | |
| """Scores messages using LLM and provides detailed reasoning.""" | |
| SYSTEM = """You are a STRICT judge for the magicpin AI Challenge. You score merchant engagement messages. | |
| SCORING DIMENSIONS (0-10 each, be strict - 5 is average, 7+ is good, 9+ is excellent): | |
| 1. SPECIFICITY: Does the message have VERIFIABLE facts? | |
| - Numbers (percentages, counts, prices) | |
| - Dates/times | |
| - Source citations | |
| - Concrete claims vs vague statements | |
| 2. CATEGORY FIT: Does the voice match the business type? | |
| - Dentists: clinical, peer-to-peer, technical OK, use "Dr." prefix | |
| - Salons: warm, friendly, practical | |
| - Restaurants: operator-to-operator | |
| - Gyms: coaching, motivational | |
| - Pharmacies: trustworthy, precise | |
| 3. MERCHANT FIT: Is it personalized to THIS merchant? | |
| - Uses their name/owner name correctly | |
| - References their actual data (not fabricated) | |
| - Honors language preference | |
| 4. TRIGGER RELEVANCE: Does it connect to WHY NOW? | |
| - Clear reason for this specific message | |
| - Uses data from the trigger payload | |
| - Not a generic nudge | |
| 5. ENGAGEMENT COMPULSION: Would they reply? | |
| - Loss aversion, curiosity, social proof | |
| - Clear CTA | |
| - Low friction ask | |
| PENALTIES: | |
| - Fabricating data not in context: -2 | |
| - Exposing internal jargon to merchant: -1 | |
| RESPOND ONLY WITH THIS EXACT JSON FORMAT: | |
| { | |
| "specificity": <0-10>, | |
| "specificity_reason": "<why this score, 1-2 sentences>", | |
| "category_fit": <0-10>, | |
| "category_fit_reason": "<why this score>", | |
| "merchant_fit": <0-10>, | |
| "merchant_fit_reason": "<why this score>", | |
| "decision_quality": <0-10>, | |
| "decision_quality_reason": "<why this score>", | |
| "engagement_compulsion": <0-10>, | |
| "engagement_reason": "<why this score>", | |
| "hint": "<one sentence guidance for improvement, cryptic not direct>" | |
| }""" | |
| def __init__(self, llm: LLMProvider, dataset: DatasetLoader): | |
| self.llm = llm | |
| self.dataset = dataset | |
| def score(self, action: Dict, category: Dict, merchant: Dict, | |
| trigger: Dict, customer: Dict = None) -> ScoreResult: | |
| """Score a message and return detailed results.""" | |
| body = action.get("body", "") | |
| prompt = f"""SCORE THIS MESSAGE: | |
| === CONTEXT PROVIDED TO BOT === | |
| Category: {category.get('slug', 'unknown')} | |
| Voice: {category.get('voice', {}).get('tone', 'unknown')} | |
| Taboos: {category.get('voice', {}).get('vocab_taboo', [])[:5]} | |
| Merchant: {merchant.get('identity', {}).get('name', 'unknown')} | |
| Owner: {merchant.get('identity', {}).get('owner_first_name', 'unknown')} | |
| Locality: {merchant.get('identity', {}).get('locality', 'unknown')} | |
| Languages: {merchant.get('identity', {}).get('languages', [])} | |
| Performance: views={merchant.get('performance', {}).get('views', '?')}, calls={merchant.get('performance', {}).get('calls', '?')}, ctr={merchant.get('performance', {}).get('ctr', '?')} | |
| Signals: {merchant.get('signals', [])} | |
| Active Offers: {[o.get('title') for o in merchant.get('offers', []) if o.get('status') == 'active']} | |
| Trigger Kind: {trigger.get('kind', 'unknown')} | |
| Trigger Payload: {json.dumps(trigger.get('payload', {}))} | |
| Trigger Urgency: {trigger.get('urgency', '?')} | |
| Customer: {json.dumps(customer.get('identity', {})) if customer else 'None (merchant-facing)'} | |
| === BOT'S MESSAGE === | |
| Body ({len(body)} chars): "{body}" | |
| CTA: {action.get('cta', 'none')} | |
| Send As: {action.get('send_as', 'vera')} | |
| Score each dimension 0-10 with clear reasoning. Be STRICT.""" | |
| try: | |
| print_llm("Analyzing message...") | |
| response = self.llm.complete(prompt, self.SYSTEM) | |
| return self._parse_response(response, action) | |
| except Exception as e: | |
| print_warn(f"LLM error: {e}") | |
| return self._fallback_score(action) | |
| def _parse_response(self, response: str, action: Dict) -> ScoreResult: | |
| """Parse LLM JSON response.""" | |
| match = re.search(r'\{[\s\S]*\}', response) | |
| if not match: | |
| return self._fallback_score(action) | |
| try: | |
| data = json.loads(match.group()) | |
| result = ScoreResult( | |
| specificity=min(10, max(0, int(data.get("specificity", 5)))), | |
| specificity_reason=data.get("specificity_reason", ""), | |
| category_fit=min(10, max(0, int(data.get("category_fit", 5)))), | |
| category_fit_reason=data.get("category_fit_reason", ""), | |
| merchant_fit=min(10, max(0, int(data.get("merchant_fit", 5)))), | |
| merchant_fit_reason=data.get("merchant_fit_reason", ""), | |
| decision_quality=min(10, max(0, int(data.get("decision_quality", data.get("trigger_relevance", 5))))), | |
| decision_quality_reason=data.get("decision_quality_reason", data.get("trigger_relevance_reason", "")), | |
| engagement_compulsion=min(10, max(0, int(data.get("engagement_compulsion", 5)))), | |
| engagement_reason=data.get("engagement_reason", ""), | |
| hint=data.get("hint", "") | |
| ) | |
| return result | |
| except Exception as e: | |
| print_warn(f"Parse error: {e}") | |
| return self._fallback_score(action) | |
| def _fallback_score(self, action: Dict) -> ScoreResult: | |
| """Basic fallback scoring.""" | |
| body = action.get("body", "").lower() | |
| nums = len(re.findall(r'\d+', body)) | |
| return ScoreResult( | |
| specificity=min(10, 3 + nums * 2), | |
| specificity_reason="Fallback: counted numbers in message", | |
| category_fit=5, category_fit_reason="Could not evaluate", | |
| merchant_fit=5, merchant_fit_reason="Could not evaluate", | |
| decision_quality=5, decision_quality_reason="Could not evaluate", | |
| engagement_compulsion=5, engagement_reason="Could not evaluate", | |
| hint="LLM scoring failed - using basic heuristics" | |
| ) | |
| # ============================================================================= | |
| # MAIN JUDGE | |
| # ============================================================================= | |
| class JudgeSimulator: | |
| def __init__(self, llm: LLMProvider): | |
| self.llm = llm | |
| self.client = BotClient(BOT_URL) | |
| self.dataset = DatasetLoader(DATASET_DIR) | |
| self.scorer: Optional[LLMScorer] = None | |
| self.all_scores: List[ScoreResult] = [] | |
| def run(self, scenario: str) -> bool: | |
| print_header(f"LLM JUDGE — {scenario.upper()}") | |
| print_info(f"Bot: {BOT_URL}") | |
| print_info(f"LLM: {self.llm.name()}") | |
| if not self.dataset.load(): | |
| print_fail("Dataset load failed") | |
| return False | |
| self.scorer = LLMScorer(self.llm, self.dataset) | |
| print_info(f"Loaded: {len(self.dataset.categories)} categories, " | |
| f"{len(self.dataset.merchants)} merchants, " | |
| f"{len(self.dataset.triggers)} triggers") | |
| scenarios = { | |
| "warmup": self._warmup, | |
| "phase2_short": self._phase2_short, | |
| "auto_reply_hell": self._auto_reply, | |
| "intent_transition": self._intent, | |
| "hostile": self._hostile, | |
| "all": self._all, | |
| "full_evaluation": self._full, | |
| } | |
| if scenario not in scenarios: | |
| print_fail(f"Unknown scenario: {scenario}") | |
| print_info(f"Available: {', '.join(scenarios.keys())}") | |
| return False | |
| success = scenarios[scenario]() | |
| self._final_summary() | |
| return success | |
| def _warmup(self) -> bool: | |
| print_section("WARMUP") | |
| data, err, lat = self.client.healthz() | |
| if err: | |
| print_fail(f"healthz: {err}") | |
| return False | |
| print_success(f"healthz ({lat:.0f}ms)") | |
| data, err, lat = self.client.metadata() | |
| if err: | |
| print_warn(f"metadata: {err}") | |
| else: | |
| print_success(f"metadata — Team: {data.get('team_name', '?')}, Model: {data.get('model', '?')}") | |
| print_section("CONTEXT PUSH") | |
| for slug, cat in self.dataset.categories.items(): | |
| data, err, _ = self.client.push_context("category", slug, 1, cat) | |
| status = "PASS" if data and data.get("accepted") else "FAIL" | |
| print(f" [{status}] category/{slug}") | |
| for mid, m in list(self.dataset.merchants.items())[:5]: | |
| data, err, _ = self.client.push_context("merchant", mid, 1, m) | |
| status = "PASS" if data and data.get("accepted") else "FAIL" | |
| short_id = mid.split('_')[1] if '_' in mid else mid[:10] | |
| print(f" [{status}] merchant/{short_id}") | |
| return True | |
| def _phase2_short(self) -> bool: | |
| if not self._warmup(): | |
| return False | |
| print_section("TICK TEST") | |
| trigs = list(self.dataset.triggers.keys())[:3] | |
| for tid in trigs: | |
| self.client.push_context("trigger", tid, 1, self.dataset.triggers[tid]) | |
| data, err, lat = self.client.tick(trigs) | |
| if err: | |
| print_fail(f"tick: {err}") | |
| return False | |
| actions = data.get("actions", []) | |
| print_info(f"Bot returned {len(actions)} action(s) ({lat:.0f}ms)") | |
| if not actions: | |
| print_warn("No actions — bot chose not to send") | |
| return True | |
| for action in actions: | |
| self._score_and_display(action) | |
| return True | |
| def _auto_reply(self) -> bool: | |
| print_section("AUTO-REPLY DETECTION") | |
| data, err, _ = self.client.healthz() | |
| if err: | |
| print_fail(f"Bot unreachable: {err}") | |
| return False | |
| mid = list(self.dataset.merchants.keys())[0] if self.dataset.merchants else "m_test" | |
| auto_msg = "Thank you for contacting us! Our team will respond shortly." | |
| for i in range(1, 5): | |
| print_info(f"Turn {i}: Sending auto-reply...") | |
| data, err, _ = self.client.reply(f"conv_auto_{i}", mid, auto_msg, i + 1) | |
| if err: | |
| print_fail(f"Error: {err}") | |
| return False | |
| action = data.get("action", "?") | |
| if action == "end": | |
| print_success(f"Turn {i}: Bot ENDED — detected auto-reply pattern!") | |
| return True | |
| elif action == "wait": | |
| wait_s = data.get("wait_seconds", "?") | |
| print_success(f"Turn {i}: Bot WAITING {wait_s}s") | |
| else: | |
| body = data.get("body", "")[:50] | |
| print_warn(f"Turn {i}: Bot sent: \"{body}...\"") | |
| print_warn("Bot never ended after 4 auto-replies") | |
| return True | |
| def _intent(self) -> bool: | |
| print_section("INTENT TRANSITION") | |
| data, err, _ = self.client.healthz() | |
| if err: | |
| print_fail(f"Bot unreachable: {err}") | |
| return False | |
| mid = list(self.dataset.merchants.keys())[0] if self.dataset.merchants else "m_test" | |
| commitment = "Ok lets do it. Whats next?" | |
| print_info(f"Merchant: \"{commitment}\"") | |
| data, err, _ = self.client.reply("conv_intent_1", mid, commitment, 2) | |
| if err: | |
| print_fail(f"Error: {err}") | |
| return False | |
| action = data.get("action", "?") | |
| body = data.get("body", "") | |
| print_info(f"Bot action: {action}") | |
| if body: | |
| print_info(f"Bot body: \"{body[:100]}{'...' if len(body) > 100 else ''}\"") | |
| qualifying = ["would you", "do you", "can you tell", "what if", "how about"] | |
| actioning = ["done", "sending", "draft", "here", "confirm", "proceed", "next"] | |
| body_lower = body.lower() | |
| if any(w in body_lower for w in actioning) and not any(w in body_lower for w in qualifying): | |
| print_success("Bot correctly switched to ACTION mode") | |
| elif any(w in body_lower for w in qualifying): | |
| print_fail("Bot is STILL QUALIFYING after commitment") | |
| else: | |
| print_warn("Response unclear") | |
| return True | |
| def _hostile(self) -> bool: | |
| print_section("HOSTILE HANDLING") | |
| data, err, _ = self.client.healthz() | |
| if err: | |
| print_fail(f"Bot unreachable: {err}") | |
| return False | |
| mid = list(self.dataset.merchants.keys())[0] if self.dataset.merchants else "m_test" | |
| hostile = "Stop messaging me. This is useless spam." | |
| print_info(f"Merchant (hostile): \"{hostile}\"") | |
| data, err, _ = self.client.reply("conv_hostile", mid, hostile, 2) | |
| if err: | |
| print_fail(f"Error: {err}") | |
| return False | |
| action = data.get("action", "?") | |
| body = data.get("body", "") | |
| print_info(f"Bot action: {action}") | |
| if action == "end": | |
| print_success("Bot correctly ENDED on hostile message") | |
| elif action == "send" and any(w in body.lower() for w in ["sorry", "apolog", "won't"]): | |
| print_success("Bot apologized gracefully") | |
| else: | |
| print_fail("Bot didn't handle hostility well") | |
| return True | |
| def _all(self) -> bool: | |
| results = [] | |
| for name, fn in [("warmup", self._warmup), ("auto_reply", self._auto_reply), | |
| ("intent", self._intent), ("hostile", self._hostile)]: | |
| try: | |
| results.append((name, fn())) | |
| except Exception as e: | |
| print_fail(f"{name} crashed: {e}") | |
| results.append((name, False)) | |
| print_section("SCENARIO RESULTS") | |
| for name, passed in results: | |
| (print_success if passed else print_fail)(name) | |
| return all(p for _, p in results) | |
| def _full(self) -> bool: | |
| if not self._warmup(): | |
| return False | |
| print_section("FULL EVALUATION") | |
| for mid, m in self.dataset.merchants.items(): | |
| self.client.push_context("merchant", mid, 1, m) | |
| for tid, t in self.dataset.triggers.items(): | |
| self.client.push_context("trigger", tid, 1, t) | |
| print_success("All contexts pushed") | |
| print_section("SCORING COMPOSITIONS") | |
| tids = list(self.dataset.triggers.keys()) | |
| for i in range(0, len(tids), 5): | |
| batch = tids[i:i+5] | |
| data, err, lat = self.client.tick(batch) | |
| if err: | |
| print_warn(f"Tick failed: {err}") | |
| continue | |
| actions = data.get("actions", []) | |
| print_info(f"Batch {i//5 + 1}: {len(actions)} actions ({lat:.0f}ms)") | |
| for action in actions: | |
| self._score_and_display(action, verbose=False) | |
| return True | |
| def _score_and_display(self, action: Dict, verbose: bool = True): | |
| """Score an action and display results.""" | |
| tid = action.get("trigger_id", "") | |
| mid = action.get("merchant_id", "") | |
| cid = action.get("customer_id") | |
| trigger = self.dataset.triggers.get(tid, {}) | |
| merchant = self.dataset.merchants.get(mid, {}) | |
| customer = self.dataset.customers.get(cid) if cid else None | |
| category = self.dataset.categories.get(merchant.get("category_slug", ""), {}) | |
| score = self.scorer.score(action, category, merchant, trigger, customer) | |
| self.all_scores.append(score) | |
| body = action.get("body", "")[:50] | |
| print(f"\n{Colors.CYAN}Message:{Colors.RESET} \"{body}...\"") | |
| print_score_bar("Specificity", score.specificity) | |
| if verbose and score.specificity_reason: | |
| print_reason(score.specificity_reason) | |
| print_score_bar("Category Fit", score.category_fit) | |
| if verbose and score.category_fit_reason: | |
| print_reason(score.category_fit_reason) | |
| print_score_bar("Merchant Fit", score.merchant_fit) | |
| if verbose and score.merchant_fit_reason: | |
| print_reason(score.merchant_fit_reason) | |
| print_score_bar("Decision Quality", score.decision_quality) | |
| if verbose and score.decision_quality_reason: | |
| print_reason(score.decision_quality_reason) | |
| print_score_bar("Engagement", score.engagement_compulsion) | |
| if verbose and score.engagement_reason: | |
| print_reason(score.engagement_reason) | |
| if score.penalties: | |
| print(f" {Colors.RED}Penalties: -{score.penalties}{Colors.RESET}") | |
| for r in score.penalty_reasons: | |
| print_reason(r) | |
| print(f"\n {Colors.BOLD}TOTAL: {score.total}/50{Colors.RESET}") | |
| if verbose and score.hint: | |
| print_hint(score.hint) | |
| def _final_summary(self): | |
| if not self.all_scores: | |
| return | |
| print_section("FINAL SUMMARY") | |
| n = len(self.all_scores) | |
| avg = ScoreResult( | |
| specificity=sum(s.specificity for s in self.all_scores) // n, | |
| category_fit=sum(s.category_fit for s in self.all_scores) // n, | |
| merchant_fit=sum(s.merchant_fit for s in self.all_scores) // n, | |
| decision_quality=sum(s.decision_quality for s in self.all_scores) // n, | |
| engagement_compulsion=sum(s.engagement_compulsion for s in self.all_scores) // n, | |
| penalties=sum(s.penalties for s in self.all_scores) | |
| ) | |
| print_info(f"Messages scored: {n}\n") | |
| print_score_bar("Avg Specificity", avg.specificity) | |
| print_score_bar("Avg Category Fit", avg.category_fit) | |
| print_score_bar("Avg Merchant Fit", avg.merchant_fit) | |
| print_score_bar("Avg Decision Quality", avg.decision_quality) | |
| print_score_bar("Avg Engagement", avg.engagement_compulsion) | |
| total = avg.total | |
| pct = (total / 50) * 100 | |
| print(f"\n{Colors.BOLD} AVERAGE SCORE: {total}/50 ({pct:.0f}%){Colors.RESET}") | |
| if pct >= 80: | |
| print(f"\n {Colors.GREEN}EXCELLENT{Colors.RESET}") | |
| elif pct >= 60: | |
| print(f"\n {Colors.YELLOW}GOOD{Colors.RESET}") | |
| elif pct >= 40: | |
| print(f"\n {Colors.YELLOW}NEEDS IMPROVEMENT{Colors.RESET}") | |
| else: | |
| print(f"\n {Colors.RED}BELOW EXPECTATIONS{Colors.RESET}") | |
| # ============================================================================= | |
| # ENTRY POINT | |
| # ============================================================================= | |
| def main(): | |
| print_header("magicpin AI Challenge — LLM Judge") | |
| # Validate configuration | |
| if LLM_PROVIDER != "ollama" and not LLM_API_KEY: | |
| print_fail("LLM_API_KEY is not set!") | |
| print_info("Edit the CONFIGURATION section at the top of this file") | |
| print_info("Set your API key for your chosen provider") | |
| sys.exit(1) | |
| # Create LLM provider | |
| try: | |
| llm = create_provider() | |
| print_info(f"LLM Provider: {llm.name()}") | |
| except Exception as e: | |
| print_fail(f"Failed to create LLM provider: {e}") | |
| sys.exit(1) | |
| # Test LLM connection | |
| print_info("Testing LLM connection...") | |
| try: | |
| test_response = llm.complete("Say 'ready' if you can hear me.", "You are a test assistant.") | |
| if test_response: | |
| print_success("LLM connected successfully") | |
| else: | |
| print_fail("LLM returned empty response") | |
| sys.exit(1) | |
| except Exception as e: | |
| print_fail(f"LLM connection failed: {e}") | |
| print_info("Check your API key and internet connection") | |
| sys.exit(1) | |
| # Run the judge | |
| judge = JudgeSimulator(llm) | |
| success = judge.run(TEST_SCENARIO) | |
| sys.exit(0 if success else 1) | |
| if __name__ == "__main__": | |
| main() | |