2023-03-15 17:02:20 -03:00
# frozen_string_literal: true
RSpec . describe DiscourseAi :: AiHelper :: AssistantController do
2024-06-19 18:01:35 -03:00
before { assign_fake_provider_to ( :ai_helper_model ) }
2025-05-23 16:23:06 +10:00
fab! ( :newuser )
fab! ( :user ) { Fabricate ( :user , refresh_auto_groups : true ) }
describe " # stream_suggestion " do
before do
Jobs . run_immediately!
SiteSetting . composer_ai_helper_allowed_groups = Group :: AUTO_GROUPS [ :trust_level_0 ]
end
it " is able to stream suggestions back on appropriate channel " do
sign_in ( user )
messages =
MessageBus . track_publish ( " /discourse-ai/ai-helper/stream_composer_suggestion " ) do
results = [ [ " hello " , " world " ] ]
DiscourseAi :: Completions :: Llm . with_prepared_responses ( results ) do
post " /discourse-ai/ai-helper/stream_suggestion.json " ,
params : {
text : " hello wrld " ,
location : " composer " ,
client_id : " 1234 " ,
mode : CompletionPrompt :: PROOFREAD ,
}
expect ( response . status ) . to eq ( 200 )
end
end
last_message = messages . last
expect ( messages . all? { | m | m . client_ids == [ " 1234 " ] } ) . to eq ( true )
expect ( messages . all? { | m | m == last_message || ! m . data [ :done ] } ) . to eq ( true )
expect ( last_message . data [ :result ] ) . to eq ( " hello world " )
expect ( last_message . data [ :done ] ) . to eq ( true )
end
end
2024-01-29 16:04:25 -03:00
2023-03-15 17:02:20 -03:00
describe " # suggest " do
2023-11-27 09:33:31 -03:00
let ( :text_to_proofread ) { " The rain in spain stays mainly in the plane. " }
2024-01-19 12:51:26 +01:00
let ( :proofread_text ) { " The rain in Spain, stays mainly in the Plane. " }
2023-11-27 09:33:31 -03:00
let ( :mode ) { CompletionPrompt :: PROOFREAD }
2023-03-15 17:02:20 -03:00
context " when not logged in " do
it " returns a 403 response " do
2023-11-27 09:33:31 -03:00
post " /discourse-ai/ai-helper/suggest " , params : { text : text_to_proofread , mode : mode }
2023-03-15 17:02:20 -03:00
expect ( response . status ) . to eq ( 403 )
end
end
context " when logged in as an user without enough privileges " do
before do
2025-05-23 16:23:06 +10:00
sign_in ( newuser )
2024-08-12 15:40:23 -07:00
SiteSetting . composer_ai_helper_allowed_groups = Group :: AUTO_GROUPS [ :staff ]
2023-03-15 17:02:20 -03:00
end
it " returns a 403 response " do
2023-11-27 09:33:31 -03:00
post " /discourse-ai/ai-helper/suggest " , params : { text : text_to_proofread , mode : mode }
2023-03-15 17:02:20 -03:00
expect ( response . status ) . to eq ( 403 )
end
end
context " when logged in as an allowed user " do
before do
sign_in ( user )
user . group_ids = [ Group :: AUTO_GROUPS [ :trust_level_1 ] ]
2024-08-12 15:40:23 -07:00
SiteSetting . composer_ai_helper_allowed_groups = Group :: AUTO_GROUPS [ :trust_level_1 ]
2023-03-15 17:02:20 -03:00
end
it " returns a 400 if the helper mode is invalid " do
invalid_mode = " asd "
2023-11-27 09:33:31 -03:00
post " /discourse-ai/ai-helper/suggest " ,
params : {
text : text_to_proofread ,
mode : invalid_mode ,
}
2023-03-15 17:02:20 -03:00
expect ( response . status ) . to eq ( 400 )
end
it " returns a 400 if the text is blank " do
post " /discourse-ai/ai-helper/suggest " , params : { mode : mode }
expect ( response . status ) . to eq ( 400 )
end
2023-03-22 16:00:28 -03:00
it " returns a generic error when the completion call fails " do
2023-11-29 15:17:46 +11:00
DiscourseAi :: Completions :: Llm
2023-11-27 09:33:31 -03:00
. any_instance
2024-01-04 23:53:47 +11:00
. expects ( :generate )
2023-11-27 09:33:31 -03:00
. raises ( DiscourseAi :: Completions :: Endpoints :: Base :: CompletionFailed )
2023-03-22 16:00:28 -03:00
2023-11-27 09:33:31 -03:00
post " /discourse-ai/ai-helper/suggest " , params : { mode : mode , text : text_to_proofread }
2023-03-22 16:00:28 -03:00
expect ( response . status ) . to eq ( 502 )
end
2023-03-15 17:02:20 -03:00
it " returns a suggestion " do
2023-11-27 09:33:31 -03:00
expected_diff =
" <div class= \" inline-diff \" ><p>The rain in <ins>Spain</ins><ins>,</ins><ins> </ins><del>spain </del>stays mainly in the <ins>Plane</ins><del>plane</del>.</p></div> "
2023-03-15 17:02:20 -03:00
2024-01-19 12:51:26 +01:00
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ proofread_text ] ) do
2023-11-27 09:33:31 -03:00
post " /discourse-ai/ai-helper/suggest " , params : { mode : mode , text : text_to_proofread }
2023-03-15 17:02:20 -03:00
2023-11-27 09:33:31 -03:00
expect ( response . status ) . to eq ( 200 )
2024-01-19 12:51:26 +01:00
expect ( response . parsed_body [ " suggestions " ] . first ) . to eq ( proofread_text )
2023-11-27 09:33:31 -03:00
expect ( response . parsed_body [ " diff " ] ) . to eq ( expected_diff )
end
2023-03-15 17:02:20 -03:00
end
2023-12-11 19:26:56 -03:00
it " uses custom instruction when using custom_prompt mode " do
translated_text = " Un usuario escribio esto "
expected_diff =
" <div class= \" inline-diff \" ><p><ins>Un </ins><ins>usuario </ins><ins>escribio </ins><ins>esto</ins><del>A </del><del>user </del><del>wrote </del><del>this</del></p></div> "
2024-01-29 16:04:25 -03:00
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ translated_text ] ) do
2023-12-11 19:26:56 -03:00
post " /discourse-ai/ai-helper/suggest " ,
params : {
mode : CompletionPrompt :: CUSTOM_PROMPT ,
text : " A user wrote this " ,
custom_prompt : " Translate to Spanish " ,
}
expect ( response . status ) . to eq ( 200 )
expect ( response . parsed_body [ " suggestions " ] . first ) . to eq ( translated_text )
expect ( response . parsed_body [ " diff " ] ) . to eq ( expected_diff )
end
end
2024-10-03 02:36:35 +09:00
2024-11-28 08:12:27 +09:00
it " prevents double render when mode is ILLUSTRATE_POST " do
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ proofread_text ] ) do
expect {
post " /discourse-ai/ai-helper/suggest " ,
params : {
mode : CompletionPrompt :: ILLUSTRATE_POST ,
text : text_to_proofread ,
force_default_locale : true ,
}
} . not_to raise_error
expect ( response . status ) . to eq ( 200 )
end
end
2024-10-03 02:36:35 +09:00
context " when performing numerous requests " do
it " rate limits " do
RateLimiter . enable
rate_limit = described_class :: RATE_LIMITS [ " default " ]
amount = rate_limit [ :amount ]
amount . times do
post " /discourse-ai/ai-helper/suggest " , params : { mode : mode , text : text_to_proofread }
expect ( response . status ) . to eq ( 200 )
end
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ proofread_text ] ) do
post " /discourse-ai/ai-helper/suggest " , params : { mode : mode , text : text_to_proofread }
expect ( response . status ) . to eq ( 429 )
end
end
end
2023-03-15 17:02:20 -03:00
end
end
2024-02-19 09:56:28 -08:00
2025-03-11 11:16:06 -07:00
describe " # suggest_title " do
fab! ( :topic )
fab! ( :post_1 ) { Fabricate ( :post , topic : topic , raw : " I love apples " ) }
fab! ( :post_3 ) { Fabricate ( :post , topic : topic , raw : " I love mangos " ) }
fab! ( :post_2 ) { Fabricate ( :post , topic : topic , raw : " I love bananas " ) }
context " when logged in as an allowed user " do
before do
sign_in ( user )
user . group_ids = [ Group :: AUTO_GROUPS [ :trust_level_1 ] ]
SiteSetting . composer_ai_helper_allowed_groups = Group :: AUTO_GROUPS [ :trust_level_1 ]
end
context " when suggesting titles with a topic_id " do
let ( :title_suggestions ) do
" <item>What are your favourite fruits?</item><item>Love for fruits</item><item>Fruits are amazing</item><item>Favourite fruit list</item><item>Fruit share topic</item> "
end
let ( :title_suggestions_array ) do
[
" What are your favourite fruits? " ,
" Love for fruits " ,
" Fruits are amazing " ,
" Favourite fruit list " ,
" Fruit share topic " ,
]
end
it " returns title suggestions based on all topic post context " do
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ title_suggestions ] ) do
post " /discourse-ai/ai-helper/suggest_title " , params : { topic_id : topic . id }
expect ( response . status ) . to eq ( 200 )
expect ( response . parsed_body [ " suggestions " ] ) . to eq ( title_suggestions_array )
end
end
end
context " when suggesting titles with input text " do
let ( :title_suggestions ) do
" <item>Apples - the best fruit</item><item>Why apples are great</item><item>Apples are the best fruit</item><item>My love for apples</item><item>I love apples</item> "
end
let ( :title_suggestions_array ) do
[
" Apples - the best fruit " ,
" Why apples are great " ,
" Apples are the best fruit " ,
" My love for apples " ,
" I love apples " ,
]
end
it " returns title suggestions based on the input text " do
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ title_suggestions ] ) do
post " /discourse-ai/ai-helper/suggest_title " , params : { text : post_1 . raw }
expect ( response . status ) . to eq ( 200 )
expect ( response . parsed_body [ " suggestions " ] ) . to eq ( title_suggestions_array )
end
end
end
end
end
2024-02-19 09:56:28 -08:00
describe " # caption_image " do
2024-06-27 16:24:44 -03:00
let ( :image ) { plugin_file_from_fixtures ( " 100x100.jpg " ) }
let ( :upload ) { UploadCreator . new ( image , " image.jpg " ) . create_for ( Discourse . system_user . id ) }
2024-02-20 12:43:39 +10:00
let ( :image_url ) { " #{ Discourse . base_url } #{ upload . url } " }
2024-02-19 09:56:28 -08:00
let ( :caption ) { " A picture of a cat sitting on a table " }
2024-02-21 10:10:22 -08:00
let ( :caption_with_attrs ) do
" A picture of a cat sitting on a table ( #{ I18n . t ( " discourse_ai.ai_helper.image_caption.attribution " ) } ) "
end
2024-10-23 18:38:29 -03:00
let ( :bad_caption ) { " A picture of a cat \n sitting on a |table| " }
2024-02-19 09:56:28 -08:00
2024-07-24 16:29:47 -03:00
before { assign_fake_provider_to ( :ai_helper_image_caption_model ) }
2024-10-23 18:38:29 -03:00
def request_caption ( params , caption = " A picture of a cat sitting on a table " )
2024-07-24 16:29:47 -03:00
DiscourseAi :: Completions :: Llm . with_prepared_responses ( [ caption ] ) do
post " /discourse-ai/ai-helper/caption_image " , params : params
yield ( response )
end
end
2024-02-19 09:56:28 -08:00
context " when logged in as an allowed user " do
before do
sign_in ( user )
2024-08-12 15:40:23 -07:00
SiteSetting . composer_ai_helper_allowed_groups = Group :: AUTO_GROUPS [ :trust_level_1 ]
2024-02-19 09:56:28 -08:00
end
it " returns the suggested caption for the image " do
2024-07-24 16:29:47 -03:00
request_caption ( { image_url : image_url , image_url_type : " long_url " } ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] ) . to eq ( caption_with_attrs )
2024-10-23 18:38:29 -03:00
end
end
it " returns a cleaned up caption from the LLM " do
request_caption ( { image_url : image_url , image_url_type : " long_url " } , bad_caption ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] ) . to eq ( caption_with_attrs )
2024-07-24 16:29:47 -03:00
end
2024-02-19 09:56:28 -08:00
end
2024-11-12 15:52:46 -03:00
it " truncates the caption from the LLM " do
request_caption ( { image_url : image_url , image_url_type : " long_url " } , caption * 10 ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] . size ) . to be < caption . size * 10
end
end
2024-05-27 10:49:24 -07:00
context " when the image_url is a short_url " do
let ( :image_url ) { upload . short_url }
it " returns the suggested caption for the image " do
2024-07-24 16:29:47 -03:00
request_caption ( { image_url : image_url , image_url_type : " short_url " } ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] ) . to eq ( caption_with_attrs )
end
2024-05-27 10:49:24 -07:00
end
end
context " when the image_url is a short_path " do
let ( :image_url ) { " #{ Discourse . base_url } #{ upload . short_path } " }
it " returns the suggested caption for the image " do
2024-07-24 16:29:47 -03:00
request_caption ( { image_url : image_url , image_url_type : " short_path " } ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] ) . to eq ( caption_with_attrs )
end
end
end
it " returns a 502 error when the completion call fails " do
DiscourseAi :: Completions :: Llm . with_prepared_responses (
[ DiscourseAi :: Completions :: Endpoints :: Base :: CompletionFailed . new ] ,
) do
2024-05-27 10:49:24 -07:00
post " /discourse-ai/ai-helper/caption_image " ,
params : {
image_url : image_url ,
2024-07-24 16:29:47 -03:00
image_url_type : " long_url " ,
2024-05-27 10:49:24 -07:00
}
2024-07-24 16:29:47 -03:00
expect ( response . status ) . to eq ( 502 )
2024-05-27 10:49:24 -07:00
end
end
2024-02-19 09:56:28 -08:00
it " returns a 400 error when the image_url is blank " do
post " /discourse-ai/ai-helper/caption_image "
expect ( response . status ) . to eq ( 400 )
end
2024-02-20 12:43:39 +10:00
it " returns a 404 error if no upload is found " do
post " /discourse-ai/ai-helper/caption_image " ,
params : {
image_url : " http://blah.com/img.jpeg " ,
2024-05-27 10:49:24 -07:00
image_url_type : " long_url " ,
2024-02-20 12:43:39 +10:00
}
expect ( response . status ) . to eq ( 404 )
end
context " for secure uploads " do
2024-03-05 16:48:28 +01:00
fab! ( :group )
2024-02-20 12:43:39 +10:00
fab! ( :private_category ) { Fabricate ( :private_category , group : group ) }
2024-06-27 16:24:44 -03:00
let ( :image ) { plugin_file_from_fixtures ( " 100x100.jpg " ) }
let ( :upload ) { UploadCreator . new ( image , " image.jpg " ) . create_for ( Discourse . system_user . id ) }
2024-02-20 12:43:39 +10:00
let ( :image_url ) { " #{ Discourse . base_url } /secure-uploads/ #{ upload . url } " }
2024-06-27 16:24:44 -03:00
before do
Jobs . run_immediately!
# this is done so the after_save callbacks for site settings to make
# UploadReference records works
@original_provider = SiteSetting . provider
SiteSetting . provider = SiteSettings :: DbProvider . new ( SiteSetting )
setup_s3
stub_s3_store
2024-07-24 16:29:47 -03:00
assign_fake_provider_to ( :ai_helper_image_caption_model )
2024-06-27 16:24:44 -03:00
SiteSetting . secure_uploads = true
2024-08-12 15:40:23 -07:00
SiteSetting . composer_ai_helper_allowed_groups = Group :: AUTO_GROUPS [ :trust_level_1 ]
2024-07-24 16:29:47 -03:00
2024-08-12 15:40:23 -07:00
Group . find ( SiteSetting . composer_ai_helper_allowed_groups_map . first ) . add ( user )
2024-06-27 16:24:44 -03:00
user . reload
stub_request (
:get ,
" http://s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/ #{ upload . sha1 } . #{ upload . extension } " ,
) . to_return ( status : 200 , body : " " , headers : { } )
end
after { SiteSetting . provider = @original_provider }
2024-02-20 12:43:39 +10:00
it " returns a 403 error if the user cannot access the secure upload " do
2024-11-14 06:58:24 +11:00
# hosted-site plugin edge case, it enables embeddings
SiteSetting . ai_embeddings_enabled = false
2024-06-27 16:24:44 -03:00
create_post (
title : " Secure upload post " ,
raw : " This is a new post <img src= \" #{ upload . url } \" /> " ,
category : private_category ,
user : Discourse . system_user ,
)
2024-05-27 10:49:24 -07:00
post " /discourse-ai/ai-helper/caption_image " ,
params : {
image_url : image_url ,
image_url_type : " long_url " ,
}
2024-02-20 12:43:39 +10:00
expect ( response . status ) . to eq ( 403 )
end
it " returns a 200 message and caption if user can access the secure upload " do
group . add ( user )
2024-06-27 16:24:44 -03:00
2024-07-24 16:29:47 -03:00
request_caption ( { image_url : image_url , image_url_type : " long_url " } ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] ) . to eq ( caption_with_attrs )
end
2024-02-20 12:43:39 +10:00
end
context " if the input URL is for a secure upload but not on the secure-uploads path " do
let ( :image_url ) { " #{ Discourse . base_url } #{ upload . url } " }
it " creates a signed URL properly and makes the caption " do
group . add ( user )
2024-07-24 16:29:47 -03:00
request_caption ( { image_url : image_url , image_url_type : " long_url " } ) do | r |
expect ( r . status ) . to eq ( 200 )
expect ( r . parsed_body [ " caption " ] ) . to eq ( caption_with_attrs )
end
2024-02-20 12:43:39 +10:00
end
end
end
2024-10-03 02:36:35 +09:00
context " when performing numerous requests " do
it " rate limits " do
RateLimiter . enable
rate_limit = described_class :: RATE_LIMITS [ " caption_image " ]
amount = rate_limit [ :amount ]
amount . times do
request_caption ( { image_url : image_url , image_url_type : " long_url " } ) do | r |
expect ( r . status ) . to eq ( 200 )
end
end
request_caption ( { image_url : image_url , image_url_type : " long_url " } ) do | r |
expect ( r . status ) . to eq ( 429 )
end
end
end
2024-02-19 09:56:28 -08:00
end
end
2023-03-15 17:02:20 -03:00
end