From 126dcefe9b112ed8a8e11f82027623dc5aff4f6a Mon Sep 17 00:00:00 2001 From: hydrauluu Date: Thu, 2 Apr 2026 11:29:09 +0600 Subject: [PATCH 1/3] update requirements.txt --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 6276962b3fb3ef11e26a0861d2fdc3e5c03b03bf Mon Sep 17 00:00:00 2001 From: hydrauluu Date: Thu, 2 Apr 2026 11:33:05 +0600 Subject: [PATCH 2/3] LangGraph review agent --- app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 181 bytes app/__pycache__/main.cpython-314.pyc | Bin 0 -> 1141 bytes app/__pycache__/schemas.cpython-314.pyc | Bin 0 -> 1902 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 187 bytes app/agent/__pycache__/graph.cpython-314.pyc | Bin 0 -> 9024 bytes app/agent/graph.py | 204 ++++++++++++++++++ app/main.py | 22 ++ app/schemas.py | 20 ++ 8 files changed, 246 insertions(+) create mode 100644 app/__pycache__/__init__.cpython-314.pyc create mode 100644 app/__pycache__/main.cpython-314.pyc create mode 100644 app/__pycache__/schemas.cpython-314.pyc create mode 100644 app/agent/__pycache__/__init__.cpython-314.pyc create mode 100644 app/agent/__pycache__/graph.cpython-314.pyc diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28ffd7d06a44d029c8406b541b37c2efd0b8ce18 GIT binary patch literal 181 zcmdPqE_Uk|RF-7q=fxKkNM@v}Ah`6F;z(S1vui`uC%rfC{r1h9_qFfW zs%1dB@baf{%K-RO8xtywmC@&@q;L%=S%Xy~$qB6+YsRW6%~eZUnQg8WR&8mYfK74| zs6`8BNq%2CoAyaq8V8fbO{aNyY`k(%d()!MSAkKRF%!d19toVy3Gjo2Kl zlQyAb1Ha_t5zO9rq3BT@7;{{3Lb%QW!B)iJ>{UC~B8A#^) zyoS;B(1o5t3jcsh8ZWi@W0=WF;3BaAn^;zsE?1R8;pcr=Q6(XHuO)k& zDNt3pn}pGh9}0mBp=vjJK_`^E6MIUqR*$~|*qkLXm)}C&QiW_ulpRJl<4+lX17GL* zKd2kLf`>w1(d$T|=lJdduWGE4H{RD-@p(i_$YLEu3V&6nA8y>=_}!g;bm{BMUtJ!$ zGyONUm>#;v`*i5e^|yxZe4h{9BagQBr;4TW$Sl|FbmibExX1P?i~W@+=YE|#joQFk z$v_w5^_BORl#XDM61oZfyh=W%UMlnI9rECRu;Flvxz9zZBR7%-){;vi{^sI{P#E;K0De8kt}${jIg^iGKm(A{AT! literal 0 HcmV?d00001 diff --git a/app/__pycache__/schemas.cpython-314.pyc b/app/__pycache__/schemas.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddc0ffa8f1cd1253d06ff8c89402b8fdea6057ff GIT binary patch literal 1902 zcma)6TW=dh6rS0e?+$6>q^+B}3F6{jVj*}yqEriW2e%eoP*7JQ;ollP=jHHm#DYRMHa8wW@YimwJg5$O^IbC1M-v^bI}! zM;Z}p0&7jNX2d3dO-`|voqCa!(~t4jGF7Q}T*0n~b=H87Tcjxdf)VZ?)}ie*ZZ;eeeb9K-Tv2= zR!44!fz#sQ7tE8Qg1cYjk>eNLEf&a%+iF#Wx6PWaSZj4u)^Xe*2&F5TUEn-*a3kx&L`vG7+zQ8WWE-8jiC^c@EveJY3;8d%X2Cs^}mY9#L=n>(Pl^M;WC{0JbD;wLqlzh=lWPD22TR14B2Th zl@#nYuOq~gj`;o{wmSS1sS$M?(iS-gk1aqcNOu0If))ZI;9`$cp2bk91^YeGE z4@{U1EOM;SHx>>O@DWGx7_6`HMeVZl5BBN>&a7L{kp)@2&XGh@@JJo6LjidGm1Ry#T&-@#$eN(cm>UN{Ad#?Pc+ zf!AQ@hisFCq0KmCixLPtCiJk)m?ZiLn>kLr6(`;lUdWlUd{miG8PVR_g8l=&O{j!u zH=8c+D3fs>^0LXX6Di#fq%s;lw3H|v86cl6kK%0bW)y<@5g!A@3dtPbefQzpeWMi1 zB>Tc{@z?Bf-&lPrmc?D~+mpBe1v5yJJP;4p`^L&3g+!WU7sgv0WM(8)&hiB~hZhhQ z5ta~ML3kCRjBpO&Ji-ZtlL(lx|48vABoU$;-~?{aCHuuv_r3kItKIkamrLDi`^zic z4f#IeCE3(C+BwxT%4y(WrqbQD%aEOzWfDtRexftUGf y@QEcNDgb}#l+s7!%p+19S(N5?sv|fyKT7Jfuu~opc#X0qJ-hSC69TVj!+!wr@SJG? literal 0 HcmV?d00001 diff --git a/app/agent/__pycache__/__init__.cpython-314.pyc b/app/agent/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db2c9a2506469fa78e1769f3900da3f4245a7eb4 GIT binary patch literal 187 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 literal 0 HcmV?d00001 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 From cf1a1b87a33e347bec9f5c2b64de1c4ae5111951 Mon Sep 17 00:00:00 2001 From: hydrauluu Date: Thu, 2 Apr 2026 11:49:04 +0600 Subject: [PATCH 3/3] README.md --- README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) 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