2
0

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

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:
2026-02-12 00:55:52 -05:00
parent c9f6c4e7d2
commit 14338d8fd4
9 changed files with 74 additions and 100 deletions

View File

@@ -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)"`

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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,

View File

@@ -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:

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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)