feat: legacy features to ruby llm (#12994)

This commit is contained in:
Aakash Bakhle
2025-12-11 14:17:28 +05:30
committed by GitHub
parent f2054e703a
commit 1de8d3e56d
39 changed files with 860 additions and 755 deletions

View File

@@ -13,7 +13,7 @@ RSpec.describe Captain::Documents::ResponseBuilderJob, type: :job do
before do
allow(Captain::Llm::FaqGeneratorService).to receive(:new)
.with(document.content, document.account.locale_english_name)
.with(document.content, document.account.locale_english_name, account_id: document.account_id)
.and_return(faq_generator)
allow(faq_generator).to receive(:generate).and_return(faqs)
end
@@ -52,7 +52,7 @@ RSpec.describe Captain::Documents::ResponseBuilderJob, type: :job do
before do
allow(Captain::Llm::FaqGeneratorService).to receive(:new)
.with(spanish_document.content, 'portuguese')
.with(spanish_document.content, 'portuguese', account_id: spanish_document.account_id)
.and_return(spanish_faq_generator)
allow(spanish_faq_generator).to receive(:generate).and_return(faqs)
end
@@ -61,7 +61,7 @@ RSpec.describe Captain::Documents::ResponseBuilderJob, type: :job do
described_class.new.perform(spanish_document)
expect(Captain::Llm::FaqGeneratorService).to have_received(:new)
.with(spanish_document.content, 'portuguese')
.with(spanish_document.content, 'portuguese', account_id: spanish_document.account_id)
end
end

View File

@@ -4,49 +4,42 @@ RSpec.describe Captain::Llm::ConversationFaqService do
let(:captain_assistant) { create(:captain_assistant) }
let(:conversation) { create(:conversation, first_reply_created_at: Time.zone.now) }
let(:service) { described_class.new(captain_assistant, conversation) }
let(:client) { instance_double(OpenAI::Client) }
let(:embedding_service) { instance_double(Captain::Llm::EmbeddingService) }
let(:mock_chat) { instance_double(RubyLLM::Chat) }
let(:sample_faqs) do
[
{ 'question' => 'What is the purpose?', 'answer' => 'To help users.' },
{ 'question' => 'How does it work?', 'answer' => 'Through AI.' }
]
end
let(:mock_response) do
instance_double(RubyLLM::Message, content: { faqs: sample_faqs }.to_json)
end
before do
create(:installation_config) { create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key') }
allow(OpenAI::Client).to receive(:new).and_return(client)
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
allow(Captain::Llm::EmbeddingService).to receive(:new).and_return(embedding_service)
allow(RubyLLM).to receive(:chat).and_return(mock_chat)
allow(mock_chat).to receive(:with_temperature).and_return(mock_chat)
allow(mock_chat).to receive(:with_params).and_return(mock_chat)
allow(mock_chat).to receive(:with_instructions).and_return(mock_chat)
allow(mock_chat).to receive(:ask).and_return(mock_response)
end
describe '#generate_and_deduplicate' do
let(:sample_faqs) do
[
{ 'question' => 'What is the purpose?', 'answer' => 'To help users.' },
{ 'question' => 'How does it work?', 'answer' => 'Through AI.' }
]
end
let(:openai_response) do
{
'choices' => [
{
'message' => {
'content' => { faqs: sample_faqs }.to_json
}
}
]
}
end
context 'when successful' do
before do
allow(client).to receive(:chat).and_return(openai_response)
allow(embedding_service).to receive(:get_embedding).and_return([0.1, 0.2, 0.3])
allow(captain_assistant.responses).to receive(:nearest_neighbors).and_return([])
end
it 'creates new FAQs' do
it 'creates new FAQs for valid conversation content' do
expect do
service.generate_and_deduplicate
end.to change(captain_assistant.responses, :count).by(2)
end
it 'saves the correct FAQ content' do
it 'saves FAQs with pending status linked to conversation' do
service.generate_and_deduplicate
expect(
captain_assistant.responses.pluck(:question, :answer, :status, :documentable_id)
@@ -63,6 +56,11 @@ RSpec.describe Captain::Llm::ConversationFaqService do
it 'returns an empty array without generating FAQs' do
expect(service.generate_and_deduplicate).to eq([])
end
it 'does not call the LLM API' do
expect(RubyLLM).not_to receive(:chat)
service.generate_and_deduplicate
end
end
context 'when finding duplicates' do
@@ -70,9 +68,6 @@ RSpec.describe Captain::Llm::ConversationFaqService do
create(:captain_assistant_response, assistant: captain_assistant, question: 'Similar question', answer: 'Similar answer')
end
let(:similar_neighbor) do
# Using OpenStruct here to mock as the Captain:AssistantResponse does not implement
# neighbor_distance as a method or attribute rather it is returned directly
# from SQL query in neighbor gem
OpenStruct.new(
id: 1,
question: existing_response.question,
@@ -82,87 +77,78 @@ RSpec.describe Captain::Llm::ConversationFaqService do
end
before do
allow(client).to receive(:chat).and_return(openai_response)
allow(embedding_service).to receive(:get_embedding).and_return([0.1, 0.2, 0.3])
allow(captain_assistant.responses).to receive(:nearest_neighbors).and_return([similar_neighbor])
end
it 'filters out duplicate FAQs' do
it 'filters out duplicate FAQs based on embedding similarity' do
expect do
service.generate_and_deduplicate
end.not_to change(captain_assistant.responses, :count)
end
end
context 'when OpenAI API fails' do
context 'when LLM API fails' do
before do
allow(client).to receive(:chat).and_raise(OpenAI::Error.new('API Error'))
allow(mock_chat).to receive(:ask).and_raise(RubyLLM::Error.new(nil, 'API Error'))
allow(Rails.logger).to receive(:error)
end
it 'handles the error and returns empty array' do
expect(Rails.logger).to receive(:error).with('OpenAI API Error: API Error')
it 'returns empty array and logs the error' do
expect(Rails.logger).to receive(:error).with('LLM API Error: API Error')
expect(service.generate_and_deduplicate).to eq([])
end
end
context 'when JSON parsing fails' do
let(:invalid_response) do
{
'choices' => [
{
'message' => {
'content' => 'invalid json'
}
}
]
}
instance_double(RubyLLM::Message, content: 'invalid json')
end
before do
allow(client).to receive(:chat).and_return(invalid_response)
allow(mock_chat).to receive(:ask).and_return(invalid_response)
end
it 'handles JSON parsing errors' do
it 'handles JSON parsing errors gracefully' do
expect(Rails.logger).to receive(:error).with(/Error in parsing GPT processed response:/)
expect(service.generate_and_deduplicate).to eq([])
end
end
context 'when response content is nil' do
let(:nil_response) do
instance_double(RubyLLM::Message, content: nil)
end
before do
allow(mock_chat).to receive(:ask).and_return(nil_response)
end
it 'returns empty array' do
expect(service.generate_and_deduplicate).to eq([])
end
end
end
describe '#chat_parameters' do
it 'includes correct model and response format' do
params = service.send(:chat_parameters)
expect(params[:model]).to eq('gpt-4o-mini')
expect(params[:response_format]).to eq({ type: 'json_object' })
end
it 'includes system prompt and conversation content' do
allow(Captain::Llm::SystemPromptsService).to receive(:conversation_faq_generator).and_return('system prompt')
params = service.send(:chat_parameters)
expect(params[:messages]).to include(
{ role: 'system', content: 'system prompt' },
{ role: 'user', content: conversation.to_llm_text }
)
end
describe 'language handling' do
context 'when conversation has different language' do
let(:account) { create(:account, locale: 'fr') }
let(:conversation) do
create(:conversation, account: account,
first_reply_created_at: Time.zone.now)
create(:conversation, account: account, first_reply_created_at: Time.zone.now)
end
it 'includes system prompt with correct language' do
allow(Captain::Llm::SystemPromptsService).to receive(:conversation_faq_generator)
before do
allow(embedding_service).to receive(:get_embedding).and_return([0.1, 0.2, 0.3])
allow(captain_assistant.responses).to receive(:nearest_neighbors).and_return([])
end
it 'uses account language for system prompt' do
expect(Captain::Llm::SystemPromptsService).to receive(:conversation_faq_generator)
.with('french')
.and_return('system prompt in french')
.at_least(:once)
.and_call_original
params = service.send(:chat_parameters)
expect(params[:messages]).to include(
{ role: 'system', content: 'system prompt in french' }
)
service.generate_and_deduplicate
end
end
end

View File

@@ -4,58 +4,40 @@ RSpec.describe Captain::Llm::FaqGeneratorService do
let(:content) { 'Sample content for FAQ generation' }
let(:language) { 'english' }
let(:service) { described_class.new(content, language) }
let(:client) { instance_double(OpenAI::Client) }
let(:mock_chat) { instance_double(RubyLLM::Chat) }
let(:sample_faqs) do
[
{ 'question' => 'What is this service?', 'answer' => 'It generates FAQs.' },
{ 'question' => 'How does it work?', 'answer' => 'Using AI technology.' }
]
end
let(:mock_response) do
instance_double(RubyLLM::Message, content: { faqs: sample_faqs }.to_json)
end
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
allow(OpenAI::Client).to receive(:new).and_return(client)
allow(RubyLLM).to receive(:chat).and_return(mock_chat)
allow(mock_chat).to receive(:with_temperature).and_return(mock_chat)
allow(mock_chat).to receive(:with_params).and_return(mock_chat)
allow(mock_chat).to receive(:with_instructions).and_return(mock_chat)
allow(mock_chat).to receive(:ask).and_return(mock_response)
end
describe '#generate' do
let(:sample_faqs) do
[
{ 'question' => 'What is this service?', 'answer' => 'It generates FAQs.' },
{ 'question' => 'How does it work?', 'answer' => 'Using AI technology.' }
]
end
let(:openai_response) do
{
'choices' => [
{
'message' => {
'content' => { faqs: sample_faqs }.to_json
}
}
]
}
end
context 'when successful' do
before do
allow(client).to receive(:chat).and_return(openai_response)
allow(Captain::Llm::SystemPromptsService).to receive(:faq_generator).and_return('system prompt')
end
it 'returns parsed FAQs' do
it 'returns parsed FAQs from the LLM response' do
result = service.generate
expect(result).to eq(sample_faqs)
end
it 'calls OpenAI client with chat parameters' do
expect(client).to receive(:chat).with(parameters: hash_including(
model: 'gpt-4o-mini',
response_format: { type: 'json_object' },
messages: array_including(
hash_including(role: 'system'),
hash_including(role: 'user', content: content)
)
))
it 'sends content to LLM with JSON response format' do
expect(mock_chat).to receive(:with_params).with(response_format: { type: 'json_object' }).and_return(mock_chat)
service.generate
end
it 'calls SystemPromptsService with correct language' do
expect(Captain::Llm::SystemPromptsService).to receive(:faq_generator).with(language)
it 'uses SystemPromptsService with the specified language' do
expect(Captain::Llm::SystemPromptsService).to receive(:faq_generator).with(language).at_least(:once).and_call_original
service.generate
end
end
@@ -63,23 +45,57 @@ RSpec.describe Captain::Llm::FaqGeneratorService do
context 'with different language' do
let(:language) { 'spanish' }
before do
allow(client).to receive(:chat).and_return(openai_response)
end
it 'passes the correct language to SystemPromptsService' do
expect(Captain::Llm::SystemPromptsService).to receive(:faq_generator).with('spanish')
expect(Captain::Llm::SystemPromptsService).to receive(:faq_generator).with('spanish').at_least(:once).and_call_original
service.generate
end
end
context 'when OpenAI API fails' do
context 'when LLM API fails' do
before do
allow(client).to receive(:chat).and_raise(OpenAI::Error.new('API Error'))
allow(mock_chat).to receive(:ask).and_raise(RubyLLM::Error.new(nil, 'API Error'))
allow(Rails.logger).to receive(:error)
end
it 'handles the error and returns empty array' do
expect(Rails.logger).to receive(:error).with('OpenAI API Error: API Error')
it 'returns empty array and logs the error' do
expect(Rails.logger).to receive(:error).with('LLM API Error: API Error')
expect(service.generate).to eq([])
end
end
context 'when response content is nil' do
let(:nil_response) { instance_double(RubyLLM::Message, content: nil) }
before do
allow(mock_chat).to receive(:ask).and_return(nil_response)
end
it 'returns empty array' do
expect(service.generate).to eq([])
end
end
context 'when JSON parsing fails' do
let(:invalid_response) { instance_double(RubyLLM::Message, content: 'invalid json') }
before do
allow(mock_chat).to receive(:ask).and_return(invalid_response)
end
it 'logs error and returns empty array' do
expect(Rails.logger).to receive(:error).with(/Error in parsing GPT processed response:/)
expect(service.generate).to eq([])
end
end
context 'when response is missing faqs key' do
let(:missing_key_response) { instance_double(RubyLLM::Message, content: '{"data": []}') }
before do
allow(mock_chat).to receive(:ask).and_return(missing_key_response)
end
it 'returns empty array via KeyError rescue' do
expect(service.generate).to eq([])
end
end

View File

@@ -4,40 +4,38 @@ RSpec.describe Captain::Onboarding::WebsiteAnalyzerService do
let(:website_url) { 'https://example.com' }
let(:service) { described_class.new(website_url) }
let(:mock_crawler) { instance_double(Captain::Tools::SimplePageCrawlService) }
let(:mock_client) { instance_double(OpenAI::Client) }
let(:mock_chat) { instance_double(RubyLLM::Chat) }
let(:business_info) do
{
'business_name' => 'Example Corp',
'suggested_assistant_name' => 'Alex from Example Corp',
'description' => 'You specialize in helping customers with business solutions and support'
}
end
let(:mock_response) do
instance_double(RubyLLM::Message, content: business_info.to_json)
end
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
allow(Captain::Tools::SimplePageCrawlService).to receive(:new).and_return(mock_crawler)
allow(service).to receive(:client).and_return(mock_client)
allow(service).to receive(:model).and_return('gpt-3.5-turbo')
allow(RubyLLM).to receive(:chat).and_return(mock_chat)
allow(mock_chat).to receive(:with_temperature).and_return(mock_chat)
allow(mock_chat).to receive(:with_params).and_return(mock_chat)
allow(mock_chat).to receive(:with_instructions).and_return(mock_chat)
allow(mock_chat).to receive(:ask).and_return(mock_response)
end
describe '#analyze' do
context 'when website content is available and OpenAI call is successful' do
let(:openai_response) do
{
'choices' => [{
'message' => {
'content' => {
'business_name' => 'Example Corp',
'suggested_assistant_name' => 'Alex from Example Corp',
'description' => 'You specialize in helping customers with business solutions and support'
}.to_json
}
}]
}
end
context 'when website content is available and LLM call is successful' do
before do
allow(mock_crawler).to receive(:body_text_content).and_return('Welcome to Example Corp')
allow(mock_crawler).to receive(:page_title).and_return('Example Corp - Home')
allow(mock_crawler).to receive(:meta_description).and_return('Leading provider of business solutions')
allow(mock_crawler).to receive(:favicon_url).and_return('https://example.com/favicon.ico')
allow(mock_client).to receive(:chat).and_return(openai_response)
end
it 'returns success' do
it 'returns successful analysis with extracted business info' do
result = service.analyze
expect(result[:success]).to be true
@@ -49,14 +47,19 @@ RSpec.describe Captain::Onboarding::WebsiteAnalyzerService do
favicon_url: 'https://example.com/favicon.ico'
)
end
it 'uses low temperature for deterministic analysis' do
expect(mock_chat).to receive(:with_temperature).with(0.1).and_return(mock_chat)
service.analyze
end
end
context 'when website content is errored' do
context 'when website content fetch raises an error' do
before do
allow(mock_crawler).to receive(:body_text_content).and_raise(StandardError, 'Network error')
end
it 'returns error' do
it 'returns error response' do
result = service.analyze
expect(result[:success]).to be false
@@ -64,14 +67,14 @@ RSpec.describe Captain::Onboarding::WebsiteAnalyzerService do
end
end
context 'when website content is unavailable' do
context 'when website content is empty' do
before do
allow(mock_crawler).to receive(:body_text_content).and_return('')
allow(mock_crawler).to receive(:page_title).and_return('')
allow(mock_crawler).to receive(:meta_description).and_return('')
end
it 'returns error' do
it 'returns error for unavailable content' do
result = service.analyze
expect(result[:success]).to be false
@@ -79,21 +82,57 @@ RSpec.describe Captain::Onboarding::WebsiteAnalyzerService do
end
end
context 'when OpenAI error' do
context 'when LLM call fails' do
before do
allow(mock_crawler).to receive(:body_text_content).and_return('Welcome to Example Corp')
allow(mock_crawler).to receive(:page_title).and_return('Example Corp - Home')
allow(mock_crawler).to receive(:meta_description).and_return('Leading provider of business solutions')
allow(mock_crawler).to receive(:favicon_url).and_return('https://example.com/favicon.ico')
allow(mock_client).to receive(:chat).and_raise(StandardError, 'API error')
allow(mock_chat).to receive(:ask).and_raise(StandardError, 'API error')
end
it 'returns error' do
it 'returns error response with message' do
result = service.analyze
expect(result[:success]).to be false
expect(result[:error]).to eq('API error')
end
end
context 'when LLM returns invalid JSON' do
let(:invalid_response) { instance_double(RubyLLM::Message, content: 'not valid json') }
before do
allow(mock_crawler).to receive(:body_text_content).and_return('Welcome to Example Corp')
allow(mock_crawler).to receive(:page_title).and_return('Example Corp - Home')
allow(mock_crawler).to receive(:meta_description).and_return('Leading provider of business solutions')
allow(mock_crawler).to receive(:favicon_url).and_return('https://example.com/favicon.ico')
allow(mock_chat).to receive(:ask).and_return(invalid_response)
end
it 'returns error for parsing failure' do
result = service.analyze
expect(result[:success]).to be false
expect(result[:error]).to eq('Failed to parse business information from website')
end
end
context 'when URL normalization is needed' do
let(:website_url) { 'example.com' }
before do
allow(mock_crawler).to receive(:body_text_content).and_return('Welcome')
allow(mock_crawler).to receive(:page_title).and_return('Example')
allow(mock_crawler).to receive(:meta_description).and_return('Description')
allow(mock_crawler).to receive(:favicon_url).and_return(nil)
end
it 'normalizes URL by adding https prefix' do
result = service.analyze
expect(result[:data][:website_url]).to eq('https://example.com')
end
end
end
end

View File

@@ -3,60 +3,103 @@ require 'rails_helper'
RSpec.describe Internal::AccountAnalysis::ContentEvaluatorService do
let(:service) { described_class.new }
let(:content) { 'This is some test content' }
let(:mock_moderation_result) do
instance_double(
RubyLLM::Moderation,
flagged?: false,
flagged_categories: [],
category_scores: {}
)
end
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
allow(RubyLLM).to receive(:moderate).and_return(mock_moderation_result)
end
describe '#evaluate' do
context 'when content is present' do
let(:llm_response) do
{
'choices' => [
{
'message' => {
'content' => {
'threat_level' => 'low',
'threat_summary' => 'No significant threats detected',
'detected_threats' => ['minor_concern'],
'illegal_activities_detected' => false,
'recommendation' => 'approve'
}.to_json
}
}
]
}
end
before do
allow(service).to receive(:send_to_llm).and_return(llm_response)
allow(Rails.logger).to receive(:info)
end
it 'returns the evaluation results' do
context 'when content is safe' do
it 'returns safe evaluation with approval recommendation' do
result = service.evaluate(content)
expect(result).to include(
'threat_level' => 'low',
'threat_summary' => 'No significant threats detected',
'detected_threats' => ['minor_concern'],
'threat_level' => 'safe',
'threat_summary' => 'No threats detected',
'detected_threats' => [],
'illegal_activities_detected' => false,
'recommendation' => 'approve'
)
end
it 'logs the evaluation results' do
expect(Rails.logger).to receive(:info).with('Moderation evaluation - Level: safe, Threats: ')
service.evaluate(content)
end
end
expect(Rails.logger).to have_received(:info).with('LLM evaluation - Level: low, Illegal activities: false')
context 'when content is flagged' do
let(:mock_moderation_result) do
instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: %w[harassment hate],
category_scores: { 'harassment' => 0.6, 'hate' => 0.3 }
)
end
it 'returns flagged evaluation with review recommendation' do
result = service.evaluate(content)
expect(result).to include(
'threat_level' => 'high',
'threat_summary' => 'Content flagged for: harassment, hate',
'detected_threats' => %w[harassment hate],
'illegal_activities_detected' => false,
'recommendation' => 'review'
)
end
end
context 'when content contains violence' do
let(:mock_moderation_result) do
instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: ['violence'],
category_scores: { 'violence' => 0.9 }
)
end
it 'marks illegal activities detected for violence' do
result = service.evaluate(content)
expect(result['illegal_activities_detected']).to be true
expect(result['threat_level']).to eq('critical')
end
end
context 'when content contains self-harm' do
let(:mock_moderation_result) do
instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: ['self-harm'],
category_scores: { 'self-harm' => 0.85 }
)
end
it 'marks illegal activities detected for self-harm' do
result = service.evaluate(content)
expect(result['illegal_activities_detected']).to be true
end
end
context 'when content is blank' do
let(:blank_content) { '' }
it 'returns the default evaluation without calling the LLM' do
expect(service).not_to receive(:send_to_llm)
it 'returns default evaluation without calling moderation API' do
expect(RubyLLM).not_to receive(:moderate)
result = service.evaluate(blank_content)
@@ -70,34 +113,16 @@ RSpec.describe Internal::AccountAnalysis::ContentEvaluatorService do
end
end
context 'when LLM response is nil' do
before do
allow(service).to receive(:send_to_llm).and_return(nil)
end
it 'returns the default evaluation' do
result = service.evaluate(content)
expect(result).to include(
'threat_level' => 'unknown',
'threat_summary' => 'Failed to complete content evaluation',
'detected_threats' => [],
'illegal_activities_detected' => false,
'recommendation' => 'review'
)
end
end
context 'when error occurs during evaluation' do
before do
allow(service).to receive(:send_to_llm).and_raise(StandardError.new('Test error'))
allow(Rails.logger).to receive(:error)
allow(RubyLLM).to receive(:moderate).and_raise(StandardError.new('Test error'))
end
it 'logs the error and returns default evaluation with error type' do
it 'logs error and returns default evaluation with error type' do
expect(Rails.logger).to receive(:error).with('Error evaluating content: Test error')
result = service.evaluate(content)
expect(Rails.logger).to have_received(:error).with('Error evaluating content: Test error')
expect(result).to include(
'threat_level' => 'unknown',
'threat_summary' => 'Failed to complete content evaluation',
@@ -107,5 +132,68 @@ RSpec.describe Internal::AccountAnalysis::ContentEvaluatorService do
)
end
end
context 'with threat level determination' do
it 'returns critical for scores >= 0.8' do
mock_result = instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: ['harassment'],
category_scores: { 'harassment' => 0.85 }
)
allow(RubyLLM).to receive(:moderate).and_return(mock_result)
result = service.evaluate(content)
expect(result['threat_level']).to eq('critical')
end
it 'returns high for scores between 0.5 and 0.8' do
mock_result = instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: ['harassment'],
category_scores: { 'harassment' => 0.65 }
)
allow(RubyLLM).to receive(:moderate).and_return(mock_result)
result = service.evaluate(content)
expect(result['threat_level']).to eq('high')
end
it 'returns medium for scores between 0.2 and 0.5' do
mock_result = instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: ['harassment'],
category_scores: { 'harassment' => 0.35 }
)
allow(RubyLLM).to receive(:moderate).and_return(mock_result)
result = service.evaluate(content)
expect(result['threat_level']).to eq('medium')
end
it 'returns low for scores below 0.2' do
mock_result = instance_double(
RubyLLM::Moderation,
flagged?: true,
flagged_categories: ['harassment'],
category_scores: { 'harassment' => 0.15 }
)
allow(RubyLLM).to receive(:moderate).and_return(mock_result)
result = service.evaluate(content)
expect(result['threat_level']).to eq('low')
end
end
context 'with content truncation' do
let(:long_content) { 'a' * 15_000 }
it 'truncates content to 10000 characters before sending to moderation' do
expect(RubyLLM).to receive(:moderate).with('a' * 10_000).and_return(mock_moderation_result)
service.evaluate(long_content)
end
end
end
end