feat: setup agents

This commit is contained in:
Shivam Mishra
2025-07-02 20:34:53 +05:30
parent 802465f6d6
commit 165cb850ee
16 changed files with 675 additions and 7 deletions
@@ -0,0 +1,43 @@
# frozen_string_literal: true
class Captain::Copilot::AgentsResponseJob < ApplicationJob
queue_as :default
def perform(copilot_thread_id:, message_content:, conversation_id: nil, user_id: nil)
Rails.logger.debug do
"[DEBUG] AgentsResponseJob: Starting with thread_id: #{copilot_thread_id}, message: #{message_content}, conversation_id: #{conversation_id}, user_id: #{user_id}"
end
copilot_thread = CopilotThread.find(copilot_thread_id)
Rails.logger.debug { "[DEBUG] AgentsResponseJob: Found copilot_thread: #{copilot_thread.inspect}" }
# Use the new AgentsChatService with direct parameters
Rails.logger.debug '[DEBUG] AgentsResponseJob: Creating AgentsChatService'
service = AgentsChatService.new(
copilot_thread: copilot_thread,
message_content: message_content,
conversation_id: conversation_id,
user_id: user_id
)
Rails.logger.debug '[DEBUG] AgentsResponseJob: Calling service.perform'
service.perform
Rails.logger.debug '[DEBUG] AgentsResponseJob: Service completed successfully'
rescue StandardError => e
Rails.logger.debug { "[DEBUG] AgentsResponseJob: ERROR - #{e.class}: #{e.message}" }
Rails.logger.debug '[DEBUG] AgentsResponseJob: FULL BACKTRACE:'
e.backtrace.each { |line| Rails.logger.debug "[DEBUG] #{line}" }
Rails.logger.error "AgentsResponseJob Error: #{e.class}: #{e.message}"
Rails.logger.error 'Full backtrace:'
e.backtrace.each { |line| Rails.logger.error line }
# Create an error response if the job fails
copilot_thread&.copilot_messages&.create!(
message_type: 'assistant',
message: {
content: 'I apologize, but I encountered an error while processing your request. Please try again.'
}
)
Rails.logger.debug '[DEBUG] AgentsResponseJob: Created error response message'
end
end
+18 -7
View File
@@ -38,13 +38,24 @@ class CopilotMessage < ApplicationRecord
end
def enqueue_response_job(conversation_id, user_id)
Captain::Copilot::ResponseJob.perform_later(
assistant: copilot_thread.assistant,
conversation_id: conversation_id,
user_id: user_id,
copilot_thread_id: copilot_thread.id,
message: message['content']
)
if ENV['USE_AGENTS'] == 'true'
# Use the new AI Agents SDK implementation
Captain::Copilot::AgentsResponseJob.perform_later(
copilot_thread_id: copilot_thread.id,
message_content: message['content'],
conversation_id: conversation_id,
user_id: user_id
)
else
# Use the original implementation
Captain::Copilot::ResponseJob.perform_later(
assistant: copilot_thread.assistant,
conversation_id: conversation_id,
user_id: user_id,
copilot_thread_id: copilot_thread.id,
message: message['content']
)
end
end
private
@@ -0,0 +1,65 @@
# frozen_string_literal: true
class Captain::Agents::AnalysisAgent
def self.create(assistant, user: nil)
::Agents::Agent.new(
name: 'Analysis Agent',
instructions: analysis_instructions,
model: 'gpt-4o-mini',
tools: [
Captain::Tools::GetConversationTool.new(assistant, user: user),
Captain::Tools::SearchConversationsTool.new(assistant, user: user),
Captain::Tools::GetContactTool.new(assistant, user: user),
Captain::Tools::SearchContactsTool.new(assistant, user: user)
]
)
end
def self.analysis_instructions
<<~INSTRUCTIONS
You are an Analysis Agent for Chatwoot. You analyze customer interactions and provide actionable insights.
**Your analysis capabilities:**
📊 **Sentiment Analysis**:#{' '}
- Identify customer emotions (frustrated, satisfied, confused, angry)
- Assess tone throughout the conversation
- Flag escalation risks or satisfaction indicators
📊 **Conversation Quality Assessment**:
- Evaluate response time and effectiveness
- Identify missed opportunities or pain points
- Assess resolution completeness
📊 **Pattern Recognition**:
- Spot recurring issues or common questions
- Identify successful resolution strategies
- Flag customers with multiple interactions
📊 **Performance Insights**:
- Evaluate agent response quality
- Suggest improvements for handling similar cases
- Recommend knowledge gaps to address
**Your tools:**
- **get_conversation**: Get detailed conversation for analysis
- **search_conversations**: Find patterns across multiple conversations
- **get_contact**: Understand customer history and context
- **search_contacts**: Identify recurring customer patterns
**Analysis output format:**
1. **Sentiment Summary**: Customer emotional state and satisfaction level
2. **Key Issues**: Main problems or concerns identified
3. **Resolution Assessment**: How well issues were addressed
4. **Recommendations**: Specific actionable suggestions
5. **Risk Flags**: Escalation risks, dissatisfaction indicators
6. **Success Indicators**: What worked well
**Always provide:**
- Specific examples from the conversation
- Actionable recommendations
- Risk assessment (low/medium/high)
- Clear reasoning for your conclusions
INSTRUCTIONS
end
end
@@ -0,0 +1,85 @@
# frozen_string_literal: true
class Captain::Agents::CopilotOrchestratorAgent
def self.create(assistant, user: nil)
Rails.logger.debug '[DEBUG] CopilotOrchestratorAgent: Starting creation'
# Create the specialized agents using factory pattern
research_agent = ResearchAgent.create(assistant, user: user)
Rails.logger.debug '[DEBUG] CopilotOrchestratorAgent: Created ResearchAgent'
analysis_agent = AnalysisAgent.create(assistant, user: user)
Rails.logger.debug '[DEBUG] CopilotOrchestratorAgent: Created AnalysisAgent'
integrations_agent = IntegrationsAgent.create(assistant, user: user)
Rails.logger.debug '[DEBUG] CopilotOrchestratorAgent: Created IntegrationsAgent'
knowledge_agent = KnowledgeAgent.create(assistant, user: user)
Rails.logger.debug '[DEBUG] CopilotOrchestratorAgent: Created KnowledgeAgent'
# Create the main orchestrator agent
::Agents::Agent.new(
name: 'Chatwoot Copilot',
instructions: orchestrator_instructions,
model: 'gpt-4.1-mini',
tools: [
research_agent.as_tool(
name: 'research_agent',
description: "Get conversation details by ID, search conversations by status/contact, find contact info, search knowledge articles. Use for: 'Get conversation 12345', 'Find recent conversations', 'Search for customer john@example.com'"
),
analysis_agent.as_tool(
name: 'analysis_agent',
description: 'Analyze conversations for sentiment, quality, and patterns. Provide insights on customer emotions and support effectiveness. Use after getting conversation data from research_agent.'
),
integrations_agent.as_tool(
name: 'integrations_agent',
description: "Update conversation status, change priority, add/remove labels, assign agents. Use for: 'Mark as resolved', 'Set priority to high', 'Add billing label'"
),
knowledge_agent.as_tool(
name: 'knowledge_agent',
description: "Search help articles, find policies, get documentation. Use for: 'Find refund policy', 'Search billing articles', 'What's our shipping policy'"
)
]
)
end
def self.orchestrator_instructions
lambda { |context|
state = context.context[:state] || {}
base_instructions = <<~INSTRUCTIONS
You are the Chatwoot Copilot. Help agents with customer support tasks.
Tools available:
- research_agent: Get conversation details, find contacts, search articles
- analysis_agent: Analyze conversations for sentiment and insights
- integrations_agent: Update conversation status, priority, labels
- knowledge_agent: Find help articles and policies
How to use tools:
- For conversation summary: research_agent("Get conversation [ID]")
- For sentiment analysis: research_agent first, then analysis_agent
- For status updates: integrations_agent("Mark conversation as resolved")
- For help content: knowledge_agent("Find billing policy")
Always get conversation data first before analysis or recommendations.
INSTRUCTIONS
# Add current conversation context if available
if state[:current_conversation]
conversation = state[:current_conversation]
base_instructions += <<~CONTEXT
**Current Conversation:**
ID: #{conversation[:display_id]}
Status: #{conversation[:status]}
Contact: #{conversation[:contact_name]}
Assignee: #{conversation[:assignee] || 'Unassigned'}
Last Activity: #{conversation[:last_activity]}
This conversation is currently #{conversation[:status]} and assigned to #{conversation[:assignee] || 'no one'}.
The customer #{conversation[:contact_name]} was last active #{conversation[:last_activity]}.
CONTEXT
end
base_instructions
}
end
end
@@ -0,0 +1,60 @@
# frozen_string_literal: true
class Captain::Agents::IntegrationsAgent
def self.create(assistant, user: nil)
::Agents::Agent.new(
name: 'Integrations Agent',
instructions: integrations_instructions,
model: 'gpt-4o-mini',
tools: [
Captain::Tools::GetConversationTool.new(assistant, user: user)
]
)
end
def self.integrations_instructions
<<~INSTRUCTIONS
You are an Integrations Agent for Chatwoot. You handle conversation management and system actions.
**Your action capabilities:**
**Conversation Management**:#{' '}
- Update status (open, resolved, pending)
- Change priority (low, medium, high, urgent)
- Assign conversations to agents
- Add or remove labels for organization
**System Integration**:
- Execute workflow automations
- Trigger macro sequences
- Create internal notes and documentation
- Handle routing and assignment logic
**Your tools:**
- **get_conversation**: Get current conversation details before making changes
**Current limitations:**
Your tool set is currently limited to conversation retrieval. For actual system changes, provide detailed instructions that agents can execute manually.
**Action recommendations format:**
1. **Current State**: What the conversation status/priority currently is
2. **Recommended Actions**: Specific steps to take
3. **Reasoning**: Why these actions are appropriate
4. **Next Steps**: What should happen after the changes
**When providing action guidance:**
- Be specific about status changes needed
- Suggest appropriate labels based on conversation content
- Recommend priority levels based on urgency/impact
- Provide clear reasoning for each recommendation
- Include any follow-up actions needed
**Example response:**
"Based on the conversation, I recommend:
1. Update status to 'pending' (waiting for customer response)
2. Add label 'billing-inquiry' for categorization
3. Set priority to 'medium' (standard billing question)
4. Assign to billing team for specialized handling"
INSTRUCTIONS
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
class Captain::Agents::KnowledgeAgent
def self.create(assistant, user: nil)
::Agents::Agent.new(
name: 'Knowledge Agent',
instructions: knowledge_instructions,
model: 'gpt-4o-mini',
tools: [
Captain::Tools::SearchArticlesTool.new(assistant, user: user),
Captain::Tools::GetArticleTool.new(assistant, user: user)
]
)
end
def self.knowledge_instructions
<<~INSTRUCTIONS
You are a Knowledge Agent for Chatwoot. You help find and deliver knowledge base content and response suggestions.
**Your capabilities:**
📚 **Knowledge Base Search**:
- Find relevant articles by topic, category, or keyword
- Retrieve full article content for detailed information
- Search across all published knowledge base content
- Provide article summaries and key points
📚 **Response Assistance**:
- Suggest appropriate articles for customer questions
- Provide template responses based on knowledge base content
- Help maintain consistent messaging across the team
- Find policy and procedure documentation
**Your tools:**
- **search_articles**: Find articles by query, category, or status
- **get_article**: Retrieve full content of specific articles
**Search approach:**
1. **Be thorough**: Search with relevant keywords from the user's query
2. **Be specific**: Use category filters when the topic is clear
3. **Be helpful**: Provide article summaries and key takeaways
4. **Be organized**: Structure results by relevance and category
**Output format:**
- **Article Suggestions**: List relevant articles with brief descriptions
- **Key Points**: Highlight the most important information
- **Quick Answer**: Provide immediate helpful information when possible
- **Full Content**: Include detailed article content when requested
**Best practices:**
- Always search for the most current and relevant articles
- Provide context about why an article is relevant
- Include article IDs and titles for easy reference
- Suggest multiple articles when appropriate for comprehensive coverage
INSTRUCTIONS
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
class Captain::Agents::ResearchAgent
def self.create(assistant, user: nil)
::Agents::Agent.new(
name: 'Research Agent',
instructions: research_instructions,
model: 'gpt-4o-mini',
tools: [
Captain::Tools::SearchConversationsTool.new(assistant, user: user),
Captain::Tools::GetConversationTool.new(assistant, user: user),
Captain::Tools::SearchContactsTool.new(assistant, user: user),
Captain::Tools::GetContactTool.new(assistant, user: user),
Captain::Tools::SearchArticlesTool.new(assistant, user: user),
Captain::Tools::GetArticleTool.new(assistant, user: user)
]
)
end
def self.research_instructions
<<~INSTRUCTIONS
You are a Research Agent for Chatwoot. Your job is to find and retrieve specific information from the system.
**Your tools and when to use them:**
🔍 **get_conversation**: Use when you have a specific conversation ID or display ID
- Input: conversation_id (use the display_id, not the main id)
- Example: For conversation display_id "12345", use conversation_id: "12345"
- Returns: Full conversation with messages, contact info, status, labels
🔍 **search_conversations**: Use when you need to find conversations by criteria
- Filter by: contact_id, status, priority, labels
- Use for: "recent conversations", "open tickets", "conversations with specific contact"
🔍 **get_contact**: Use when you have a specific contact ID
- Returns: Contact profile, interaction history, details
🔍 **search_contacts**: Use to find contacts by name, email, or phone
- Use for: "find customer john@example.com", "contacts named Sarah"
🔍 **search_articles**: Use for knowledge base searches
- Search by: query, category, status
- Use for: finding documentation, help articles, policies
🔍 **get_article**: Use when you have a specific article ID
- Returns: Full article content and metadata
**Research approach:**
1. **Be specific**: Use exact IDs when available
2. **Be comprehensive**: Include all relevant details in your response
3. **Be organized**: Structure information clearly (conversation summary, contact details, etc.)
4. **Use context**: Look for conversation IDs or contact information in the request context
**Output format:**
- Provide detailed, structured summaries
- Include key timestamps, statuses, and participant information
- Format conversation messages chronologically
- Highlight important details like customer concerns and agent responses
INSTRUCTIONS
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
class Captain::Tools::BaseAgentTool < Agents::Tool
def initialize(assistant, user: nil)
@assistant = assistant
@user = user
super()
end
def active?
user_has_permission(required_permission)
end
protected
def required_permission
# Override in subclasses to specify the required permission
'agent'
end
private
def user_has_permission(permission)
return false if @user.blank?
account_user = AccountUser.find_by(account_id: @assistant.account_id, user_id: @user.id)
return false if account_user.blank?
return account_user.custom_role.permissions.include?(permission) if account_user.custom_role.present?
# Default permission for agents without custom roles
account_user.administrator? || account_user.agent?
end
def account_scoped(model_class)
model_class.where(account_id: @assistant.account_id)
end
def log_tool_usage(action, details = {})
Rails.logger.info do
"#{self.class.name}: #{action} by user #{@user&.id} for assistant #{@assistant&.id} - #{details.inspect}"
end
end
end
@@ -0,0 +1,21 @@
class Captain::Tools::GetArticleTool < BaseAgentTool
description 'Get details of an article including its content and metadata'
param :article_id, type: 'number', desc: 'The ID of the article to retrieve'
def perform(_tool_context, article_id:)
log_tool_usage('get_article', { article_id: article_id })
return 'Missing required parameters' if article_id.blank?
article = account_scoped(Article).find_by(id: article_id)
return 'Article not found' if article.nil?
article.to_llm_text
end
protected
def required_permission
'knowledge_base_manage'
end
end
@@ -0,0 +1,21 @@
class Captain::Tools::GetContactTool < BaseAgentTool
description 'Get details of a contact including their profile information'
param :contact_id, type: 'number', desc: 'The ID of the contact to retrieve'
def perform(_tool_context, contact_id:)
log_tool_usage('get_contact', { contact_id: contact_id })
return 'Missing required parameters' if contact_id.blank?
contact = account_scoped(Contact).find_by(id: contact_id)
return 'Contact not found' if contact.nil?
contact.to_llm_text
end
protected
def required_permission
'contact_manage'
end
end
@@ -0,0 +1,21 @@
class Captain::Tools::GetConversationTool < BaseAgentTool
description 'Get details of a conversation including messages and context'
param :conversation_id, type: 'string', desc: 'The display ID of the conversation to retrieve'
def perform(_tool_context, conversation_id:)
log_tool_usage('get_conversation', { conversation_id: conversation_id })
return 'Missing required parameters' if conversation_id.blank?
conversation = account_scoped(::Conversation).find_by(display_id: conversation_id)
return 'Conversation not found' if conversation.nil?
conversation.to_llm_text
end
def active?
user_has_permission('conversation_manage') ||
user_has_permission('conversation_unassigned_manage') ||
user_has_permission('conversation_participating_manage')
end
end
@@ -0,0 +1,40 @@
class Captain::Tools::SearchArticlesTool < BaseAgentTool
description 'Search articles based on parameters'
param :query, type: 'string', desc: 'Search articles by title or content (partial match)'
param :category_id, type: 'number', desc: 'Filter articles by category ID', required: false
param :status, type: 'string', desc: 'Filter articles by status (draft, published, archived)', required: false
def perform(_tool_context, query:, category_id: nil, status: nil)
log_tool_usage('search_articles', { query: query, category_id: category_id, status: status })
return 'Missing required parameters' if query.blank?
articles = fetch_articles(query, category_id, status)
return 'No articles found' unless articles.exists?
total_count = articles.count
articles = articles.limit(100)
<<~RESPONSE
#{total_count > 100 ? "Found #{total_count} articles (showing first 100)" : "Total number of articles: #{total_count}"}
#{articles.map(&:to_llm_text).join("\n---\n")}
RESPONSE
end
protected
def required_permission
'knowledge_base_manage'
end
private
def fetch_articles(query, category_id, status)
articles = account_scoped(Article)
articles = articles.where('title ILIKE :query OR content ILIKE :query', query: "%#{query}%") if query.present?
articles = articles.where(category_id: category_id) if category_id.present?
articles = articles.where(status: status) if status.present?
articles
end
end
@@ -0,0 +1,48 @@
class Captain::Tools::SearchContactsTool < BaseAgentTool
description 'Search contacts based on parameters'
param :query, type: 'string', desc: 'Search contacts by name, email, phone', required: false
param :inbox_id, type: 'number', desc: 'Filter contacts by inbox ID', required: false
param :labels, type: 'string', desc: 'Filter contacts by labels (comma-separated)', required: false
def perform(_tool_context, query: nil, inbox_id: nil, labels: nil)
log_tool_usage('search_contacts', { query: query, inbox_id: inbox_id, labels: labels })
contacts = get_contacts(query, inbox_id, labels)
return 'No contacts found' unless contacts.exists?
total_count = contacts.count
contacts = contacts.limit(100)
<<~RESPONSE
#{total_count > 100 ? "Found #{total_count} contacts (showing first 100)" : "Total number of contacts: #{total_count}"}
#{contacts.map(&:to_llm_text).join("\n---\n")}
RESPONSE
end
protected
def required_permission
'contact_manage'
end
private
def get_contacts(query, inbox_id, labels)
contacts = account_scoped(Contact)
if query.present?
contacts = contacts.where(
'name ILIKE :query OR email ILIKE :query OR phone_number ILIKE :query',
query: "%#{query}%"
)
end
contacts = contacts.where(inbox_id: inbox_id) if inbox_id.present?
if labels.present?
label_array = labels.split(',').map(&:strip)
contacts = contacts.tagged_with(label_array, any: true)
end
contacts
end
end
@@ -0,0 +1,51 @@
class Captain::Tools::SearchConversationsTool < BaseAgentTool
description 'Search conversations based on parameters'
param :contact_id, type: 'number', desc: 'Filter conversations by contact ID', required: false
param :status, type: 'string', desc: 'Filter conversations by status (open, resolved, pending, snoozed)', required: false
param :priority, type: 'string', desc: 'Filter conversations by priority (low, medium, high, urgent)', required: false
param :labels, type: 'string', desc: 'Filter conversations by labels (comma-separated)', required: false
def perform(_tool_context, contact_id: nil, status: nil, priority: nil, labels: nil)
log_tool_usage('search_conversations', { contact_id: contact_id, status: status, priority: priority, labels: labels })
conversations = get_conversations(status, contact_id, priority, labels)
return 'No conversations found' unless conversations.exists?
total_count = conversations.count
conversations = conversations.limit(100)
<<~RESPONSE
#{total_count > 100 ? "Found #{total_count} conversations (showing first 100)" : "Total number of conversations: #{total_count}"}
#{conversations.map { |conversation| conversation.to_llm_text(include_contact_details: true) }.join("\n---\n")}
RESPONSE
end
def active?
user_has_permission('conversation_manage') ||
user_has_permission('conversation_unassigned_manage') ||
user_has_permission('conversation_participating_manage')
end
private
def get_conversations(status, contact_id, priority, labels)
conversations = permissible_conversations
conversations = conversations.where(contact_id: contact_id) if contact_id.present?
conversations = conversations.where(status: status) if status.present?
conversations = conversations.where(priority: priority) if priority.present?
if labels.present?
label_array = labels.split(',').map(&:strip)
conversations = conversations.tagged_with(label_array, any: true)
end
conversations
end
def permissible_conversations
Conversations::PermissionFilterService.new(
account_scoped(::Conversation),
@user,
@assistant.account
).perform
end
end
@@ -0,0 +1,19 @@
class Captain::Tools::SearchDocumentationTool < BaseAgentTool
description 'Search documentation and help docs'
param :query, type: 'string', desc: 'Search query for documentation'
def perform(_tool_context, query:)
log_tool_usage('search_documentation', { query: query })
return 'Missing required parameters' if query.blank?
# Call the existing search documentation service
Captain::Tools::SearchDocumentationService.new(@assistant, user: @user).execute({
'query' => query
})
end
def active?
true # Documentation search is available to all users
end
end
@@ -0,0 +1,21 @@
class Captain::Tools::SearchLinearIssuesTool < BaseAgentTool
description 'Search Linear issues and development context'
param :query, type: 'string', desc: 'Search query for Linear issues'
def perform(_tool_context, query:)
log_tool_usage('search_linear_issues', { query: query })
return 'Missing required parameters' if query.blank?
# Call the existing search linear issues service
Captain::Tools::Copilot::SearchLinearIssuesService.new(@assistant, user: @user).execute({
'query' => query
})
end
protected
def required_permission
'conversation_manage' # Basic permission check for technical context
end
end