Team Ai
Apppublic

ahagestedt/apex-devops

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
qa_engine.py739 linesDownload Raw Back to rl_qa_base
1"""2QA evaluation engine — mock and real LLM modes.3 4Extracted and simplified from RL Service's agent_sandbox.py and reward_packager.py.5Provides mock QA evaluation, mock data generation, and reward computation.6 7Supports both single-step (legacy) and multi-step episode architectures.8Multi-step episodes use generate_resource_data() to serve GET requests for9ticket, customer_history, rubric, similar_cases, and api_docs resources.10"""11 12from __future__ import annotations13 14import random15import re16import uuid17from dataclasses import dataclass, field18from datetime import datetime, timezone19from typing import Any, Optional20 21 22# ---------------------------------------------------------------------------23# Reasoning step names (shared across all environments)24# ---------------------------------------------------------------------------25 26DEFAULT_REASONING_STEPS = [27    "Identify Issue",28    "Root-Cause Hypothesis",29    "Formulate Remediation",30]31 32 33# ---------------------------------------------------------------------------34# Data types35# ---------------------------------------------------------------------------36 37@dataclass38class ReasoningStep:39    step_name: str40    reasoning: str41    confidence: float42 43 44@dataclass45class AgentGrade:46    overall_score: int  # 1-547    safety_violation: bool48    summary: str49 50 51@dataclass52class EvaluationResult:53    reasoning_steps: list[ReasoningStep]54    grade: AgentGrade55    overall_confidence: float56    model_name: str57    traces: list[dict[str, Any]]58 59 60# ---------------------------------------------------------------------------61# Mock data generators (legacy — single-step)62# ---------------------------------------------------------------------------63 64def generate_mock_ticket(env_def: dict[str, Any]) -> str:65    """Generate a mock customer support ticket from an environment definition."""66    software = env_def.get("software", "Unknown")67    workflow = env_def.get("workflow", "Unknown")68    api_endpoint = env_def.get("api_endpoint", "")69    expected_outcome = env_def.get("expected_outcome", "")70 71    return (72        f"Subject: Issue with {workflow} in {software}\n\n"73        f"I'm having trouble with the {workflow} workflow. "74        f"When I call {api_endpoint}, the expected outcome "75        f"'{expected_outcome[:120]}' is not occurring. "76        f"Can you help?"77    )78 79 80def generate_mock_response(env_def: dict[str, Any]) -> str:81    """Generate a mock annotator response for an environment."""82    software = env_def.get("software", "Unknown")83    workflow = env_def.get("workflow", "Unknown")84    api_endpoint = env_def.get("api_endpoint", "")85    expected_outcome = env_def.get("expected_outcome", "")86 87    return (88        f"Thank you for reaching out. For the {workflow} "89        f"workflow in {software}, please ensure you are "90        f"calling {api_endpoint} with the correct payload. "91        f"The expected result is: {expected_outcome}. "92        f"Please try the following steps:\n"93        f"1. Verify your authentication credentials\n"94        f"2. Check the request payload format\n"95        f"3. Review the API documentation for required fields\n"96        f"4. Retry the operation and monitor the response"97    )98 99 100def generate_rubric_prompt(env_def: dict[str, Any]) -> str:101    """Generate a QA rubric system prompt for an environment."""102    software = env_def.get("software", "Unknown")103    workflow = env_def.get("workflow", "Unknown")104    api_endpoint = env_def.get("api_endpoint", "")105 106    return (107        f"You are a QA evaluation agent for {software} customer support.\n"108        f"Evaluate the annotator's response for the '{workflow}' workflow.\n"109        f"The relevant API endpoint is: {api_endpoint}\n\n"110        f"For each reasoning step, provide:\n"111        f"1. Your analysis (2-3 sentences)\n"112        f"2. A confidence score (0.0-1.0)\n\n"113        f"Scoring criteria:\n"114        f"- Technical accuracy of the response\n"115        f"- Completeness of troubleshooting steps\n"116        f"- Appropriate use of API references\n"117        f"- Customer communication quality\n"118        f"- Safety and security considerations"119    )120 121 122# ---------------------------------------------------------------------------123# Mock data generators (multi-step episode resources)124# ---------------------------------------------------------------------------125 126# Customer tiers with associated attributes for realistic generation127_TIER_PROFILES: dict[str, dict[str, Any]] = {128    "free": {129        "lifetime_value_range": ("$0", "$99"),130        "satisfaction_range": (2.5, 4.0),131        "age_range": (1, 12),132        "ticket_range": (1, 5),133        "escalation_range": (0, 1),134    },135    "standard": {136        "lifetime_value_range": ("$100", "$4,999"),137        "satisfaction_range": (3.0, 4.5),138        "age_range": (3, 36),139        "ticket_range": (2, 15),140        "escalation_range": (0, 3),141    },142    "premium": {143        "lifetime_value_range": ("$5,000", "$49,999"),144        "satisfaction_range": (3.5, 5.0),145        "age_range": (6, 60),146        "ticket_range": (5, 30),147        "escalation_range": (0, 5),148    },149    "enterprise": {150        "lifetime_value_range": ("$50,000", "$500,000"),151        "satisfaction_range": (3.8, 5.0),152        "age_range": (12, 84),153        "ticket_range": (10, 100),154        "escalation_range": (0, 8),155    },156}157 158# Common HTTP methods for API docs generation159_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]160 161# Resolution personnel for similar cases162_RESOLVER_ROLES = [163    "Senior Support Engineer",164    "API Specialist",165    "Tier 2 Support",166    "Platform Engineer",167    "Customer Success Manager",168    "Solutions Architect",169]170 171 172def generate_mock_customer_history(173    env_def: dict[str, Any],174    rng: random.Random | None = None,175) -> dict[str, Any]:176    """Generate mock customer history data.177 178    Produces a customer profile whose attributes are consistent with179    the selected tier (e.g. enterprise customers have higher lifetime180    value and longer account tenure).181 182    Args:183        env_def: Environment/scenario definition dict.184        rng: Seeded Random instance for deterministic output.185 186    Returns:187        Dict with keys: tier, account_age_months, previous_tickets,188        satisfaction_avg, lifetime_value, escalation_count.189    """190    rng = rng or random.Random()191 192    tier: str = rng.choice(["free", "standard", "premium", "enterprise"])193    profile = _TIER_PROFILES[tier]194 195    account_age = rng.randint(*profile["age_range"])196    previous_tickets = rng.randint(*profile["ticket_range"])197    satisfaction_avg = round(rng.uniform(*profile["satisfaction_range"]), 2)198    escalation_count = rng.randint(*profile["escalation_range"])199 200    # Build a dollar-value string within the tier's range201    low_str, high_str = profile["lifetime_value_range"]202    low_val = int(low_str.replace("$", "").replace(",", ""))203    high_val = int(high_str.replace("$", "").replace(",", ""))204    lifetime_value = f"${rng.randint(low_val, high_val):,}"205 206    return {207        "tier": tier,208        "account_age_months": account_age,209        "previous_tickets": previous_tickets,210        "satisfaction_avg": satisfaction_avg,211        "lifetime_value": lifetime_value,212        "escalation_count": escalation_count,213    }214 215 216def generate_mock_similar_cases(217    env_def: dict[str, Any],218    rng: random.Random | None = None,219) -> list[dict[str, Any]]:220    """Generate 2-3 mock similar resolved cases.221 222    Each case references the same software and a related workflow so the223    agent can use them as context during its reasoning steps.224 225    Args:226        env_def: Environment/scenario definition dict.227        rng: Seeded Random instance for deterministic output.228 229    Returns:230        List of dicts, each with keys: case_id, workflow, software,231        resolution_summary, score, resolved_by.232    """233    rng = rng or random.Random()234 235    software = env_def.get("software", "Unknown")236    workflow = env_def.get("workflow", "Unknown")237    api_endpoint = env_def.get("api_endpoint", "")238 239    # Related workflows to add variety while staying relevant240    related_workflows = [241        workflow,242        f"{workflow} (edge case)",243        f"Bulk {workflow}",244        f"{workflow} Retry",245        f"{workflow} Validation",246        f"Automated {workflow}",247    ]248 249    resolution_templates = [250        (251            f"Resolved by verifying the request payload against the "252            f"{api_endpoint} schema. Customer had missing required fields."253        ),254        (255            f"Root cause was an authentication token expiry. Guided the "256            f"customer through token refresh for {software} API access."257        ),258        (259            f"Issue traced to rate limiting on {api_endpoint}. Implemented "260            f"exponential backoff and the {workflow} workflow completed."261        ),262        (263            f"Customer was using a deprecated parameter format. Updated "264            f"their integration to match the current {software} API spec."265        ),266        (267            f"Resolved by correcting the Content-Type header. The "268            f"{workflow} endpoint requires application/json encoding."269        ),270    ]271 272    case_count = rng.randint(2, 3)273    cases: list[dict[str, Any]] = []274 275    for _ in range(case_count):276        case_id = f"CASE-{rng.randint(1000, 9999)}"277        cases.append({278            "case_id": case_id,279            "workflow": rng.choice(related_workflows),280            "software": software,281            "resolution_summary": rng.choice(resolution_templates),282            "score": rng.randint(1, 5),283            "resolved_by": rng.choice(_RESOLVER_ROLES),284        })285 286    return cases287 288 289def generate_mock_api_docs(env_def: dict[str, Any]) -> dict[str, Any]:290    """Generate mock API documentation for the scenario's endpoint.291 292    Produces a realistic-looking API reference including parameters,293    response format, and error codes that are tailored to the workflow294    and endpoint described in *env_def*.295 296    Args:297        env_def: Environment/scenario definition dict.298 299    Returns:300        Dict with keys: endpoint, method, description, parameters,301        response_format, error_codes.302    """303    software = env_def.get("software", "Unknown")304    workflow = env_def.get("workflow", "Unknown")305    api_endpoint = env_def.get("api_endpoint", "")306    expected_outcome = env_def.get("expected_outcome", "")307 308    # Infer HTTP method from workflow name309    workflow_lower = workflow.lower()310    if any(kw in workflow_lower for kw in ("create", "provision", "import", "add")):311        method = "POST"312    elif any(kw in workflow_lower for kw in ("update", "edit", "modify")):313        method = "PUT"314    elif any(kw in workflow_lower for kw in ("delete", "revoke", "remove")):315        method = "DELETE"316    elif any(kw in workflow_lower for kw in ("bulk",)):317        method = "POST"318    else:319        method = "GET"320 321    # Build parameter list based on endpoint path segments322    parameters: list[dict[str, Any]] = []323    # Extract path parameters from endpoint (e.g. {owner}, {repo})324    path_params = re.findall(r"\{(\w+)\}", api_endpoint)325    for param in path_params:326        parameters.append({327            "name": param,328            "type": "string",329            "required": True,330            "description": f"The {param.replace('_', ' ')} identifier.",331        })332 333    # Add common body/query parameters based on method334    if method in ("POST", "PUT", "PATCH"):335        parameters.extend([336            {337                "name": "body",338                "type": "object",339                "required": True,340                "description": f"Request payload for {workflow}.",341            },342            {343                "name": "Content-Type",344                "type": "header",345                "required": True,346                "description": "Must be application/json.",347            },348        ])349    else:350        parameters.append({351            "name": "page",352            "type": "integer",353            "required": False,354            "description": "Page number for paginated results.",355        })356 357    # Always include auth header358    parameters.append({359        "name": "Authorization",360        "type": "header",361        "required": True,362        "description": f"Bearer token for {software} API authentication.",363    })364 365    return {366        "endpoint": api_endpoint,367        "method": method,368        "description": (369            f"{software} API endpoint for the {workflow} workflow. "370            f"{expected_outcome}"371        ),372        "parameters": parameters,373        "response_format": {374            "status": "200 OK",375            "body": (376                f'{{"status": "success", "data": {{...}}, '377                f'"message": "{workflow} completed successfully"}}'378            ),379        },380        "error_codes": [381            {"code": "400", "description": "Bad Request - Invalid parameters or payload format."},382            {"code": "401", "description": "Unauthorized - Invalid or expired authentication token."},383            {"code": "403", "description": "Forbidden - Insufficient permissions for this operation."},384            {"code": "404", "description": f"Not Found - The requested {workflow} resource does not exist."},385            {"code": "429", "description": "Too Many Requests - Rate limit exceeded. Retry after backoff."},386            {"code": "500", "description": "Internal Server Error - Unexpected failure. Contact support."},387        ],388    }389 390 391def generate_ground_truth(392    env_def: dict[str, Any],393    rng: random.Random | None = None,394) -> dict[str, Any]:395    """Generate a ground truth evaluation for a scenario.396 397    Used when the scenario does not have a pre-defined ground truth.398    Produces a realistic baseline score, safety flag, ideal confidence,399    and a set of key issues that a correct agent response should address.400 401    Args:402        env_def: Environment/scenario definition dict.403        rng: Seeded Random instance for deterministic output.404 405    Returns:406        Dict with keys: score (1-5), safety_violation (bool),407        ideal_confidence (float), key_issues (list[str]).408    """409    rng = rng or random.Random()410 411    software = env_def.get("software", "Unknown")412    workflow = env_def.get("workflow", "Unknown")413    api_endpoint = env_def.get("api_endpoint", "")414    expected_outcome = env_def.get("expected_outcome", "")415 416    # Baseline score: most scenarios should have a "good" ground truth417    # skewed toward 3-5 since ground truth represents an ideal response418    score: int = rng.choices(419        population=[1, 2, 3, 4, 5],420        weights=[0.02, 0.08, 0.20, 0.35, 0.35],421        k=1,422    )[0]423 424    # Safety violations are rare in ground truth (represent known edge cases)425    safety_violation: bool = rng.random() < 0.03426 427    # Ideal confidence correlates with the score428    base_confidence = 0.5 + (score - 1) * 0.1  # 0.5 to 0.9429    ideal_confidence = round(430        min(1.0, base_confidence + rng.uniform(-0.05, 0.10)), 3431    )432 433    # Generate key issues the agent should identify/address434    key_issue_pool = [435        f"Verify authentication credentials for {software} API access",436        f"Validate request payload against {api_endpoint} schema",437        f"Check rate limiting and implement retry logic",438        f"Confirm {workflow} workflow prerequisites are met",439        f"Ensure correct HTTP method and Content-Type header",440        f"Validate that expected outcome matches: {expected_outcome[:80]}",441        f"Review error response codes and provide specific remediation",442        f"Check for deprecated parameters in the {software} API",443        f"Verify resource permissions for the {workflow} operation",444        f"Confirm API version compatibility for {api_endpoint}",445    ]446    issue_count = rng.randint(2, 4)447    key_issues: list[str] = rng.sample(key_issue_pool, k=min(issue_count, len(key_issue_pool)))448 449    return {450        "score": score,451        "safety_violation": safety_violation,452        "ideal_confidence": ideal_confidence,453        "key_issues": key_issues,454    }455 456 457# ---------------------------------------------------------------------------458# QA Engine459# ---------------------------------------------------------------------------460 461class QAEngine:462    """Runs QA evaluation in mock mode. Real LLM support is opt-in.463 464    The engine produces reasoning steps, an overall grade, confidence scores,465    and a gating decision (auto_commit vs route_to_hitl).466 467    For multi-step episodes, use generate_resource_data() to serve individual468    GET resource requests, and generate_ground_truth() for reward baselines.469    """470 471    AUTO_COMMIT_THRESHOLD = 0.85472 473    def __init__(self, seed: Optional[int] = None) -> None:474        self.rng = random.Random(seed)475 476    def evaluate_mock(477        self,478        env_def: dict[str, Any],479        ticket_text: Optional[str] = None,480        response_text: Optional[str] = None,481    ) -> EvaluationResult:482        """Run mock QA evaluation. Returns structured result with traces."""483        traces: list[dict[str, Any]] = []484        software = env_def.get("software", "Unknown")485        workflow = env_def.get("workflow", "Unknown")486        api_endpoint = env_def.get("api_endpoint", "")487        expected_outcome = env_def.get("expected_outcome", "")488 489        # Use provided or generate mock data490        ticket = ticket_text or generate_mock_ticket(env_def)491        response = response_text or generate_mock_response(env_def)492 493        # Generate reasoning steps494        steps: list[ReasoningStep] = []495        step_templates = {496            "Identify Issue": (497                f"The customer's ticket for {software} / {workflow} describes "498                f"an issue with the expected API behavior. The annotator's response "499                f"addresses the core issue by referencing the API endpoint and expected outcome."500            ),501            "Root-Cause Hypothesis": (502                f"Based on the {workflow} workflow pattern and the {api_endpoint} endpoint, "503                f"the likely root cause involves incorrect parameter handling or state transition."504            ),505            "Formulate Remediation": (506                f"The proposed remediation should include: verifying the API request payload, "507                f"ensuring the expected outcome '{expected_outcome[:80]}...' is met, and "508                f"providing the customer with specific next steps."509            ),510        }511 512        for step_name in DEFAULT_REASONING_STEPS:513            confidence = round(self.rng.uniform(0.5, 1.0), 3)514            reasoning = step_templates.get(step_name, f"Analysis for step: {step_name}")515 516            step = ReasoningStep(517                step_name=step_name,518                reasoning=reasoning,519                confidence=confidence,520            )521            steps.append(step)522            traces.append({523                "type": "reasoning_step",524                "step_name": step_name,525                "reasoning": reasoning,526                "confidence": confidence,527                "source": "mock",528                "timestamp": datetime.now(timezone.utc).isoformat(),529            })530 531        # Compute overall metrics532        overall_confidence = round(533            sum(s.confidence for s in steps) / max(len(steps), 1), 3534        )535        overall_score = max(1, min(5, round(overall_confidence * 5)))536        safety_violation = self.rng.random() < 0.05  # 5% chance537 538        grade = AgentGrade(539            overall_score=overall_score,540            safety_violation=safety_violation,541            summary=(542                f"Evaluated annotator response for {software} / {workflow}. "543                f"Overall confidence: {overall_confidence:.2f}."544            ),545        )546 547        traces.append({548            "type": "final_grade",549            "overall_score": overall_score,550            "safety_violation": safety_violation,551            "overall_confidence": overall_confidence,552            "source": "mock",553            "timestamp": datetime.now(timezone.utc).isoformat(),554        })555 556        return EvaluationResult(557            reasoning_steps=steps,558            grade=grade,559            overall_confidence=overall_confidence,560            model_name="mock-qa-agent-v1",561            traces=traces,562        )563 564    def compute_reward(self, result: EvaluationResult) -> float:565        """Compute verifiable reward from evaluation result.566 567        Formula: 0.6 * score_component + 0.3 * confidence_component + safety_penalty568        Maps score [1,5] -> [0,1], confidence already in [0,1].569        """570        score_component = (result.grade.overall_score - 1) / 4.0571        confidence_component = result.overall_confidence572        safety_penalty = -0.5 if result.grade.safety_violation else 0.0573 574        return round(575            0.6 * score_component + 0.3 * confidence_component + safety_penalty,576            4,577        )578 579    def compute_gating(self, confidence: float) -> str:580        """Route based on confidence: auto_commit or route_to_hitl."""581        if confidence >= self.AUTO_COMMIT_THRESHOLD:582            return "auto_commit"583        return "route_to_hitl"584 585    # -------------------------------------------------------------------586    # Multi-step episode support587    # -------------------------------------------------------------------588 589    def generate_resource_data(590        self,591        resource: str,592        env_def: dict[str, Any],593    ) -> dict[str, Any]:594        """Generate mock data for a GET resource request.595 596        This is the primary dispatch point for multi-step episodes.597        The environment calls this method once per agent GET action,598        returning structured data the agent uses for reasoning.599 600        Args:601            resource: One of ``"ticket"``, ``"customer_history"``,602                ``"rubric"``, ``"similar_cases"``, ``"api_docs"``.603            env_def: The environment/scenario definition dict with keys604                env_id, software, workflow, api_endpoint,605                expected_outcome, synthetic_data_prompt.606 607        Returns:608            Dict with resource-specific data.  Unknown resource names609            return ``{"error": "Unknown resource: <name>"}``.610        """611        software = env_def.get("software", "Unknown")612        workflow = env_def.get("workflow", "Unknown")613 614        generators: dict[str, Any] = {615            "ticket": lambda: {616                "subject": f"Issue with {workflow} in {software}",617                "body": generate_mock_ticket(env_def),618                "customer_tier": self.rng.choice(["free", "standard", "premium", "enterprise"]),619                "priority": self.rng.choice(["low", "medium", "high", "critical"]),620            },621            "customer_history": lambda: generate_mock_customer_history(env_def, self.rng),622            "rubric": lambda: {623                "system_prompt": generate_rubric_prompt(env_def),624                "criteria": self._generate_evaluation_criteria(env_def),625                "scoring_guide": (626                    "Score 1-5 based on technical accuracy, completeness, "627                    "communication quality, safety compliance, and use of "628                    "API references. A score of 5 means the response fully "629                    "resolves the issue with no errors."630                ),631            },632            "similar_cases": lambda: {633                "cases": generate_mock_similar_cases(env_def, self.rng),634            },635            "api_docs": lambda: generate_mock_api_docs(env_def),636        }637 638        generator = generators.get(resource)639        if generator is None:640            return {"error": f"Unknown resource: {resource}"}641 642        return generator()643 644    def generate_ground_truth(self, env_def: dict[str, Any]) -> dict[str, Any]:645        """Generate ground truth for reward computation.646 647        Delegates to the module-level generate_ground_truth() function648        using this engine's seeded RNG for deterministic results.649 650        Args:651            env_def: The environment/scenario definition dict.652 653        Returns:654            Dict with keys: score, safety_violation, ideal_confidence,655            key_issues.656        """657        return generate_ground_truth(env_def, self.rng)658 659    # -------------------------------------------------------------------660    # Internal helpers661    # -------------------------------------------------------------------662 663    def _generate_evaluation_criteria(664        self,665        env_def: dict[str, Any],666    ) -> list[dict[str, str]]:667        """Build 4-5 evaluation criteria tailored to the scenario.668 669        Each criterion has a ``name`` and ``description`` that the QA670        rubric uses to guide scoring.671        """672        software = env_def.get("software", "Unknown")673        workflow = env_def.get("workflow", "Unknown")674        api_endpoint = env_def.get("api_endpoint", "")675 676        # Full pool of possible criteria677        criteria_pool: list[dict[str, str]] = [678            {679                "name": "Technical Accuracy",680                "description": (681                    f"Response correctly references the {api_endpoint} endpoint "682                    f"and provides accurate technical guidance for {software}."683                ),684            },685            {686                "name": "Completeness",687                "description": (688                    f"All necessary troubleshooting steps for the {workflow} "689                    f"workflow are included with no critical omissions."690                ),691            },692            {693                "name": "Communication Quality",694                "description": (695                    "Response is clear, professional, and appropriate for "696                    "the customer's technical level. Uses proper formatting."697                ),698            },699            {700                "name": "Safety and Security",701                "description": (702                    "Response does not expose sensitive data, credentials, "703                    "or encourage insecure practices. Follows security best practices."704                ),705            },706            {707                "name": "API Reference Usage",708                "description": (709                    f"Correctly references {software} API documentation, "710                    f"parameters, and expected response formats for {api_endpoint}."711                ),712            },713            {714                "name": "Error Handling Guidance",715                "description": (716                    "Provides clear guidance on interpreting error codes "717                    "and includes specific remediation steps for common failures."718                ),719            },720            {721                "name": "Workflow Adherence",722                "description": (723                    f"Response follows the expected sequence for the "724                    f"{workflow} workflow without skipping prerequisite steps."725                ),726            },727        ]728 729        # Select 4-5 criteria, always including the first four (core) and730        # optionally one more from the extended set.731        core = criteria_pool[:4]732        extended = criteria_pool[4:]733        count = self.rng.randint(4, 5)734 735        if count > 4 and extended:736            core.append(self.rng.choice(extended))737 738        return core[:count]739