226 lines
7.8 KiB
Plaintext
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
|
|
} |