refactor(ai): consolidate ai operation types and reduce duplication
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 7m11s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m21s
Build and Release / Lint (push) Successful in 7m32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 7m11s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m21s
Build and Release / Lint (push) Successful in 7m32s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Refactor AI service layer to reduce code duplication and improve consistency. Changes: - Rename AIOperationRequest to OperationRequest for consistency - Extract shared logic for issue-targeted operations (respond, triage) into triggerIssueAIOp helper - Standardize field alignment in struct definitions - Remove redundant error handling patterns This reduces the API operations file by ~40 lines while maintaining identical functionality.
This commit is contained in:
@@ -24,7 +24,7 @@ type OperationLog struct {
|
||||
Tier int `xorm:"NOT NULL"` // 1 or 2
|
||||
TriggerEvent string `xorm:"VARCHAR(100) NOT NULL"`
|
||||
TriggerUserID int64 `xorm:"INDEX"`
|
||||
TargetID int64 `xorm:"INDEX"` // issue/PR ID
|
||||
TargetID int64 `xorm:"INDEX"` // issue/PR ID
|
||||
TargetType string `xorm:"VARCHAR(20)"` // "issue", "pull", "commit"
|
||||
Provider string `xorm:"VARCHAR(20)"`
|
||||
Model string `xorm:"VARCHAR(100)"`
|
||||
|
||||
@@ -207,11 +207,11 @@ type GenerateIssueResponseRequest struct {
|
||||
|
||||
// GenerateIssueResponseResponse is the response from generating an issue response
|
||||
type GenerateIssueResponseResponse struct {
|
||||
Response string `json:"response"`
|
||||
FollowUpQuestions []string `json:"follow_up_questions,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
Response string `json:"response"`
|
||||
FollowUpQuestions []string `json:"follow_up_questions,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
}
|
||||
|
||||
// HealthCheckResponse is the response from a health check
|
||||
|
||||
@@ -172,11 +172,11 @@ const (
|
||||
|
||||
// AI errors (AI_)
|
||||
const (
|
||||
AIDisabled ErrorCode = "AI_DISABLED"
|
||||
AIUnitNotEnabled ErrorCode = "AI_UNIT_NOT_ENABLED"
|
||||
AIDisabled ErrorCode = "AI_DISABLED"
|
||||
AIUnitNotEnabled ErrorCode = "AI_UNIT_NOT_ENABLED"
|
||||
AIOperationNotFound ErrorCode = "AI_OPERATION_NOT_FOUND"
|
||||
AIRateLimitExceeded ErrorCode = "AI_RATE_LIMIT_EXCEEDED"
|
||||
AIServiceError ErrorCode = "AI_SERVICE_ERROR"
|
||||
AIServiceError ErrorCode = "AI_SERVICE_ERROR"
|
||||
AIOperationDisabled ErrorCode = "AI_OPERATION_DISABLED"
|
||||
)
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ func TriggerAIReview(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.AIOperationRequest{
|
||||
if err := ai.EnqueueOperation(&ai.OperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: "code-review",
|
||||
Tier: 1,
|
||||
@@ -302,7 +302,41 @@ func TriggerAIReview(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": "AI code review has been queued",
|
||||
"message": "AI code review has been queued",
|
||||
"issue_id": issue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// triggerIssueAIOp is a shared helper for issue-targeted AI operations (respond, triage).
|
||||
func triggerIssueAIOp(ctx *context.APIContext, operation, successMsg string) {
|
||||
issueIndex := ctx.PathParamInt64("issue")
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.IssueNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.OperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: operation,
|
||||
Tier: 1,
|
||||
TriggerEvent: "api.manual",
|
||||
TriggerUserID: ctx.Doer.ID,
|
||||
TargetID: issue.ID,
|
||||
TargetType: "issue",
|
||||
}); err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": successMsg,
|
||||
"issue_id": issue.ID,
|
||||
})
|
||||
}
|
||||
@@ -312,44 +346,13 @@ func TriggerAIRespond(ctx *context.APIContext) {
|
||||
if getRepoAIConfig(ctx) == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.AllowAutoRespond {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Auto-respond is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
issueIndex := ctx.PathParamInt64("issue")
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.IssueNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.AIOperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: "issue-response",
|
||||
Tier: 1,
|
||||
TriggerEvent: "api.manual",
|
||||
TriggerUserID: ctx.Doer.ID,
|
||||
TargetID: issue.ID,
|
||||
TargetType: "issue",
|
||||
}); err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": "AI response has been queued",
|
||||
"issue_id": issue.ID,
|
||||
})
|
||||
triggerIssueAIOp(ctx, "issue-response", "AI response has been queued")
|
||||
}
|
||||
|
||||
// TriggerAITriage manually triggers AI triage for an issue
|
||||
@@ -357,44 +360,13 @@ func TriggerAITriage(ctx *context.APIContext) {
|
||||
if getRepoAIConfig(ctx) == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.EnableIssueTriage {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Issue triage is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
issueIndex := ctx.PathParamInt64("issue")
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.IssueNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.AIOperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: "issue-triage",
|
||||
Tier: 1,
|
||||
TriggerEvent: "api.manual",
|
||||
TriggerUserID: ctx.Doer.ID,
|
||||
TargetID: issue.ID,
|
||||
TargetType: "issue",
|
||||
}); err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": "AI triage has been queued",
|
||||
"issue_id": issue.ID,
|
||||
})
|
||||
triggerIssueAIOp(ctx, "issue-triage", "AI triage has been queued")
|
||||
}
|
||||
|
||||
// TriggerAIExplain triggers an AI explanation of code
|
||||
@@ -456,7 +428,7 @@ func TriggerAIFix(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.AIOperationRequest{
|
||||
if err := ai.EnqueueOperation(&ai.OperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: "agent-fix",
|
||||
Tier: 2,
|
||||
|
||||
@@ -6,9 +6,10 @@ package ai
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
actions_model "code.gitcaddy.com/server/v3/models/actions"
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
issues_model "code.gitcaddy.com/server/v3/models/issues"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
@@ -64,13 +65,13 @@ func triggerAgentWorkflow(ctx context.Context, repo *repo_model.Repository, aiCf
|
||||
for name, config := range workflowDispatch.Inputs {
|
||||
switch name {
|
||||
case "issue_number":
|
||||
inputs[name] = fmt.Sprintf("%d", issue.Index)
|
||||
inputs[name] = strconv.FormatInt(issue.Index, 10)
|
||||
case "issue_title":
|
||||
inputs[name] = issue.Title
|
||||
case "issue_body":
|
||||
inputs[name] = issueBody
|
||||
case "max_run_minutes":
|
||||
inputs[name] = fmt.Sprintf("%d", getAgentMaxRunMinutes(aiCfg))
|
||||
inputs[name] = strconv.Itoa(getAgentMaxRunMinutes(aiCfg))
|
||||
case "system_instructions":
|
||||
inputs[name] = aiCfg.SystemInstructions
|
||||
default:
|
||||
|
||||
@@ -52,7 +52,7 @@ func escalateToStaff(ctx context.Context, repo *repo_model.Repository, aiCfg *re
|
||||
opLog.Operation, opLog.Operation, opLog.Status,
|
||||
)
|
||||
if opLog.ErrorMessage != "" {
|
||||
comment += fmt.Sprintf("\n**Error:** %s", opLog.ErrorMessage)
|
||||
comment += "\n**Error:** " + opLog.ErrorMessage
|
||||
}
|
||||
|
||||
if _, err := issue_service.CreateIssueComment(ctx, botUser, repo, issue, comment, nil); err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
notify_service "code.gitcaddy.com/server/v3/services/notify"
|
||||
)
|
||||
|
||||
var aiOperationQueue *queue.WorkerPoolQueue[*AIOperationRequest]
|
||||
var aiOperationQueue *queue.WorkerPoolQueue[*OperationRequest]
|
||||
|
||||
// Init initializes the AI service integration: queue and notifier.
|
||||
func Init(ctx context.Context) error {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (n *aiNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, _
|
||||
}
|
||||
|
||||
if aiCfg.AutoRespondToIssues && setting.AI.AllowAutoRespond {
|
||||
if err := EnqueueOperation(&AIOperationRequest{
|
||||
if err := EnqueueOperation(&OperationRequest{
|
||||
RepoID: issue.RepoID,
|
||||
Operation: "issue-response",
|
||||
Tier: 1,
|
||||
@@ -80,7 +80,7 @@ func (n *aiNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, _
|
||||
}
|
||||
|
||||
if aiCfg.AutoTriageIssues && setting.AI.EnableIssueTriage {
|
||||
if err := EnqueueOperation(&AIOperationRequest{
|
||||
if err := EnqueueOperation(&OperationRequest{
|
||||
RepoID: issue.RepoID,
|
||||
Operation: "issue-triage",
|
||||
Tier: 1,
|
||||
@@ -117,7 +117,7 @@ func (n *aiNotifier) CreateIssueComment(ctx context.Context, doer *user_model.Us
|
||||
return
|
||||
}
|
||||
|
||||
if err := EnqueueOperation(&AIOperationRequest{
|
||||
if err := EnqueueOperation(&OperationRequest{
|
||||
RepoID: repo.ID,
|
||||
Operation: "issue-response",
|
||||
Tier: 1,
|
||||
@@ -155,7 +155,7 @@ func (n *aiNotifier) NewPullRequest(ctx context.Context, pr *issues_model.PullRe
|
||||
}
|
||||
|
||||
if aiCfg.AutoReviewPRs && setting.AI.AllowAutoReview {
|
||||
if err := EnqueueOperation(&AIOperationRequest{
|
||||
if err := EnqueueOperation(&OperationRequest{
|
||||
RepoID: pr.Issue.RepoID,
|
||||
Operation: "code-review",
|
||||
Tier: 1,
|
||||
@@ -190,7 +190,7 @@ func (n *aiNotifier) PullRequestSynchronized(ctx context.Context, doer *user_mod
|
||||
}
|
||||
|
||||
if aiCfg.AutoReviewPRs && setting.AI.AllowAutoReview {
|
||||
if err := EnqueueOperation(&AIOperationRequest{
|
||||
if err := EnqueueOperation(&OperationRequest{
|
||||
RepoID: pr.Issue.RepoID,
|
||||
Operation: "code-review",
|
||||
Tier: 1,
|
||||
@@ -229,7 +229,7 @@ func (n *aiNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.Use
|
||||
// Check if any added label matches the agent trigger labels
|
||||
for _, label := range addedLabels {
|
||||
if slices.Contains(aiCfg.AgentTriggerLabels, label.Name) {
|
||||
if err := EnqueueOperation(&AIOperationRequest{
|
||||
if err := EnqueueOperation(&OperationRequest{
|
||||
RepoID: issue.RepoID,
|
||||
Operation: "agent-fix",
|
||||
Tier: 2,
|
||||
|
||||
@@ -5,6 +5,7 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -19,11 +20,11 @@ import (
|
||||
issue_service "code.gitcaddy.com/server/v3/services/issue"
|
||||
)
|
||||
|
||||
// AIOperationRequest represents a queued AI operation
|
||||
type AIOperationRequest struct {
|
||||
// OperationRequest represents a queued AI operation
|
||||
type OperationRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Operation string `json:"operation"` // "code-review", "issue-response", "issue-triage", "workflow-inspect", "agent-fix"
|
||||
Tier int `json:"tier"` // 1 or 2
|
||||
Tier int `json:"tier"` // 1 or 2
|
||||
TriggerEvent string `json:"trigger_event"` // e.g. "issue.created"
|
||||
TriggerUserID int64 `json:"trigger_user_id"` // who triggered the event
|
||||
TargetID int64 `json:"target_id"` // issue/PR ID
|
||||
@@ -31,15 +32,15 @@ type AIOperationRequest struct {
|
||||
}
|
||||
|
||||
// EnqueueOperation adds an AI operation to the processing queue
|
||||
func EnqueueOperation(req *AIOperationRequest) error {
|
||||
func EnqueueOperation(req *OperationRequest) error {
|
||||
if aiOperationQueue == nil {
|
||||
return fmt.Errorf("AI operation queue not initialized")
|
||||
return errors.New("AI operation queue not initialized")
|
||||
}
|
||||
return aiOperationQueue.Push(req)
|
||||
}
|
||||
|
||||
// handleAIOperation is the queue worker that processes AI operations
|
||||
func handleAIOperation(items ...*AIOperationRequest) []*AIOperationRequest {
|
||||
func handleAIOperation(items ...*OperationRequest) []*OperationRequest {
|
||||
for _, req := range items {
|
||||
if err := processOperation(context.Background(), req); err != nil {
|
||||
log.Error("AI operation failed [repo:%d op:%s target:%d]: %v", req.RepoID, req.Operation, req.TargetID, err)
|
||||
@@ -48,7 +49,7 @@ func handleAIOperation(items ...*AIOperationRequest) []*AIOperationRequest {
|
||||
return nil
|
||||
}
|
||||
|
||||
func processOperation(ctx context.Context, req *AIOperationRequest) error {
|
||||
func processOperation(ctx context.Context, req *OperationRequest) error {
|
||||
// Load repo
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, req.RepoID)
|
||||
if err != nil {
|
||||
@@ -156,10 +157,10 @@ func handleIssueResponse(ctx context.Context, repo *repo_model.Repository, aiCfg
|
||||
|
||||
client := ai.GetClient()
|
||||
resp, err := client.GenerateIssueResponse(ctx, &ai.GenerateIssueResponseRequest{
|
||||
RepoID: repo.ID,
|
||||
IssueID: issue.ID,
|
||||
Title: issue.Title,
|
||||
Body: issue.Content,
|
||||
RepoID: repo.ID,
|
||||
IssueID: issue.ID,
|
||||
Title: issue.Title,
|
||||
Body: issue.Content,
|
||||
CustomInstructions: aiCfg.IssueInstructions,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -180,7 +181,7 @@ func handleIssueResponse(ctx context.Context, repo *repo_model.Repository, aiCfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleIssueTriage(ctx context.Context, repo *repo_model.Repository, aiCfg *repo_model.AIConfig, opLog *ai_model.OperationLog) error {
|
||||
func handleIssueTriage(ctx context.Context, repo *repo_model.Repository, _ *repo_model.AIConfig, opLog *ai_model.OperationLog) error {
|
||||
issue, err := issues_model.GetIssueByID(ctx, opLog.TargetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load issue %d: %w", opLog.TargetID, err)
|
||||
@@ -208,7 +209,7 @@ func handleIssueTriage(ctx context.Context, repo *repo_model.Repository, aiCfg *
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleCodeReview(ctx context.Context, repo *repo_model.Repository, aiCfg *repo_model.AIConfig, opLog *ai_model.OperationLog) error {
|
||||
func handleCodeReview(ctx context.Context, repo *repo_model.Repository, _ *repo_model.AIConfig, opLog *ai_model.OperationLog) error {
|
||||
issue, err := issues_model.GetIssueByID(ctx, opLog.TargetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load issue %d: %w", opLog.TargetID, err)
|
||||
|
||||
Reference in New Issue
Block a user