Spaces:
Sleeping
Sleeping
Download env/data_loader.py from rogerdemello/email-copilot-env: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/rogerdemello/email-copilot-env/resolve/main/env/data_loader.py
- Command line
-
hf download hf://spaces/rogerdemello/email-copilot-env/env/data_loader.py
-
curl -L -o data_loader.py https://huggingface.co/spaces/rogerdemello/email-copilot-env/resolve/main/env/data_loader.py
12 kB
| from __future__ import annotations | |
| import copy | |
| import random | |
| import threading | |
| from pathlib import Path | |
| from typing import Any | |
| import yaml | |
| _lock = threading.Lock() | |
| _cache: dict[Path, tuple[int, int, Any]] = {} | |
| # ============================================================================= | |
| # Synthetic Scenario Combinators | |
| # ============================================================================= | |
| # Adversarial email wording patterns for subtle manipulation | |
| ADVERSARIAL_PATTERNS: dict[str, list[dict[str, str]]] = { | |
| "urgency_manipulation": [ | |
| { | |
| "pattern": "Need it ASAP", | |
| "replacement": "Need it by end of day", | |
| "subtle_variant": "Need it when you get a chance", | |
| }, | |
| {"pattern": "URGENT", "replacement": "Important", "subtle_variant": "Time-sensitive"}, | |
| { | |
| "pattern": "Immediate attention required", | |
| "replacement": "Please review when available", | |
| "subtle_variant": "Kindly review at your convenience", | |
| }, | |
| ], | |
| "social_proof": [ | |
| { | |
| "pattern": "Everyone is waiting", | |
| "replacement": "Several team members are ready", | |
| "subtle_variant": "The team is prepared to proceed", | |
| }, | |
| { | |
| "pattern": "As discussed", | |
| "replacement": "Following up on", | |
| "subtle_variant": "Building on our previous conversation", | |
| }, | |
| ], | |
| "artificial_deadline": [ | |
| { | |
| "pattern": "Must respond today", | |
| "replacement": "Please respond when possible", | |
| "subtle_variant": "Response appreciated this week", | |
| }, | |
| { | |
| "pattern": "Deadline is tomorrow", | |
| "replacement": "Target response: end of week", | |
| "subtle_variant": "Ideally would appreciate response by Friday", | |
| }, | |
| ], | |
| "guilt_trip": [ | |
| { | |
| "pattern": "I've been waiting", | |
| "replacement": "Following up", | |
| "subtle_variant": "Checking in on this", | |
| }, | |
| { | |
| "pattern": "No one else can do this", | |
| "replacement": "Your expertise would be valuable", | |
| "subtle_variant": "Your input would be helpful", | |
| }, | |
| ], | |
| "false_authority": [ | |
| { | |
| "pattern": "The CEO wants", | |
| "replacement": "Leadership is interested", | |
| "subtle_variant": "Senior leadership has requested", | |
| }, | |
| { | |
| "pattern": "Legal requires", | |
| "replacement": "Compliance recommends", | |
| "subtle_variant": "We should consider", | |
| }, | |
| ], | |
| } | |
| # Conflicting deadline scenarios | |
| CONFLICTING_DEADLINE_TEMPLATES: list[dict[str, Any]] = [ | |
| { | |
| "description": "Two urgent items with overlapping deadlines", | |
| "email_a": {"deadline_minutes": 45, "priority_hint": "high", "business_value": 0.9}, | |
| "email_b": {"deadline_minutes": 30, "priority_hint": "high", "business_value": 0.85}, | |
| }, | |
| { | |
| "description": "Critical deadline vs important deadline", | |
| "email_a": {"deadline_minutes": 60, "priority_hint": "high", "business_value": 0.98}, | |
| "email_b": {"deadline_minutes": 90, "priority_hint": "medium", "business_value": 0.7}, | |
| }, | |
| { | |
| "description": "Multiple items due within same hour", | |
| "emails": [ | |
| {"deadline_minutes": 45, "priority_hint": "high"}, | |
| {"deadline_minutes": 50, "priority_hint": "high"}, | |
| {"deadline_minutes": 55, "priority_hint": "medium"}, | |
| ], | |
| }, | |
| ] | |
| def apply_adversarial_pattern( | |
| text: str, | |
| pattern_type: str, | |
| rng: random.Random, | |
| apply_subtle: bool = False, | |
| ) -> str: | |
| """Apply adversarial wording pattern to text.""" | |
| patterns = ADVERSARIAL_PATTERNS.get(pattern_type, []) | |
| if not patterns: | |
| return text | |
| entry = rng.choice(patterns) | |
| if apply_subtle and entry.get("subtle_variant"): | |
| replacement = entry["subtle_variant"] | |
| else: | |
| replacement = entry.get("replacement", entry["pattern"]) | |
| return text.replace(entry["pattern"], replacement) | |
| def generate_conflicting_deadlines( | |
| scenario: dict[str, Any], | |
| difficulty: str, | |
| rng: random.Random, | |
| ) -> list[dict[str, Any]]: | |
| """Generate conflicting deadline scenarios based on difficulty.""" | |
| if difficulty == "easy": | |
| return [] | |
| conflicts = [] | |
| num_conflicts = 1 if difficulty == "medium" else rng.randint(1, 2) | |
| for _ in range(num_conflicts): | |
| template = rng.choice(CONFLICTING_DEADLINE_TEMPLATES) | |
| conflict = { | |
| "description": template["description"], | |
| "type": "conflicting_deadline", | |
| } | |
| if "emails" in template: | |
| conflict["emails"] = template["emails"] | |
| else: | |
| conflict["email_a"] = template["email_a"].copy() | |
| conflict["email_b"] = template["email_b"].copy() | |
| conflicts.append(conflict) | |
| return conflicts | |
| def generate_interruptions( | |
| base_scenario: dict[str, Any], | |
| difficulty: str, | |
| rng: random.Random, | |
| ) -> list[dict[str, Any]]: | |
| """Generate random mid-episode interruptions based on difficulty.""" | |
| existing_interruptions = base_scenario.get("interruptions", []) | |
| if difficulty == "easy": | |
| # No additional interruptions for easy | |
| return existing_interruptions | |
| # Base interruption probability | |
| base_prob = 0.3 if difficulty == "medium" else 0.5 | |
| # Generate additional interruptions | |
| additional = [] | |
| max_additional = 0 if difficulty == "medium" else rng.randint(0, 2) | |
| for _ in range(max_additional): | |
| if rng.random() < base_prob: | |
| trigger_range = [ | |
| rng.randint(30, 60) if difficulty == "medium" else rng.randint(15, 45), | |
| rng.randint(60, 90) if difficulty == "medium" else rng.randint(45, 75), | |
| ] | |
| additional.append( | |
| { | |
| "trigger_minute_range": trigger_range, | |
| "email": { | |
| "id": f"int_{rng.randint(1000, 9999)}", | |
| "sender": rng.choice( | |
| [ | |
| "colleague@company.com", | |
| "manager@company.com", | |
| "vendor@external.com", | |
| "client@partner.com", | |
| ] | |
| ), | |
| "sender_role": rng.choice(["internal", "internal", "vendor", "client"]), | |
| "subject": rng.choice( | |
| [ | |
| "Quick question", | |
| "Follow-up on earlier item", | |
| "Status check", | |
| "Request for input", | |
| ] | |
| ), | |
| "body": rng.choice( | |
| [ | |
| "Can you take a quick look?", | |
| "Need your thoughts when available.", | |
| "Checking on next steps.", | |
| "Would appreciate your input.", | |
| ] | |
| ), | |
| "priority_hint": rng.choice(["low", "medium", "high"]), | |
| "deadline_minutes": rng.choice([30, 60, 120, 240]), | |
| "business_value": round(rng.uniform(0.4, 0.8), 2), | |
| "risk_tag": rng.choice(["none", "ops", "finance"]), | |
| }, | |
| } | |
| ) | |
| return existing_interruptions + additional | |
| def scale_difficulty( | |
| base_scenario: dict[str, Any], | |
| difficulty: str, | |
| rng: random.Random, | |
| ) -> dict[str, Any]: | |
| """Scale scenario complexity based on difficulty level.""" | |
| scenario = copy.deepcopy(base_scenario) | |
| if difficulty == "hard": | |
| # Add adversarial patterns to existing emails | |
| emails = scenario.get("emails", []) | |
| for email in emails: | |
| if rng.random() < 0.3: # 30% chance of adversarial pattern | |
| pattern_type = rng.choice(list(ADVERSARIAL_PATTERNS.keys())) | |
| # Apply to subject | |
| if "subject" in email: | |
| email["subject"] = apply_adversarial_pattern( | |
| email["subject"], pattern_type, rng, apply_subtle=True | |
| ) | |
| # Apply to body | |
| if "body" in email: | |
| email["body"] = apply_adversarial_pattern( | |
| email["body"], pattern_type, rng, apply_subtle=True | |
| ) | |
| # Reduce time budget slightly for hard | |
| current_budget = scenario.get("time_budget", 180) | |
| scenario["time_budget"] = int(current_budget * 0.85) | |
| elif difficulty == "medium": | |
| # Reduce time budget moderately | |
| current_budget = scenario.get("time_budget", 180) | |
| scenario["time_budget"] = int(current_budget * 0.92) | |
| # Add conflicting deadlines for medium/hard | |
| if difficulty in ("medium", "hard"): | |
| conflicts = generate_conflicting_deadlines(scenario, difficulty, rng) | |
| scenario["conflicting_deadlines"] = conflicts | |
| # Generate interruptions | |
| scenario["interruptions"] = generate_interruptions(scenario, difficulty, rng) | |
| return scenario | |
| def resolve_scenario_path( | |
| base_task_id: str, | |
| seed: int, | |
| dir_path: Path, | |
| ) -> Path: | |
| """Resolve which scenario YAML to load for ``base_task_id``. | |
| Default behaviour (the :class:`~env.config.Settings` ``scenario_variants`` | |
| flag is off) resolves exactly ``{base_task_id}.yaml`` -- byte-identical to | |
| historical behaviour. When the flag is on, all ``{base_task_id}*.yaml`` files | |
| (canonical + authored variants) are globbed, sorted deterministically, and | |
| one is chosen via ``random.Random(seed)`` so the selection is reproducible. | |
| """ | |
| from .config import get_settings | |
| canonical = dir_path / f"{base_task_id}.yaml" | |
| if not get_settings().scenario_variants: | |
| return canonical | |
| candidates = sorted(p for p in dir_path.glob(f"{base_task_id}*.yaml") if p.is_file()) | |
| if not candidates: | |
| return canonical | |
| return random.Random(seed).choice(candidates) | |
| def generate_synthetic_scenario( | |
| base_task_id: str, | |
| seed: int, | |
| difficulty: str = "medium", | |
| scenarios_dir: Path | None = None, | |
| ) -> dict[str, Any]: | |
| """Generate a synthetic scenario with difficulty scaling.""" | |
| rng = random.Random(seed) | |
| dir_path = scenarios_dir or SCENARIOS_DIR | |
| path = resolve_scenario_path(base_task_id, seed, dir_path) | |
| if not path.exists(): | |
| raise ValueError(f"Missing scenario file: {dir_path / f'{base_task_id}.yaml'}") | |
| base_scenario = load_yaml(path) | |
| scaled = scale_difficulty(base_scenario, difficulty, rng) | |
| return scaled | |
| SCENARIOS_DIR = Path(__file__).resolve().parents[1] / "data" / "scenarios" | |
| def load_yaml(path: Path) -> Any: | |
| if not path.exists(): | |
| raise RuntimeError(f"Missing YAML file: {path}") | |
| stat = path.stat() | |
| mtime_ns = stat.st_mtime_ns | |
| size = stat.st_size | |
| with _lock: | |
| cached = _cache.get(path) | |
| if cached and cached[0] == mtime_ns and cached[1] == size: | |
| return copy.deepcopy(cached[2]) | |
| with path.open("r", encoding="utf-8") as handle: | |
| data = yaml.safe_load(handle) or {} | |
| with _lock: | |
| _cache[path] = (mtime_ns, size, data) | |
| return copy.deepcopy(data) | |
| def clear_yaml_cache() -> None: | |
| with _lock: | |
| _cache.clear() | |