from __future__ import annotations import inspect import json from dataclasses import dataclass from typing import Any from pydantic import ValidationError from .config import RuntimeConfig, vllm_extra_body from .schemas import FixCandidate def extract_json_object(text: str) -> str | None: start = text.find("{") if start == -1: return None depth = 0 in_string = False escaped = False for index in range(start, len(text)): char = text[index] if in_string: if escaped: escaped = False elif char == "\\": escaped = True elif char == '"': in_string = False continue if char == '"': in_string = True elif char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: return text[start : index + 1] return None def coerce_fix_candidate(raw: dict[str, Any]) -> dict[str, Any]: patch = raw.get("patch") or raw.get("diff") or "" fixed_file = raw.get("fixed_file") or raw.get("content") or raw.get("file_content") resolved = raw.get("resolved_policy_ids") or raw.get("resolved") or [] if isinstance(resolved, str): resolved = [resolved] return { "patch": patch, "fixed_file": fixed_file, "resolved_policy_ids": resolved, "explanation": raw.get("explanation") or raw.get("summary") or "", "verification_commands": raw.get("verification_commands") or [], "risk_notes": raw.get("risk_notes") or [], "requires_human_approval": bool(raw.get("requires_human_approval", True)), } def parse_fix_candidate(text: str) -> tuple[FixCandidate | None, bool]: json_text = extract_json_object(text) if json_text is None: return None, False try: return FixCandidate.model_validate_json(json_text), True except ValidationError: try: raw = json.loads(json_text) if not isinstance(raw, dict): return None, False return FixCandidate.model_validate(coerce_fix_candidate(raw)), True except Exception: return None, False @dataclass(slots=True) class ChatResult: text: str used_json_mode: bool class OpenAICompatibleLLM: """Thin OpenAI-compatible client used by the PatchAgent.""" def __init__(self, config: RuntimeConfig): from openai import OpenAI self.config = config self.client = OpenAI(base_url=config.base_url, api_key=config.api_key) def chat(self, messages: list[dict[str, str]], json_mode: bool = True) -> ChatResult: kwargs: dict[str, Any] = { "model": self.config.model, "messages": messages, "temperature": self.config.temperature, "top_p": self.config.top_p, "max_tokens": self.config.max_tokens, "extra_body": vllm_extra_body(), } if json_mode: kwargs["response_format"] = {"type": "json_object"} try: completion = self.client.chat.completions.create(**kwargs) return ChatResult(completion.choices[0].message.content or "{}", used_json_mode=json_mode) except Exception: kwargs.pop("response_format", None) kwargs.pop("extra_body", None) completion = self.client.chat.completions.create(**kwargs) return ChatResult(completion.choices[0].message.content or "{}", used_json_mode=False) def make_pydantic_openai_model(config: RuntimeConfig) -> Any | None: """Create a PydanticAI OpenAI-compatible model when the library is present.""" try: from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider provider = OpenAIProvider(base_url=config.base_url, api_key=config.api_key) return OpenAIChatModel(config.model, provider=provider) except Exception: pass try: from pydantic_ai.models.openai import OpenAIModel from pydantic_ai.providers.openai import OpenAIProvider provider = OpenAIProvider(base_url=config.base_url, api_key=config.api_key) return OpenAIModel(config.model, provider=provider) except Exception: return None def make_pydantic_agent(model: Any, output_model: type[Any], system_prompt: str) -> Any | None: """Compatibility wrapper for PydanticAI versions. Some environments use `result_type`, newer ones may use `output_type`. The notebook hit this exact API mismatch, so the submission code adapts at runtime. """ if model is None: return None try: from pydantic_ai import Agent signature = inspect.signature(Agent) kwargs: dict[str, Any] = {} if "output_type" in signature.parameters: kwargs["output_type"] = output_model elif "result_type" in signature.parameters: kwargs["result_type"] = output_model if "instructions" in signature.parameters: kwargs["instructions"] = system_prompt elif "system_prompt" in signature.parameters: kwargs["system_prompt"] = system_prompt if "model" in signature.parameters: kwargs["model"] = model return Agent(**kwargs) return Agent(model, **kwargs) except Exception: return None