From 14338d8fd45b280d0fa54e5dcd8832469841b45c Mon Sep 17 00:00:00 2001 From: logikonline Date: Thu, 12 Feb 2026 00:55:52 -0500 Subject: [PATCH] refactor(ai): consolidate ai operation types and reduce duplication 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. --- models/ai/operation_log.go | 2 +- modules/ai/types.go | 10 +-- modules/errors/codes.go | 6 +- routers/api/v2/ai_operations.go | 106 ++++++++++++-------------------- services/ai/agent.go | 7 ++- services/ai/escalation.go | 2 +- services/ai/init.go | 2 +- services/ai/notifier.go | 12 ++-- services/ai/queue.go | 27 ++++---- 9 files changed, 74 insertions(+), 100 deletions(-) diff --git a/models/ai/operation_log.go b/models/ai/operation_log.go index ff1ed80075..f9fde4b986 100644 --- a/models/ai/operation_log.go +++ b/models/ai/operation_log.go @@ -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)"` diff --git a/modules/ai/types.go b/modules/ai/types.go index a2b94183f8..8808f12a1f 100644 --- a/modules/ai/types.go +++ b/modules/ai/types.go @@ -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 diff --git a/modules/errors/codes.go b/modules/errors/codes.go index ca0ed5ba96..0091dcab15 100644 --- a/modules/errors/codes.go +++ b/modules/errors/codes.go @@ -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" ) diff --git a/routers/api/v2/ai_operations.go b/routers/api/v2/ai_operations.go index e825ae088d..04a5e55552 100644 --- a/routers/api/v2/ai_operations.go +++ b/routers/api/v2/ai_operations.go @@ -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, diff --git a/services/ai/agent.go b/services/ai/agent.go index e7ec0755ef..74e9d3f72b 100644 --- a/services/ai/agent.go +++ b/services/ai/agent.go @@ -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: diff --git a/services/ai/escalation.go b/services/ai/escalation.go index 549de881eb..b8118867a7 100644 --- a/services/ai/escalation.go +++ b/services/ai/escalation.go @@ -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 { diff --git a/services/ai/init.go b/services/ai/init.go index 92cce4d385..ac69aa0a26 100644 --- a/services/ai/init.go +++ b/services/ai/init.go @@ -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 { diff --git a/services/ai/notifier.go b/services/ai/notifier.go index 42748aebc6..39ade5f9d3 100644 --- a/services/ai/notifier.go +++ b/services/ai/notifier.go @@ -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, diff --git a/services/ai/queue.go b/services/ai/queue.go index 6c1f2a64f5..3059418174 100644 --- a/services/ai/queue.go +++ b/services/ai/queue.go @@ -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)