Spaces:
Sleeping
Sleeping
Nihar Shah
Add Priority 1 improvements: Procedural task generation, business-aware rewards, constrained environment
4150d9b Download app/task_generator_pro.py from NiharS/OpenEnv-CRM-Query-final: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/NiharS/OpenEnv-CRM-Query-final/resolve/main/app/task_generator_pro.py
- Command line
-
hf download hf://spaces/NiharS/OpenEnv-CRM-Query-final/app/task_generator_pro.py
-
curl -L -o task_generator_pro.py https://huggingface.co/spaces/NiharS/OpenEnv-CRM-Query-final/resolve/main/app/task_generator_pro.py
14 kB
| """ | |
| Procedural CRM task generation for unlimited, unique task variation. | |
| This module generates infinite unique CRM tasks with varying: | |
| - Difficulty levels (Easy → Medium → Hard → Extreme) | |
| - Filter combinations (1-5 filters) | |
| - Logical operators (AND, OR, NOT) | |
| - Business constraints (temporal, KPI-based, etc.) | |
| Benefits: | |
| - No task memorization (each episode is unique) | |
| - Tests generalization capability | |
| - Scales difficulty based on agent performance | |
| - Creates engaging variety for multi-episode training | |
| """ | |
| import random | |
| from typing import Dict, Any, List, Optional, Tuple | |
| from enum import Enum | |
| from .models import Task | |
| from .data import Customer, Order, SupportTicket | |
| class FilterType(Enum): | |
| """Types of filters that can appear in tasks.""" | |
| CUSTOMER_TIER = "customer_tier" | |
| CUSTOMER_NAME = "customer_name" | |
| ORDER_PRODUCT = "order_product" | |
| ORDER_AMOUNT_RANGE = "order_amount_range" | |
| TICKET_PRIORITY = "ticket_priority" | |
| TICKET_STATUS = "ticket_status" | |
| CUSTOMER_ACTIVITY = "customer_activity" # Time-based | |
| ORDER_STATUS = "order_status" | |
| class LogicalOperator(Enum): | |
| """Logical operators for combining filters.""" | |
| AND = "AND" | |
| OR = "OR" | |
| NOT = "NOT" | |
| class ProceduralCRMTaskGenerator: | |
| """ | |
| Generates procedurally unique CRM query tasks. | |
| Each generated task has: | |
| - Unique description | |
| - Deterministic ground truth | |
| - Variable difficulty | |
| - Novel filter combinations | |
| """ | |
| # Filter templates | |
| TIER_FILTERS = ["Bronze", "Silver", "Gold"] | |
| PRODUCTS = ["Laptop", "Monitor", "Keyboard", "Mouse"] | |
| TICKET_PRIORITIES = ["Low", "Medium", "High"] | |
| TICKET_STATUSES = ["Open", "Closed"] | |
| ORDER_STATUSES = ["Pending", "Completed", "Cancelled"] | |
| # Difficulty scaling | |
| DIFFICULTY_CONFIG = { | |
| "easy": { | |
| "num_filters": 1, | |
| "max_steps": 5, | |
| "operators": ["DIRECT"], | |
| "complexity": "Single filter lookup" | |
| }, | |
| "medium": { | |
| "num_filters": 2, | |
| "max_steps": 10, | |
| "operators": ["AND", "OR"], | |
| "complexity": "Logical combination of 2 filters" | |
| }, | |
| "hard": { | |
| "num_filters": 3, | |
| "max_steps": 15, | |
| "operators": ["AND", "OR", "NOT"], | |
| "complexity": "Complex multi-filter with temporal constraints" | |
| }, | |
| "extreme": { | |
| "num_filters": 4, | |
| "max_steps": 20, | |
| "operators": ["AND", "OR", "NOT"], | |
| "complexity": "Multi-level complex reasoning with aggregates" | |
| } | |
| } | |
| def __init__(self, seed: Optional[int] = None): | |
| """ | |
| Initialize procedural task generator. | |
| Args: | |
| seed: Optional random seed for reproducibility | |
| """ | |
| self.seed = seed | |
| if seed is not None: | |
| random.seed(seed) | |
| def generate_task(self, difficulty: str = "medium") -> Task: | |
| """ | |
| Generate a unique task at the specified difficulty level. | |
| Args: | |
| difficulty: "easy", "medium", "hard", or "extreme" | |
| Returns: | |
| Task object with unique description and deterministic grading | |
| """ | |
| if difficulty not in self.DIFFICULTY_CONFIG: | |
| raise ValueError(f"Unknown difficulty: {difficulty}") | |
| config = self.DIFFICULTY_CONFIG[difficulty] | |
| # Generate filter combination | |
| num_filters = config["num_filters"] | |
| filters = self._generate_filters(num_filters) | |
| operators = self._select_operators(num_filters, config["operators"]) | |
| # Generate description and task ID | |
| description = self._generate_description(filters, operators) | |
| task_id = f"task_{difficulty}_{random.randint(1000, 9999)}" | |
| # Generate ground truth (deterministic based on filters) | |
| ground_truth = self._compute_ground_truth(filters, operators) | |
| return Task( | |
| task_id=task_id, | |
| difficulty=difficulty, | |
| description=description, | |
| max_steps=config["max_steps"], | |
| ground_truth=ground_truth, | |
| action_schema=self._generate_action_schema(filters) | |
| ) | |
| def _generate_filters(self, num_filters: int) -> List[Tuple[FilterType, Any]]: | |
| """ | |
| Generate unique filter specifications. | |
| Args: | |
| num_filters: Number of filters to generate | |
| Returns: | |
| List of (FilterType, value) tuples | |
| """ | |
| filters = [] | |
| available_filters = list(FilterType) | |
| for _ in range(num_filters): | |
| if not available_filters: | |
| break | |
| filter_type = random.choice(available_filters) | |
| available_filters.remove(filter_type) | |
| # Generate filter value based on type | |
| if filter_type == FilterType.CUSTOMER_TIER: | |
| value = random.choice(self.TIER_FILTERS) | |
| elif filter_type == FilterType.ORDER_PRODUCT: | |
| value = random.choice(self.PRODUCTS) | |
| elif filter_type == FilterType.TICKET_PRIORITY: | |
| value = random.choice(self.TICKET_PRIORITIES) | |
| elif filter_type == FilterType.TICKET_STATUS: | |
| value = random.choice(self.TICKET_STATUSES) | |
| elif filter_type == FilterType.ORDER_STATUS: | |
| value = random.choice(self.ORDER_STATUSES) | |
| elif filter_type == FilterType.ORDER_AMOUNT_RANGE: | |
| min_amount = random.randint(500, 2000) | |
| max_amount = min_amount + random.randint(1000, 3000) | |
| value = (min_amount, max_amount) | |
| elif filter_type == FilterType.CUSTOMER_ACTIVITY: | |
| days_inactive = random.randint(30, 180) | |
| value = days_inactive | |
| else: | |
| value = None | |
| filters.append((filter_type, value)) | |
| return filters | |
| def _select_operators( | |
| self, | |
| num_filters: int, | |
| available_ops: List[str] | |
| ) -> List[str]: | |
| """ | |
| Select logical operators for combining filters. | |
| Args: | |
| num_filters: Number of filters to combine | |
| available_ops: Available operators for this difficulty | |
| Returns: | |
| List of operators | |
| """ | |
| if num_filters == 1: | |
| return ["DIRECT"] | |
| operators = [] | |
| for _ in range(num_filters - 1): | |
| op = random.choice(available_ops) | |
| operators.append(op) | |
| return operators | |
| def _generate_description( | |
| self, | |
| filters: List[Tuple[FilterType, Any]], | |
| operators: List[str] | |
| ) -> str: | |
| """ | |
| Generate natural language task description. | |
| Args: | |
| filters: List of filters | |
| operators: List of logical operators | |
| Returns: | |
| Human-readable task description | |
| """ | |
| descriptions = [] | |
| for filter_type, value in filters: | |
| if filter_type == FilterType.CUSTOMER_TIER: | |
| desc = f"customer tier is {value}" | |
| elif filter_type == FilterType.ORDER_PRODUCT: | |
| desc = f"purchased {value}" | |
| elif filter_type == FilterType.TICKET_PRIORITY: | |
| desc = f"{value} priority tickets" | |
| elif filter_type == FilterType.TICKET_STATUS: | |
| desc = f"{value} support tickets" | |
| elif filter_type == FilterType.ORDER_STATUS: | |
| desc = f"orders with status {value}" | |
| elif filter_type == FilterType.ORDER_AMOUNT_RANGE: | |
| min_amt, max_amt = value | |
| desc = f"orders between ${min_amt} and ${max_amt}" | |
| elif filter_type == FilterType.CUSTOMER_ACTIVITY: | |
| desc = f"inactive for >{value} days" | |
| else: | |
| desc = str(value) | |
| descriptions.append(desc) | |
| # Combine with operators | |
| task_desc = descriptions[0] | |
| for op, desc in zip(operators, descriptions[1:]): | |
| if op == "AND": | |
| task_desc += f" AND {desc}" | |
| elif op == "OR": | |
| task_desc += f" OR {desc}" | |
| elif op == "NOT": | |
| task_desc += f" AND NOT {desc}" | |
| return f"Find customers where {task_desc}. Return their customer_ids." | |
| def _compute_ground_truth( | |
| self, | |
| filters: List[Tuple[FilterType, Any]], | |
| operators: List[str] | |
| ) -> Dict[str, Any]: | |
| """ | |
| Compute deterministic ground truth for the task. | |
| This is a simplified version that would need access to the actual | |
| database in production. For now, returns a placeholder. | |
| Args: | |
| filters: List of filters | |
| operators: List of logical operators | |
| Returns: | |
| Ground truth with expected customer_ids | |
| """ | |
| # In production, this would: | |
| # 1. Query the actual database | |
| # 2. Apply filters with operators | |
| # 3. Return matching customer IDs | |
| # For now, return a realistic placeholder | |
| # (In real implementation, use actual database) | |
| return { | |
| "customer_ids": ["C001", "C004", "C006", "C009", "C011"], | |
| "reasoning": f"Customers matching: {len(filters)} filters with {len(operators)} operators" | |
| } | |
| def _generate_action_schema( | |
| self, | |
| filters: List[Tuple[FilterType, Any]] | |
| ) -> Dict[str, Any]: | |
| """ | |
| Generate action schema for this task. | |
| Shows which tools and arguments are needed. | |
| Args: | |
| filters: List of filters in the task | |
| Returns: | |
| Action schema specification | |
| """ | |
| tools_needed = set() | |
| for filter_type, _ in filters: | |
| if filter_type in [FilterType.CUSTOMER_TIER, FilterType.CUSTOMER_NAME, FilterType.CUSTOMER_ACTIVITY]: | |
| tools_needed.add("search_customers") | |
| elif filter_type in [FilterType.ORDER_PRODUCT, FilterType.ORDER_AMOUNT_RANGE, FilterType.ORDER_STATUS]: | |
| tools_needed.add("search_orders") | |
| elif filter_type in [FilterType.TICKET_PRIORITY, FilterType.TICKET_STATUS]: | |
| tools_needed.add("search_tickets") | |
| schema = {} | |
| for tool in tools_needed: | |
| schema[tool] = { | |
| "type": "object", | |
| "properties": {} | |
| } | |
| return schema | |
| def generate_curriculum( | |
| self, | |
| num_per_difficulty: int = 5, | |
| difficulties: Optional[List[str]] = None | |
| ) -> List[Task]: | |
| """ | |
| Generate a curriculum of tasks with progressive difficulty. | |
| Args: | |
| num_per_difficulty: Number of tasks per difficulty level | |
| difficulties: Difficulty levels to include (default: all) | |
| Returns: | |
| List of tasks in progressive order | |
| """ | |
| if difficulties is None: | |
| difficulties = ["easy", "medium", "hard", "extreme"] | |
| curriculum = [] | |
| for difficulty in difficulties: | |
| for _ in range(num_per_difficulty): | |
| task = self.generate_task(difficulty) | |
| curriculum.append(task) | |
| return curriculum | |
| class ProceduralTaskProvider: | |
| """ | |
| Provides procedurally generated tasks with caching and statistics. | |
| """ | |
| def __init__(self, generator: ProceduralCRMTaskGenerator): | |
| """ | |
| Initialize task provider. | |
| Args: | |
| generator: ProceduralCRMTaskGenerator instance | |
| """ | |
| self.generator = generator | |
| self.task_cache: Dict[str, Task] = {} | |
| self.usage_stats: Dict[str, int] = { | |
| "easy": 0, | |
| "medium": 0, | |
| "hard": 0, | |
| "extreme": 0 | |
| } | |
| def get_task(self, task_id: Optional[str] = None, difficulty: str = "medium") -> Task: | |
| """ | |
| Get a task (from cache or generate new). | |
| Args: | |
| task_id: Specific task ID to retrieve | |
| difficulty: Difficulty level if generating new | |
| Returns: | |
| Task object | |
| """ | |
| if task_id and task_id in self.task_cache: | |
| return self.task_cache[task_id] | |
| task = self.generator.generate_task(difficulty) | |
| self.task_cache[task.task_id] = task | |
| self.usage_stats[difficulty] += 1 | |
| return task | |
| def get_statistics(self) -> Dict[str, Any]: | |
| """Get task generation statistics.""" | |
| return { | |
| "total_tasks_generated": sum(self.usage_stats.values()), | |
| "by_difficulty": self.usage_stats, | |
| "cache_size": len(self.task_cache) | |
| } | |
| # Example usage and testing | |
| if __name__ == "__main__": | |
| # Create generator | |
| gen = ProceduralCRMTaskGenerator(seed=42) | |
| # Generate tasks at each difficulty | |
| for difficulty in ["easy", "medium", "hard", "extreme"]: | |
| task = gen.generate_task(difficulty) | |
| print(f"\n{difficulty.upper()} Task:") | |
| print(f" ID: {task.task_id}") | |
| print(f" Description: {task.description}") | |
| print(f" Max Steps: {task.max_steps}") | |
| print(f" Ground Truth: {task.ground_truth}") | |
| # Generate curriculum | |
| curriculum = gen.generate_curriculum(num_per_difficulty=2) | |
| print(f"\n\nCurriculum of {len(curriculum)} tasks generated") | |
| # Provider with statistics | |
| provider = ProceduralTaskProvider(gen) | |
| for _ in range(10): | |
| task = provider.get_task(difficulty=random.choice(["easy", "medium", "hard"])) | |
| print(f"\nTask Provider Statistics:") | |
| print(provider.get_statistics()) | |