diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..09e5e97 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,49 @@ +name: CI/CD Pipeline + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + +jobs: + ci: + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: '^1.20' + - name: Lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + working-directory: server + + - name: Test + run: go test ./internal/... + working-directory: server + + - name: Build + run: go build ./... + working-directory: server + + cd: + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: '^1.20' + - name: Build release binary + run: go build -o build/app + working-directory: server + + - name: Upload binaries + uses: actions/upload-artifact@v4 + with: + name: release-binaries + path: server/build/ diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..7304b75 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,36 @@ +name: Static Analysis (golangci-lint) + +on: + pull_request: + paths: + - 'server/**.go' + - 'server/go.mod' + - 'server/go.sum' + push: + branches: [ main ] + paths: + - 'server/**.go' + - 'server/go.mod' + - 'server/go.sum' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Install golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + working-directory: server + args: --timeout=5m + + - name: Run golangci-lint + run: golangci-lint run ./server/... --timeout=5m diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b242572 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8acac3c..763dcfd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,42 +1,78 @@ -# Guidelines for Contributing to This Repository -We’re glad you want to contribute! To get started, please **fork the repository**, create your own branch from your fork, and follow the guidelines below to ensure your changes align with the project’s workflow and quality standards. +# Guidelines for Contributing to CodeLookout -## Working on changes +We’re glad you want to contribute! Please read these rules and setup instructions before submitting a pull request. -- Always **create a new branch** for your work. -- **Push regularly** to avoid data loss. -- We follow a **linear commit history** β€” always use `git rebase` instead of `git merge`. +## Contribution Rules -## Branch naming conventions +- **Fork the repository** and create a new branch for your work. +- **Branch naming:** + - New features: `feature/` + - Bug fixes: `fix/` + - Hotfixes: `hotfix/` + - Security: `security/` + - Maintenance: `maintenance/` + - Other/dev: `/` +- **Push regularly** to your branch. +- **Linear commit history:** Use `git rebase` (not `git merge`). +- **Pull Requests:** + - Use clear, descriptive titles and detailed descriptions. + - Reference related issues if applicable. + - Use **Squash and Merge** after approval. +- **Commit messages:** + - Write clear, meaningful commit messages ([guide](https://cbea.ms/git-commit/)). +- **Tests:** + - Add or update tests for new features and bug fixes. +- **Code style:** + - Follow Go best practices and project conventions. +- **Review:** + - Address all review comments before merging. -Branch names should reflect the purpose of the work and follow a consistent structure. +## Local Setup Instructions -### Recommended patterns +Follow these steps to set up CodeLookout for local development: -| Type | Pattern | Example | Notes | -| ---------------------- | -------------------------- | ------------------------------------------------ | ---------------------------------------------------- | -| New Features | `feature/` | `feature/integrate-openai-apis-for-code-summary` | For new user-facing features | -| Bug Fixes | `fix/` | `fix/auth-error` | For bug fixes | -| Hotfixes | `hotfix/` | `hotfix/breaking-changes-openai-sdk` | For urgent, critical fixes | -| Security Updates/Fixes | `security/` | `security/suppress-server-headers` | For security-related changes | -| Maintenance | `maintenance/` | `maintenance/run-integration-tests-in-workflows` | For internal improvements or upkeep | -| Development / Other | `/` | `alex/job-queue-poc` | For work not covered above; often temporary branches | +1. **Clone your fork and install dependencies:** + ```sh + git clone https://github.com//CodeLookout.git + cd CodeLookout + cd server + go mod download + ``` -> **Tip:** Avoid vague names like `fix/bug-fix`. Be clear and descriptive. +2. **Configure environment variables:** + - Copy or create a `.env` file (see `server/development.md` for details). + - Set up a local PostgreSQL and Redis instance, or use Docker Compose. -## Pull request conventions +3. **Run database migrations:** + ```sh + ./scripts/run_migrations.sh + ``` -GitHub will automatically apply the default PR template located [here](https://github.com/Mentro-Org/CodeLookout/blob/main/.github/PULL_REQUEST_TEMPLATE.md). +4. **Start the development server:** + - With Docker Compose: + ```sh + docker-compose up + ./scripts/run_smeeclient.sh # For GitHub webhook forwarding + ``` + - Or locally: + ```sh + go run cmd/main.go + ./scripts/run_smeeclient.sh + ``` -### PR guidelines +5. **Access the app:** + - API: `http://localhost:8080/api/` + - Analytics Dashboard: `http://localhost:8080/analytics` -- **Title**: Clearly describe the purpose of the PR. -- **Description**: Include a detailed explanation of what changes were made and why. -- **Merge strategy**: Use **`Squash and Merge`** once all reviews and checks are approved. +6. **Run/test LLM and SonarQube CLI tools:** + ```sh + make test-llm + make test-sonarqube + ``` -## Commit message conventions +7. **See `server/development.md` for more details and troubleshooting.** -Good commit messages are essential for a readable project history. Please follow this guide: +--- -πŸ‘‰ [How to Write a Git Commit Message](https://cbea.ms/git-commit/) +Thank you for contributing to CodeLookout! diff --git a/bin/act b/bin/act new file mode 100755 index 0000000..0f8b18b Binary files /dev/null and b/bin/act differ diff --git a/server/.dev.env b/server/.dev.env index c1d0bbe..44f23bb 100644 --- a/server/.dev.env +++ b/server/.dev.env @@ -11,4 +11,7 @@ OPENAI_API_KEY=your-openai-key DATABASE_URL=postgres://user:pass@localhost:5432/dbname # QUEUE_SIZE=100 REDIS_ADDRESS=localhost:6379 -WORKER_CONCURRENCY=5 \ No newline at end of file +WORKER_CONCURRENCY=5 + +LLM_ENDPOINT="https://api-inference.huggingface.co/models/gpt2" +LLM_AUTH_TOKEN="your_huggingface_token" # (if required, or leave blank for public models) \ No newline at end of file diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..d2bbfad --- /dev/null +++ b/server/Makefile @@ -0,0 +1,10 @@ +# Makefile for testing LLM and SonarQube integrations + +.PHONY: test-llm test-sonarqube + + +test-llm: + go run ./cmd/llm_cli/main.go + +test-sonarqube: + go run ./cmd/sonarqube_cli/main.go diff --git a/server/architecture.md b/server/architecture.md index 60fb84d..f383c21 100644 --- a/server/architecture.md +++ b/server/architecture.md @@ -1,35 +1,70 @@ ## PR Review Processing Flow (Redis + Asynq + Worker) -+----------------------+ +------------------------+ +------------------+ -| GitHub Webhook | -----> | HTTP Server (API) | | AI Review | -| (PR Open/Update) | | - Validates Webhook | | Generator (LLM)| -+----------------------+ | - Enqueues Task (Asynq)| +------------------+ - +------------------------+ - | - v - +------------------+ - | Redis (Asynq DB)| - | - Stores Queued | - | Tasks | - +------------------+ - | - v - +-----------------------------+ - | Asynq Worker (Goroutine) | - | - Dequeues Task | - | - Fetches PR Diff | - | - Builds Prompt | - | - Calls AI API | - | - Posts Comments via GitHub | - +-----------------------------+ +```mermaid +flowchart LR + ghwebhook[GitHub Webhook PR Open or Update] --> httpapi[HTTP Server API] + httpapi --> redis[Redis Asynq DB] + redis --> worker[Asynq Worker] + worker --> ai[AI Review Engine LLM] + ai --> ghapi[GitHub API Post Comments] + + httpapi --> validate[Validate Webhook] + validate --> enqueue[Enqueue Task Asynq] + enqueue --> redis + + worker --> dequeue[Dequeues Task] + dequeue --> fetch[Fetch PR Diff] + fetch --> buildprompt[Build Prompt] + buildprompt --> callai[Call AI API] + callai --> postcomment[Post Comments] + postcomment --> ghapi + +``` + ++----------------------+ +------------------------+ +------------------+ +| GitHub Webhook | -----> | HTTP Server (API) | | AI Review | +| (PR Open/Update) | | - Validates Webhook | | Generator (LLM)| ++----------------------+ | - Enqueues Task (Asynq)| +------------------+ ++------------------------+ +| +v ++------------------+ +| Redis (Asynq DB)| +| - Stores Queued | +| Tasks | ++------------------+ +| +v ++-----------------------------+ +| Asynq Worker (Goroutine) | +| - Dequeues Task | +| - Fetches PR Diff | +| - Builds Prompt | +| - Calls AI API | +| - Posts Comments via GitHub | ++-----------------------------+ ## Understand graceful shutdown of services - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ main() β”‚ - β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”œβ”€ initializeDependencies() - β”œβ”€ signal listener goroutine ───┐ - β”œβ”€ RunWorker() goroutine β”‚ (listens for ctx cancel) - β”œβ”€ startServer() goroutine β”‚ (listens for ctx cancel) - └─ wg.Wait() <β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ waits for both \ No newline at end of file + +```mermaid +flowchart TD + main[main function] --> init[Initialize Dependencies] + init --> sig[SIGINT Listener Goroutine] + init --> runworker[Run Worker Goroutine] + init --> startserver[Start Server Goroutine] + sig --> wait[Wait for Shutdown] + runworker --> wait + startserver --> wait + + +``` + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ main() β”‚ +β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +β”‚ +β”œβ”€ initializeDependencies() +β”œβ”€ signal listener goroutine ───┐ +β”œβ”€ RunWorker() goroutine β”‚ (listens for ctx cancel) +β”œβ”€ startServer() goroutine β”‚ (listens for ctx cancel) +└─ wg.Wait() <β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ waits for both diff --git a/server/cmd/llm_cli/main.go b/server/cmd/llm_cli/main.go new file mode 100644 index 0000000..40846ec --- /dev/null +++ b/server/cmd/llm_cli/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + "github.com/nalindalal/CodeLookout/server/internal/config" + "github.com/nalindalal/CodeLookout/server/internal/llm" +) + +// This CLI demonstrates LLM integration using RESTLLMClient +func main() { + cfg := config.Load() + if cfg.LLMEndpoint == "" { + fmt.Println("LLM_ENDPOINT not set in config. Exiting.") + os.Exit(1) + } + // Optionally support LLM_AUTH_TOKEN for HuggingFace + authToken := os.Getenv("LLM_AUTH_TOKEN") + client := &llm.RESTLLMClient{Endpoint: cfg.LLMEndpoint, AuthToken: authToken} + code := "func add(a int, b int) int { return a + b }" + fmt.Println("Sending code to LLM endpoint:", cfg.LLMEndpoint) + result, err := client.AnalyzeCode(code) + if err != nil { + fmt.Println("LLM error:", err) + os.Exit(1) + } + fmt.Println("LLM review result:") + fmt.Println(result) +} diff --git a/server/cmd/main.go b/server/cmd/main.go index 77b69b1..4823657 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -1,116 +1,23 @@ package main import ( - "context" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "sync" - "syscall" - "time" - - "github.com/Mentro-Org/CodeLookout/internal/api" - "github.com/Mentro-Org/CodeLookout/internal/config" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/Mentro-Org/CodeLookout/internal/db" - githubclient "github.com/Mentro-Org/CodeLookout/internal/github" - "github.com/Mentro-Org/CodeLookout/internal/llm" - "github.com/Mentro-Org/CodeLookout/internal/queue" - "github.com/Mentro-Org/CodeLookout/internal/worker" - "github.com/joho/godotenv" + "log" + "sync" ) -func initializeDependencies() (*core.AppDeps, error) { - cfg := config.Load() - ctx := context.Background() - - dbPool := db.ConnectDB(ctx, cfg) - ghClientFactory := githubclient.NewClientFactory(cfg) - - aiClient, err := llm.NewClient(cfg) - if err != nil { - return nil, fmt.Errorf("AI client error: %w", err) - } - - taskClient := queue.NewTaskClient(cfg.RedisAddress) - - return &core.AppDeps{ - Config: cfg, - GHClientFactory: ghClientFactory, - AIClient: aiClient, - DBPool: dbPool, - TaskClient: taskClient, - }, nil -} - -func startServer(ctx context.Context, deps *core.AppDeps) error { - address := fmt.Sprintf(":%s", deps.Config.Port) - srv := http.Server{ - Addr: address, - Handler: api.NewRouter(deps), - } - - go func() { - <-ctx.Done() - log.Println("Shutting down HTTP server...") - ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctxTimeout); err != nil { - log.Printf("HTTP server shutdown error: %v", err) - } - }() - - log.Printf("Server is listening at http://localhost%s\n", address) - return srv.ListenAndServe() -} - func main() { - // Only load .env in local/dev environments - if os.Getenv("APP_ENV") != "production" { - _ = godotenv.Load() - } - - appDeps, err := initializeDependencies() - if err != nil { - log.Fatalf("failed to initialize dependencies: %v", err) - } - defer appDeps.DBPool.Close() - - ctx, cancel := context.WithCancel(context.Background()) - var wg sync.WaitGroup - - // graceful shutdown - go func() { - c := make(chan os.Signal, 1) - // Whenever the program receives a SIGINT (Ctrl+C) or - // SIGTERM (common in Docker/K8s), send it into channel c. - // channel "c" subscribe to those signals. - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c - log.Println("Shutting down gracefully...") - cancel() - }() - - // Start Asynq worker in background - wg.Add(1) - go func() { - defer wg.Done() - worker.RunWorker(ctx, appDeps) - }() - - // Start HTTP server - wg.Add(1) - go func() { - defer wg.Done() - if err := startServer(ctx, appDeps); err != nil { - log.Printf("HTTP server exited: %v", err) - cancel() - } - }() - - // Wait for everything to shut down - wg.Wait() - log.Println("App shutdown complete.") + var wg sync.WaitGroup + + // Example: start a goroutine + wg.Add(1) + go func() { + defer wg.Done() + // Do some work here + log.Println("Worker finished") + }() + + // Wait for everything to shut down + wg.Wait() + log.Println("App shutdown complete.") } + diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go new file mode 100644 index 0000000..5046a12 --- /dev/null +++ b/server/cmd/server/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/nalindalal/CodeLookout/server/internal/api" + "github.com/nalindalal/CodeLookout/server/internal/config" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/nalindalal/CodeLookout/server/internal/db" + githubclient "github.com/nalindalal/CodeLookout/server/internal/github" + "github.com/nalindalal/CodeLookout/server/internal/llm" + "github.com/nalindalal/CodeLookout/server/internal/queue" + "github.com/nalindalal/CodeLookout/server/internal/worker" + "github.com/joho/godotenv" +) + +func initializeDependencies() (*core.AppDeps, error) { + cfg := config.Load() + ctx := context.Background() + + dbPool := db.ConnectDB(ctx, cfg) + ghClientFactory := githubclient.NewClientFactory(cfg) + + aiClient, err := llm.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("AI client error: %w", err) + } + + taskClient := queue.NewTaskClient(cfg.RedisAddress) + + return &core.AppDeps{ + Config: cfg, + GHClientFactory: ghClientFactory, + AIClient: aiClient, + DBPool: dbPool, + TaskClient: taskClient, + }, nil +} + +func startServer(ctx context.Context, deps *core.AppDeps) error { + address := fmt.Sprintf(":%s", deps.Config.Port) + srv := http.Server{ + Addr: address, + Handler: api.NewRouter(deps), + } + + go func() { + <-ctx.Done() + log.Println("Shutting down HTTP server...") + ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctxTimeout); err != nil { + log.Printf("HTTP server shutdown error: %v", err) + } + }() + + log.Printf("Server is listening at http://localhost%s\n", address) + return srv.ListenAndServe() +} + +func main() { + // Only load .env in local/dev environments + if os.Getenv("APP_ENV") != "production" { + _ = godotenv.Load() + } + + appDeps, err := initializeDependencies() + if err != nil { + log.Fatalf("failed to initialize dependencies: %v", err) + } + defer appDeps.DBPool.Close() + + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + + // graceful shutdown + go func() { + c := make(chan os.Signal, 1) + // Whenever the program receives a SIGINT (Ctrl+C) or + // SIGTERM (common in Docker/K8s), send it into channel c. + // channel "c" subscribe to those signals. + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + log.Println("Shutting down gracefully...") + cancel() + }() + + // Start Asynq worker in background + wg.Add(1) + go func() { + defer wg.Done() + worker.RunWorker(ctx, appDeps) + }() + + // Start HTTP server (blocking) + if err := startServer(ctx, appDeps); err != nil && err != http.ErrServerClosed { + log.Fatalf("server error: %v", err) + } + + wg.Wait() +} diff --git a/server/cmd/sonarqube_cli/main.go b/server/cmd/sonarqube_cli/main.go new file mode 100644 index 0000000..c7662ef --- /dev/null +++ b/server/cmd/sonarqube_cli/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + + "github.com/nalindalal/CodeLookout/server/internal/config" + "github.com/nalindalal/CodeLookout/server/internal/llm" +) + +// This CLI demonstrates SonarQube integration using SonarQubeClient +func main() { + cfg := config.Load() + if cfg.SonarQubeEndpoint == "" || cfg.SonarQubeToken == "" { + fmt.Println("SONARQUBE_ENDPOINT or SONARQUBE_TOKEN not set in config. Exiting.") + os.Exit(1) + } + client := &llm.SonarQubeClient{Endpoint: cfg.SonarQubeEndpoint, Token: cfg.SonarQubeToken} + codePath := "./" // Example: analyze current directory + fmt.Println("Sending code path to SonarQube endpoint:", cfg.SonarQubeEndpoint) + result, err := client.AnalyzeCode(codePath) + if err != nil { + fmt.Println("SonarQube error:", err) + os.Exit(1) + } + fmt.Println("SonarQube analysis result:") + fmt.Println(result) +} diff --git a/server/code-review-tools-research.md b/server/code-review-tools-research.md new file mode 100644 index 0000000..aa54e95 --- /dev/null +++ b/server/code-review-tools-research.md @@ -0,0 +1,103 @@ +# Research: CodeAnt, SonarQube, and Similar Tools + +## 1. Overview of Tools + +### CodeAnt AI +- **Features:** + - AI-powered code review + - Automated PR feedback + - Security, maintainability, and best practices checks + - Integrates with GitHub, GitLab, Bitbucket + - Inline suggestions and explanations + - Customizable rules and team policies +- **Architecture/Workflow:** + - Listens to PR events via VCS integration + - Runs static analysis and AI review on code diffs + - Posts inline comments and summary reports + - Dashboard for team insights and trends +- **Strengths:** + - Fast, automated feedback + - AI explanations for suggestions + - Good integration with developer workflow +- **Weaknesses/UX Gaps:** + - May lack deep context for complex code + - Customization can be limited by AI model + - Potential for noisy or generic feedback + +### SonarQube +- **Features:** + - Deep static code analysis (bugs, vulnerabilities, code smells) + - Quality gates and metrics + - Multi-language support + - Integrates with CI/CD pipelines + - Rich dashboards and historical trends +- **Architecture/Workflow:** + - Runs as a server (self-hosted or cloud) + - Analyzes code on push/PR via scanner + - Results pushed to dashboard and optionally to PR +- **Strengths:** + - Comprehensive, customizable rules + - Enterprise-grade reporting and governance + - Strong support for legacy and monorepos +- **Weaknesses/UX Gaps:** + - Setup and maintenance overhead + - UI can be overwhelming + - Feedback loop slower than inline/AI tools + +### Other Tools (e.g., DeepCode, Codacy, Sider) +- **Common Features:** + - Automated code review + - Security and quality checks + - Integrations with VCS and CI/CD + - Some use AI/ML for suggestions +- **Common Gaps:** + - Generic feedback + - Limited context awareness + - Varying support for custom rules + +--- + +## 2. What Works Well +- Seamless integration with PR workflow (inline comments, status checks) +- Fast, actionable feedback +- Customizable rules and policies +- Clear, actionable explanations +- Team/organization dashboards for trends + +## 3. What Could Be Improved +- Context-aware feedback (project-specific, historical context) +- Reducing noise and false positives +- More intuitive UI/UX (less overwhelming, more focused) +- Better onboarding and configuration +- Combining AI with deterministic/static rules +- Feedback prioritization (critical vs. minor) + +--- + +## 4. Proposed CodeLookout Architecture & Flow + +### High-Level Flow +1. **PR Event Triggered**: On PR open/update, CodeLookout is triggered (via GitHub Action or App). +2. **Static & AI Analysis**: Run static analyzers (e.g., golangci-lint) and AI review on code diffs. +3. **Contextual Feedback**: Combine static and AI results, filter/prioritize based on project context and history. +4. **Inline & Summary Comments**: Post actionable, prioritized feedback as inline comments and a summary report. +5. **Dashboard & Trends**: Aggregate results for team/org dashboard, track trends and recurring issues. +6. **Continuous Learning**: Use feedback from developers to improve AI suggestions and reduce noise. + +### Architectural Insights +- **Hybrid Analysis**: Combine deterministic static analysis with AI/ML for context-aware suggestions. +- **Feedback Prioritization**: Use severity, frequency, and project context to rank issues. +- **Customizability**: Allow teams to tune rules, ignore patterns, and provide feedback to the system. +- **Scalability**: Support both GitHub Actions (easy setup) and App (advanced features, org-wide install). +- **Developer Experience**: Focus on clear, concise, and actionable feedback with minimal friction. + +--- + +## 5. References +- [CodeAnt AI](https://www.codeant.ai/ai-code-review) +- [SonarQube](https://www.sonarsource.com/products/sonarqube/) +- [DeepCode](https://www.deepcode.ai/) +- [Codacy](https://www.codacy.com/) +- [Sider](https://sider.review/) + +_Last updated: September 17, 2025_ \ No newline at end of file diff --git a/server/github-app-vs-action.md b/server/github-app-vs-action.md new file mode 100644 index 0000000..bf203f1 --- /dev/null +++ b/server/github-app-vs-action.md @@ -0,0 +1,75 @@ +# GitHub App vs GitHub Action: MVP Integration Comparison + +## 1. What is a GitHub App? +A **GitHub App** is an integration that interacts with the GitHub API as a first-class actor. It can listen to webhooks, perform actions on behalf of users or itself, and has fine-grained permissions. GitHub Apps are installed directly on organizations or repositories and can be distributed via the GitHub Marketplace. + +## 2. What is a GitHub Action? +A **GitHub Action** is a workflow automation tool that runs in response to GitHub events (like push, pull request, etc.) within GitHub Actions CI/CD. Actions are defined in YAML files in the repository and run in GitHub-hosted or self-hosted runners. + +--- + +## 3. Use Cases +| Use Case | GitHub App | GitHub Action | +|------------------------|----------------------------------------------|-----------------------------------------------| +| Automated code review | Yes (via webhooks & API) | Yes (as part of CI workflow) | +| Custom UI integration | Yes (can add checks, comments, UI elements) | Limited (outputs to PR checks/comments) | +| Scheduled jobs | Yes (via external scheduler) | Yes (via workflow schedule trigger) | +| Access to all repos | Yes (with org-level install) | Only where workflow is present | +| Marketplace listing | Yes | Yes | + +--- + +## 4. Pros and Cons + +### GitHub App +**Pros:** +- Fine-grained permissions (least privilege) +- Can act independently of user actions +- Can listen to all webhooks +- Can provide richer UI integrations (checks, status, etc.) +- Scalable (runs on your infrastructure) + +**Cons:** +- More complex to set up (web server, authentication, hosting) +- Requires external infrastructure +- More complex deployment and maintenance + +### GitHub Action +**Pros:** +- Easy to set up (just add YAML to repo) +- Runs on GitHub-hosted infrastructure +- Integrated with GitHub UI (Actions tab) +- No need for external hosting + +**Cons:** +- Coarse permissions (repo-level) +- Limited to workflow events and runner environment +- Harder to share state across repos/orgs +- Less flexible for custom UI or API integrations + +--- + +## 5. Recommendation for CodeLookout MVP +For the MVP, if the goal is to: +- Quickly integrate static analysis into PRs +- Minimize infrastructure and setup +- Leverage GitHub’s built-in CI/CD + +**GitHub Action** is recommended for the MVP. It allows fast iteration, easy deployment, and minimal maintenance. You can later migrate to a GitHub App for more advanced features, scalability, and fine-grained permissions. + +If you need: +- Organization-wide integration +- Custom UI (checks, dashboards) +- Advanced permissions or event handling + +**GitHub App** is the better long-term solution. + +--- + +## 6. References +- [GitHub Apps Documentation](https://docs.github.com/en/developers/apps/getting-started-with-apps/about-apps) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) + +--- + +_Last updated: September 17, 2025_ \ No newline at end of file diff --git a/server/go-static-analyzers-research.md b/server/go-static-analyzers-research.md new file mode 100644 index 0000000..8a4d842 --- /dev/null +++ b/server/go-static-analyzers-research.md @@ -0,0 +1,65 @@ +# Best Static Analyzers for Go: Research & Recommendations + +## 1. Popular Static Analyzers for Go + +### golangci-lint +- **Features:** Aggregates multiple linters (40+), fast, easy CI integration, customizable config, supports Go modules. +- **Checks:** Code style, bugs, security, performance, dead code, complexity, etc. +- **Integration:** GitHub Actions, GitLab CI, local pre-commit, IDE plugins. +- **Pros:** One-stop solution, highly configurable, active community, fast. +- **Cons:** May require tuning to reduce noise, some linters overlap. + +### SonarQube +- **Features:** Deep static analysis, code smells, bugs, vulnerabilities, quality gates, dashboards. +- **Checks:** Security, maintainability, code coverage, duplications. +- **Integration:** CI/CD, PR decoration, dashboards. +- **Pros:** Enterprise features, multi-language, rich reporting. +- **Cons:** Setup/maintenance overhead, less Go-specific than golangci-lint, slower feedback. + +### Staticcheck +- **Features:** Advanced static analysis, bug finding, code simplification, performance. +- **Integration:** CLI, CI, IDEs, can be used standalone or via golangci-lint. +- **Pros:** High signal-to-noise, focused on correctness. +- **Cons:** Limited to bug-finding, not style. + +### Gosec +- **Features:** Security analyzer for Go code. +- **Checks:** Common security issues (SQL injection, hardcoded creds, etc.). +- **Integration:** CLI, CI, can be used via golangci-lint. +- **Pros:** Focused on security, easy to add. +- **Cons:** Security only, not general code quality. + +### Other Tools +- **Revive:** Fast, configurable linter (style, best practices). +- **Errcheck:** Finds unchecked errors. +- **Megacheck:** Deprecated, replaced by Staticcheck. + +--- + +## 2. Comparison Table +| Tool | Aggregates | Security | Custom Rules | CI/CD | Speed | Reporting | +|----------------|------------|----------|--------------|-------|-------|-----------| +| golangci-lint | Yes | Yes | Yes | Yes | Fast | Good | +| SonarQube | No | Yes | Yes | Yes | Med | Excellent | +| Staticcheck | No | No | No | Yes | Fast | Basic | +| Gosec | No | Yes | No | Yes | Fast | Basic | +| Revive | No | No | Yes | Yes | Fast | Basic | + +--- + +## 3. Recommendations for CodeLookout +- **Primary:** Use `golangci-lint` as the main static analyzer (aggregates most useful checks, easy CI integration, highly configurable). +- **Supplement:** For enterprise/large orgs, consider SonarQube for dashboards, governance, and multi-language support. +- **Security:** Ensure `gosec` is enabled in golangci-lint config for security checks. +- **Customization:** Tune `.golangci.yml` to match project needs and reduce noise. + +--- + +## 4. References +- [golangci-lint](https://golangci-lint.run/) +- [SonarQube](https://www.sonarsource.com/products/sonarqube/) +- [Staticcheck](https://staticcheck.io/) +- [Gosec](https://github.com/securego/gosec) +- [Revive](https://github.com/mgechev/revive) + +_Last updated: September 17, 2025_ \ No newline at end of file diff --git a/server/go.mod b/server/go.mod index 782c73f..67cf8c3 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,4 +1,4 @@ -module github.com/Mentro-Org/CodeLookout +module github.com/nalindalal/CodeLookout/server go 1.24.3 @@ -6,33 +6,34 @@ require ( github.com/bradleyfalzon/ghinstallation/v2 v2.15.0 github.com/go-chi/chi/v5 v5.2.1 github.com/google/go-github/v72 v72.0.0 + github.com/hibiken/asynq v0.25.1 github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/sashabaranov/go-openai v1.40.0 + github.com/stretchr/testify v1.11.1 ) require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/google/go-github/v71 v71.0.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/redis/go-redis/v9 v9.7.0 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/spf13/cast v1.7.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/time v0.8.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect -) - -require ( github.com/google/go-querystring v1.1.0 // indirect - github.com/hibiken/asynq v0.25.1 + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum index 16e8eaa..0ae9bf8 100644 --- a/server/go.sum +++ b/server/go.sum @@ -61,8 +61,8 @@ github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cA github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= @@ -79,6 +79,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/internal/analyticsui/index.html b/server/internal/analyticsui/index.html new file mode 100644 index 0000000..281952b --- /dev/null +++ b/server/internal/analyticsui/index.html @@ -0,0 +1,85 @@ + + + + + + LLM Analytics Dashboard + + + +

