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:
Pranav
2025-05-15 17:47:37 -07:00
committed by GitHub
parent 4f4ef0389b
commit bce1f58e86
11 changed files with 189 additions and 7 deletions

View File

@@ -33,6 +33,7 @@
#
class Article < ApplicationRecord
include PgSearch::Model
include LlmFormattable
has_many :associated_articles,
class_name: :Article,

View File

@@ -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

View 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

View File

@@ -1,5 +1,5 @@
class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter
def format
def format(*)
sections = []
sections << "Contact ID: ##{@record.id}"
sections << 'Contact Attributes:'

View File

@@ -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

View File

@@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter
@record = record
end
def format
def format(*)
# override this
end
end

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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