284 lines
9.4 KiB
Go
284 lines
9.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/llm"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
)
|
|
|
|
// 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
|
|
responseRepo *repository.CaptainAssistantResponseRepo
|
|
documentRepo *repository.CaptainDocumentRepo
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
func (s *CaptainBulkActionService) SetCaptainResourceRepos(responseRepo *repository.CaptainAssistantResponseRepo, documentRepo *repository.CaptainDocumentRepo) {
|
|
s.responseRepo = responseRepo
|
|
s.documentRepo = documentRepo
|
|
}
|
|
|
|
// --- 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"`
|
|
}
|
|
|
|
type ChatwootBulkActionRequest struct {
|
|
Type string `json:"type"`
|
|
IDs []uint `json:"ids"`
|
|
Fields struct {
|
|
Status string `json:"status"`
|
|
} `json:"fields"`
|
|
}
|
|
|
|
type ChatwootBulkActionResult struct {
|
|
AssistantResponses []model.CaptainAssistantResponse
|
|
IDs []uint
|
|
Count int64
|
|
Empty bool
|
|
}
|
|
|
|
// --- 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) ExecuteChatwoot(ctx context.Context, accountID uint, req *ChatwootBulkActionRequest) (*ChatwootBulkActionResult, error) {
|
|
if req == nil || req.Type == "" || len(req.IDs) == 0 || req.Fields.Status == "" {
|
|
return nil, fmt.Errorf("invalid bulk action request")
|
|
}
|
|
|
|
switch req.Type {
|
|
case "AssistantResponse":
|
|
return s.executeAssistantResponseBulk(ctx, accountID, req)
|
|
case "AssistantDocument":
|
|
return s.executeAssistantDocumentBulk(ctx, accountID, req)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported bulk action type: %s", req.Type)
|
|
}
|
|
}
|
|
|
|
func (s *CaptainBulkActionService) executeAssistantResponseBulk(ctx context.Context, accountID uint, req *ChatwootBulkActionRequest) (*ChatwootBulkActionResult, error) {
|
|
if s.responseRepo == nil {
|
|
return nil, fmt.Errorf("assistant response repository unavailable")
|
|
}
|
|
|
|
db := s.responseRepo.DB().WithContext(ctx)
|
|
switch req.Fields.Status {
|
|
case "approve":
|
|
if err := db.Model(&model.CaptainAssistantResponse{}).
|
|
Where("account_id = ? AND id IN ? AND status = ?", accountID, req.IDs, model.ResponseStatusPending).
|
|
Update("status", model.ResponseStatusApproved).Error; err != nil {
|
|
return nil, fmt.Errorf("approve assistant responses: %w", err)
|
|
}
|
|
var responses []model.CaptainAssistantResponse
|
|
if err := db.Preload("Assistant").Where("account_id = ? AND id IN ?", accountID, req.IDs).Find(&responses).Error; err != nil {
|
|
return nil, fmt.Errorf("load assistant responses: %w", err)
|
|
}
|
|
return &ChatwootBulkActionResult{AssistantResponses: responses}, nil
|
|
case "delete":
|
|
if err := db.Where("account_id = ? AND id IN ?", accountID, req.IDs).Delete(&model.CaptainAssistantResponse{}).Error; err != nil {
|
|
return nil, fmt.Errorf("delete assistant responses: %w", err)
|
|
}
|
|
return &ChatwootBulkActionResult{Empty: true}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported assistant response status: %s", req.Fields.Status)
|
|
}
|
|
}
|
|
|
|
func (s *CaptainBulkActionService) executeAssistantDocumentBulk(ctx context.Context, accountID uint, req *ChatwootBulkActionRequest) (*ChatwootBulkActionResult, error) {
|
|
if s.documentRepo == nil {
|
|
return nil, fmt.Errorf("document repository unavailable")
|
|
}
|
|
|
|
db := s.documentRepo.DB().WithContext(ctx)
|
|
switch req.Fields.Status {
|
|
case "delete":
|
|
result := db.Where("account_id = ? AND id IN ?", accountID, req.IDs).Delete(&model.CaptainDocument{})
|
|
if result.Error != nil {
|
|
return nil, fmt.Errorf("delete documents: %w", result.Error)
|
|
}
|
|
return &ChatwootBulkActionResult{Count: result.RowsAffected}, nil
|
|
case "sync":
|
|
now := time.Now().Unix()
|
|
result := db.Model(&model.CaptainDocument{}).
|
|
Where("account_id = ? AND id IN ?", accountID, req.IDs).
|
|
Updates(map[string]any{
|
|
"sync_status": model.DocumentSyncStatusPending,
|
|
"last_sync_attempted_at": &now,
|
|
"last_sync_error_code": "",
|
|
})
|
|
if result.Error != nil {
|
|
return nil, fmt.Errorf("sync documents: %w", result.Error)
|
|
}
|
|
return &ChatwootBulkActionResult{IDs: req.IDs, Count: result.RowsAffected}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported document status: %s", req.Fields.Status)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|