CoolFace
Apppublic

YashR05/pullrequest-arena

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
inference.py403 linesDownload Raw Back to root
1"""2inference.py — Baseline Inference Script for PullRequest Arena3 4Runs an AI agent (via OpenAI-compatible API) through all tasks in the5environment and logs results in the strict format required by OpenEnv.6 7Environment Variables:8    API_BASE_URL  : Base URL for the OpenAI-compatible API endpoint9    MODEL_NAME    : Model identifier to use for inference10    HF_TOKEN      : HuggingFace / API token for authentication11 12Logging Format:13    [START] task=<task> env=<env> model=<model>14    [STEP]  step=1 action=... reward=... done=false error=null15    [END]   success=true steps=3 score=1.00 rewards=0.0,0.5,1.016"""17 18import json19import os20import sys21import time22 23from openai import OpenAI24 25from models import ReviewAction26 27class UnifiedEnv:28    def __init__(self):29        self.env_url = os.environ.get("OPENENV_BASE_URL", "")30        self.is_remote = bool(self.env_url)31        32        if self.is_remote:33            from client import PullRequestEnv34            import time35            import sys36            37            self.client = PullRequestEnv(base_url=self.env_url).sync()38            39            # Wait for server to boot (up to 60s)40            connected = False41            last_err = None42            for i in range(15):43                try:44                    self.client.__enter__()45                    connected = True46                    break47                except Exception as e:48                    last_err = e49                    time.sleep(4)50                    51            if not connected:52                print(f"[FATAL] Could not connect to remote env {self.env_url}: {last_err}")53                sys.exit(1)54        else:55            from server.pullrequest_environment import PullRequestEnvironment56            self.client = PullRequestEnvironment()57 58    def reset(self, task_id):59        if self.is_remote:60            res = self.client.reset(task_id=task_id)61            return res.observation62        else:63            return self.client.reset(task_id=task_id)64 65    def step(self, action_dict):66        action_obj = ReviewAction(**action_dict)67        if self.is_remote:68            res = self.client.step(action_obj)69            raw_reward = res.reward if res.reward is not None else 0.570            return res.observation, max(0.01, min(0.99, float(raw_reward))), res.done or False71        else:72            obs = self.client.step(action_obj)73            raw_reward = obs.reward if obs.reward is not None else 0.574            return obs, max(0.01, min(0.99, float(raw_reward))), obs.done or False75            76    def close(self):77        if self.is_remote:78            try:79                self.client.__exit__(None, None, None)80            except Exception:81                pass82 83 84# ---------------------------------------------------------------------------85# Constants86# ---------------------------------------------------------------------------87 88ENV_NAME = "pullrequest-arena"89VALID_ACTION_TYPES = {"approve", "request_changes", "comment", "suggest_fix"}90 91# System prompt instructing the LLM to act as a code reviewer.92SYSTEM_PROMPT = """You are an expert code reviewer. You will be given a pull request with a code diff to review.93 94Your job is to analyze the code and decide on one of the following actions:95- "approve": The code is correct and ready to merge.96- "request_changes": The code has bugs, errors, or security issues that must be fixed.97- "comment": You want to leave a comment or question about the code.98- "suggest_fix": You want to suggest a specific code improvement or refactoring.99- "submit_patch": You want to exactly submit a code patch solving the issue.100 101Respond ONLY with valid JSON in this exact format, no other text:102{103  "type": "<action_type>",104  "comment": "<your detailed review comment explaining the issue and how to fix it>",105  "patch": "<the diff format fix if type is submit_patch, otherwise empty string>"106}107 108Be thorough in your comment. Explain what the problem is and how to fix it."""109 110 111def build_review_prompt(observation: dict) -> str:112    """113    Build the user prompt from a PR observation.114 115    Args:116        observation: Dict with pr_title, pr_description, files_changed,117                     code_diff, language, tests_passed, repository_context.118 119    Returns:120        Formatted prompt string for the LLM.121    """122    files = ", ".join(observation.get("files_changed", []))123    tests_status = "passing" if observation.get("tests_passed") else "failing"124 125    return f"""## Pull Request Review126 127**Title:** {observation['pr_title']}128**Description:** {observation['pr_description']}129**Files Changed:** {files}130**Language:** {observation['language']}131**Tests:** {tests_status}132**Repository Context:** {observation.get('repository_context', 'N/A')}133 134### Code Diff135```{observation['language']}136{observation['code_diff']}137```138 139Review this code diff carefully. Identify any bugs, security issues, style problems, or improvements. Respond with your action and comment as JSON."""140 141 142def parse_llm_response(response_text: str) -> dict:143    """144    Parse the LLM response into a structured action dict.145 146    Attempts to extract JSON from the response. Falls back to147    a heuristic parser if JSON parsing fails.148 149    Args:150        response_text: Raw text response from the LLM.151 152    Returns:153        Action dict with "type" and "comment" keys.154    """155    text = response_text.strip()156 157    # Try direct JSON parse158    try:159        action = json.loads(text)160        if isinstance(action, dict) and "type" in action and "comment" in action:161            action["type"] = action["type"].strip().lower()162            if action["type"] in VALID_ACTION_TYPES:163                return action164    except json.JSONDecodeError:165        pass166 167    # Try extracting JSON from markdown code block168    if "```" in text:169        for block in text.split("```"):170            block = block.strip()171            # Remove optional language identifier (e.g., "json")172            if block.startswith("json"):173                block = block[4:].strip()174            try:175                action = json.loads(block)176                if isinstance(action, dict) and "type" in action:177                    action["type"] = action["type"].strip().lower()178                    if action["type"] in VALID_ACTION_TYPES:179                        action.setdefault("comment", "")180                        return action181            except (json.JSONDecodeError, ValueError):182                continue183 184    # Try finding JSON object in the text with braces185    start = text.find("{")186    end = text.rfind("}") + 1187    if start != -1 and end > start:188        try:189            action = json.loads(text[start:end])190            if isinstance(action, dict) and "type" in action:191                action["type"] = action["type"].strip().lower()192                if action["type"] in VALID_ACTION_TYPES:193                    action.setdefault("comment", "")194                    return action195        except json.JSONDecodeError:196            pass197 198    # Heuristic fallback: detect action type from text199    text_lower = text.lower()200    detected_type = "comment"  # default201    for action_type in ["request_changes", "suggest_fix", "approve", "comment"]:202        if action_type in text_lower or action_type.replace("_", " ") in text_lower:203            detected_type = action_type204            break205 206    return {"type": detected_type, "comment": text}207 208 209def call_llm(client: OpenAI, model: str, observation: dict) -> dict:210    """211    Call the LLM to review a pull request.212 213    Args:214        client: OpenAI client instance.215        model: Model identifier.216        observation: PR observation dict.217 218    Returns:219        Parsed action dict with "type" and "comment".220    """221    prompt = build_review_prompt(observation)222 223    response = client.chat.completions.create(224        model=model,225        messages=[226            {"role": "system", "content": SYSTEM_PROMPT},227            {"role": "user", "content": prompt},228        ],229        temperature=0.0,  # Deterministic for reproducibility230        max_tokens=512,231    )232 233    response_text = response.choices[0].message.content or ""234    return parse_llm_response(response_text)235 236 237# ---------------------------------------------------------------------------238# Logging helpers (strict format)239# ---------------------------------------------------------------------------240 241def log_start(task_id: str, env_name: str, model: str) -> None:242    """Log the start of a task evaluation."""243    print(f"[START] task={task_id} env={env_name} model={model}")244 245 246def log_step(247    step: int,248    action: dict,249    reward: float,250    done: bool,251    error: str | None = None,252) -> None:253    """Log a single step in strict format."""254    action_str = json.dumps(action, separators=(",", ":")).replace("\n", "").replace("\r", "")255    done_str = str(done).lower()256    error_str = error if error else "null"257    print(258        f"[STEP] step={step} action={action_str} "259        f"reward={reward:.2f} done={done_str} error={error_str}"260    )261 262 263def log_end(264    success: bool,265    steps: int,266    rewards: list[float],267) -> None:268    """Log the end of a task evaluation."""269    success_str = str(success).lower()270    # Clamp each reward to (0, 1) for safety271    safe_rewards = [max(0.01, min(0.99, r)) for r in rewards]272    score = safe_rewards[-1] if safe_rewards else 0.01273    rewards_str = ",".join(f"{r:.2f}" for r in safe_rewards)274    print(275        f"[END] success={success_str} steps={steps} "276        f"score={score:.2f} rewards={rewards_str}"277    )278 279 280# ---------------------------------------------------------------------------281# Main282# ---------------------------------------------------------------------------283 284def run_inference() -> None:285    """286    Run the baseline inference agent through all tasks.287 288    Reads configuration from environment variables:289        API_BASE_URL : OpenAI-compatible API base URL290        MODEL_NAME   : Model identifier291        HF_TOKEN     : Authentication token292    """293    # --- Read configuration ---294    api_base_url = os.environ.get("API_BASE_URL", "https://router.huggingface.co/v1")295    model_name = os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")296    hf_token = os.environ.get("HF_TOKEN") or os.environ.get("OPENAI_API_KEY", "dummy_token_for_validation")297 298    # --- Initialize OpenAI client ---299    client = OpenAI(300        base_url=api_base_url,301        api_key=hf_token302    )303 304    # --- Initialize environment ---305    env = UnifiedEnv()306    task_ids = [str(i) for i in range(1, 20)]307    total_tasks = len(task_ids)308 309    print(f"PullRequest Arena — Baseline Inference")310    print(f"Model: {model_name}")311    print(f"API:   {api_base_url}")312    print(f"Tasks: {total_tasks}")313    print("=" * 60)314 315    # --- Run agent through all tasks ---316    all_rewards: list[float] = []317    all_success: list[bool] = []318    start_time = time.time()319 320    for task_id in task_ids:321        observation_obj = env.reset(task_id=task_id)322        # convert OpenEnv observation model to dict323        observation = observation_obj.model_dump() if hasattr(observation_obj, "model_dump") else dict(observation_obj)324 325        log_start(task_id=task_id, env_name=ENV_NAME, model=model_name)326 327        step_num = 0328        episode_rewards: list[float] = []329        success = False330        error_msg = None331 332        try:333            # Build prompt and call LLM334            action = call_llm(client, model_name, observation)335            step_num += 1336 337            # Submit action to environment338            obs_obj, reward, done = env.step(action)339            episode_rewards.append(reward)340 341            log_step(342                step=step_num,343                action=action,344                reward=reward,345                done=done,346                error=None,347            )348 349            success = True350 351        except Exception as e:352            step_num += 1353            error_msg = str(e)354            episode_rewards.append(0.01)355            log_step(356                step=step_num,357                action={"type": "error", "comment": error_msg},358                reward=0.01,359                done=True,360                error=error_msg,361            )362            success = False363 364        log_end(365            success=success,366            steps=step_num,367            rewards=episode_rewards,368        )369 370        all_rewards.extend(episode_rewards)371        all_success.append(success)372 373        print()  # Blank line between tasks374 375    env.close()376 377    # --- Aggregate results ---378    elapsed = time.time() - start_time379    total_score = sum(all_rewards) / len(all_rewards) if all_rewards else 0.01380    tasks_passed = sum(1 for s in all_success if s)381 382    print("=" * 60)383    print("SUMMARY")384    print(f"  Tasks:      {tasks_passed}/{total_tasks} completed")385    print(f"  Avg Score:  {total_score:.2f}")386    print(f"  Total Time: {elapsed:.1f}s")387    print(f"  All Rewards: {','.join(f'{r:.2f}' for r in all_rewards)}")388    print("=" * 60)389 390 391def main():392    import sys393    try:394        run_inference()395    except Exception as e:396        print(f"[ERROR] inference failed: {e}")397        import traceback398        traceback.print_exc()399        sys.exit(1)400 401if __name__ == "__main__":402    main()403