mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 02:32:29 +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 | ||||
|   include PgSearch::Model | ||||
|   include LlmFormattable | ||||
|  | ||||
|   has_many :associated_articles, | ||||
|            class_name: :Article, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| module LlmFormattable | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   def to_llm_text | ||||
|     LlmFormatter::LlmTextFormatterService.new(self).format | ||||
|   def to_llm_text(config = {}) | ||||
|     LlmFormatter::LlmTextFormatterService.new(self).format(config) | ||||
|   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 | ||||
|   def format | ||||
|   def format(*) | ||||
|     sections = [] | ||||
|     sections << "Contact ID: ##{@record.id}" | ||||
|     sections << 'Contact Attributes:' | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter | ||||
|   def format | ||||
|   def format(config = {}) | ||||
|     sections = [] | ||||
|     sections << "Conversation ID: ##{@record.display_id}" | ||||
|     sections << "Channel: #{@record.inbox.channel.name}" | ||||
| @@ -10,6 +10,7 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter | ||||
|                   'No messages in this conversation' | ||||
|                 end | ||||
|  | ||||
|     sections << "Contact Details: #{@record.contact.to_llm_text}" if config[:include_contact_details] | ||||
|     sections.join("\n") | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter | ||||
|     @record = record | ||||
|   end | ||||
|  | ||||
|   def format | ||||
|   def format(*) | ||||
|     # override this | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -3,9 +3,9 @@ class LlmFormatter::LlmTextFormatterService | ||||
|     @record = record | ||||
|   end | ||||
|  | ||||
|   def format | ||||
|   def format(config = {}) | ||||
|     formatter_class = find_formatter | ||||
|     formatter_class.new(@record).format | ||||
|     formatter_class.new(@record).format(config) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|   | ||||
| @@ -167,4 +167,26 @@ RSpec.describe Article do | ||||
|       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 | ||||
|   | ||||
							
								
								
									
										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) | ||||
|       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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pranav
					Pranav