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

226 lines
7.8 KiB
Plaintext

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
}