CoolFace
Apppublic

raunakratan/priority-mind-lite

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
app.py513 linesDownload Raw Back to root
1"""2PriorityMind-Lite: Gradio Web Interface for Hugging Face Spaces3================================================================4This provides an interactive demo of the PriorityMind-Lite environment5where users can interact with the AI customer support triage agent.6 7Deployed at: https://huggingface.co/spaces/raunakratan/priority-mind-lite8"""9 10from __future__ import annotations11 12import asyncio13import json14import os15from pathlib import Path16from typing import Any17 18if os.name == "nt":19    try:20        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())21    except AttributeError:22        pass23 24import gradio as gr25from dotenv import load_dotenv26 27from environment import PriorityMindEnv28from inference import mock_action29from models import Action, Observation30from utils import format_partial_signal31 32load_dotenv(Path(__file__).parent / ".env")33 34API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")35MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")36HF_TOKEN = os.getenv("HF_TOKEN", "").strip()37 38# Try to import openai for live mode39try:40    from openai import OpenAI41 42    OPENAI_AVAILABLE = True43except ImportError:44    OPENAI_AVAILABLE = False45 46 47APP_THEME = gr.themes.Soft()48 49 50def get_client() -> OpenAI | None:51    """Get OpenAI client for HF Router if available."""52    if not HF_TOKEN or not OPENAI_AVAILABLE:53        return None54    return OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN, timeout=12)55 56 57def configure_runtime() -> None:58    """Use a Windows-friendly event loop policy for Gradio/Uvicorn."""59    if os.name != "nt":60        return61    try:62        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())63    except (AttributeError, RuntimeError):64        pass65 66 67def _llm_prompt(obs: Observation) -> str:68    """Build the LLM prompt for action selection."""69    return f"""You are a customer support triage agent.70 71Observation:72- ticket_text: {obs.ticket_text}73- sentiment: {obs.sentiment:.2f}74- category: {obs.category}75- priority: {obs.priority}76- attempts: {obs.attempts}77- resolved: {str(obs.resolved).lower()}78 79Choose exactly one next action:80- categorize with content billing/technical/general/complaint81- prioritize with priority low/medium/high/urgent82- respond with a concise, empathetic support message83- escalate84- resolve85 86Return JSON only with keys action_type, content, priority.87"""88 89 90def choose_action(obs: Observation, task: str, client: OpenAI | None) -> tuple[Action, str]:91    """Choose the next action using LLM if available, else use heuristic.92 93    Returns:94        A tuple of (action, action_source) where action_source is one of:95        "LLM", "heuristic", or "heuristic fallback".96    """97    if client is not None:98        try:99            response = client.chat.completions.create(100                model=MODEL_NAME,101                messages=[{"role": "user", "content": _llm_prompt(obs)}],102                temperature=0.0,103                response_format={"type": "json_object"},104            )105            payload = response.choices[0].message.content106            if payload:107                data = json.loads(payload)108                action = Action(**data)109                return action, "LLM"110        except Exception:111            return mock_action(obs, task), "heuristic fallback"112 113    return mock_action(obs, task), "heuristic"114 115 116def format_action(action: Action) -> str:117    """Format action for display."""118    parts = [action.action_type.upper()]119    if action.content:120        parts.append(f'"{action.content[:60]}{"..." if len(action.content) > 60 else ""}"')121    if action.priority:122        parts.append(f"priority={action.priority}")123    return " -> ".join(parts)124 125 126def _action_mode_label(mode: str) -> str:127    labels = {128        "llm_only": "LLM policy",129        "mixed": "LLM policy with heuristic fallback",130        "heuristic_only": "Heuristic policy",131        "heuristic_fallback": "Heuristic policy after LLM fallback",132    }133    return labels.get(mode, mode)134 135 136def _reward_mode_label(mode: str) -> str:137    labels = {138        "hybrid_llm": "Hybrid grading (LLM + rules)",139        "hybrid_mixed": "Hybrid grading with programmatic fallback",140        "programmatic_fallback": "Programmatic fallback grading",141        "programmatic_only": "Programmatic grading only",142    }143    return labels.get(mode, mode)144 145 146def run_episode(task: str, use_llm: bool) -> tuple[list[dict[str, Any]], dict[str, Any]]:147    """Run a complete episode and return step-by-step results plus mode summary."""148    client = get_client() if use_llm else None149    # Use a fixed seed for reproducible demo results150    env = PriorityMindEnv(task=task, seed=42, enable_llm=client is not None)151    obs = env.reset()152 153    steps = []154    step_num = 0155    llm_action_steps = 0156    reward_llm_steps = 0157    reward_fallback_steps = 0158 159    while True:160        step_num += 1161        action, action_source = choose_action(obs, task, client)162        if action_source == "LLM":163            llm_action_steps += 1164 165        obs, reward, done, info = env.step(action)166        reward_used_fallback = bool(info.get("used_fallback", False))167        if not env.grader.enable_llm:168            reward_source = "Programmatic grading"169        else:170            reward_source = "Programmatic fallback" if reward_used_fallback else "LLM judge"171        if reward_used_fallback:172            reward_fallback_steps += 1173        else:174            reward_llm_steps += 1175 176        step_info = {177            "step": step_num,178            "observation": {179                "ticket_text": obs.ticket_text,180                "sentiment": f"{obs.sentiment:+.2f}",181                "category": obs.category or "Not set",182                "priority": obs.priority or "Not set",183                "attempts": obs.attempts,184                "resolved": obs.resolved,185            },186            "action": format_action(action),187            "action_source": action_source,188            "action_error": info.get("last_action_error"),189            "reward": {190                "score": f"{reward.score:.2f}",191                "reasoning": reward.reasoning[:200] + "..." if len(reward.reasoning) > 200 else reward.reasoning,192            },193            "reward_source": reward_source,194            "partial_signals": {195                "empathy": format_partial_signal(reward.partial_signals.get("empathy")),196                "efficiency": format_partial_signal(reward.partial_signals.get("efficiency")),197                "strategy": format_partial_signal(reward.partial_signals.get("strategy")),198            },199            "done": done,200        }201        steps.append(step_info)202 203        if done:204            break205 206    if not use_llm or client is None:207        action_mode = "heuristic_only"208    elif llm_action_steps == len(steps):209        action_mode = "llm_only"210    elif llm_action_steps > 0:211        action_mode = "mixed"212    else:213        action_mode = "heuristic_fallback"214 215    if not env.grader.enable_llm:216        reward_mode = "programmatic_only"217    elif reward_llm_steps == len(steps):218        reward_mode = "hybrid_llm"219    elif reward_llm_steps > 0:220        reward_mode = "hybrid_mixed"221    else:222        reward_mode = "programmatic_fallback"223 224    summary = {225        "action_mode": action_mode,226        "reward_mode": reward_mode,227        "client_available": client is not None,228        "llm_requested": use_llm,229        "total_steps": len(steps),230        "llm_action_steps": llm_action_steps,231        "heuristic_action_steps": len(steps) - llm_action_steps,232        "reward_llm_steps": reward_llm_steps,233        "reward_fallback_steps": reward_fallback_steps,234    }235    return steps, summary236 237 238def run_demo(task: str, use_llm: bool) -> str:239    """Run demo and return formatted HTML results."""240    try:241        steps, summary = run_episode(task, use_llm)242 243        output = []244        output.append(f"<h1>PriorityMind-Lite Demo</h1>")245        output.append(f"<h2>Task: {task.upper()}</h2>")246        output.append(f"<p><strong>Action Policy:</strong> {_action_mode_label(summary['action_mode'])}</p>")247        output.append(f"<p><strong>Reward Grading:</strong> {_reward_mode_label(summary['reward_mode'])}</p>")248        249        if use_llm and not summary["client_available"]:250            output.append("<div class='alert alert-warning'>[!] Note: HF_TOKEN or OpenAI client unavailable, using heuristic actions and programmatic grading only.</div>")251        elif summary["action_mode"] == "mixed":252            output.append(f"<div class='alert alert-info'>[i] Action generation used LLM in {summary['llm_action_steps']}/{summary['total_steps']} steps, heuristic fallback for the rest.</div>")253        elif summary["action_mode"] == "heuristic_fallback":254            output.append("<div class='alert alert-warning'>[!] Every action fell back to heuristic policy after LLM failure.</div>")255 256        if summary["reward_mode"] == "hybrid_mixed":257            output.append(f"<div class='alert alert-info'>[i] Reward grading used LLM judge in {summary['reward_llm_steps']}/{summary['total_steps']} steps, programmatic fallback in the rest.</div>")258        elif summary["reward_mode"] == "programmatic_fallback":259            output.append("<div class='alert alert-warning'>[!] Reward grading attempted live evaluation, but every step fell back to programmatic grader.</div>")260 261        output.append("<div class='steps-container'>")262 263        for step_info in steps:264            output.append(f"<div class='step-card'>")265            output.append(f"<h3>Step {step_info['step']}</h3>")266            output.append(f"<p><strong>Action:</strong> {step_info['action']} <span class='badge badge-secondary'>({step_info['action_source']})</span></p>")267            output.append(f"<p><strong>Reward Source:</strong> {step_info['reward_source']}</p>")268            output.append(f"<p><strong>Reward Score:</strong> <span class='score'>{step_info['reward']['score']}</span></p>")269            output.append(f"<p><strong>Reasoning:</strong> {step_info['reward']['reasoning']}</p>")270            271            # Progress bars for partial signals272            empathy_val = float(step_info['partial_signals']['empathy'].split('/')[0])273            efficiency_val = float(step_info['partial_signals']['efficiency'].split('/')[0])274            strategy_val = float(step_info['partial_signals']['strategy'].split('/')[0])275            276            def get_color(val):277                if val >= 0.8:278                    return "#28a745"  # green279                elif val >= 0.6:280                    return "#ffc107"  # yellow281                else:282                    return "#dc3545"  # red283            284            output.append("<div class='signals'>")285            output.append(f"<div class='signal'><span class='icon'>EMPATHY</span> <div class='progress-bar'><div class='progress-fill' style='width: {empathy_val*100}%; background-color: {get_color(empathy_val)}'></div></div> <span class='signal-value'>{step_info['partial_signals']['empathy']}</span></div>")286            output.append(f"<div class='signal'><span class='icon'>EFFICIENCY</span> <div class='progress-bar'><div class='progress-fill' style='width: {efficiency_val*100}%; background-color: {get_color(efficiency_val)}'></div></div> <span class='signal-value'>{step_info['partial_signals']['efficiency']}</span></div>")287            output.append(f"<div class='signal'><span class='icon'>STRATEGY</span> <div class='progress-bar'><div class='progress-fill' style='width: {strategy_val*100}%; background-color: {get_color(strategy_val)}'></div></div> <span class='signal-value'>{step_info['partial_signals']['strategy']}</span></div>")288            output.append("</div>")289            290            if step_info["action_error"]:291                output.append(f"<p class='error'>[ERROR] Action Error: {step_info['action_error']}</p>")292            if step_info['done']:293                output.append("<p class='success'>[OK] Episode Complete</p>")294            output.append("</div>")295 296        output.append("</div>")297 298        # Summary with score chart299        scores = [float(s['reward']['score']) for s in steps]300        avg_score = sum(scores) / len(scores) if scores else 0301        min_score = min(scores) if scores else 0302        max_score = max(scores) if scores else 0303        304        # Simple bar chart for scores305        chart_html = "<div class='score-chart'>"306        for i, score in enumerate(scores, 1):307            color = get_color(score)308            chart_html += f"<div class='chart-bar' style='height: {score*100}px; background-color: {color}' title='Step {i}: {score:.2f}'></div>"309        chart_html += "</div>"310        311        output.append("<div class='summary'>")312        output.append("---")313        output.append("<h3>Summary</h3>")314        output.append(f"<p><strong>Average Score:</strong> <span class='avg-score'>{avg_score:.2f}</span></p>")315        output.append(f"<p><strong>Min Score:</strong> {min_score:.2f} | <strong>Max Score:</strong> {max_score:.2f}</p>")316        output.append(f"<p><strong>Total Steps:</strong> {len(steps)}</p>")317        output.append("<h4>Score Progression</h4>")318        output.append(chart_html)319        output.append("</div>")320 321        # Add CSS322        css = """323        <style>324        body { color: #000; }325        h1, h2, h3, h4, h5, h6 { color: #000 !important; }326        .alert { padding: 12px 15px; margin: 10px 0; border-radius: 5px; font-weight: 500; }327        .alert-warning { background-color: #fff3cd; border: 2px solid #ffc107; color: #000; }328        .alert-info { background-color: #d1ecf1; border: 2px solid #17a2b8; color: #000; }329        .alert-danger { background-color: #f8d7da; border: 2px solid #dc3545; color: #000; }330        .step-card { border: 2px solid #ddd; border-radius: 8px; padding: 20px; margin: 15px 0; background-color: #fff; }331        .step-card h3 { color: #000; margin: 0 0 10px 0; }332        .step-card p { color: #000; margin: 8px 0; }333        .badge { display: inline-block; padding: 4px 8px; font-size: 0.85em; border-radius: 3px; background-color: #6c757d; color: white; }334        .badge-secondary { background-color: #6c757d; }335        .score { font-size: 1.2em; font-weight: bold; color: #0056b3; }336        .signals { margin-top: 15px; }337        .signal { display: flex; align-items: center; margin: 10px 0; }338        .signal .icon { width: 100px; font-weight: bold; font-size: 0.9em; color: #000; }339        .progress-bar { flex: 1; height: 15px; background-color: #e9ecef; border-radius: 5px; margin: 0 10px; overflow: hidden; border: 1px solid #ccc; }340        .progress-fill { height: 100%; transition: width 0.3s; }341        .signal-value { width: 80px; text-align: right; font-weight: bold; color: #000; }342        .error { color: #000; font-weight: bold; }343        .success { color: #000; font-weight: bold; }344        .summary { margin-top: 30px; padding: 25px; background-color: #fff; border: 3px solid #007bff; border-radius: 8px; }345        .summary h3 { color: #000; margin: 0 0 15px 0; font-size: 1.5em; }346        .summary h4 { color: #000; margin: 15px 0 10px 0; }347        .summary p { color: #000; font-size: 1.1em; margin: 10px 0; font-weight: 500; }348        .avg-score { font-size: 1.3em; font-weight: bold; color: #000; }349        .score-chart { display: flex; align-items: end; justify-content: space-around; height: 150px; margin: 15px 0; padding: 15px; background-color: #f8f9fa; border-radius: 5px; border: 2px solid #ddd; }350        .chart-bar { width: 25px; min-height: 10px; border-radius: 3px 3px 0 0; margin: 0 3px; }351        .steps-container { margin: 20px 0; }352        </style>353        """354 355        return css + "\n".join(output)356 357    except Exception as e:358        return f"<div class='alert alert-danger'>Error: {str(e)}</div>"359 360 361def compare_modes() -> str:362    """Run comparison between LLM-rewarded and heuristic modes."""363    output = []364    output.append("<h2>Mode Comparison</h2>")365 366    for task in ["easy", "medium", "hard"]:367        output.append(f"<h3>Task: {task.upper()}</h3>")368 369        output.append("<div class='mode-section'>")370        output.append("<h4>LLM-Enabled Mode</h4>")371        try:372            steps_llm, summary_llm = run_episode(task, use_llm=True)373            scores_llm = [float(s['reward']['score']) for s in steps_llm]374            avg_llm = sum(scores_llm) / len(scores_llm) if scores_llm else 0375            output.append(f"<p><strong>Action Policy:</strong> {_action_mode_label(summary_llm['action_mode'])}</p>")376            output.append(f"<p><strong>Reward Grading:</strong> {_reward_mode_label(summary_llm['reward_mode'])}</p>")377            output.append(f"<p><strong>Average Score:</strong> <span class='score'>{avg_llm:.2f}</span></p>")378            output.append(f"<p><strong>Steps:</strong> {len(steps_llm)}</p>")379        except Exception as e:380            output.append(f"<p class='error'>Error: {str(e)}</p>")381        output.append("</div>")382 383        output.append("<div class='mode-section'>")384        output.append("<h4>Heuristic Mode</h4>")385        try:386            steps_heur, summary_heur = run_episode(task, use_llm=False)387            scores_heur = [float(s['reward']['score']) for s in steps_heur]388            avg_heur = sum(scores_heur) / len(scores_heur) if scores_heur else 0389            output.append(f"<p><strong>Action Policy:</strong> {_action_mode_label(summary_heur['action_mode'])}</p>")390            output.append(f"<p><strong>Reward Grading:</strong> {_reward_mode_label(summary_heur['reward_mode'])}</p>")391            output.append(f"<p><strong>Average Score:</strong> <span class='score'>{avg_heur:.2f}</span></p>")392            output.append(f"<p><strong>Steps:</strong> {len(steps_heur)}</p>")393        except Exception as e:394            output.append(f"<p class='error'>Error: {str(e)}</p>")395        output.append("</div>")396 397    output.append("<hr>")398    output.append("<p><em>Note: LLM-enabled mode requires HF_TOKEN to be configured. The app reports action generation and reward grading separately so fallback is explicit.</em></p>")399 400    css = """401    <style>402    h1, h2, h3, h4, h5, h6 { color: #000 !important; }403    .mode-section { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 10px 0; background-color: #f9f9f9; }404    .score { font-size: 1.2em; font-weight: bold; color: #000; }405    .error { color: #000; }406    </style>407    """408 409    return css + "\n".join(output)410 411 412# Create Gradio Interface413with gr.Blocks(title="PriorityMind-Lite") as demo:414    gr.Markdown("""415    # PriorityMind-Lite416    ### LLM-Rewarded Customer Support Ticket Triage Environment417 418    **Meta PyTorch OpenEnv Hackathon 2026 | Team Axiom (IIT Madras)**419 420    This demo showcases an AI agent that learns to triage customer support tickets421    using rewards evaluated by Llama โ€” not hardcoded rules.422    """)423 424    with gr.Tab("Interactive Demo"):425        with gr.Row():426            with gr.Column():427                task_dropdown = gr.Dropdown(428                    choices=["easy", "medium", "hard"],429                    value="hard",430                    label="Select Task Difficulty"431                )432                llm_toggle = gr.Checkbox(433                    value=True,434                    label="Use LLM for actions and grading (requires HF_TOKEN)"435                )436                run_button = gr.Button("Run Demo", variant="primary")437 438            with gr.Column():439                output_html = gr.HTML(label="Demo Results")440 441        run_button.click(442            fn=run_demo,443            inputs=[task_dropdown, llm_toggle],444            outputs=output_html445        )446 447    with gr.Tab("Mode Comparison"):448        compare_button = gr.Button("Compare enabled vs heuristic", variant="secondary")449        comparison_output = gr.HTML()450 451    with gr.Tab("Mode Comparison"):452        compare_button = gr.Button("Compare enabled vs heuristic", variant="secondary")453        comparison_output = gr.HTML()454 455        compare_button.click(456            fn=compare_modes,457            inputs=[],458            outputs=comparison_output459        )460 461    with gr.Tab("About"):462        gr.Markdown("""463        ### How It Works464 465        1. **Environment Reset**: Generate a customer support ticket with sentiment and true category466        2. **Agent Takes Action**: Choose from categorize, prioritize, respond, escalate, or resolve467        3. **Hybrid Grader Evaluates**:468           - LLM Evaluation (60%): Llama judges on empathy, efficiency, strategy469           - Fallback (40%): Deterministic rules ensure reliability470        4. **Reward Signal**: Normalized score [0.0, 1.0] with partial signals471 472        ### Key Innovation473 474        Instead of defining "good customer service" mathematically, we let a language model475        (Llama) judge each agent action on dimensions like empathy, efficiency, and strategy.476 477        ### Technology Stack478 479        - **Framework**: OpenEnv (Meta)480        - **Model**: Llama 3.1 8B Instruct (via HF Router)481        - **Language**: Python 3.10482        """)483 484# Mount OpenEnv server endpoints for hackathon validation485try:486    from environment import PriorityMindEnv487    from models import Action, Observation488    from openenv.core.env_server.http_server import create_app489 490    # Create OpenEnv HTTP server491    openenv_app = create_app(492        PriorityMindEnv,493        Action,494        Observation,495        env_name="priority-mind-lite",496        max_concurrent_envs=1,497    )498 499    # Mount OpenEnv endpoints at root level for hackathon validation500    demo.app.mount("/", openenv_app)501 502except Exception as e:503    print(f"Warning: Could not mount OpenEnv server endpoints: {e}")504    print("This is expected if openenv-core is not installed or if running locally.")505 506if __name__ == "__main__":507    configure_runtime()508    demo.launch(509        server_name=os.getenv("SERVER_NAME", "0.0.0.0"),510        server_port=int(os.getenv("PORT", "7860")),511        theme=APP_THEME,512    )513