package service import ( "context" "fmt" "strings" "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" ) // CopilotContextService provides conversation context injection for Copilot LLM calls. // Reference: Chatwoot Captain::Copilot::ChatService — current_viewing_history + account context // // This service retrieves the conversation the agent is currently viewing, // formats it as LLM context, and enriches Copilot prompts with real data. type CopilotContextService struct { messageRepo *repository.MessageRepo conversationRepo *repository.ConversationRepo contactRepo *repository.ContactRepo llmProvider llm.Provider } // NewCopilotContextService creates a new CopilotContextService. func NewCopilotContextService( messageRepo *repository.MessageRepo, conversationRepo *repository.ConversationRepo, contactRepo *repository.ContactRepo, llmProvider llm.Provider, ) *CopilotContextService { return &CopilotContextService{ messageRepo: messageRepo, conversationRepo: conversationRepo, contactRepo: contactRepo, llmProvider: llmProvider, } } // --- Conversation Context Building --- // Reference: Chatwoot Captain::Copilot::ChatService#current_viewing_history // ConversationContext holds formatted context from the currently viewed conversation. type ConversationContext struct { ConversationID uint `json:"conversation_id"` ContactName string `json:"contact_name,omitempty"` ContactEmail string `json:"contact_email,omitempty"` Messages []ContextMessage `json:"messages"` Summary string `json:"summary,omitempty"` } // ContextMessage is a simplified message format for LLM context injection. type ContextMessage struct { Role string `json:"role"` // "customer" or "agent" Content string `json:"content"` Timestamp string `json:"timestamp,omitempty"` } // GetCurrentViewingContext retrieves and formats the conversation context // that the agent is currently viewing, to inject into Copilot LLM calls. func (s *CopilotContextService) GetCurrentViewingContext(ctx context.Context, accountID, conversationID uint) (*ConversationContext, error) { // Get conversation messages (last N messages for context) messages, _, err := s.messageRepo.FindByConversation(ctx, conversationID, 0, 20) if err != nil { applogger.L().Errorf("GetCurrentViewingContext FindByConversation: %v", err) return nil, fmt.Errorf("retrieve conversation messages: %w", err) } // Get conversation for contact info conversation, err := s.conversationRepo.FindByID(ctx, conversationID) if err != nil { applogger.L().Errorf("GetCurrentViewingContext GetByID: %v", err) return nil, fmt.Errorf("retrieve conversation: %w", err) } // Build context context := &ConversationContext{ ConversationID: conversationID, } // Add contact details if available if conversation.ContactID != 0 { contact, err := s.contactRepo.FindByID(ctx, conversation.ContactID) if err == nil && contact != nil { context.ContactName = contact.Name context.ContactEmail = contact.Email } } // Format messages for LLM context for _, m := range messages { role := "customer" if m.MessageType == "outgoing" { role = "agent" } // Skip activity/template messages if m.MessageType == "activity" || m.ContentType != "text" { continue } context.Messages = append(context.Messages, ContextMessage{ Role: role, Content: m.Content, Timestamp: m.CreatedAt.Format("2006-01-02 15:04:05"), }) } return context, nil } // BuildCopilotSystemPrompt constructs the system prompt for Copilot chat. // Incorporates assistant configuration, product context, and available tools. // Reference: Chatwoot Captain::Copilot::ChatService system_message building func BuildCopilotSystemPrompt(assistant *model.CaptainAssistant, assistantConfig *model.AssistantConfig, context *ConversationContext) string { var parts []string // Base identity parts = append(parts, "You are an AI copilot assistant helping a customer support agent.") // Product context from assistant config if assistantConfig != nil && assistantConfig.ProductName != "" { parts = append(parts, fmt.Sprintf("The agent is supporting customers of the product: %s.", assistantConfig.ProductName)) } // Assistant name and description if assistant != nil { parts = append(parts, fmt.Sprintf("Your name is %s.", assistant.Name)) if assistant.Description != "" { parts = append(parts, fmt.Sprintf("Your role: %s.", assistant.Description)) } } // Response guidelines if assistant != nil && assistant.ResponseGuidelines != nil { guidelines, _ := assistant.GetResponseGuidelines() if guidelines != "" { parts = append(parts, "Response guidelines:\n" + guidelines) } } // Feature flags context if assistantConfig != nil { if assistantConfig.FeatureFAQ { parts = append(parts, "You have access to FAQ knowledge base search. Use it to find relevant answers.") } if assistantConfig.FeatureMemory { parts = append(parts, "You can remember previous interactions with this customer.") } if assistantConfig.FeatureContactAttributes { parts = append(parts, "You can access customer contact attributes and history.") } } // Conversation context injection if context != nil && len(context.Messages) > 0 { parts = append(parts, "\nCurrently viewed conversation context:") if context.ContactName != "" { parts = append(parts, fmt.Sprintf("Customer: %s", context.ContactName)) if context.ContactEmail != "" { parts = append(parts, fmt.Sprintf("Email: %s", context.ContactEmail)) } } parts = append(parts, "Conversation messages:") for _, msg := range context.Messages { parts = append(parts, fmt.Sprintf("[%s] %s: %s", msg.Timestamp, msg.Role, msg.Content)) } } // Behavioral guidelines parts = append(parts, "\nBehavior guidelines:") parts = append(parts, "- Be concise and actionable in your suggestions.") parts = append(parts, "- Prioritize accuracy over speed; cite sources when possible.") parts = append(parts, "- When suggesting replies, tailor them to the customer's tone and urgency.") parts = append(parts, "- If you're unsure, say so rather than guessing.") parts = append(parts, "- Always respond in the same language as the user's message.") return strings.Join(parts, "\n") } // BuildCopilotToolDefinitions creates tool definitions for Copilot LLM function calling. // Reference: Chatwoot Captain::Copilot tools — GetConversation, SearchConversations, // GetContact, SearchArticles, SearchContacts, SearchLinearIssues, SearchDocumentation func BuildCopilotToolDefinitions() []llm.ToolDefinition { return []llm.ToolDefinition{ { Type: "function", Function: llm.ToolFunction{ Name: "search_documentation", Description: "Search the knowledge base / FAQ documentation for relevant information to answer the customer's question.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "query": map[string]interface{}{ "type": "string", "description": "The search query to find relevant documentation.", }, }, "required": []string{"query"}, }, }, }, { Type: "function", Function: llm.ToolFunction{ Name: "get_conversation", Description: "Get details about a specific conversation including messages and metadata.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "conversation_id": map[string]interface{}{ "type": "integer", "description": "The ID of the conversation to retrieve.", }, }, "required": []string{"conversation_id"}, }, }, }, { Type: "function", Function: llm.ToolFunction{ Name: "search_conversations", Description: "Search conversations by status, assignee, or content to find related cases.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "query": map[string]interface{}{ "type": "string", "description": "Search query for conversation content.", }, "status": map[string]interface{}{ "type": "string", "description": "Filter by conversation status (open, closed, pending).", }, }, "required": []string{"query"}, }, }, }, { Type: "function", Function: llm.ToolFunction{ Name: "get_contact", Description: "Get details about a specific contact including name, email, and custom attributes.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "contact_id": map[string]interface{}{ "type": "integer", "description": "The ID of the contact to retrieve.", }, }, "required": []string{"contact_id"}, }, }, }, { Type: "function", Function: llm.ToolFunction{ Name: "search_contacts", Description: "Search contacts by name, email, or custom attributes.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "query": map[string]interface{}{ "type": "string", "description": "Search query for contact information.", }, }, "required": []string{"query"}, }, }, }, { Type: "function", Function: llm.ToolFunction{ Name: "get_article", Description: "Get a specific help center article by ID.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "article_id": map[string]interface{}{ "type": "integer", "description": "The ID of the article to retrieve.", }, }, "required": []string{"article_id"}, }, }, }, { Type: "function", Function: llm.ToolFunction{ Name: "search_articles", Description: "Search help center articles by title or content.", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "query": map[string]interface{}{ "type": "string", "description": "Search query for article content.", }, }, "required": []string{"query"}, }, }, }, } }