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.
413 lines
10 KiB
Go
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)
|
|
}
|