diff --git a/README.md b/README.md index 8266829..40c5aaa 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# code-review-agent +# Code Review Agent + +AI-powered Python code review agent built with LangGraph and FastAPI. +Analyzes Python code for typing issues, code quality, and security vulnerabilities. + +## What it does + +Accepts Python code via REST API and returns a structured review report with: +- **Score** (0–10) +- **Issues** — typing, quality, security, structure with severity levels +- **Suggestions** — concrete improvements +- **Summary** — overall assessment + +Non-Python code is rejected automatically. + +## Agent flow +``` +check_if_python → analyze_structure → check_typing → check_quality → check_security → generate_report +``` + +## Project structure +``` +├── app/ +│ ├── agent/ +│ │ ├── graph.py # LangGraph agent definition +│ │ └── tools.py +│ ├── main.py # FastAPI endpoints +│ └── schemas.py # Pydantic models +├── requirements.txt +└── README.md +``` + +## Setup +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +Create `.env` file in project root: +``` +GOOGLE_API_KEY=your_key_here +``` + +## Run +```bash +uvicorn app.main:app --reload +``` + +API docs available at `http://127.0.0.1:8000/docs` + +## Usage +```bash +curl -X POST http://127.0.0.1:8000/review \ + -H "Content-Type: application/json" \ + -d '{"code": "def add(a, b):\n return a + b"}' +``` + +## Limitations + +- Python only, max 5000 characters +- 5 sequential LLM calls per request — slow on large inputs +- Depends on Gemini API quota +- LLM may hallucinate issues diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..28ffd7d Binary files /dev/null and b/app/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/__pycache__/main.cpython-314.pyc b/app/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..7728699 Binary files /dev/null and b/app/__pycache__/main.cpython-314.pyc differ diff --git a/app/__pycache__/schemas.cpython-314.pyc b/app/__pycache__/schemas.cpython-314.pyc new file mode 100644 index 0000000..ddc0ffa Binary files /dev/null and b/app/__pycache__/schemas.cpython-314.pyc differ diff --git a/app/agent/__pycache__/__init__.cpython-314.pyc b/app/agent/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..db2c9a2 Binary files /dev/null and b/app/agent/__pycache__/__init__.cpython-314.pyc differ diff --git a/app/agent/__pycache__/graph.cpython-314.pyc b/app/agent/__pycache__/graph.cpython-314.pyc new file mode 100644 index 0000000..272e897 Binary files /dev/null and b/app/agent/__pycache__/graph.cpython-314.pyc differ diff --git a/app/agent/graph.py b/app/agent/graph.py index e69de29..2968276 100644 --- a/app/agent/graph.py +++ b/app/agent/graph.py @@ -0,0 +1,204 @@ +from typing import TypedDict +from langgraph.graph import StateGraph, END +from langchain_google_genai import ChatGoogleGenerativeAI +from dotenv import load_dotenv +import json +import re + +load_dotenv() + +llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash") + + +class ReviewState(TypedDict): + code: str + is_python: bool + structure: str + typing_issues: str + quality_issues: str + security_issues: str + report: dict + + +def check_if_python(state: ReviewState) -> ReviewState: + prompt = """Is the following code written in Python? Reply with only "yes" or "no". + +Code: +{code}""".format(code=state["code"]) + response = llm.invoke(prompt) + is_python = response.content.strip().lower().startswith("yes") + return {**state, "is_python": is_python} + + +def reject(state: ReviewState) -> ReviewState: + return { + **state, + "report": { + "error": "Only Python code is supported.", + "issues": [], + "suggestions": [], + "score": None, + }, + } + + +def analyze_structure(state: ReviewState) -> ReviewState: + prompt = """Analyze the structure of this Python code. +List: functions, classes, imports, and overall organization. +Be concise. + +Code: +{code}""".format(code=state["code"]) + response = llm.invoke(prompt) + return {**state, "structure": response.content} + + +def check_typing(state: ReviewState) -> ReviewState: + prompt = """Review the type annotations in this Python code. +Check for: +- Missing type hints on function arguments and return types +- Incorrect use of Optional, Union, List, Dict etc. +- Missing Pydantic model validation if applicable +- Any type-unsafe patterns + +Code: +{code}""".format(code=state["code"]) + response = llm.invoke(prompt) + return {**state, "typing_issues": response.content} + + +def check_quality(state: ReviewState) -> ReviewState: + prompt = """Review the code quality of this Python code. +Check for: +- Poor naming conventions +- Missing or incomplete docstrings +- Functions that are too complex or too long +- Code duplication +- Violations of PEP8 + +Code: +{code}""".format(code=state["code"]) + response = llm.invoke(prompt) + return {**state, "quality_issues": response.content} + + +def check_security(state: ReviewState) -> ReviewState: + prompt = """Review this Python code for security issues. +Check for: +- Use of eval() or exec() +- SQL injection vulnerabilities +- Files opened without context manager +- Hardcoded secrets or passwords +- Unsafe deserialization + +Code: +{code}""".format(code=state["code"]) + response = llm.invoke(prompt) + return {**state, "security_issues": response.content} + + +def generate_report(state: ReviewState) -> ReviewState: + prompt = """You are a senior Python code reviewer. +Based on the analysis below, generate a structured code review report. + +Structure analysis: +{structure} + +Typing issues: +{typing_issues} + +Code quality issues: +{quality_issues} + +Security issues: +{security_issues} + +Respond ONLY with a valid JSON object in this exact format, no markdown, no extra text: +{{ + "score": , + "issues": [ + {{"type": "typing|quality|security|structure", "severity": "low|medium|high", "description": "..."}} + ], + "suggestions": [ + "suggestion 1", + "suggestion 2" + ], + "summary": "overall summary in 2-3 sentences" +}}""".format( + structure=state["structure"], + typing_issues=state["typing_issues"], + quality_issues=state["quality_issues"], + security_issues=state["security_issues"], + ) + + response = llm.invoke(prompt) + + raw = response.content.strip() + raw = re.sub(r"^```json\s*", "", raw) + raw = re.sub(r"```$", "", raw).strip() + + try: + report = json.loads(raw) + except json.JSONDecodeError: + report = {"error": "Failed to parse report", "raw": raw} + + return {**state, "report": report} + + +def route_after_check(state: ReviewState) -> str: + if state["is_python"]: + return "analyze_structure" + return "reject" + + +def build_graph(): + graph = StateGraph(ReviewState) + + graph.add_node("check_if_python", check_if_python) + graph.add_node("reject", reject) + graph.add_node("analyze_structure", analyze_structure) + graph.add_node("check_typing", check_typing) + graph.add_node("check_quality", check_quality) + graph.add_node("check_security", check_security) + graph.add_node("generate_report", generate_report) + + graph.set_entry_point("check_if_python") + + graph.add_conditional_edges( + "check_if_python", + route_after_check, + {"analyze_structure": "analyze_structure", "reject": "reject"}, + ) + + graph.add_edge("reject", END) + graph.add_edge("analyze_structure", "check_typing") + graph.add_edge("check_typing", "check_quality") + graph.add_edge("check_quality", "check_security") + graph.add_edge("check_security", "generate_report") + graph.add_edge("generate_report", END) + + return graph.compile() + + +if __name__ == "__main__": + agent = build_graph() + test_code = """ +def add(a, b): + return a + b + +class Calculator: + def multiply(self, x, y): + return x * y +""" + result = agent.invoke( + { + "code": test_code, + "is_python": False, + "structure": "", + "typing_issues": "", + "quality_issues": "", + "security_issues": "", + "report": {}, + } + ) + print(json.dumps(result["report"], indent=2)) diff --git a/app/main.py b/app/main.py index e69de29..a2ca119 100644 --- a/app/main.py +++ b/app/main.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from app.schemas import ReviewRequest, ReviewResponse +from app.agent.graph import build_graph + +app = FastAPI(title="Python Code Review Agent") +graph = build_graph() + + +@app.post("/review", response_model=ReviewResponse) +def review_code(request: ReviewRequest): + result = graph.invoke( + { + "code": request.code, + "is_python": False, + "structure": "", + "typing_issues": "", + "quality_issues": "", + "security_issues": "", + "report": {}, + } + ) + return ReviewResponse(**result["report"]) diff --git a/app/schemas.py b/app/schemas.py index e69de29..11d17fb 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from typing import Optional + + +class ReviewRequest(BaseModel): + code: str + + +class ReviewIssue(BaseModel): + type: str + severity: str + description: str + + +class ReviewResponse(BaseModel): + score: Optional[int] = None + issues: list[ReviewIssue] = [] + suggestions: list[str] = [] + summary: Optional[str] = None + error: Optional[str] = None diff --git a/requirements.txt b/requirements.txt index e69de29..e9ad4e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi +langgraph +langchain-google-genai +langchain-core +uvicorn +python-dotenv