LLM Analytics Dashboard

+
+ + + + + + + + +
+ + + + + + + + + + + + + +
TimePRRepoPromptResponseDuration (ms)Error
+ + + diff --git a/server/internal/api/analytics.go b/server/internal/api/analytics.go new file mode 100644 index 0000000..a01158b --- /dev/null +++ b/server/internal/api/analytics.go @@ -0,0 +1,58 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" + "context" + "fmt" + + "github.com/nalindalal/CodeLookout/server/internal/core" + db "github.com/nalindalal/CodeLookout/server/internal/db" +) + +type AnalyticsResponse struct { + Results []db.LLMAnalytics `json:"results"` +} + +// GET /api/analytics?limit=50&offset=0 +func HandleLLMAnalytics(appDeps *core.AppDeps) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + limit := 50 + offset := 0 + if l := r.URL.Query().Get("limit"); l != "" { + if v, err := strconv.Atoi(l); err == nil { + limit = v + } + } + if o := r.URL.Query().Get("offset"); o != "" { + if v, err := strconv.Atoi(o); err == nil { + offset = v + } + } + // Filters + filters := db.LLMAnalyticsFilters{ + Error: r.URL.Query().Get("error"), + Repo: r.URL.Query().Get("repo"), + Owner: r.URL.Query().Get("owner"), + PRNumber: r.URL.Query().Get("pr_number"), + Since: r.URL.Query().Get("since"), + Until: r.URL.Query().Get("until"), + } + results, err := db.ListLLMAnalyticsFiltered(ctx, appDeps.DBPool, limit, offset, filters) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + if _, err := w.Write([]byte("error fetching analytics")); err != nil { + // Optionally log the error + fmt.Println("error writing response:", err) + } + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(AnalyticsResponse{Results: results}); err != nil { + // Optionally log the error + fmt.Println("error encoding response:", err) + } + } +} diff --git a/server/internal/api/router.go b/server/internal/api/router.go index f80eaf9..a32d675 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -1,12 +1,11 @@ package api import ( - "net/http" - - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/Mentro-Org/CodeLookout/internal/handlers" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" + "net/http" + "github.com/nalindalal/CodeLookout/server/internal/handlers" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/nalindalal/CodeLookout/server/internal/core" ) func NewRouter(appDeps *core.AppDeps) http.Handler { @@ -14,13 +13,17 @@ func NewRouter(appDeps *core.AppDeps) http.Handler { r.Use(middleware.Logger) - r.Route("/api", func(r chi.Router) { - r.Get("/health-check", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - }) - r.Post("/webhook", handlers.HandleWebhook(appDeps)) - }) + r.Route("/api", func(r chi.Router) { + r.Get("/health-check", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) // No error to check for WriteHeader + }) + r.Post("/webhook", handlers.HandleWebhook(appDeps)) + r.Get("/analytics", HandleLLMAnalytics(appDeps)) + }) + // Serve analytics dashboard UI + r.Get("/analytics", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "internal/analyticsui/index.html") + }) return r } diff --git a/server/internal/api/router_test.go b/server/internal/api/router_test.go new file mode 100644 index 0000000..7546820 --- /dev/null +++ b/server/internal/api/router_test.go @@ -0,0 +1,19 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHealthCheckRoute(t *testing.T) { + router := NewRouter(nil) + req := httptest.NewRequest("GET", "/api/health-check", nil) + rw := httptest.NewRecorder() + + router.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) +} diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 20b97bd..b2e4f37 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -7,6 +7,7 @@ import ( "github.com/kelseyhightower/envconfig" ) +// Removed duplicate struct declaration type Config struct { Port string `envconfig:"PORT" default:"8080"` AppEnv string `envconfig:"APP_ENV" default:"development"` @@ -21,6 +22,13 @@ type Config struct { WebhookSecret string `envconfig:"WEBHOOK_SECRET" required:"true"` GithubAppPrivateKeyPath string `envconfig:"GITHUB_APP_PRIVATE_KEY_PATH" required:"true"` + // LLM REST endpoint for HuggingFace, Ollama, etc. + LLMEndpoint string `envconfig:"LLM_ENDPOINT"` + + // SonarQube integration + SonarQubeEndpoint string `envconfig:"SONARQUBE_ENDPOINT"` + SonarQubeToken string `envconfig:"SONARQUBE_TOKEN"` + GithubAppPrivateKey []byte `ignored:"true"` // not from env } diff --git a/server/internal/core/interface.go b/server/internal/core/interface.go index d7d2176..f0122af 100644 --- a/server/internal/core/interface.go +++ b/server/internal/core/interface.go @@ -1,14 +1,14 @@ package core import ( - "context" + "context" - "github.com/Mentro-Org/CodeLookout/internal/config" - ghclient "github.com/Mentro-Org/CodeLookout/internal/github" - "github.com/Mentro-Org/CodeLookout/internal/llm" - "github.com/Mentro-Org/CodeLookout/internal/queue" - "github.com/google/go-github/v72/github" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/nalindalal/CodeLookout/server/internal/config" + ghclient "github.com/nalindalal/CodeLookout/server/internal/github" + "github.com/nalindalal/CodeLookout/server/internal/llm" + "github.com/nalindalal/CodeLookout/server/internal/queue" + "github.com/google/go-github/v72/github" + "github.com/jackc/pgx/v5/pgxpool" ) type AppDeps struct { diff --git a/server/internal/db/db.go b/server/internal/db/db.go index d736c59..b9a7679 100644 --- a/server/internal/db/db.go +++ b/server/internal/db/db.go @@ -1,12 +1,12 @@ package db import ( - "context" - "log" + "context" + "log" - "github.com/Mentro-Org/CodeLookout/internal/config" - // pgxpool provides a PostgreSQL connection pool based on the pgx driver. - "github.com/jackc/pgx/v5/pgxpool" + "github.com/nalindalal/CodeLookout/server/internal/config" + // pgxpool provides a PostgreSQL connection pool based on the pgx driver. + "github.com/jackc/pgx/v5/pgxpool" ) func ConnectDB(ctx context.Context, cfg *config.Config) *pgxpool.Pool { diff --git a/server/internal/db/llm_analytics.go b/server/internal/db/llm_analytics.go new file mode 100644 index 0000000..aba913b --- /dev/null +++ b/server/internal/db/llm_analytics.go @@ -0,0 +1,28 @@ +package db + +import ( + "context" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type LLMAnalytics struct { + ID int64 + CreatedAt time.Time + Prompt string + Response string + DurationMs int + Error string + PRNumber int + Repo string + Owner string +} + +func InsertLLMAnalytics(ctx context.Context, db *pgxpool.Pool, a *LLMAnalytics) error { + _, err := db.Exec(ctx, ` + INSERT INTO llm_analytics (prompt, response, duration_ms, error, pr_number, repo, owner) + VALUES ($1, $2, $3, $4, $5, $6, $7) + `, a.Prompt, a.Response, a.DurationMs, a.Error, a.PRNumber, a.Repo, a.Owner) + return err +} diff --git a/server/internal/db/llm_analytics_list.go b/server/internal/db/llm_analytics_list.go new file mode 100644 index 0000000..d830d04 --- /dev/null +++ b/server/internal/db/llm_analytics_list.go @@ -0,0 +1,68 @@ +package db + +import ( + "context" + "strconv" + "github.com/jackc/pgx/v5/pgxpool" +) + +type LLMAnalyticsFilters struct { + Error string + Repo string + Owner string + PRNumber string + Since string // ISO8601 or date string + Until string +} + +func ListLLMAnalyticsFiltered(ctx context.Context, db *pgxpool.Pool, limit, offset int, f LLMAnalyticsFilters) ([]LLMAnalytics, error) { + query := `SELECT id, created_at, prompt, response, duration_ms, error, pr_number, repo, owner FROM llm_analytics WHERE 1=1` + args := []interface{}{} + i := 1 + if f.Error != "" { + query += ` AND error ILIKE '%' || $` + strconv.Itoa(i) + ` || '%'` + args = append(args, f.Error) + i++ + } + if f.Repo != "" { + query += ` AND repo = $` + strconv.Itoa(i) + args = append(args, f.Repo) + i++ + } + if f.Owner != "" { + query += ` AND owner = $` + strconv.Itoa(i) + args = append(args, f.Owner) + i++ + } + if f.PRNumber != "" { + query += ` AND pr_number = $` + strconv.Itoa(i) + args = append(args, f.PRNumber) + i++ + } + if f.Since != "" { + query += ` AND created_at >= $` + strconv.Itoa(i) + args = append(args, f.Since) + i++ + } + if f.Until != "" { + query += ` AND created_at <= $` + strconv.Itoa(i) + args = append(args, f.Until) + i++ + } + query += ` ORDER BY created_at DESC LIMIT $` + strconv.Itoa(i) + ` OFFSET $` + strconv.Itoa(i+1) + args = append(args, limit, offset) + rows, err := db.Query(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + var results []LLMAnalytics + for rows.Next() { + var a LLMAnalytics + if err := rows.Scan(&a.ID, &a.CreatedAt, &a.Prompt, &a.Response, &a.DurationMs, &a.Error, &a.PRNumber, &a.Repo, &a.Owner); err != nil { + return nil, err + } + results = append(results, a) + } + return results, nil +} \ No newline at end of file diff --git a/server/internal/github/client_factory.go b/server/internal/github/client_factory.go index b254345..585844d 100644 --- a/server/internal/github/client_factory.go +++ b/server/internal/github/client_factory.go @@ -1,14 +1,14 @@ package githubclient import ( - "context" - "log" - "net/http" - "sync" - - "github.com/Mentro-Org/CodeLookout/internal/config" - "github.com/bradleyfalzon/ghinstallation/v2" - "github.com/google/go-github/v72/github" + "context" + "log" + "net/http" + "sync" + + "github.com/nalindalal/CodeLookout/server/internal/config" + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/google/go-github/v72/github" ) // GitHub App installations have unique IDs, and each requires its own scoped authentication token. diff --git a/server/internal/handlers/review/general_comment.go b/server/internal/handlers/review/general_comment.go index 3685d95..0840ec0 100644 --- a/server/internal/handlers/review/general_comment.go +++ b/server/internal/handlers/review/general_comment.go @@ -1,10 +1,10 @@ package review import ( - "log" + "log" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/google/go-github/v72/github" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/google/go-github/v72/github" ) type GeneralComment struct { diff --git a/server/internal/handlers/review/handle_ai_review_response.go b/server/internal/handlers/review/handle_ai_review_response.go index a899dca..0cfd7b3 100644 --- a/server/internal/handlers/review/handle_ai_review_response.go +++ b/server/internal/handlers/review/handle_ai_review_response.go @@ -1,20 +1,52 @@ package review import ( - "context" - "log" + "context" + "log" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/Mentro-Org/CodeLookout/internal/llm" - "github.com/Mentro-Org/CodeLookout/internal/queue" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/nalindalal/CodeLookout/server/internal/llm" + "github.com/nalindalal/CodeLookout/server/internal/queue" ) func HandleReviewResponseFromAI(ctx context.Context, payload queue.PRReviewTaskPayload, appDeps *core.AppDeps, aiJsonResponse string) error { reviewResp, err := llm.ParseReviewResponse(aiJsonResponse) if err != nil { + log.Printf("[AI Review] Failed to parse LLM response: %v\nRaw response: %s", err, aiJsonResponse) + // Post fallback comment to PR + reviewCtx := core.ReviewContext{ + Ctx: ctx, + Payload: payload, + AppDeps: appDeps, + } + fallback := &InlineComment{ + Body: "[CodeLookout] LLM review failed: unable to parse AI response. Please try again or contact support.", + Path: "", + StartLine: 0, + Line: 0, + } + _ = fallback.Execute(&reviewCtx) return err } + // Validate required fields + if reviewResp.Action == "" || len(reviewResp.Comments) == 0 { + log.Printf("[AI Review] LLM response missing required fields: %+v", reviewResp) + reviewCtx := core.ReviewContext{ + Ctx: ctx, + Payload: payload, + AppDeps: appDeps, + } + fallback := &InlineComment{ + Body: "[CodeLookout] LLM review failed: incomplete AI response. Please try again or contact support.", + Path: "", + StartLine: 0, + Line: 0, + } + _ = fallback.Execute(&reviewCtx) + return nil + } + reviewCtx := core.ReviewContext{ Ctx: ctx, Payload: payload, diff --git a/server/internal/handlers/review/inline_comment.go b/server/internal/handlers/review/inline_comment.go index 5bbfd2c..73b31b4 100644 --- a/server/internal/handlers/review/inline_comment.go +++ b/server/internal/handlers/review/inline_comment.go @@ -1,11 +1,11 @@ package review import ( - "fmt" - "log" + "fmt" + "log" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/google/go-github/v72/github" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/google/go-github/v72/github" ) type InlineComment struct { diff --git a/server/internal/handlers/review/review_submission.go b/server/internal/handlers/review/review_submission.go index e0b00a0..e844b35 100644 --- a/server/internal/handlers/review/review_submission.go +++ b/server/internal/handlers/review/review_submission.go @@ -1,10 +1,10 @@ package review import ( - "log" + "log" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/google/go-github/v72/github" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/google/go-github/v72/github" ) type ReviewSubmission struct { diff --git a/server/internal/handlers/review_handler.go b/server/internal/handlers/review_handler.go index 46fe71a..72bd532 100644 --- a/server/internal/handlers/review_handler.go +++ b/server/internal/handlers/review_handler.go @@ -1,16 +1,18 @@ package handlers import ( - "context" - "encoding/json" - "fmt" - "log" - - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/Mentro-Org/CodeLookout/internal/handlers/review" - "github.com/Mentro-Org/CodeLookout/internal/llm" - "github.com/Mentro-Org/CodeLookout/internal/queue" - "github.com/hibiken/asynq" + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/nalindalal/CodeLookout/server/internal/handlers/review" + "github.com/nalindalal/CodeLookout/server/internal/llm" + "github.com/nalindalal/CodeLookout/server/internal/queue" + db "github.com/nalindalal/CodeLookout/server/internal/db" + "github.com/hibiken/asynq" ) func HandleReviewForPR(ctx context.Context, t *asynq.Task, appDeps *core.AppDeps) error { @@ -42,22 +44,46 @@ func HandleReviewForPR(ctx context.Context, t *asynq.Task, appDeps *core.AppDeps promptText := llm.BuildPRReviewPrompt(&payload, files) var response string - if appDeps.Config.AppEnv == "development" { - response, err = appDeps.AIClient.GenerateSampleReviewForPR() - if err != nil { - return err - } - } else { - response, err = appDeps.AIClient.GenerateReviewForPR(ctx, promptText) - if err != nil { - return err - } - } + var durationMs int + var llmErr error + if appDeps.Config.AppEnv == "development" { + response, llmErr = appDeps.AIClient.GenerateSampleReviewForPR() + if llmErr != nil { + return llmErr + } + } else { + start := time.Now() + response, llmErr = appDeps.AIClient.GenerateReviewForPR(ctx, promptText) + durationMs = int(time.Since(start).Milliseconds()) + } - err = review.HandleReviewResponseFromAI(ctx, payload, appDeps, response) - if err != nil { - return err - } + // Persist LLM analytics (always, including errors) + _ = db.InsertLLMAnalytics(ctx, appDeps.DBPool, &db.LLMAnalytics{ + Prompt: promptText, + Response: response, + DurationMs: durationMs, + Error: errToString(llmErr), + PRNumber: payload.PRNumber, + Repo: payload.Repo, + Owner: payload.Owner, + }) + + if llmErr != nil { + return llmErr + } + + err = review.HandleReviewResponseFromAI(ctx, payload, appDeps, response) + if err != nil { + return err + } + + return nil +} - return nil +// errToString returns the error message or empty string +func errToString(err error) string { + if err == nil { + return "" + } + return err.Error() } diff --git a/server/internal/handlers/webhook_handler.go b/server/internal/handlers/webhook_handler.go index f3ddda8..1dd5e91 100644 --- a/server/internal/handlers/webhook_handler.go +++ b/server/internal/handlers/webhook_handler.go @@ -1,13 +1,13 @@ package handlers import ( - "log" - "net/http" + "log" + "net/http" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/Mentro-Org/CodeLookout/internal/queue" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/nalindalal/CodeLookout/server/internal/queue" - "github.com/google/go-github/v72/github" + "github.com/google/go-github/v72/github" ) func HandleWebhook(appDeps *core.AppDeps) http.HandlerFunc { @@ -49,7 +49,6 @@ func HandleWebhook(appDeps *core.AppDeps) http.HandlerFunc { return } - w.WriteHeader(http.StatusAccepted) - return + w.WriteHeader(http.StatusAccepted) // No error to check for WriteHeader } } diff --git a/server/internal/llm/client.go b/server/internal/llm/client.go index 3fbb752..f1e43f9 100644 --- a/server/internal/llm/client.go +++ b/server/internal/llm/client.go @@ -1,7 +1,167 @@ package llm -import "context" +import ( + "context" + "bytes" + "encoding/json" + "io" + "net/http" + "fmt" + "time" +) +// Ensure SonarQubeClient implements AIClient for code review integration +func (c *SonarQubeClient) GenerateReviewForPR(ctx context.Context, prompt string) (string, error) { + // TODO: Implement SonarQube analysis and return review string + // This is a stub for integration with SonarQube REST API + return c.AnalyzeCode(prompt) +} + +func (c *SonarQubeClient) GenerateSampleReviewForPR() (string, error) { + // TODO: Optionally load a sample review from disk or return a static example + return "[Sample SonarQube review placeholder]", nil +} +// Example usage of LLM integration in Go +// +// func ExampleLLMUsage() { +// // Configure your LLM endpoint (e.g., HuggingFace Inference API, Ollama, etc.) +// llmClient := &RESTLLMClient{Endpoint: "http://localhost:11434/api/generate"} +// code := "func add(a int, b int) int { return a + b }" +// result, err := llmClient.AnalyzeCode(code) +// if err != nil { +// fmt.Println("LLM error:", err) +// return +// } +// fmt.Println("LLM review result:", result) +// } +// Ensure RESTLLMClient implements AIClient for code review integration +func (c *RESTLLMClient) GenerateReviewForPR(ctx context.Context, prompt string) (string, error) { + // Analytics: log prompt and timing + start := time.Now() + result, err := c.AnalyzeCode(prompt) + duration := time.Since(start) + if err != nil { + fmt.Printf("[LLM] ERROR: %v\nPrompt: %s\nDuration: %v\n", err, prompt, duration) + } else { + fmt.Printf("[LLM] SUCCESS\nPrompt: %s\nResponse: %s\nDuration: %v\n", prompt, result, duration) + } + return result, err +} + +func (c *RESTLLMClient) GenerateSampleReviewForPR() (string, error) { + // TODO: Optionally load a sample review from disk or return a static example + return "[Sample LLM review placeholder]", nil +} + +// LLMClient defines the interface for interacting with an LLM service (e.g., HuggingFace, Ollama, etc.) +type LLMClient interface { + // AnalyzeCode takes code and returns a review or suggestions. + AnalyzeCode(code string) (string, error) +} + +// RESTLLMClient is a client that calls an external LLM REST API (Python, HuggingFace, Ollama, etc.) +type RESTLLMClient struct { + Endpoint string + AuthToken string // Optional: for HuggingFace, etc. +} + +// AnalyzeCode sends code to the LLM REST API and returns the response. +func (c *RESTLLMClient) AnalyzeCode(code string) (string, error) { + // Real implementation: POST code to LLM endpoint (e.g., HuggingFace, Ollama) + payload := map[string]string{"inputs": code} // HuggingFace expects "inputs" + body, err := json.Marshal(payload) + if err != nil { + fmt.Printf("[LLM] ERROR: failed to marshal payload: %v\n", err) + return "", err + } + req, err := http.NewRequest("POST", c.Endpoint, bytes.NewBuffer(body)) + if err != nil { + fmt.Printf("[LLM] ERROR: failed to create request: %v\n", err) + return "", err + } + req.Header.Set("Content-Type", "application/json") + if c.AuthToken != "" { + req.Header.Set("Authorization", "Bearer "+c.AuthToken) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Printf("[LLM] ERROR: request failed: %v\n", err) + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + fmt.Printf("[LLM] ERROR: API returned status: %s\n", resp.Status) + return "", fmt.Errorf("LLM API returned status: %s", resp.Status) + } + out, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("[LLM] ERROR: failed to read response: %v\n", err) + return "", err + } + // HuggingFace returns JSON array of results, Ollama returns plain text + fmt.Printf("[LLM] Raw response: %s\n", string(out)) + return string(out), nil +} +// --- SonarQube Integration (Static Analysis) --- +// See: server/go-static-analyzers-research.md for research and recommendations. +// +// SonarQubeClient connects to a real SonarQube server via REST API. +type SonarQubeClient struct { + Endpoint string + Token string +} + +// AnalyzeCode triggers SonarQube analysis and fetches results. +func (c *SonarQubeClient) AnalyzeCode(projectKey string) (string, error) { + // Real implementation: Fetch issues for a project from SonarQube + // projectKey: the SonarQube project key (must be scanned already) + url := c.Endpoint + "/api/issues/search?componentKeys=" + projectKey + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + if c.Token != "" { + req.Header.Set("Authorization", "Bearer "+c.Token) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("SonarQube API returned status: %s", resp.Status) + } + out, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(out), nil +} + + +// --- SonarQube Integration (Static Analysis) --- +// See: server/go-static-analyzers-research.md for research and recommendations. +// +// To integrate SonarQube: +// 1. Deploy SonarQube server (self-hosted or cloud). +// 2. Use SonarQube scanner CLI or REST API to analyze codebase. +// 3. Implement a Go client (SonarQubeClient) that can trigger analysis and fetch results. +// 4. Parse and combine SonarQube results with LLM/AI review for unified feedback. +// 5. Post SonarQube findings as PR comments or status checks. +// +// Example stub for future implementation: +// +// type SonarQubeClient struct { +// Endpoint string +// Token string +// } +// +// func (c *SonarQubeClient) AnalyzeCode(codePath string) (string, error) { +// // TODO: Call SonarQube scanner or REST API, return results as string/JSON +// return "[SonarQube analysis result placeholder]", nil +// } + +// AIClient is the main interface for AI-powered code review (OpenAI, REST LLM, etc.) type AIClient interface { GenerateReviewForPR(ctx context.Context, prompt string) (string, error) GenerateSampleReviewForPR() (string, error) diff --git a/server/internal/llm/factory.go b/server/internal/llm/factory.go index c6b28a1..a3f24b6 100644 --- a/server/internal/llm/factory.go +++ b/server/internal/llm/factory.go @@ -1,16 +1,24 @@ package llm import ( - "fmt" + "fmt" - "github.com/Mentro-Org/CodeLookout/internal/config" + "github.com/nalindalal/CodeLookout/server/internal/config" ) +// NewClient returns an AIClient based on configuration (OpenAI, REST LLM, etc.) func NewClient(cfg *config.Config) (AIClient, error) { - switch cfg.AIProvider { - case "openai": - return NewOpenAIClient(cfg), nil - default: - return nil, fmt.Errorf("unsupported AI provider: %s", cfg.AIProvider) - } + switch cfg.AIProvider { + case "openai": + return NewOpenAIClient(cfg), nil + case "restllm": + // Example: REST-based LLM (HuggingFace, Ollama, etc.) + return &RESTLLMClient{Endpoint: cfg.LLMEndpoint}, nil + case "sonarqube": + // SonarQube integration (see go-static-analyzers-research.md) + // Requires SONARQUBE_ENDPOINT and SONARQUBE_TOKEN in config + return &SonarQubeClient{Endpoint: cfg.SonarQubeEndpoint, Token: cfg.SonarQubeToken}, nil + default: + return nil, fmt.Errorf("unsupported AI provider: %s", cfg.AIProvider) + } } diff --git a/server/internal/llm/openai_client.go b/server/internal/llm/openai_client.go index df753fc..1f82281 100644 --- a/server/internal/llm/openai_client.go +++ b/server/internal/llm/openai_client.go @@ -1,15 +1,14 @@ package llm import ( - "context" - "io" - "log" - "os" - "path/filepath" - "sync" + "context" + "io" + "os" + "path/filepath" + "sync" - "github.com/Mentro-Org/CodeLookout/internal/config" - openai "github.com/sashabaranov/go-openai" + "github.com/nalindalal/CodeLookout/server/internal/config" + openai "github.com/sashabaranov/go-openai" ) type OpenAIClient struct { @@ -52,14 +51,20 @@ func (c *OpenAIClient) GenerateSampleReviewForPR() (string, error) { jsonPath := filepath.Join(rootDir, "data", "openai-review.json") file, err := os.Open(jsonPath) if err != nil { - log.Fatalf("Failed to open file: %v", err) + return "", err } - defer file.Close() - + defer func() { + cerr := file.Close() + if cerr != nil { + // Log the error for now + println("warning: failed to close file:", cerr.Error()) + } + }() bytes, err := io.ReadAll(file) if err != nil { - log.Fatalf("Failed to read file: %v", err) + return "", err } return string(bytes), nil } + diff --git a/server/internal/llm/prompt_builder.go b/server/internal/llm/prompt_builder.go index a6201d2..f73cbec 100644 --- a/server/internal/llm/prompt_builder.go +++ b/server/internal/llm/prompt_builder.go @@ -1,10 +1,10 @@ package llm import ( - "fmt" + "fmt" - "github.com/Mentro-Org/CodeLookout/internal/queue" - "github.com/google/go-github/v72/github" + "github.com/nalindalal/CodeLookout/server/internal/queue" + "github.com/google/go-github/v72/github" ) func BuildPRReviewPrompt(payload *queue.PRReviewTaskPayload, files []*github.CommitFile) string { diff --git a/server/internal/worker/worker.go b/server/internal/worker/worker.go index 75ed7c1..5197596 100644 --- a/server/internal/worker/worker.go +++ b/server/internal/worker/worker.go @@ -1,13 +1,13 @@ package worker import ( - "context" - "log" + "context" + "log" - "github.com/Mentro-Org/CodeLookout/internal/core" - "github.com/Mentro-Org/CodeLookout/internal/handlers" - "github.com/Mentro-Org/CodeLookout/internal/queue" - "github.com/hibiken/asynq" + "github.com/nalindalal/CodeLookout/server/internal/core" + "github.com/nalindalal/CodeLookout/server/internal/handlers" + "github.com/nalindalal/CodeLookout/server/internal/queue" + "github.com/hibiken/asynq" ) // This worker picks review jobs form queue and execute required actions diff --git a/server/llm-integration-research.md b/server/llm-integration-research.md new file mode 100644 index 0000000..92d6ccb --- /dev/null +++ b/server/llm-integration-research.md @@ -0,0 +1,65 @@ +# LLM Integration: HuggingFace & Open-Source Models + +## 1. Capabilities & Features +### HuggingFace Models +- Wide range of LLMs (e.g., Llama, Mistral, Falcon, GPT-Neo, CodeGen) +- Tasks: code generation, summarization, Q&A, code review, translation, etc. +- Model hub with pre-trained and fine-tuned models +- Hosted inference API and self-hosting options + +### Other Notable Open-Source LLMs +- **Petals**: Distributed inference for large models (e.g., Llama 2) +- **Ollama**: Local LLM runner with simple API, supports multiple models +- **OpenLLM**: Unified interface for serving open-source LLMs + +## 2. Integration Scope +- Use HuggingFace `transformers` and `huggingface_hub` Python libraries for model access +- Options: + - Use HuggingFace Inference API (cloud, easy, paid) + - Self-host models (more control, resource intensive) + - Use third-party runners (Ollama, Petals) for distributed/local inference +- Integrate via REST API, gRPC, or direct Python calls + +## 3. Benefits & Challenges +| Aspect | Benefits | Challenges | +|---------------|-----------------------------------------------|---------------------------------------------| +| Performance | Fast with hosted API, scalable with Petals | Self-hosting needs GPU/CPU resources | +| Accuracy | SOTA models, can fine-tune for code | May need tuning for domain-specific tasks | +| Cost | Free for open-source/self-hosted, pay for API | Hardware/infra cost for self-hosting | +| Integration | Python SDKs, REST API, CLI tools | Language/runtime bridging (Go ↔ Python) | +| Customization | Can fine-tune or select best model | Fine-tuning requires data and compute | + +## 4. Pre-built Libraries & Tools +- `transformers` (HuggingFace): Model loading, inference, pipelines +- `huggingface_hub`: Model download, versioning, sharing +- `petals`: Distributed inference for large models +- `ollama`: Local LLM runner with REST API +- `openllm`: Unified serving for open-source LLMs + +## 5. Integration Process & Use Cases +- **Process:** + 1. Select model (e.g., CodeLlama, StarCoder) + 2. Decide hosting (HuggingFace API, self-host, Ollama, Petals) + 3. Integrate via Python SDK or REST API + 4. Bridge Go ↔ Python (e.g., via REST, gRPC, or subprocess) + 5. Use for code review, summarization, explanation, etc. +- **Use Cases:** + - Automated code review suggestions + - Code summarization and documentation + - PR feedback and inline comments + - Security and best-practices checks + +## 6. Recommendations +- For MVP: Use HuggingFace Inference API or Ollama for quick integration +- For advanced: Self-host with `transformers` or distributed with Petals +- Use REST API for Go ↔ Python communication +- Evaluate models for code review tasks (CodeLlama, StarCoder, etc.) + +## 7. References +- [HuggingFace Models](https://huggingface.co/models) +- [transformers](https://github.com/huggingface/transformers) +- [huggingface_hub](https://github.com/huggingface/huggingface_hub) +- [Petals](https://github.com/bigscience-workshop/petals) +- [Ollama](https://github.com/ollama/ollama) + +_Last updated: September 17, 2025_ \ No newline at end of file diff --git a/server/migrations/000010_create_llm_analytics_table.down.sql b/server/migrations/000010_create_llm_analytics_table.down.sql new file mode 100644 index 0000000..4fc2297 --- /dev/null +++ b/server/migrations/000010_create_llm_analytics_table.down.sql @@ -0,0 +1,2 @@ +-- +goose Down +DROP TABLE IF EXISTS llm_analytics; diff --git a/server/migrations/000010_create_llm_analytics_table.up.sql b/server/migrations/000010_create_llm_analytics_table.up.sql new file mode 100644 index 0000000..7a502d9 --- /dev/null +++ b/server/migrations/000010_create_llm_analytics_table.up.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS llm_analytics ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + prompt TEXT NOT NULL, + response TEXT, + duration_ms INTEGER NOT NULL, + error TEXT, + pr_number INTEGER, + repo TEXT, + owner TEXT +);