# tools.py — LangChain tools for AutoReview AI import os import re from github import Github from dotenv import load_dotenv from langchain.tools import tool from langchain_groq import ChatGroq from langchain_core.messages import HumanMessage, SystemMessage load_dotenv() # ── LLM ────────────────────────────────── llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0) # ── GITHUB CLIENT ───────────────────────── def get_github_client(github_token: str | None = None): if github_token: return Github(github_token) token = os.environ.get("GITHUB_TOKEN", "") return Github(token) if token else Github() # ───────────────────────────────────────── # CORE FETCH LOGIC (shared by the tool below and review_pr_simple) # ───────────────────────────────────────── def fetch_pr_data(pr_url: str, github_token: str | None = None) -> dict: """ Fetches PR metadata + per-file diffs as structured data. Returns a dict (not a string) so callers can loop over files individually instead of working with one giant truncated blob. """ pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)" match = re.search(pattern, pr_url) if not match: return {"success": False, "error": "Invalid GitHub PR URL. Format: https://github.com/owner/repo/pull/123"} owner = match.group(1) repo = match.group(2) pr_num = int(match.group(3)) try: g = get_github_client(github_token) repo_obj = g.get_repo(f"{owner}/{repo}") pr = repo_obj.get_pull(pr_num) files = pr.get_files() file_diffs = [] for f in files: if f.patch: # only files with actual changes file_diffs.append({ "filename": f.filename, "status": f.status, # added/modified/deleted "additions": f.additions, "deletions": f.deletions, "patch": f.patch[:3000] # limit patch size }) return { "success": True, "pr_title": pr.title, "pr_description": pr.body or "No description", "pr_number": pr_num, "owner": owner, "repo": repo, "base_branch": pr.base.ref, "head_branch": pr.head.ref, "head_sha": pr.head.sha, # needed by post_inline_comment "total_files": pr.changed_files, "additions": pr.additions, "deletions": pr.deletions, "files": file_diffs } except Exception as e: return {"success": False, "error": f"Error fetching PR: {str(e)}"} def format_pr_data(data: dict) -> str: """Turns structured PR data into the readable text block the agent reads.""" output = f""" PR TITLE: {data['pr_title']} PR NUMBER: #{data['pr_number']} DESCRIPTION: {data['pr_description'][:500]} BRANCH: {data['head_branch']} → {data['base_branch']} COMMIT_SHA: {data['head_sha']} STATS: +{data['additions']} additions, -{data['deletions']} deletions, {data['total_files']} files changed FILES CHANGED: """ for f in data["files"][:10]: # max 10 files output += f""" --- {f['filename']} ({f['status']}) +{f['additions']}/-{f['deletions']} --- {f['patch'][:2000]} """ return output.strip() # ───────────────────────────────────────── # TOOL 1 — FETCH PR DIFF # ───────────────────────────────────────── @tool def fetch_pr_diff(pr_url: str, github_token: str | None = None) -> str: """ Fetches the diff and metadata from a GitHub Pull Request URL. Returns structured info: PR title, description, commit SHA, changed files and their diffs. Input: GitHub PR URL like https://github.com/owner/repo/pull/123 """ data = fetch_pr_data(pr_url, github_token) if not data["success"]: return f"ERROR: {data['error']}" return format_pr_data(data) # ───────────────────────────────────────── # TOOL 2 — ANALYZE DIFF # ───────────────────────────────────────── @tool def analyze_diff(file_info: str) -> str: """ Analyzes a code diff and returns review comments. Input: string containing filename and its diff/patch Returns: structured review with bugs, security issues, suggestions """ try: response = llm.invoke([ SystemMessage(content="""You are a senior software engineer doing a code review. Analyze the code diff carefully and provide specific, actionable feedback. Return your review in EXACTLY this format: BUGS: - : or NONE SECURITY: - : or NONE STYLE: - or NONE SUGGESTIONS: - or NONE VERDICT: APPROVE or REQUEST_CHANGES SUMMARY: """), HumanMessage(content=f""" Review this code change: {file_info} Be specific — mention exact line numbers when possible. Focus on real issues, not nitpicks. """) ]) return response.content except Exception as e: return f"ERROR analyzing diff: {str(e)}" # ───────────────────────────────────────── # TOOL 3 — POST INLINE COMMENTS # ───────────────────────────────────────── @tool def post_inline_comment(comment_data: str, github_token: str | None = None) -> str: """ Posts an inline review comment on a GitHub PR. Input format: 'owner|repo|pr_number|commit_sha|filename|line_number|comment_body' Returns: success or error message """ try: parts = comment_data.split("|") if len(parts) < 7: return "ERROR: Need format: owner|repo|pr_number|commit_sha|filename|line_number|comment" owner = parts[0].strip() repo = parts[1].strip() pr_number = int(parts[2].strip()) commit_sha = parts[3].strip() filename = parts[4].strip() line = int(parts[5].strip()) body = "|".join(parts[6:]).strip() if not github_token: return "ERROR: GitHub token required to post comments" g = Github(github_token) repo_obj = g.get_repo(f"{owner}/{repo}") pr = repo_obj.get_pull(pr_number) commit = repo_obj.get_commit(commit_sha) pr.create_review_comment( body=f"🤖 **AutoReview AI:** {body}", commit=commit, path=filename, line=line ) return f"✅ Comment posted on {filename} line {line}" except Exception as e: return f"ERROR posting comment: {str(e)}" # ───────────────────────────────────────── # TOOL 4 — POST OVERALL REVIEW # ───────────────────────────────────────── @tool def post_overall_review(review_data: str, github_token: str | None = None) -> str: """ Posts an overall PR review (approve or request changes) on GitHub. Input format: 'owner|repo|pr_number|APPROVE or REQUEST_CHANGES|summary_body' Returns: success or error message """ try: parts = review_data.split("|") owner = parts[0].strip() repo = parts[1].strip() pr_num = int(parts[2].strip()) event = parts[3].strip() # APPROVE or REQUEST_CHANGES body = "|".join(parts[4:]).strip() if event not in ("APPROVE", "REQUEST_CHANGES", "COMMENT"): event = "COMMENT" if not github_token: return "Review NOT posted (GitHub token missing)" g = Github(github_token) repo_obj = g.get_repo(f"{owner}/{repo}") pr = repo_obj.get_pull(pr_num) review_body = f"""🤖 **AutoReview AI Report** {body} --- *Reviewed by AutoReview AI — LangChain + Groq* """ pr.create_review(body=review_body, event=event) return f"✅ Overall review posted: {event}" except Exception as e: return f"ERROR posting review: {str(e)}"