package service import ( "context" "encoding/json" "fmt" "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" ) // --- Rewrite Reply Service --- // Reference: Chatwoot Captain::Copilot::RewriteReplyService // // Rewrites a draft or existing reply using LLM, applying tone adjustments, // grammar fixes, and style guidelines from the assistant config. // RewriteReplyRequest holds parameters for the rewrite operation. type RewriteReplyRequest struct { OriginalContent string `json:"original_content" binding:"required"` Tone string `json:"tone,omitempty"` // e.g. "professional", "friendly", "casual" Language string `json:"language,omitempty"` // e.g. "en", "zh", "es" ConversationID uint `json:"conversation_id,omitempty"` // optional: inject conversation context ThreadID uint `json:"thread_id,omitempty"` // optional: use thread assistant config } // RewriteReplyResult holds the rewritten reply and metadata. type RewriteReplyResult struct { RewrittenContent string `json:"rewritten_content"` ToneApplied string `json:"tone_applied,omitempty"` LanguageApplied string `json:"language_applied,omitempty"` OriginalContent string `json:"original_content"` } // RewriteReplyService provides LLM-powered reply rewriting. type RewriteReplyService struct { copilotContextService *CopilotContextService assistantRepo *repository.CaptainAssistantRepo llmProvider llm.Provider } // NewRewriteReplyService creates a new RewriteReplyService. func NewRewriteReplyService( copilotContextService *CopilotContextService, assistantRepo *repository.CaptainAssistantRepo, llmProvider llm.Provider, ) *RewriteReplyService { return &RewriteReplyService{ copilotContextService: copilotContextService, assistantRepo: assistantRepo, llmProvider: llmProvider, } } // RewriteReply rewrites a draft reply with tone/style adjustments and optional conversation context. func (s *RewriteReplyService) RewriteReply(ctx context.Context, accountID uint, req *RewriteReplyRequest) (*RewriteReplyResult, error) { if req.OriginalContent == "" { return nil, fmt.Errorf("original_content is required") } // Build system prompt parts := []string{ "You are a professional communication assistant. Your task is to rewrite the given reply.", } // Apply tone instruction if req.Tone != "" { parts = append(parts, fmt.Sprintf("Adjust the tone to be %s.", req.Tone)) } else { parts = append(parts, "Maintain a professional and helpful tone.") } // Apply language instruction if req.Language != "" { parts = append(parts, fmt.Sprintf("Write the reply in %s language.", req.Language)) } // Apply assistant guidelines if thread is provided var assistant *model.CaptainAssistant var assistantConfig *model.AssistantConfig if req.ThreadID != 0 { // Look up assistant via thread (thread has assistant_id) assistant = s.resolveAssistantFromThread(ctx, req.ThreadID) if assistant != nil { cfg, err := assistant.GetConfig() if err == nil { assistantConfig = cfg } guidelines, _ := assistant.GetResponseGuidelines() if guidelines != "" { parts = append(parts, "Response guidelines:\n" + guidelines) } if assistant.Name != "" { parts = append(parts, fmt.Sprintf("You represent %s.", assistant.Name)) } if assistantConfig != nil && assistantConfig.ProductName != "" { parts = append(parts, fmt.Sprintf("Product name: %s", assistantConfig.ProductName)) } } } systemPrompt := BuildCopilotSystemPrompt(assistant, assistantConfig, nil) // Override the base prompt with rewrite-specific instructions systemPrompt = systemPrompt + "\n\n" + "Your specific task now: Rewrite the draft reply provided by the user. " + "Keep the core meaning but improve clarity, professionalism, and style." for _, p := range parts { systemPrompt += "\n" + p } // Inject conversation context if available if req.ConversationID != 0 { convContext, ctxErr := s.copilotContextService.GetCurrentViewingContext(ctx, accountID, req.ConversationID) if ctxErr == nil && convContext != nil { contextJSON, _ := json.Marshal(convContext) systemPrompt += "\n\nCurrent conversation context:\n" + string(contextJSON) } } // Build messages messages := []llm.ChatMessage{ {Role: "system", Content: systemPrompt}, {Role: "user", Content: fmt.Sprintf("Please rewrite this reply:\n\n%s", req.OriginalContent)}, } // Call LLM llmResp, err := s.llmProvider.ChatCompletion(ctx, llm.ChatRequest{ Model: "gpt-4", Messages: messages, Temperature: 0.7, MaxTokens: 1024, }) if err != nil { applogger.L().Errorf("RewriteReply LLM call: %v", err) return nil, fmt.Errorf("rewrite generation failed: %w", err) } result := &RewriteReplyResult{ RewrittenContent: llmResp.Message.Content, ToneApplied: req.Tone, LanguageApplied: req.Language, OriginalContent: req.OriginalContent, } return result, nil } // resolveAssistantFromThread looks up the assistant associated with a copilot thread. func (s *RewriteReplyService) resolveAssistantFromThread(ctx context.Context, threadID uint) *model.CaptainAssistant { // We need to find the thread first, then its assistant // But RewriteReplyService doesn't have threadRepo — use assistantRepo directly // In practice, the handler passes the assistant_id from the thread // For now, return nil; the handler will pass assistant_id directly via a separate lookup return nil } // RewriteReplyWithAssistant rewrites using an explicitly provided assistant. func (s *RewriteReplyService) RewriteReplyWithAssistant(ctx context.Context, accountID uint, req *RewriteReplyRequest, assistantID uint) (*RewriteReplyResult, error) { assistant, err := s.assistantRepo.GetByID(ctx, assistantID) if err != nil || assistant == nil { return nil, fmt.Errorf("assistant not found: %w", err) } // Build system prompt parts := []string{ "You are a professional communication assistant. Your task is to rewrite the given reply.", } if req.Tone != "" { parts = append(parts, fmt.Sprintf("Adjust the tone to be %s.", req.Tone)) } else { parts = append(parts, "Maintain a professional and helpful tone.") } if req.Language != "" { parts = append(parts, fmt.Sprintf("Write the reply in %s language.", req.Language)) } cfg, _ := assistant.GetConfig() guidelines, _ := assistant.GetResponseGuidelines() if guidelines != "" { parts = append(parts, "Response guidelines:\n" + guidelines) } systemPrompt := BuildCopilotSystemPrompt(assistant, cfg, nil) systemPrompt += "\n\nYour specific task now: Rewrite the draft reply. Keep the core meaning but improve clarity, professionalism, and style." for _, p := range parts { systemPrompt += "\n" + p } // Inject conversation context if available if req.ConversationID != 0 { convContext, ctxErr := s.copilotContextService.GetCurrentViewingContext(ctx, accountID, req.ConversationID) if ctxErr == nil && convContext != nil { contextJSON, _ := json.Marshal(convContext) systemPrompt += "\n\nCurrent conversation context:\n" + string(contextJSON) } } messages := []llm.ChatMessage{ {Role: "system", Content: systemPrompt}, {Role: "user", Content: fmt.Sprintf("Please rewrite this reply:\n\n%s", req.OriginalContent)}, } llmResp, llmErr := s.llmProvider.ChatCompletion(ctx, llm.ChatRequest{ Model: "gpt-4", Messages: messages, Temperature: 0.7, MaxTokens: 1024, }) if llmErr != nil { applogger.L().Errorf("RewriteReplyWithAssistant LLM call: %v", llmErr) return nil, fmt.Errorf("rewrite generation failed: %w", llmErr) } result := &RewriteReplyResult{ RewrittenContent: llmResp.Message.Content, ToneApplied: req.Tone, LanguageApplied: req.Language, OriginalContent: req.OriginalContent, } return result, nil }