mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	chore: Update LLM formatter classes to include additional details (#11491)
This PR introduces support for optionally exposing more data during LLM function calls. This will be useful as we expand Copilot’s capabilities. Changes included: - Add support for ArticleLlmFormatter - Add missing specs for ContactLLMFormatter and ArticleLLMFormatter - Add additional spec for ConversationLLMFormatter based on config
This commit is contained in:
		| @@ -33,6 +33,7 @@ | |||||||
| # | # | ||||||
| class Article < ApplicationRecord | class Article < ApplicationRecord | ||||||
|   include PgSearch::Model |   include PgSearch::Model | ||||||
|  |   include LlmFormattable | ||||||
|  |  | ||||||
|   has_many :associated_articles, |   has_many :associated_articles, | ||||||
|            class_name: :Article, |            class_name: :Article, | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| module LlmFormattable | module LlmFormattable | ||||||
|   extend ActiveSupport::Concern |   extend ActiveSupport::Concern | ||||||
|  |  | ||||||
|   def to_llm_text |   def to_llm_text(config = {}) | ||||||
|     LlmFormatter::LlmTextFormatterService.new(self).format |     LlmFormatter::LlmTextFormatterService.new(self).format(config) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								app/services/llm_formatter/article_llm_formatter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/services/llm_formatter/article_llm_formatter.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | class LlmFormatter::ArticleLlmFormatter | ||||||
|  |   attr_reader :article | ||||||
|  |  | ||||||
|  |   def initialize(article) | ||||||
|  |     @article = article | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def format(*) | ||||||
|  |     <<~TEXT | ||||||
|  |       Title: #{article.title} | ||||||
|  |       ID: #{article.id} | ||||||
|  |       Status: #{article.status} | ||||||
|  |       Category: #{article.category&.name || 'Uncategorized'} | ||||||
|  |       Author: #{article.author&.name || 'Unknown'} | ||||||
|  |       Views: #{article.views} | ||||||
|  |       Created At: #{article.created_at} | ||||||
|  |       Updated At: #{article.updated_at} | ||||||
|  |       Content: | ||||||
|  |       #{article.content} | ||||||
|  |     TEXT | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter | class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter | ||||||
|   def format |   def format(*) | ||||||
|     sections = [] |     sections = [] | ||||||
|     sections << "Contact ID: ##{@record.id}" |     sections << "Contact ID: ##{@record.id}" | ||||||
|     sections << 'Contact Attributes:' |     sections << 'Contact Attributes:' | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter | class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter | ||||||
|   def format |   def format(config = {}) | ||||||
|     sections = [] |     sections = [] | ||||||
|     sections << "Conversation ID: ##{@record.display_id}" |     sections << "Conversation ID: ##{@record.display_id}" | ||||||
|     sections << "Channel: #{@record.inbox.channel.name}" |     sections << "Channel: #{@record.inbox.channel.name}" | ||||||
| @@ -10,6 +10,7 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter | |||||||
|                   'No messages in this conversation' |                   'No messages in this conversation' | ||||||
|                 end |                 end | ||||||
|  |  | ||||||
|  |     sections << "Contact Details: #{@record.contact.to_llm_text}" if config[:include_contact_details] | ||||||
|     sections.join("\n") |     sections.join("\n") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter | |||||||
|     @record = record |     @record = record | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def format |   def format(*) | ||||||
|     # override this |     # override this | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ class LlmFormatter::LlmTextFormatterService | |||||||
|     @record = record |     @record = record | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def format |   def format(config = {}) | ||||||
|     formatter_class = find_formatter |     formatter_class = find_formatter | ||||||
|     formatter_class.new(@record).format |     formatter_class.new(@record).format(config) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|   | |||||||
| @@ -167,4 +167,26 @@ RSpec.describe Article do | |||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   describe '#to_llm_text' do | ||||||
|  |     it 'returns formatted article text' do | ||||||
|  |       category = create(:category, name: 'Test Category', slug: 'test_category', portal_id: portal_1.id) | ||||||
|  |       article = create(:article, title: 'Test Article', category_id: category.id, content: 'This is the content', portal_id: portal_1.id, | ||||||
|  |                                  author_id: user.id) | ||||||
|  |       expected_output = <<~TEXT | ||||||
|  |         Title: #{article.title} | ||||||
|  |         ID: #{article.id} | ||||||
|  |         Status: #{article.status} | ||||||
|  |         Category: #{category.name} | ||||||
|  |         Author: #{user.name} | ||||||
|  |         Views: #{article.views} | ||||||
|  |         Created At: #{article.created_at} | ||||||
|  |         Updated At: #{article.updated_at} | ||||||
|  |         Content: | ||||||
|  |         #{article.content} | ||||||
|  |       TEXT | ||||||
|  |  | ||||||
|  |       expect(article.to_llm_text).to eq(expected_output) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								spec/services/llm_formatter/article_llm_formatter_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								spec/services/llm_formatter/article_llm_formatter_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe LlmFormatter::ArticleLlmFormatter do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let(:portal) { create(:portal, account: account) } | ||||||
|  |   let(:category) { create(:category, slug: 'test_category', portal: portal, account: account) } | ||||||
|  |   let(:author) { create(:user, account: account) } | ||||||
|  |   let(:formatter) { described_class.new(article) } | ||||||
|  |  | ||||||
|  |   describe '#format' do | ||||||
|  |     context 'when article has all details' do | ||||||
|  |       let(:article) do | ||||||
|  |         create(:article, | ||||||
|  |                slug: 'test_article', | ||||||
|  |                portal: portal, category: category, author: author, views: 100, account: account) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'formats article details correctly' do | ||||||
|  |         expected_output = <<~TEXT | ||||||
|  |           Title: #{article.title} | ||||||
|  |           ID: #{article.id} | ||||||
|  |           Status: #{article.status} | ||||||
|  |           Category: #{category.name} | ||||||
|  |           Author: #{author.name} | ||||||
|  |           Views: #{article.views} | ||||||
|  |           Created At: #{article.created_at} | ||||||
|  |           Updated At: #{article.updated_at} | ||||||
|  |           Content: | ||||||
|  |           #{article.content} | ||||||
|  |         TEXT | ||||||
|  |  | ||||||
|  |         expect(formatter.format).to eq(expected_output) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when article has no category' do | ||||||
|  |       let(:article) { create(:article, portal: portal, category: nil, author: author, account: account) } | ||||||
|  |  | ||||||
|  |       it 'shows Uncategorized for category' do | ||||||
|  |         expect(formatter.format).to include('Category: Uncategorized') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										78
									
								
								spec/services/llm_formatter/contact_llm_formatter_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								spec/services/llm_formatter/contact_llm_formatter_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe LlmFormatter::ContactLlmFormatter do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let(:contact) { create(:contact, account: account, name: 'John Doe', email: 'john@example.com', phone_number: '+1234567890') } | ||||||
|  |   let(:formatter) { described_class.new(contact) } | ||||||
|  |  | ||||||
|  |   describe '#format' do | ||||||
|  |     context 'when contact has no notes' do | ||||||
|  |       it 'formats contact details correctly' do | ||||||
|  |         expected_output = [ | ||||||
|  |           "Contact ID: ##{contact.id}", | ||||||
|  |           'Contact Attributes:', | ||||||
|  |           'Name: John Doe', | ||||||
|  |           'Email: john@example.com', | ||||||
|  |           'Phone: +1234567890', | ||||||
|  |           'Location: ', | ||||||
|  |           'Country Code: ', | ||||||
|  |           'Contact Notes:', | ||||||
|  |           'No notes for this contact' | ||||||
|  |         ].join("\n") | ||||||
|  |  | ||||||
|  |         expect(formatter.format).to eq(expected_output) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when contact has notes' do | ||||||
|  |       before do | ||||||
|  |         create(:note, account: account, contact: contact, content: 'First interaction') | ||||||
|  |         create(:note, account: account, contact: contact, content: 'Follow up needed') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'includes notes in the output' do | ||||||
|  |         expected_output = [ | ||||||
|  |           "Contact ID: ##{contact.id}", | ||||||
|  |           'Contact Attributes:', | ||||||
|  |           'Name: John Doe', | ||||||
|  |           'Email: john@example.com', | ||||||
|  |           'Phone: +1234567890', | ||||||
|  |           'Location: ', | ||||||
|  |           'Country Code: ', | ||||||
|  |           'Contact Notes:', | ||||||
|  |           ' - First interaction', | ||||||
|  |           ' - Follow up needed' | ||||||
|  |         ].join("\n") | ||||||
|  |  | ||||||
|  |         expect(formatter.format).to eq(expected_output) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when contact has custom attributes' do | ||||||
|  |       let!(:custom_attribute) do | ||||||
|  |         create(:custom_attribute_definition, account: account, attribute_model: 'contact_attribute', attribute_display_name: 'Company') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       before do | ||||||
|  |         contact.update(custom_attributes: { custom_attribute.attribute_key => 'Acme Inc' }) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'includes custom attributes in the output' do | ||||||
|  |         expected_output = [ | ||||||
|  |           "Contact ID: ##{contact.id}", | ||||||
|  |           'Contact Attributes:', | ||||||
|  |           'Name: John Doe', | ||||||
|  |           'Email: john@example.com', | ||||||
|  |           'Phone: +1234567890', | ||||||
|  |           'Location: ', | ||||||
|  |           'Country Code: ', | ||||||
|  |           'Company: Acme Inc', | ||||||
|  |           'Contact Notes:', | ||||||
|  |           'No notes for this contact' | ||||||
|  |         ].join("\n") | ||||||
|  |  | ||||||
|  |         expect(formatter.format).to eq(expected_output) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -47,5 +47,19 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do | |||||||
|         expect(formatter.format).to eq(expected_output) |         expect(formatter.format).to eq(expected_output) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     context 'when include_contact_details is true' do | ||||||
|  |       it 'includes contact details' do | ||||||
|  |         expected_output = [ | ||||||
|  |           "Conversation ID: ##{conversation.display_id}", | ||||||
|  |           "Channel: #{conversation.inbox.channel.name}", | ||||||
|  |           'Message History:', | ||||||
|  |           'No messages in this conversation', | ||||||
|  |           "Contact Details: #{conversation.contact.to_llm_text}" | ||||||
|  |         ].join("\n") | ||||||
|  |  | ||||||
|  |         expect(formatter.format(include_contact_details: true)).to eq(expected_output) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pranav
					Pranav