|
|
from __future__ import annotations |
|
|
|
|
|
import base64 |
|
|
from typing import Dict, List, Optional |
|
|
|
|
|
from urllib.parse import urlparse |
|
|
|
|
|
import requests |
|
|
|
|
|
from setting import SETTINGS |
|
|
|
|
|
|
|
|
try: |
|
|
import openai |
|
|
except Exception: |
|
|
openai = None |
|
|
|
|
|
try: |
|
|
import anthropic |
|
|
except Exception: |
|
|
anthropic = None |
|
|
|
|
|
try: |
|
|
import google.generativeai as genai |
|
|
except Exception: |
|
|
genai = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def github_request( |
|
|
url: str, |
|
|
token: str, |
|
|
params: Optional[Dict[str, str]] = None, |
|
|
) -> Dict: |
|
|
headers = { |
|
|
"Accept": "application/vnd.github.v3+json", |
|
|
"Authorization": f"token {token}", |
|
|
} |
|
|
response = requests.get(url, headers=headers, params=params, timeout=30) |
|
|
if response.status_code == 404: |
|
|
raise FileNotFoundError(f"GitHub resource not found: {url}") |
|
|
if response.status_code == 401: |
|
|
raise PermissionError("GitHub token is invalid or lacks necessary scopes.") |
|
|
if response.status_code >= 400: |
|
|
raise RuntimeError( |
|
|
f"GitHub API request failed with status {response.status_code}: {response.text}" |
|
|
) |
|
|
return response.json() |
|
|
|
|
|
|
|
|
def fetch_file_from_pr( |
|
|
repo_name: str, |
|
|
pr_number: int, |
|
|
path: str, |
|
|
head_sha: str, |
|
|
github_token: str, |
|
|
) -> str: |
|
|
url = f"{SETTINGS.github_api_base}/repos/{repo_name}/contents/{path}" |
|
|
data = github_request(url, github_token, params={"ref": head_sha}) |
|
|
content = data.get("content") |
|
|
encoding = data.get("encoding") |
|
|
if content is None or encoding != "base64": |
|
|
raise ValueError( |
|
|
f"Unexpected content response for '{path}' (encoding={encoding!r})." |
|
|
) |
|
|
decoded = base64.b64decode(content) |
|
|
try: |
|
|
return decoded.decode("utf-8") |
|
|
except UnicodeDecodeError as exc: |
|
|
raise ValueError( |
|
|
f"File '{path}' in PR {pr_number} is not valid UTF-8 text" |
|
|
) from exc |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def call_openai( |
|
|
token: str, |
|
|
system_prompt: str, |
|
|
user_prompt: str, |
|
|
model_name: str = "gpt-5", |
|
|
) -> str: |
|
|
if openai is None: |
|
|
raise RuntimeError("openai package not installed. Install with `pip install openai`.") |
|
|
client = openai.OpenAI(api_key=token) |
|
|
params = { |
|
|
"model": model_name, |
|
|
"messages": [ |
|
|
{"role": "system", "content": system_prompt}, |
|
|
{"role": "user", "content": user_prompt}, |
|
|
], |
|
|
} |
|
|
|
|
|
if model_name not in {"gpt-5"}: |
|
|
params["temperature"] = 0.2 |
|
|
response = client.chat.completions.create(**params) |
|
|
return response.choices[0].message.content.strip() |
|
|
|
|
|
|
|
|
def call_anthropic( |
|
|
token: str, |
|
|
system_prompt: str, |
|
|
user_prompt: str, |
|
|
model_name: str = "claude-3-5-sonnet-20240620", |
|
|
) -> str: |
|
|
if anthropic is None: |
|
|
raise RuntimeError("anthropic package not installed. Install with `pip install anthropic`.") |
|
|
client = anthropic.Anthropic(api_key=token) |
|
|
response = client.messages.create( |
|
|
model=model_name, |
|
|
system=system_prompt, |
|
|
max_tokens=1500, |
|
|
temperature=0.2, |
|
|
messages=[{"role": "user", "content": user_prompt}], |
|
|
) |
|
|
return "".join(block.text for block in response.content if hasattr(block, "text")).strip() |
|
|
|
|
|
|
|
|
def call_gemini( |
|
|
token: str, |
|
|
system_prompt: str, |
|
|
user_prompt: str, |
|
|
model_name: str = "gemini-1.5-pro", |
|
|
) -> str: |
|
|
if genai is None: |
|
|
raise RuntimeError("google-generativeai package not installed. Install with `pip install google-generativeai`.") |
|
|
genai.configure(api_key=token) |
|
|
model = genai.GenerativeModel(model_name) |
|
|
prompt = f"{system_prompt}\n\n{user_prompt}" |
|
|
response = model.generate_content(prompt, generation_config={"temperature": 0.2}) |
|
|
return response.text.strip() |
|
|
|
|
|
|
|
|
PROVIDERS = { |
|
|
"openai": call_openai, |
|
|
"anthropic": call_anthropic, |
|
|
"gemini": call_gemini, |
|
|
} |
|
|
|
|
|
|
|
|
def dispatch_review( |
|
|
provider: str, |
|
|
token: str, |
|
|
system_prompt: str, |
|
|
user_prompt: str, |
|
|
model_name: str, |
|
|
) -> str: |
|
|
if provider not in PROVIDERS: |
|
|
raise ValueError(f"Unknown provider '{provider}'. Choose from: {', '.join(PROVIDERS)}") |
|
|
return PROVIDERS[provider](token, system_prompt, user_prompt, model_name) |
|
|
|