From 3171312cdd009fecd1ce93067583899a185e5832 Mon Sep 17 00:00:00 2001 From: aakashb95 Date: Fri, 10 Jul 2026 23:35:50 +0530 Subject: [PATCH] feat(articles): use Responses API for search terms --- .../app/models/enterprise/concerns/article.rb | 50 ++++++++++++++---- spec/models/article_spec.rb | 52 +++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/enterprise/app/models/enterprise/concerns/article.rb b/enterprise/app/models/enterprise/concerns/article.rb index 6be262fef..00ff4a01b 100644 --- a/enterprise/app/models/enterprise/concerns/article.rb +++ b/enterprise/app/models/enterprise/concerns/article.rb @@ -1,6 +1,23 @@ module Enterprise::Concerns::Article extend ActiveSupport::Concern + SEARCH_TERMS_FEATURE = 'help_center_article_generation' + SEARCH_TERMS_SCHEMA = { + name: 'article_search_terms', + schema: { + type: 'object', + properties: { + search_terms: { + type: 'array', + items: { type: 'string' } + } + }, + required: %w[search_terms], + additionalProperties: false + }, + strict: true + }.freeze + included do after_save :add_article_embedding, if: -> { saved_change_to_title? || saved_change_to_description? || saved_change_to_content? } @@ -67,23 +84,38 @@ module Enterprise::Concerns::Article { role: 'system', content: article_to_search_terms_prompt }, { role: 'user', content: "title: #{title} \n description: #{description} \n content: #{content}" } ] - headers = { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{openai_api_key}" } - body = { model: 'gpt-4o', messages: messages, response_format: { type: 'json_object' } }.to_json - Rails.logger.info "Requesting Chat GPT with body: #{body}" - response = HTTParty.post(openai_api_url, headers: headers, body: body) - Rails.logger.info "Chat GPT response: #{response.body}" - JSON.parse(response.parsed_response['choices'][0]['message']['content'])['search_terms'] + + response = responses_client.create( + model: search_terms_route[:model], + messages: messages, + schema: SEARCH_TERMS_SCHEMA, + reasoning_effort: search_terms_route[:reasoning_effort], + metadata: { + account_id: account_id, + article_id: id, + feature: 'article_search_terms' + } + ) + + JSON.parse(response[:message])['search_terms'] end private + def search_terms_route + @search_terms_route ||= Llm::FeatureRouter.resolve(feature: SEARCH_TERMS_FEATURE, account: account) + end + + def responses_client + @responses_client ||= Llm::ResponsesClient.new(api_key: openai_api_key, api_base: openai_api_base) + end + def openai_api_key InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value.presence || raise(I18n.t('captain.api_key_missing')) end - def openai_api_url + def openai_api_base endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/' - endpoint = endpoint.chomp('/') - "#{endpoint}/v1/chat/completions" + endpoint.chomp('/') end end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index cdad2d9f4..f47be701a 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -208,6 +208,58 @@ RSpec.describe Article do end end + describe '#generate_article_search_terms' do + let(:responses_client) { instance_double(Llm::ResponsesClient) } + let(:article) do + create( + :article, + account: account, + category: category_1, + content: 'How to configure billing invoices and payment reminders', + description: 'Billing setup guide', + portal: portal_1, + author: user, + title: 'Configure billing' + ) + end + + before do + InstallationConfig.find_or_initialize_by(name: 'CAPTAIN_OPEN_AI_API_KEY').tap do |config| + config.value = 'sk-test' + config.save! + end + InstallationConfig.find_or_initialize_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT').tap do |config| + config.value = 'https://api.openai.test/v1' + config.save! + end + allow(Llm::ResponsesClient).to receive(:new).and_return(responses_client) + allow(responses_client).to receive(:create).and_return( + { message: { search_terms: ['billing setup', 'invoice reminders'] }.to_json } + ) + end + + it 'uses Responses API with the help center article model route' do + expect(article.generate_article_search_terms).to eq(['billing setup', 'invoice reminders']) + + expect(Llm::ResponsesClient).to have_received(:new).with( + api_key: 'sk-test', + api_base: 'https://api.openai.test/v1' + ) + expect(responses_client).to have_received(:create).with( + hash_including( + model: Llm::Models.default_model_for('help_center_article_generation'), + reasoning_effort: Llm::Models.reasoning_effort_for('help_center_article_generation'), + schema: Enterprise::Concerns::Article::SEARCH_TERMS_SCHEMA, + metadata: hash_including( + account_id: account.id, + article_id: article.id, + feature: 'article_search_terms' + ) + ) + ) + end + end + describe '.update_positions' do let!(:article_a) { create(:article, portal: portal_1, category: category_1, author: user, position: 10) } let!(:article_b) { create(:article, portal: portal_1, category: category_1, author: user, position: 11) }