186 lines
5.6 KiB
Go
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
|
|
}
|