Files
gochat/internal/service/captain_bulk_action_service.go
T
2026-06-04 15:44:48 +08:00

186 lines
5.6 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"github.com/gochat/gochat/internal/llm"
"github.com/gochat/gochat/internal/repository"
pkgvalidator "github.com/gochat/gochat/pkg/validator"
applogger "github.com/gochat/gochat/pkg/logger"
)
// BulkActionType defines the type of bulk AI operation.
type BulkActionType string
const (
BulkActionLabelSuggestion BulkActionType = "label_suggestion"
BulkActionReplySuggestion BulkActionType = "reply_suggestion"
BulkActionFollowUp BulkActionType = "follow_up"
)
// CaptainBulkActionService handles batch AI operations on multiple conversations.
// Reference: Chatwoot Captain::BulkActionsController + M12 PRD
type CaptainBulkActionService struct {
conversationRepo *repository.ConversationRepo
messageRepo *repository.MessageRepo
assistantRepo *repository.CaptainAssistantRepo
preferenceRepo *repository.CaptainPreferenceRepo
llmProvider llm.Provider
taskExtendedSvc *CaptainTaskExtendedService
responseSvc *CaptainAssistantResponseService
}
func NewCaptainBulkActionService(
conversationRepo *repository.ConversationRepo,
messageRepo *repository.MessageRepo,
assistantRepo *repository.CaptainAssistantRepo,
preferenceRepo *repository.CaptainPreferenceRepo,
llmProvider llm.Provider,
taskExtendedSvc *CaptainTaskExtendedService,
responseSvc *CaptainAssistantResponseService,
) *CaptainBulkActionService {
return &CaptainBulkActionService{
conversationRepo: conversationRepo,
messageRepo: messageRepo,
assistantRepo: assistantRepo,
preferenceRepo: preferenceRepo,
llmProvider: llmProvider,
taskExtendedSvc: taskExtendedSvc,
responseSvc: responseSvc,
}
}
// --- Request/Response DTOs ---
// BulkActionRequest is the input for a bulk AI operation.
type BulkActionRequest struct {
Action BulkActionType `json:"action" validate:"required"`
ConversationIDs []uint `json:"conversation_ids" validate:"required,min=1"`
AssistantID uint `json:"assistant_id,omitempty"`
// Optional: for reply_suggestion, whether to auto-send
SendMessages bool `json:"send_messages,omitempty"`
}
// BulkActionResult holds the results of a bulk operation.
type BulkActionResult struct {
Action BulkActionType `json:"action"`
Processed int `json:"processed"`
Failed int `json:"failed"`
Results []json.RawMessage `json:"results"`
}
// --- Business logic ---
// Execute performs a bulk AI action on multiple conversations.
func (s *CaptainBulkActionService) Execute(ctx context.Context, accountID uint, req *BulkActionRequest) (*BulkActionResult, error) {
// Validate request
if err := pkgvalidator.ValidateStruct(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
// Validate action type
validActions := map[BulkActionType]bool{
BulkActionLabelSuggestion: true,
BulkActionReplySuggestion: true,
BulkActionFollowUp: true,
}
if !validActions[req.Action] {
return nil, fmt.Errorf("unsupported bulk action: %s", req.Action)
}
switch req.Action {
case BulkActionLabelSuggestion:
return s.bulkLabelSuggestion(ctx, accountID, req)
case BulkActionReplySuggestion:
return s.bulkReplySuggestion(ctx, accountID, req)
case BulkActionFollowUp:
return s.bulkFollowUp(ctx, accountID, req)
default:
return nil, fmt.Errorf("unsupported bulk action: %s", req.Action)
}
}
func (s *CaptainBulkActionService) bulkLabelSuggestion(ctx context.Context, accountID uint, req *BulkActionRequest) (*BulkActionResult, error) {
query := &LabelSuggestionQuery{
ConversationIDs: req.ConversationIDs,
AssistantID: req.AssistantID,
}
result, err := s.taskExtendedSvc.SuggestLabels(ctx, accountID, query)
if err != nil {
return nil, fmt.Errorf("bulk label suggestion: %w", err)
}
var results []json.RawMessage
for _, s := range result.Suggestions {
raw, _ := json.Marshal(s)
results = append(results, raw)
}
return &BulkActionResult{
Action: BulkActionLabelSuggestion,
Processed: len(result.Suggestions),
Failed: len(req.ConversationIDs) - len(result.Suggestions),
Results: results,
}, nil
}
func (s *CaptainBulkActionService) bulkReplySuggestion(ctx context.Context, accountID uint, req *BulkActionRequest) (*BulkActionResult, error) {
var results []json.RawMessage
processed := 0
failed := 0
for _, convID := range req.ConversationIDs {
respReq := &ProcessResponseRequest{
ConversationID: convID,
AssistantID: req.AssistantID,
SendMessage: req.SendMessages,
}
result, err := s.responseSvc.ProcessResponse(ctx, accountID, respReq)
if err != nil {
applogger.L().Warnf("Bulk reply suggestion failed for conversation %d: %v", convID, err)
failed++
continue
}
raw, _ := json.Marshal(result)
results = append(results, raw)
processed++
}
return &BulkActionResult{
Action: BulkActionReplySuggestion,
Processed: processed,
Failed: failed,
Results: results,
}, nil
}
func (s *CaptainBulkActionService) bulkFollowUp(ctx context.Context, accountID uint, req *BulkActionRequest) (*BulkActionResult, error) {
query := &FollowUpQuery{
ConversationIDs: req.ConversationIDs,
AssistantID: req.AssistantID,
}
result, err := s.taskExtendedSvc.SuggestFollowUp(ctx, accountID, query)
if err != nil {
return nil, fmt.Errorf("bulk follow-up: %w", err)
}
var results []json.RawMessage
for _, f := range result.FollowUps {
raw, _ := json.Marshal(f)
results = append(results, raw)
}
return &BulkActionResult{
Action: BulkActionFollowUp,
Processed: len(result.FollowUps),
Failed: len(req.ConversationIDs) - len(result.FollowUps),
Results: results,
}, nil
}