Krishp1 commited on
Commit
c9c95fa
Β·
verified Β·
1 Parent(s): e08e9c1

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +59 -0
  2. main.py +179 -0
  3. requirements.txt +6 -3
  4. tools.py +218 -0
app.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py β€” Streamlit UI for AutoReview AI
2
+
3
+ import streamlit as st
4
+ from main import review_pr_simple
5
+
6
+ # ── PAGE CONFIG ───────────────────────────
7
+ st.set_page_config(
8
+ page_title="AutoReview AI",
9
+ page_icon="πŸ€–",
10
+ layout="centered"
11
+ )
12
+
13
+ # ── TITLE ─────────────────────────────────
14
+ st.title("πŸ€– AutoReview AI")
15
+ st.markdown("*Autonomous GitHub PR Reviewer powered by LLaMA + Groq*")
16
+ st.divider()
17
+
18
+ # ── INPUT ─────────────────────────────────
19
+ pr_url = st.text_input(
20
+ "Enter GitHub PR URL",
21
+ placeholder="https://github.com/owner/repo/pull/123"
22
+ )
23
+
24
+ # ── BUTTON ────────────────────────────────
25
+ if st.button("πŸ” Review PR", type="primary"):
26
+
27
+ # check if URL is empty
28
+ if not pr_url:
29
+ st.warning("Please enter a PR URL first!")
30
+
31
+ else:
32
+ # show spinner while reviewing
33
+ with st.spinner("πŸ€– Reviewing PR... this may take 30 seconds"):
34
+ result = review_pr_simple(pr_url)
35
+
36
+ # if success
37
+ if result["success"]:
38
+ st.success("βœ… Review Complete!")
39
+ st.divider()
40
+
41
+ # show PR data
42
+ st.subheader("πŸ“‹ PR Summary")
43
+ st.code(result["pr_data"][:500])
44
+
45
+ # show analysis
46
+ st.subheader("πŸ” Code Analysis")
47
+ st.write(result["analysis"])
48
+
49
+ # show verdict
50
+ st.subheader("βœ… Final Verdict")
51
+ st.write(result["verdict"])
52
+
53
+ # if failed
54
+ else:
55
+ st.error(f"❌ Failed: {result['error']}")
56
+
57
+ # ── FOOTER ────────────────────────────────
58
+ st.divider()
59
+ st.caption("Built with LangChain + Groq + Streamlit")
main.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py β€” AutoReview AI
2
+ # LangChain-based autonomous GitHub PR reviewer
3
+
4
+ import os
5
+ import re
6
+ from dotenv import load_dotenv
7
+
8
+ from langchain_groq import ChatGroq
9
+ from langchain.agents import AgentExecutor, create_tool_calling_agent
10
+ from langchain_core.prompts import ChatPromptTemplate
11
+ from langchain_core.messages import HumanMessage, SystemMessage
12
+
13
+ from tools import fetch_pr_diff, analyze_diff, post_inline_comment, post_overall_review
14
+
15
+ load_dotenv()
16
+
17
+
18
+ # ── LLM ──────────────────────────────────
19
+ llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0)
20
+
21
+ # ── TOOLS ─────────────────────────────────
22
+ tools = [fetch_pr_diff, analyze_diff, post_inline_comment, post_overall_review]
23
+
24
+ # ── PROMPT ────────────────────────────────
25
+ prompt = ChatPromptTemplate.from_messages([
26
+ ("system", """You are AutoReview AI β€” an expert autonomous GitHub PR reviewer.
27
+
28
+ Your job when given a PR URL:
29
+ 1. Use fetch_pr_diff to get the PR details and code changes
30
+ 2. Use analyze_diff for each changed file to find bugs, security issues, style problems
31
+ 3. Use post_inline_comment to post specific comments on problematic lines
32
+ 4. Use post_overall_review to post a final APPROVE or REQUEST_CHANGES verdict
33
+
34
+ Be thorough but concise. Focus on real issues that matter.
35
+ Always complete all 4 steps β€” don't stop after fetching.
36
+
37
+ If GitHub token is not available, still analyze and return the review as text.
38
+ """),
39
+ ("human", "{input}"),
40
+ ("placeholder", "{agent_scratchpad}")
41
+ ])
42
+
43
+ # ── AGENT ─────────────────────────────────
44
+ agent = create_tool_calling_agent(llm, tools, prompt)
45
+ agent_executor = AgentExecutor(
46
+ agent=agent,
47
+ tools=tools,
48
+ verbose=True,
49
+ max_iterations=10,
50
+ handle_parsing_errors=True
51
+ )
52
+
53
+ # ─────────────────────────────────────────
54
+ # MAIN REVIEW FUNCTION
55
+ # ─────────────────────────────────────────
56
+ def review_pr(pr_url: str) -> dict:
57
+ """
58
+ Main function to review a GitHub PR.
59
+ Returns structured review result.
60
+ """
61
+ print(f"\n{'='*60}")
62
+ print(f"πŸ” AutoReview AI starting review...")
63
+ print(f"πŸ“Ž PR: {pr_url}")
64
+ print("="*60)
65
+
66
+ try:
67
+ result = agent_executor.invoke({
68
+ "input": f"""
69
+ Please review this GitHub Pull Request: {pr_url}
70
+
71
+ Steps to follow:
72
+ 1. Fetch the PR diff using fetch_pr_diff
73
+ 2. Analyze each changed file using analyze_diff
74
+ 3. Post inline comments for specific issues using post_inline_comment
75
+ 4. Post overall verdict using post_overall_review
76
+
77
+ Provide a thorough code review focusing on:
78
+ - Bugs and logic errors
79
+ - Security vulnerabilities
80
+ - Code style and best practices
81
+ - Performance issues
82
+ """
83
+ })
84
+
85
+ output = result.get("output", "")
86
+ print(f"\nβœ… Review complete!")
87
+ print(f"\nOutput:\n{output}")
88
+
89
+ return {
90
+ "success": True,
91
+ "pr_url": pr_url,
92
+ "review": output,
93
+ "error": None
94
+ }
95
+
96
+ except Exception as e:
97
+ error_msg = str(e)
98
+ print(f"\n❌ Error: {error_msg}")
99
+ return {
100
+ "success": False,
101
+ "pr_url": pr_url,
102
+ "review": None,
103
+ "error": error_msg
104
+ }
105
+
106
+ # ─────────────────────────────────────────
107
+ # SIMPLE REVIEW (no GitHub token needed)
108
+ # For demo purposes
109
+ # ─────────────────────────────────────────
110
+ def review_pr_simple(pr_url: str) -> dict:
111
+ """
112
+ Reviews PR without posting comments to GitHub.
113
+ Returns full analysis as text β€” good for demo.
114
+ """
115
+ print(f"\nπŸ” Fetching PR data...")
116
+
117
+ # Step 1: Fetch diff
118
+ diff_result = fetch_pr_diff.invoke(pr_url)
119
+
120
+ if diff_result.startswith("ERROR"):
121
+ return {"success": False, "error": diff_result, "review": None}
122
+
123
+ print("βœ… PR fetched!")
124
+ print(f"\nπŸ€– Analyzing code...")
125
+
126
+ # Step 2: Analyze
127
+ analysis = analyze_diff.invoke(diff_result[:4000])
128
+
129
+ print("βœ… Analysis complete!")
130
+
131
+ # Step 3: Generate final verdict
132
+ verdict_response = llm.invoke([
133
+ SystemMessage("You are a senior engineer. Give a final PR review verdict."),
134
+ HumanMessage(f"""
135
+ Based on this PR analysis, write a complete review report:
136
+
137
+ PR DATA:
138
+ {diff_result[:2000]}
139
+
140
+ ANALYSIS:
141
+ {analysis}
142
+
143
+ Write a professional review with:
144
+ 1. Overall verdict (APPROVE / REQUEST CHANGES)
145
+ 2. Key issues found
146
+ 3. Positive aspects
147
+ 4. Specific suggestions
148
+ """)
149
+ ])
150
+
151
+ return {
152
+ "success": True,
153
+ "pr_url": pr_url,
154
+ "pr_data": diff_result,
155
+ "analysis": analysis,
156
+ "verdict": verdict_response.content,
157
+ "error": None
158
+ }
159
+
160
+ # ─────────────────────────────────────────
161
+ # RUN
162
+ # ─────────────────────────────────────────
163
+ if __name__ == "__main__":
164
+ # Test with a real public PR
165
+ test_pr = "https://github.com/langchain-ai/langchain/pull/1"
166
+
167
+ print("Mode: Simple review (no GitHub token needed for demo)")
168
+ result = review_pr_simple(test_pr)
169
+
170
+ if result["success"]:
171
+ print(f"\n{'='*60}")
172
+ print("πŸ“‹ PR DATA SUMMARY:")
173
+ print(result["pr_data"][:500])
174
+ print(f"\nπŸ” ANALYSIS:")
175
+ print(result["analysis"])
176
+ print(f"\nβœ… FINAL VERDICT:")
177
+ print(result["verdict"])
178
+ else:
179
+ print(f"❌ Failed: {result['error']}")
requirements.txt CHANGED
@@ -1,3 +1,6 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
1
+ langchain==0.2.0
2
+ langchain-groq==0.1.6
3
+ # langchain-core==0.2.0
4
+ PyGithub==2.3.0
5
+ python-dotenv==1.0.0
6
+ streamlit==1.35.0
tools.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tools.py β€” LangChain tools for AutoReview AI
2
+
3
+ import os
4
+ import re
5
+ from github import Github
6
+ from dotenv import load_dotenv
7
+ from langchain.tools import tool
8
+ from langchain_groq import ChatGroq
9
+ from langchain_core.messages import HumanMessage, SystemMessage
10
+ load_dotenv()
11
+ print("Tools key:",os.getenv("GROQ_API_KEY"))
12
+ # ── LLM ──────────────────────────────────
13
+ llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0)
14
+
15
+ # ── GITHUB CLIENT ─────────────────────────
16
+ def get_github_client():
17
+ token = os.environ.get("GITHUB_TOKEN", "")
18
+ return Github(token) if token else Github()
19
+
20
+ # ─────────────────────────────────────────
21
+ # TOOL 1 β€” FETCH PR DIFF
22
+ # ─────────────────────────────────────────
23
+ @tool
24
+ def fetch_pr_diff(pr_url: str) -> str:
25
+ """
26
+ Fetches the diff and metadata from a GitHub Pull Request URL.
27
+ Returns structured info: PR title, description, changed files and their diffs.
28
+ Input: GitHub PR URL like https://github.com/owner/repo/pull/123
29
+ """
30
+ try:
31
+ # Parse owner, repo, PR number from URL
32
+ pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
33
+ match = re.search(pattern, pr_url)
34
+ if not match:
35
+ return "ERROR: Invalid GitHub PR URL. Format: https://github.com/owner/repo/pull/123"
36
+
37
+ owner = match.group(1)
38
+ repo = match.group(2)
39
+ pr_num = int(match.group(3))
40
+
41
+ g = get_github_client()
42
+ repo_obj= g.get_repo(f"{owner}/{repo}")
43
+ pr = repo_obj.get_pull(pr_num)
44
+
45
+ # Get changed files
46
+ files = pr.get_files()
47
+ file_diffs = []
48
+
49
+ for f in files:
50
+ if f.patch: # only files with actual changes
51
+ file_diffs.append({
52
+ "filename": f.filename,
53
+ "status": f.status, # added/modified/deleted
54
+ "additions":f.additions,
55
+ "deletions":f.deletions,
56
+ "patch": f.patch[:3000] # limit patch size
57
+ })
58
+
59
+ result = {
60
+ "pr_title": pr.title,
61
+ "pr_description": pr.body or "No description",
62
+ "pr_number": pr_num,
63
+ "owner": owner,
64
+ "repo": repo,
65
+ "base_branch": pr.base.ref,
66
+ "head_branch": pr.head.ref,
67
+ "total_files": pr.changed_files,
68
+ "additions": pr.additions,
69
+ "deletions": pr.deletions,
70
+ "files": file_diffs
71
+ }
72
+
73
+ # Format as readable string for LLM
74
+ output = f"""
75
+ PR TITLE: {result['pr_title']}
76
+ PR NUMBER: #{result['pr_number']}
77
+ DESCRIPTION: {result['pr_description'][:500]}
78
+ BRANCH: {result['head_branch']} β†’ {result['base_branch']}
79
+ STATS: +{result['additions']} additions, -{result['deletions']} deletions, {result['total_files']} files changed
80
+
81
+ FILES CHANGED:
82
+ """
83
+ for f in file_diffs[:10]: # max 10 files
84
+ output += f"""
85
+ --- {f['filename']} ({f['status']}) +{f['additions']}/-{f['deletions']} ---
86
+ {f['patch'][:2000]}
87
+ """
88
+ return output.strip()
89
+
90
+ except Exception as e:
91
+ return f"ERROR fetching PR: {str(e)}"
92
+
93
+ # ─────────────────────────────────────────
94
+ # TOOL 2 β€” ANALYZE DIFF
95
+ # ─────────────────────────────────────────
96
+ @tool
97
+ def analyze_diff(file_info: str) -> str:
98
+ """
99
+ Analyzes a code diff and returns review comments.
100
+ Input: string containing filename and its diff/patch
101
+ Returns: structured review with bugs, security issues, suggestions
102
+ """
103
+ response = llm.invoke([
104
+ SystemMessage(content="""You are a senior software engineer doing a code review.
105
+ Analyze the code diff carefully and provide specific, actionable feedback.
106
+
107
+ Return your review in EXACTLY this format:
108
+ BUGS:
109
+ - <line number if known>: <specific bug description> or NONE
110
+
111
+ SECURITY:
112
+ - <line number if known>: <security issue> or NONE
113
+
114
+ STYLE:
115
+ - <specific style issue> or NONE
116
+
117
+ SUGGESTIONS:
118
+ - <improvement suggestion> or NONE
119
+
120
+ VERDICT: APPROVE or REQUEST_CHANGES
121
+ SUMMARY: <one sentence overall assessment>"""),
122
+ HumanMessage(content=f"""
123
+ Review this code change:
124
+
125
+ {file_info}
126
+
127
+ Be specific β€” mention exact line numbers when possible.
128
+ Focus on real issues, not nitpicks.
129
+ """)
130
+ ])
131
+
132
+ return response.content
133
+
134
+ # ─────────────────────────────────────────
135
+ # TOOL 3 β€” POST INLINE COMMENTS
136
+ # ───────────────────────────���─────────────
137
+ @tool
138
+ def post_inline_comment(comment_data: str) -> str:
139
+ """
140
+ Posts an inline review comment on a GitHub PR.
141
+ Input format: 'owner|repo|pr_number|commit_sha|filename|line_number|comment_body'
142
+ Returns: success or error message
143
+ """
144
+ try:
145
+ parts = comment_data.split("|")
146
+ if len(parts) < 7:
147
+ return "ERROR: Need format: owner|repo|pr_number|commit_sha|filename|line_number|comment"
148
+
149
+ owner = parts[0].strip()
150
+ repo = parts[1].strip()
151
+ pr_number = int(parts[2].strip())
152
+ commit_sha = parts[3].strip()
153
+ filename = parts[4].strip()
154
+ line = int(parts[5].strip())
155
+ body = "|".join(parts[6:]).strip()
156
+
157
+ token = os.environ.get("GITHUB_TOKEN", "")
158
+ if not token:
159
+ return "ERROR: GITHUB_TOKEN not set β€” cannot post comments"
160
+
161
+ g = Github(token)
162
+ repo_obj = g.get_repo(f"{owner}/{repo}")
163
+ pr = repo_obj.get_pull(pr_number)
164
+ commit = repo_obj.get_commit(commit_sha)
165
+
166
+ pr.create_review_comment(
167
+ body=f"πŸ€– **AutoReview AI:** {body}",
168
+ commit=commit,
169
+ path=filename,
170
+ line=line
171
+ )
172
+
173
+ return f"βœ… Comment posted on {filename} line {line}"
174
+
175
+ except Exception as e:
176
+ return f"ERROR posting comment: {str(e)}"
177
+
178
+ # ─────────────────────────────────────────
179
+ # TOOL 4 β€” POST OVERALL REVIEW
180
+ # ─────────────────────────────────────────
181
+ @tool
182
+ def post_overall_review(review_data: str) -> str:
183
+ """
184
+ Posts an overall PR review (approve or request changes) on GitHub.
185
+ Input format: 'owner|repo|pr_number|APPROVE or REQUEST_CHANGES|summary_body'
186
+ Returns: success or error message
187
+ """
188
+ try:
189
+ parts = review_data.split("|")
190
+ owner = parts[0].strip()
191
+ repo = parts[1].strip()
192
+ pr_num = int(parts[2].strip())
193
+ event = parts[3].strip() # APPROVE or REQUEST_CHANGES
194
+ body = "|".join(parts[4:]).strip()
195
+
196
+ if event not in ("APPROVE", "REQUEST_CHANGES", "COMMENT"):
197
+ event = "COMMENT"
198
+
199
+ token = os.environ.get("GITHUB_TOKEN", "")
200
+ if not token:
201
+ return "Review NOT posted to GitHub (no token) β€” here is the review:\n" + body
202
+
203
+ g = Github(token)
204
+ repo_obj = g.get_repo(f"{owner}/{repo}")
205
+ pr = repo_obj.get_pull(pr_num)
206
+
207
+ review_body = f"""πŸ€– **AutoReview AI Report**
208
+
209
+ {body}
210
+
211
+ ---
212
+ *Reviewed by AutoReview AI β€” LangChain + Groq*
213
+ """
214
+ pr.create_review(body=review_body, event=event)
215
+ return f"βœ… Overall review posted: {event}"
216
+
217
+ except Exception as e:
218
+ return f"ERROR posting review: {str(e)}"