From ad41fd90f92ef83a48538d1ffc5bbf088f334aa5 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Mon, 19 May 2025 23:48:06 +0530
Subject: [PATCH 1/5] fix: Fix the translation issue on conversation filter
reload (#11513)
# Pull Request Template
## Description
This PR fixes the translation inconsistency in the
`` component, where dropdown options would
revert to English after page reload.
**Cause:**
The component used static arrays for chat status and sort options, with
translations initialized only once. After a reload, it showed system
language (English) until the user's locale was fully loaded.
**Solution:**
Replaced static arrays with computed properties to make translations
reactive. This ensures the options automatically update when the locale
changes.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Loom video
**Before**
https://www.loom.com/share/eeac97c59b21480b95ef74813d3d7fa9?sid=0481994a-8d35-4c44-87d0-c6c5a77a54fd
**After**
https://www.loom.com/share/c1bdfbdb19ca4e37bda373f0fe12527a?sid=cb5b1d19-272b-48cb-967c-9a82c2a2b028
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---
.../conversation/ConversationBasicFilter.vue | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue
index dc434f6c7..d699923f5 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBasicFilter.vue
@@ -38,7 +38,7 @@ const currentSortBy = computed(() => {
);
});
-const chatStatusOptions = [
+const chatStatusOptions = computed(() => [
{
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
value: 'open',
@@ -59,9 +59,9 @@ const chatStatusOptions = [
label: t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
value: 'all',
},
-];
+]);
-const chatSortOptions = [
+const chatSortOptions = computed(() => [
{
label: t('CHAT_LIST.SORT_ORDER_ITEMS.last_activity_at_asc.TEXT'),
value: 'last_activity_at_asc',
@@ -94,15 +94,18 @@ const chatSortOptions = [
label: t('CHAT_LIST.SORT_ORDER_ITEMS.waiting_since_desc.TEXT'),
value: 'waiting_since_desc',
},
-];
+]);
const activeChatStatusLabel = computed(
() =>
- chatStatusOptions.find(m => m.value === chatStatusFilter.value)?.label || ''
+ chatStatusOptions.value.find(m => m.value === chatStatusFilter.value)
+ ?.label || ''
);
const activeChatSortLabel = computed(
- () => chatSortOptions.find(m => m.value === chatSortFilter.value)?.label || ''
+ () =>
+ chatSortOptions.value.find(m => m.value === chatSortFilter.value)?.label ||
+ ''
);
const saveSelectedFilter = (type, value) => {
From d657f35a7680c24f8e9ab3329b9ddc85899f1fc3 Mon Sep 17 00:00:00 2001
From: Pranav
Date: Mon, 19 May 2025 15:26:38 -0700
Subject: [PATCH 2/5] feat: Introduce the concept of tool registry within
Captain (#11516)
This PR introduces the concept of a tool registry. The implementation is
straightforward: you can define a tool by creating a class with a
function name. The function name gets registered in the registry and can
be referenced during LLM calls. When the LLM invokes a tool using the
registered name, the registry locates and executes the appropriate tool.
If the LLM calls an unregistered tool, the registry returns an error
indicating that the tool is not defined.
---
enterprise/app/helpers/captain/chat_helper.rb | 67 ++++----------
.../services/captain/copilot/chat_service.rb | 7 ++
.../captain/llm/assistant_chat_service.rb | 6 ++
.../services/captain/tool_registry_service.rb | 27 ++++++
.../services/captain/tools/base_service.rb | 34 ++++++++
.../tools/search_documentation_service.rb | 49 +++++++++++
.../captain/tool_registry_service_spec.rb | 87 +++++++++++++++++++
.../search_documentation_service_spec.rb | 77 ++++++++++++++++
8 files changed, 303 insertions(+), 51 deletions(-)
create mode 100644 enterprise/app/services/captain/tool_registry_service.rb
create mode 100644 enterprise/app/services/captain/tools/base_service.rb
create mode 100644 enterprise/app/services/captain/tools/search_documentation_service.rb
create mode 100644 spec/enterprise/services/captain/tool_registry_service_spec.rb
create mode 100644 spec/enterprise/services/captain/tools/search_documentation_service_spec.rb
diff --git a/enterprise/app/helpers/captain/chat_helper.rb b/enterprise/app/helpers/captain/chat_helper.rb
index 146e8f813..bbe3851f0 100644
--- a/enterprise/app/helpers/captain/chat_helper.rb
+++ b/enterprise/app/helpers/captain/chat_helper.rb
@@ -1,24 +1,4 @@
module Captain::ChatHelper
- def search_documentation_tool
- {
- type: 'function',
- function: {
- name: 'search_documentation',
- description: "Use this function to get documentation on functionalities you don't know about.",
- parameters: {
- type: 'object',
- properties: {
- search_query: {
- type: 'string',
- description: 'The search query to look up in the documentation.'
- }
- },
- required: ['search_query']
- }
- }
- }
- end
-
def request_chat_completion
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{@messages}" }
@@ -26,13 +6,12 @@ module Captain::ChatHelper
parameters: {
model: @model,
messages: @messages,
- tools: [search_documentation_tool],
+ tools: @tool_registry&.registered_tools || [],
response_format: { type: 'json_object' }
}
)
handle_response(response)
- @response
end
def handle_response(response)
@@ -41,7 +20,7 @@ module Captain::ChatHelper
if message['tool_calls']
process_tool_calls(message['tool_calls'])
else
- @response = JSON.parse(message['content'].strip)
+ JSON.parse(message['content'].strip)
end
end
@@ -54,38 +33,20 @@ module Captain::ChatHelper
end
def process_tool_call(tool_call)
+ arguments = JSON.parse(tool_call['function']['arguments'])
+ function_name = tool_call['function']['name']
tool_call_id = tool_call['id']
- if tool_call['function']['name'] == 'search_documentation'
- query = JSON.parse(tool_call['function']['arguments'])['search_query']
- sections = fetch_documentation(query)
- append_tool_response(sections, tool_call_id)
+ if @tool_registry.respond_to?(function_name)
+ execute_tool(function_name, arguments, tool_call_id)
else
- append_tool_response('', tool_call_id)
+ process_invalid_tool_call(tool_call_id)
end
end
- def fetch_documentation(query)
- Rails.logger.debug { "[CAPTAIN][DocumentationSearch] #{query}" }
- @assistant
- .responses
- .approved
- .search(query)
- .map { |response| format_response(response) }.join
- end
-
- def format_response(response)
- formatted_response = "
- Question: #{response.question}
- Answer: #{response.answer}
- "
- if response.documentable.present? && response.documentable.try(:external_link)
- formatted_response += "
- Source: #{response.documentable.external_link}
- "
- end
-
- formatted_response
+ def execute_tool(function_name, arguments, tool_call_id)
+ result = @tool_registry.send(function_name, arguments)
+ append_tool_response(result, tool_call_id)
end
def append_tool_calls(tool_calls)
@@ -95,11 +56,15 @@ module Captain::ChatHelper
}
end
- def append_tool_response(sections, tool_call_id)
+ def process_invalid_tool_call(tool_call_id)
+ append_tool_response('Tool not available', tool_call_id)
+ end
+
+ def append_tool_response(content, tool_call_id)
@messages << {
role: 'tool',
tool_call_id: tool_call_id,
- content: "Found the following FAQs in the documentation:\n #{sections}"
+ content: content
}
end
end
diff --git a/enterprise/app/services/captain/copilot/chat_service.rb b/enterprise/app/services/captain/copilot/chat_service.rb
index 6fd3c4e68..202348825 100644
--- a/enterprise/app/services/captain/copilot/chat_service.rb
+++ b/enterprise/app/services/captain/copilot/chat_service.rb
@@ -10,6 +10,8 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService
@conversation_history = config[:conversation_history]
@previous_messages = config[:previous_messages] || []
@language = config[:language] || 'english'
+
+ register_tools
@messages = [system_message, conversation_history_context] + @previous_messages
@response = ''
end
@@ -25,6 +27,11 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService
private
+ def register_tools
+ @tool_registry = Captain::ToolRegistryService.new(@assistant)
+ @tool_registry.register_tool(Captain::Tools::SearchDocumentationService)
+ end
+
def system_message
{
role: 'system',
diff --git a/enterprise/app/services/captain/llm/assistant_chat_service.rb b/enterprise/app/services/captain/llm/assistant_chat_service.rb
index 50688f18b..8077b22c9 100644
--- a/enterprise/app/services/captain/llm/assistant_chat_service.rb
+++ b/enterprise/app/services/captain/llm/assistant_chat_service.rb
@@ -9,6 +9,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
@assistant = assistant
@messages = [system_message]
@response = ''
+ register_tools
end
def generate_response(input, previous_messages = [], role = 'user')
@@ -19,6 +20,11 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
private
+ def register_tools
+ @tool_registry = Captain::ToolRegistryService.new(@assistant)
+ @tool_registry.register_tool(Captain::Tools::SearchDocumentationService)
+ end
+
def system_message
{
role: 'system',
diff --git a/enterprise/app/services/captain/tool_registry_service.rb b/enterprise/app/services/captain/tool_registry_service.rb
new file mode 100644
index 000000000..088d89483
--- /dev/null
+++ b/enterprise/app/services/captain/tool_registry_service.rb
@@ -0,0 +1,27 @@
+class Captain::ToolRegistryService
+ attr_reader :registered_tools, :tools
+
+ def initialize(assistant)
+ @assistant = assistant
+ @registered_tools = []
+ @tools = {}
+ end
+
+ def register_tool(tool_class)
+ tool = tool_class.new(@assistant)
+ @tools[tool.name] = tool
+ @registered_tools << tool.to_registry_format
+ end
+
+ def method_missing(method_name, *arguments)
+ if @tools.key?(method_name.to_s)
+ @tools[method_name.to_s].execute(*arguments)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ @tools.key?(method_name.to_s) || super
+ end
+end
diff --git a/enterprise/app/services/captain/tools/base_service.rb b/enterprise/app/services/captain/tools/base_service.rb
new file mode 100644
index 000000000..10e44d2f4
--- /dev/null
+++ b/enterprise/app/services/captain/tools/base_service.rb
@@ -0,0 +1,34 @@
+class Captain::Tools::BaseService
+ attr_accessor :assistant
+
+ def initialize(assistant)
+ @assistant = assistant
+ end
+
+ def name
+ raise NotImplementedError, "#{self.class} must implement name"
+ end
+
+ def description
+ raise NotImplementedError, "#{self.class} must implement description"
+ end
+
+ def parameters
+ raise NotImplementedError, "#{self.class} must implement parameters"
+ end
+
+ def execute(arguments)
+ raise NotImplementedError, "#{self.class} must implement execute"
+ end
+
+ def to_registry_format
+ {
+ type: 'function',
+ function: {
+ name: name,
+ description: description,
+ parameters: parameters
+ }
+ }
+ end
+end
diff --git a/enterprise/app/services/captain/tools/search_documentation_service.rb b/enterprise/app/services/captain/tools/search_documentation_service.rb
new file mode 100644
index 000000000..672baf24a
--- /dev/null
+++ b/enterprise/app/services/captain/tools/search_documentation_service.rb
@@ -0,0 +1,49 @@
+class Captain::Tools::SearchDocumentationService < Captain::Tools::BaseService
+ def name
+ 'search_documentation'
+ end
+
+ def description
+ 'Search and retrieve documentation from knowledge base'
+ end
+
+ def parameters
+ {
+ type: 'object',
+ properties: {
+ search_query: {
+ type: 'string',
+ description: 'The search query to look up in the documentation.'
+ }
+ },
+ required: ['search_query']
+ }
+ end
+
+ def execute(arguments)
+ query = arguments['search_query']
+ Rails.logger.info { "#{self.class.name}: #{query}" }
+
+ responses = assistant.responses.approved.search(query)
+
+ return 'No FAQs found for the given query' if responses.empty?
+
+ responses.map { |response| format_response(response) }.join
+ end
+
+ private
+
+ def format_response(response)
+ formatted_response = "
+ Question: #{response.question}
+ Answer: #{response.answer}
+ "
+ if response.documentable.present? && response.documentable.try(:external_link)
+ formatted_response += "
+ Source: #{response.documentable.external_link}
+ "
+ end
+
+ formatted_response
+ end
+end
diff --git a/spec/enterprise/services/captain/tool_registry_service_spec.rb b/spec/enterprise/services/captain/tool_registry_service_spec.rb
new file mode 100644
index 000000000..e355dea0a
--- /dev/null
+++ b/spec/enterprise/services/captain/tool_registry_service_spec.rb
@@ -0,0 +1,87 @@
+require 'rails_helper'
+
+# Test tool implementation
+class TestTool < Captain::Tools::BaseService
+ def name
+ 'test_tool'
+ end
+
+ def description
+ 'A test tool for specs'
+ end
+
+ def parameters
+ {
+ type: 'object',
+ properties: {
+ test_param: {
+ type: 'string'
+ }
+ }
+ }
+ end
+
+ def execute(*args)
+ args
+ end
+end
+
+RSpec.describe Captain::ToolRegistryService do
+ let(:assistant) { create(:captain_assistant) }
+ let(:service) { described_class.new(assistant) }
+
+ describe '#initialize' do
+ it 'initializes with empty tools and registered_tools' do
+ expect(service.tools).to be_empty
+ expect(service.registered_tools).to be_empty
+ end
+ end
+
+ describe '#register_tool' do
+ let(:tool_class) { TestTool }
+
+ it 'registers a new tool' do
+ service.register_tool(tool_class)
+
+ expect(service.tools['test_tool']).to be_a(TestTool)
+ expect(service.registered_tools).to include(
+ {
+ type: 'function',
+ function: {
+ name: 'test_tool',
+ description: 'A test tool for specs',
+ parameters: {
+ type: 'object',
+ properties: {
+ test_param: {
+ type: 'string'
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+ end
+
+ describe 'method_missing' do
+ let(:tool_class) { TestTool }
+
+ before do
+ service.register_tool(tool_class)
+ end
+
+ context 'when method corresponds to a registered tool' do
+ it 'executes the tool with given arguments' do
+ result = service.test_tool(test_param: 'arg1')
+ expect(result).to eq([{ test_param: 'arg1' }])
+ end
+ end
+
+ context 'when method does not correspond to a registered tool' do
+ it 'raises NoMethodError' do
+ expect { service.unknown_tool }.to raise_error(NoMethodError)
+ end
+ end
+ end
+end
diff --git a/spec/enterprise/services/captain/tools/search_documentation_service_spec.rb b/spec/enterprise/services/captain/tools/search_documentation_service_spec.rb
new file mode 100644
index 000000000..9f5586e6b
--- /dev/null
+++ b/spec/enterprise/services/captain/tools/search_documentation_service_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+RSpec.describe Captain::Tools::SearchDocumentationService do
+ let(:assistant) { create(:captain_assistant) }
+ let(:service) { described_class.new(assistant) }
+ let(:question) { 'How to create a new account?' }
+ let(:answer) { 'You can create a new account by clicking on the Sign Up button.' }
+ let(:external_link) { 'https://example.com/docs/create-account' }
+
+ describe '#name' do
+ it 'returns the correct service name' do
+ expect(service.name).to eq('search_documentation')
+ end
+ end
+
+ describe '#description' do
+ it 'returns the service description' do
+ expect(service.description).to eq('Search and retrieve documentation from knowledge base')
+ end
+ end
+
+ describe '#parameters' do
+ it 'returns the required parameters schema' do
+ expected_schema = {
+ type: 'object',
+ properties: {
+ search_query: {
+ type: 'string',
+ description: 'The search query to look up in the documentation.'
+ }
+ },
+ required: ['search_query']
+ }
+
+ expect(service.parameters).to eq(expected_schema)
+ end
+ end
+
+ describe '#execute' do
+ let!(:response) do
+ create(
+ :captain_assistant_response,
+ assistant: assistant,
+ question: question,
+ answer: answer,
+ status: 'approved'
+ )
+ end
+
+ let(:documentable) { create(:captain_document, external_link: external_link) }
+
+ context 'when matching responses exist' do
+ before do
+ response.update(documentable: documentable)
+ allow(Captain::AssistantResponse).to receive(:search).with(question).and_return([response])
+ end
+
+ it 'returns formatted responses for the search query' do
+ result = service.execute({ 'search_query' => question })
+
+ expect(result).to include(question)
+ expect(result).to include(answer)
+ expect(result).to include(external_link)
+ end
+ end
+
+ context 'when no matching responses exist' do
+ before do
+ allow(Captain::AssistantResponse).to receive(:search).with(question).and_return([])
+ end
+
+ it 'returns an empty string' do
+ expect(service.execute({ 'search_query' => question })).to eq('No FAQs found for the given query')
+ end
+ end
+ end
+end
From 16371498ba01ee4c4265bf85d6fc0a8d706a1bee Mon Sep 17 00:00:00 2001
From: Pranav
Date: Mon, 19 May 2025 18:00:38 -0700
Subject: [PATCH 3/5] feat: Add a tool to search Linear Issues in copilot
(#11518)
This PR adds a tool to search Linear issues. If the integration is
enabled for the account, the tool will return results as expected. Also
introduces support for an `active?` method, which allows third-party
Copilot tools to be conditionally enabled based on the status of the
integration on the account.
---
.../services/captain/tool_registry_service.rb | 2 +
.../services/captain/tools/base_service.rb | 4 +
.../copilot/search_linear_issues_service.rb | 77 +++++++++++
.../captain/tool_registry_service_spec.rb | 59 ++++++---
.../search_linear_issues_service_spec.rb | 125 ++++++++++++++++++
5 files changed, 250 insertions(+), 17 deletions(-)
create mode 100644 enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb
create mode 100644 spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb
diff --git a/enterprise/app/services/captain/tool_registry_service.rb b/enterprise/app/services/captain/tool_registry_service.rb
index 088d89483..f2c234060 100644
--- a/enterprise/app/services/captain/tool_registry_service.rb
+++ b/enterprise/app/services/captain/tool_registry_service.rb
@@ -9,6 +9,8 @@ class Captain::ToolRegistryService
def register_tool(tool_class)
tool = tool_class.new(@assistant)
+ return unless tool.active?
+
@tools[tool.name] = tool
@registered_tools << tool.to_registry_format
end
diff --git a/enterprise/app/services/captain/tools/base_service.rb b/enterprise/app/services/captain/tools/base_service.rb
index 10e44d2f4..7fadc806b 100644
--- a/enterprise/app/services/captain/tools/base_service.rb
+++ b/enterprise/app/services/captain/tools/base_service.rb
@@ -31,4 +31,8 @@ class Captain::Tools::BaseService
}
}
end
+
+ def active?
+ true
+ end
end
diff --git a/enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb b/enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb
new file mode 100644
index 000000000..599fc7de9
--- /dev/null
+++ b/enterprise/app/services/captain/tools/copilot/search_linear_issues_service.rb
@@ -0,0 +1,77 @@
+class Captain::Tools::Copilot::SearchLinearIssuesService < Captain::Tools::BaseService
+ def name
+ 'search_linear_issues'
+ end
+
+ def description
+ 'Search Linear issues based on a search term'
+ end
+
+ def parameters
+ {
+ type: 'object',
+ properties: {
+ term: {
+ type: 'string',
+ description: 'The search term to find Linear issues'
+ }
+ },
+ required: %w[term]
+ }
+ end
+
+ def execute(arguments)
+ return 'Linear integration is not enabled' unless active?
+
+ term = arguments['term']
+
+ Rails.logger.info "#{self.class.name}: Service called with the search term #{term}"
+
+ return 'Missing required parameters' if term.blank?
+
+ linear_service = Integrations::Linear::ProcessorService.new(account: @assistant.account)
+ result = linear_service.search_issue(term)
+
+ return result[:error] if result[:error]
+
+ issues = result[:data]
+ return 'No issues found, I should try another similar search term' if issues.blank?
+
+ total_count = issues.length
+
+ <<~RESPONSE
+ Total number of issues: #{total_count}
+ #{issues.map { |issue| format_issue(issue) }.join("\n---\n")}
+ RESPONSE
+ end
+
+ def active?
+ @assistant.account.hooks.find_by(app_id: 'linear').present?
+ end
+
+ private
+
+ def format_issue(issue)
+ <<~ISSUE
+ Title: #{issue['title']}
+ ID: #{issue['id']}
+ State: #{issue['state']['name']}
+ Priority: #{format_priority(issue['priority'])}
+ #{issue['assignee'] ? "Assignee: #{issue['assignee']['name']}" : 'Assignee: Unassigned'}
+ #{issue['description'].present? ? "\nDescription: #{issue['description']}" : ''}
+ ISSUE
+ end
+
+ def format_priority(priority)
+ return 'No priority' if priority.nil?
+
+ case priority
+ when 0 then 'No priority'
+ when 1 then 'Urgent'
+ when 2 then 'High'
+ when 3 then 'Medium'
+ when 4 then 'Low'
+ else 'Unknown'
+ end
+ end
+end
diff --git a/spec/enterprise/services/captain/tool_registry_service_spec.rb b/spec/enterprise/services/captain/tool_registry_service_spec.rb
index e355dea0a..beb4a6a63 100644
--- a/spec/enterprise/services/captain/tool_registry_service_spec.rb
+++ b/spec/enterprise/services/captain/tool_registry_service_spec.rb
@@ -2,6 +2,13 @@ require 'rails_helper'
# Test tool implementation
class TestTool < Captain::Tools::BaseService
+ attr_accessor :tool_active
+
+ def initialize(*args)
+ super
+ @tool_active = true
+ end
+
def name
'test_tool'
end
@@ -24,6 +31,10 @@ class TestTool < Captain::Tools::BaseService
def execute(*args)
args
end
+
+ def active?
+ @tool_active
+ end
end
RSpec.describe Captain::ToolRegistryService do
@@ -40,27 +51,41 @@ RSpec.describe Captain::ToolRegistryService do
describe '#register_tool' do
let(:tool_class) { TestTool }
- it 'registers a new tool' do
- service.register_tool(tool_class)
-
- expect(service.tools['test_tool']).to be_a(TestTool)
- expect(service.registered_tools).to include(
- {
- type: 'function',
- function: {
- name: 'test_tool',
- description: 'A test tool for specs',
- parameters: {
- type: 'object',
- properties: {
- test_param: {
- type: 'string'
+ context 'when tool is active' do
+ it 'registers a new tool' do
+ service.register_tool(tool_class)
+ expect(service.tools['test_tool']).to be_a(TestTool)
+ expect(service.registered_tools).to include(
+ {
+ type: 'function',
+ function: {
+ name: 'test_tool',
+ description: 'A test tool for specs',
+ parameters: {
+ type: 'object',
+ properties: {
+ test_param: {
+ type: 'string'
+ }
}
}
}
}
- }
- )
+ )
+ end
+ end
+
+ context 'when tool is inactive' do
+ it 'does not register the tool' do
+ tool = tool_class.new(assistant)
+ tool.tool_active = false
+ allow(tool_class).to receive(:new).and_return(tool)
+
+ service.register_tool(tool_class)
+
+ expect(service.tools['test_tool']).to be_nil
+ expect(service.registered_tools).to be_empty
+ end
end
end
diff --git a/spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb b/spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb
new file mode 100644
index 000000000..f4a5225b1
--- /dev/null
+++ b/spec/enterprise/services/captain/tools/copilot/search_linear_issues_service_spec.rb
@@ -0,0 +1,125 @@
+require 'rails_helper'
+
+RSpec.describe Captain::Tools::Copilot::SearchLinearIssuesService do
+ let(:account) { create(:account) }
+ let(:assistant) { create(:captain_assistant, account: account) }
+ let(:service) { described_class.new(assistant) }
+
+ describe '#name' do
+ it 'returns the correct service name' do
+ expect(service.name).to eq('search_linear_issues')
+ end
+ end
+
+ describe '#description' do
+ it 'returns the service description' do
+ expect(service.description).to eq('Search Linear issues based on a search term')
+ end
+ end
+
+ describe '#parameters' do
+ it 'returns the expected parameter schema' do
+ expect(service.parameters).to eq(
+ {
+ type: 'object',
+ properties: {
+ term: {
+ type: 'string',
+ description: 'The search term to find Linear issues'
+ }
+ },
+ required: %w[term]
+ }
+ )
+ end
+ end
+
+ describe '#active?' do
+ context 'when Linear integration is enabled' do
+ before do
+ create(:integrations_hook, :linear, account: account)
+ end
+
+ it 'returns true' do
+ expect(service.active?).to be true
+ end
+ end
+
+ context 'when Linear integration is not enabled' do
+ it 'returns false' do
+ expect(service.active?).to be false
+ end
+ end
+ end
+
+ describe '#execute' do
+ context 'when Linear integration is not enabled' do
+ it 'returns error message' do
+ expect(service.execute({ 'term' => 'test' })).to eq('Linear integration is not enabled')
+ end
+ end
+
+ context 'when Linear integration is enabled' do
+ let(:linear_service) { instance_double(Integrations::Linear::ProcessorService) }
+
+ before do
+ create(:integrations_hook, :linear, account: account)
+ allow(Integrations::Linear::ProcessorService).to receive(:new).and_return(linear_service)
+ end
+
+ context 'when term is blank' do
+ it 'returns error message' do
+ expect(service.execute({ 'term' => '' })).to eq('Missing required parameters')
+ end
+ end
+
+ context 'when search returns error' do
+ before do
+ allow(linear_service).to receive(:search_issue).and_return({ error: 'API Error' })
+ end
+
+ it 'returns the error message' do
+ expect(service.execute({ 'term' => 'test' })).to eq('API Error')
+ end
+ end
+
+ context 'when search returns no issues' do
+ before do
+ allow(linear_service).to receive(:search_issue).and_return({ data: [] })
+ end
+
+ it 'returns no issues found message' do
+ expect(service.execute({ 'term' => 'test' })).to eq('No issues found, I should try another similar search term')
+ end
+ end
+
+ context 'when search returns issues' do
+ let(:issues) do
+ [{
+ 'title' => 'Test Issue',
+ 'id' => 'TEST-123',
+ 'state' => { 'name' => 'In Progress' },
+ 'priority' => 4,
+ 'assignee' => { 'name' => 'John Doe' },
+ 'description' => 'Test description'
+ }]
+ end
+
+ before do
+ allow(linear_service).to receive(:search_issue).and_return({ data: issues })
+ end
+
+ it 'returns formatted issues' do
+ result = service.execute({ 'term' => 'test' })
+ expect(result).to include('Total number of issues: 1')
+ expect(result).to include('Title: Test Issue')
+ expect(result).to include('ID: TEST-123')
+ expect(result).to include('State: In Progress')
+ expect(result).to include('Priority: Low')
+ expect(result).to include('Assignee: John Doe')
+ expect(result).to include('Description: Test description')
+ end
+ end
+ end
+ end
+end
From 98a50d12e047a49c863d3287c0946b4cacac14f1 Mon Sep 17 00:00:00 2001
From: Sojan
Date: Tue, 20 May 2025 00:11:50 -0700
Subject: [PATCH 4/5] Bump version to 4.2.0
---
config/app.yml | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/config/app.yml b/config/app.yml
index 9afc3f7bc..36bbdbcf1 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '4.1.0'
+ version: '4.2.0'
development:
<<: *shared
diff --git a/package.json b/package.json
index 88c775f28..90b7708f6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "4.1.0",
+ "version": "4.2.0",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
From ccb3672ff88c85308750acead257abdae8702aa1 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Tue, 20 May 2025 16:04:56 +0530
Subject: [PATCH 5/5] fix: Status not updating when creating a Linear issue
(#11523)
---
.../api/v1/accounts/integrations/linear_controller.rb | 3 ++-
lib/linear.rb | 3 ++-
.../api/v1/accounts/integrations/linear_controller_spec.rb | 1 +
spec/lib/integrations/linear/processor_service_spec.rb | 1 +
4 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb
index 4e5348e88..c66f06909 100644
--- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb
@@ -94,7 +94,8 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
end
def permitted_params
- params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
+ params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, :state_id,
+ label_ids: [])
end
def fetch_hook
diff --git a/lib/linear.rb b/lib/linear.rb
index 9a998c34a..8bf967fc3 100644
--- a/lib/linear.rb
+++ b/lib/linear.rb
@@ -57,7 +57,8 @@ class Linear
assigneeId: params[:assignee_id],
priority: params[:priority],
labelIds: params[:label_ids],
- projectId: params[:project_id]
+ projectId: params[:project_id],
+ stateId: params[:state_id]
}.compact
mutation = Linear::Mutations.issue_create(variables)
response = post({ query: mutation })
diff --git a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb
index b1341e65e..0f27e2bd2 100644
--- a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb
@@ -100,6 +100,7 @@ RSpec.describe 'Linear Integration API', type: :request do
description: 'This is a sample issue.',
assignee_id: 'user1',
priority: 'high',
+ state_id: 'state1',
label_ids: ['label1']
}
end
diff --git a/spec/lib/integrations/linear/processor_service_spec.rb b/spec/lib/integrations/linear/processor_service_spec.rb
index 07cf27654..807e93c71 100644
--- a/spec/lib/integrations/linear/processor_service_spec.rb
+++ b/spec/lib/integrations/linear/processor_service_spec.rb
@@ -76,6 +76,7 @@ describe Integrations::Linear::ProcessorService do
description: 'Issue description',
assignee_id: 'user1',
priority: 2,
+ state_id: 'state1',
label_ids: %w[bug]
}
end