2
0
Files
gitcaddy-server/routers/api/v1/repo/ai.go
logikonline 61e835358c feat(actions-manager): add AI service integration for code review and issue triage
Integrate GitCaddy AI service with support for code review, issue triage, documentation generation, code explanation, and chat interface. Add AI client module with HTTP communication, configuration settings, API routes (web and REST), service layer, and UI templates for issue sidebar. Include comprehensive configuration options in app.example.ini for enabling/disabling features and service connection settings.
2026-01-19 11:06:39 -05:00

413 lines
10 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "code.gitcaddy.com/server/v3/models/issues"
"code.gitcaddy.com/server/v3/modules/ai"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/services/context"
ai_service "code.gitcaddy.com/server/v3/services/ai"
)
// AIReviewPullRequest performs an AI-powered code review on a pull request
func AIReviewPullRequest(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/ai/review repository aiReviewPullRequest
// ---
// summary: Request AI code review for a pull request
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the pull request
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/AIReviewResponse"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "503":
// "$ref": "#/responses/serviceUnavailable"
if !ai_service.IsEnabled() {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI service is not enabled",
})
return
}
if !setting.AI.EnableCodeReview {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI code review is not enabled",
})
return
}
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
review, err := ai_service.ReviewPullRequest(ctx, pr)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, review)
}
// AITriageIssue performs AI-powered triage on an issue
func AITriageIssue(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/ai/triage repository aiTriageIssue
// ---
// summary: Request AI triage for an issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/AITriageResponse"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "503":
// "$ref": "#/responses/serviceUnavailable"
if !ai_service.IsEnabled() {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI service is not enabled",
})
return
}
if !setting.AI.EnableIssueTriage {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI issue triage is not enabled",
})
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
triage, err := ai_service.TriageIssue(ctx, issue)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, triage)
}
// AISuggestLabels suggests labels for an issue using AI
func AISuggestLabels(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/ai/suggest-labels repository aiSuggestLabels
// ---
// summary: Get AI-suggested labels for an issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/AISuggestLabelsResponse"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "503":
// "$ref": "#/responses/serviceUnavailable"
if !ai_service.IsEnabled() {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI service is not enabled",
})
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
suggestions, err := ai_service.SuggestLabels(ctx, issue)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, suggestions)
}
// ExplainCodeRequest is the request body for explaining code
type ExplainCodeRequest struct {
Code string `json:"code" binding:"Required"`
FilePath string `json:"file_path"`
StartLine int `json:"start_line"`
EndLine int `json:"end_line"`
Question string `json:"question"`
}
// AIExplainCode explains code using AI
func AIExplainCode(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/ai/explain repository aiExplainCode
// ---
// summary: Get AI explanation for code
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/ExplainCodeRequest"
// responses:
// "200":
// "$ref": "#/responses/AIExplainCodeResponse"
// "403":
// "$ref": "#/responses/forbidden"
// "503":
// "$ref": "#/responses/serviceUnavailable"
if !ai_service.IsEnabled() {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI service is not enabled",
})
return
}
if !setting.AI.EnableExplainCode {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI code explanation is not enabled",
})
return
}
var req ExplainCodeRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
return
}
explanation, err := ai_service.ExplainCode(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.StartLine, req.EndLine, req.Question)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, explanation)
}
// GenerateDocRequest is the request body for generating documentation
type GenerateDocRequest struct {
Code string `json:"code" binding:"Required"`
FilePath string `json:"file_path"`
DocType string `json:"doc_type"` // function, class, module, api
Language string `json:"language"`
Style string `json:"style"` // jsdoc, docstring, xml, markdown
}
// AIGenerateDocumentation generates documentation using AI
func AIGenerateDocumentation(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/ai/generate-docs repository aiGenerateDocumentation
// ---
// summary: Generate documentation for code using AI
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/GenerateDocRequest"
// responses:
// "200":
// "$ref": "#/responses/AIGenerateDocResponse"
// "403":
// "$ref": "#/responses/forbidden"
// "503":
// "$ref": "#/responses/serviceUnavailable"
if !ai_service.IsEnabled() {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI service is not enabled",
})
return
}
if !setting.AI.EnableDocGen {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": "AI documentation generation is not enabled",
})
return
}
var req GenerateDocRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
return
}
docs, err := ai_service.GenerateDocumentation(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.DocType, req.Language, req.Style)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, docs)
}
// AIStatus returns the status of AI features
func AIStatus(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/ai/status repository aiStatus
// ---
// summary: Get AI service status for this repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/AIStatusResponse"
status := map[string]any{
"enabled": ai_service.IsEnabled(),
"code_review_enabled": setting.AI.EnableCodeReview,
"issue_triage_enabled": setting.AI.EnableIssueTriage,
"doc_gen_enabled": setting.AI.EnableDocGen,
"explain_code_enabled": setting.AI.EnableExplainCode,
"chat_enabled": setting.AI.EnableChat,
}
if ai_service.IsEnabled() {
client := ai.GetClient()
health, err := client.CheckHealth(ctx)
if err != nil {
status["service_healthy"] = false
status["service_error"] = err.Error()
} else {
status["service_healthy"] = health.Healthy
status["service_version"] = health.Version
if health.License != nil {
status["license_tier"] = health.License.Tier
status["license_valid"] = true
}
}
}
ctx.JSON(http.StatusOK, status)
}