diff --git a/.gitignore b/.gitignore index a415184..dd498c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ venv .venv +__pycache__ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c6b8b65 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.14-alpine +WORKDIR /usr/src/llm-reviewer +COPY . . +RUN pip install -r requirements.txt +CMD uvicorn main:app --host 0.0.0.0 --port 8080 diff --git a/README.md b/README.md index 21aee2f..82089a9 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ # llm reviewer + +running server +```bash +docker build -t llm-reviewer . +docker run -d --rm -p 8080:8080 --name llm-reviewer llm-reviewer +``` + +in gitea repository fill: +- secrets: + ``` + LLM_REVIEW_OPENROUTER_KEY + ``` + +- variables: + ``` + LLM_REVIEW_HOST + LLM_REVIEW_MODEL + ``` + +put `llm-reviewer.yaml` into `.gitea/workflows/` diff --git a/llm-reviewer.yaml b/llm-reviewer.yaml new file mode 100644 index 0000000..91aed4d --- /dev/null +++ b/llm-reviewer.yaml @@ -0,0 +1,28 @@ +name: LLM PR Review + +on: + pull_request: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + llm-review: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Trigger AI Reviewer + run: | + curl -f -X POST "${{ vars.LLM_REVIEW_HOST }}" \ + -H "Content-Type: application/json" \ + -d '{ + "gitea_token": "${{ secrets.GITHUB_TOKEN }}", + "gitea_host": "${{ github.server_url }}", + "openrouter_key": "${{ secrets.LLM_REVIEW_OPENROUTER_KEY }}", + "model": "${{ vars.LLM_REVIEW_MODEL }}", + "repo": "${{ github.repository }}", + "pr_number": ${{ github.event.pull_request.number }} + }' diff --git a/main.py b/main.py new file mode 100644 index 0000000..59d34bb --- /dev/null +++ b/main.py @@ -0,0 +1,57 @@ +import requests +from fastapi import FastAPI +from openai import OpenAI +from pydantic import BaseModel + +app = FastAPI() + +class ReviewRequest(BaseModel): + gitea_token: str + gitea_host: str + openrouter_key: str + model: str + repo: str + pr_number: int + +@app.post("/") +async def review( + request_body: ReviewRequest, +): + print('starting bg task with:') + print(f'request: {request_body}') + + gitea_host = request_body.gitea_host.rstrip("/") + + headers = {"Authorization": f"token {request_body.gitea_token}"} + + diff_res = requests.get(f"{gitea_host}/api/v1/repos/{request_body.repo}/pulls/{request_body.pr_number}.diff", headers=headers) + if diff_res.status_code != 200 or not diff_res.text.strip(): + return + + diff_text = diff_res.text + + client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=request_body.openrouter_key) + + prompt = ( + "You are an automated senior code reviewer. Review the following git diff.\n" + "Focus on: critical bugs, security vulnerabilities, edge cases, and code smells.\n" + "Keep it short and clear, don't give me long solutions." + "Format your answer cleanly in Markdown with actionable feedback.\n\n" + f"```diff\n{diff_text}\n```" + ) + completion = client.chat.completions.create( + model=request_body.model, + messages=[{"role": "user", "content": prompt}] + ) + + review_content = completion.choices[0].message.content + + review_body = f"# llm review\n\n{review_content}" + + requests.post( + f"{gitea_host}/api/v1/repos/{request_body.repo}/pulls/{request_body.pr_number}/reviews", + headers=headers, + json={"body": review_body, "event": "COMMENT"} + ) + + return {"status": "done"} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..88d9aa7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +openai +requests +pydantic diff --git a/webhook.py b/webhook.py deleted file mode 100644 index e69de29..0000000