FEATURE: Test LLM configuration (#634)

This commit is contained in:
Roman Rizzi 2024-05-21 13:35:50 -03:00 committed by GitHub
parent 2b473dd4a5
commit 3a9080dd14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 179 additions and 3 deletions

View File

@ -49,6 +49,22 @@ module DiscourseAi
end
end
def test
RateLimiter.new(current_user, "llm_test_#{current_user.id}", 3, 1.minute).performed!
llm_model = LlmModel.new(ai_llm_params)
DiscourseAi::Completions::Llm.proxy_from_obj(llm_model).generate(
"How much is 1 + 1?",
user: current_user,
feature_name: "llm_validator",
)
render json: { success: true }
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed => e
render json: { success: false, error: e.message }
end
private
def ai_llm_params

View File

@ -5,3 +5,19 @@ class LlmModel < ActiveRecord::Base
tokenizer.constantize
end
end
# == Schema Information
#
# Table name: llm_models
#
# id :bigint not null, primary key
# display_name :string
# name :string not null
# provider :string not null
# tokenizer :string not null
# max_prompt_tokens :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# url :string
# api_key :string
#

View File

@ -1,3 +1,4 @@
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
export default class AiLlm extends RestModel {
@ -20,4 +21,10 @@ export default class AiLlm extends RestModel {
return attrs;
}
async testConfig() {
return await ajax(`/admin/plugins/discourse-ai/ai-llms/test.json`, {
data: { ai_llm: this.createProperties() },
});
}
}

View File

@ -7,6 +7,7 @@ import { inject as service } from "@ember/service";
import BackButton from "discourse/components/back-button";
import DButton from "discourse/components/d-button";
import { popupAjaxError } from "discourse/lib/ajax-error";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
@ -18,6 +19,10 @@ export default class AiLlmEditor extends Component {
@tracked isSaving = false;
@tracked testRunning = false;
@tracked testResult = null;
@tracked testError = null;
get selectedProviders() {
const t = (provName) => {
return I18n.t(`discourse_ai.llms.providers.${provName}`);
@ -59,6 +64,36 @@ export default class AiLlmEditor extends Component {
}
}
@action
async test() {
this.testRunning = true;
try {
const configTestResult = await this.args.model.testConfig();
this.testResult = configTestResult.success;
if (this.testResult) {
this.testError = null;
} else {
this.testError = configTestResult.error;
}
} catch (e) {
popupAjaxError(e);
} finally {
later(() => {
this.testRunning = false;
}, 1000);
}
}
get testErrorMessage() {
return I18n.t("discourse_ai.llms.tests.failure", { error: this.testError });
}
get displayTestResult() {
return this.testRunning || this.testResult !== null;
}
<template>
<BackButton
@route="adminPlugins.show.discourse-ai-llms"
@ -132,6 +167,14 @@ export default class AiLlmEditor extends Component {
</div>
<div class="control-group ai-llm-editor__action_panel">
<DButton
class="ai-llm-editor__test"
@action={{this.test}}
@disabled={{this.testRunning}}
>
{{I18n.t "discourse_ai.llms.tests.title"}}
</DButton>
<DButton
class="btn-primary ai-llm-editor__save"
@action={{this.save}}
@ -140,6 +183,27 @@ export default class AiLlmEditor extends Component {
{{I18n.t "discourse_ai.llms.save"}}
</DButton>
</div>
<div class="control-group ai-llm-editor-tests">
{{#if this.displayTestResult}}
{{#if this.testRunning}}
<div class="spinner small"></div>
{{I18n.t "discourse_ai.llms.tests.running"}}
{{else}}
{{#if this.testResult}}
<div class="ai-llm-editor-tests__success">
{{icon "check"}}
{{I18n.t "discourse_ai.llms.tests.success"}}
</div>
{{else}}
<div class="ai-llm-editor-tests__failure">
{{icon "times"}}
{{this.testErrorMessage}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
</form>
</template>
}

View File

@ -22,4 +22,14 @@
padding-left: 0.25em;
color: var(--primary-medium);
}
.ai-llm-editor-tests {
&__failure {
color: var(--danger);
}
&__success {
color: var(--success);
}
}
}

View File

@ -210,6 +210,11 @@ en:
edit: "Edit"
saved: "LLM Model Saved"
back: "Back"
tests:
title: "Run Test"
running: "Running test..."
success: "Success!"
failure: "Trying to contact the model returned this error: %{error}"
hints:
max_prompt_tokens: "Max numbers of tokens for the prompt. As a rule of thumb, this should be 50% of the model's context window."

View File

@ -49,6 +49,8 @@ Discourse::Application.routes.draw do
resources :ai_llms,
only: %i[index create show update],
path: "ai-llms",
controller: "discourse_ai/admin/ai_llms"
controller: "discourse_ai/admin/ai_llms" do
collection { get :test }
end
end
end

View File

@ -108,7 +108,7 @@ module DiscourseAi
Rails.logger.error(
"#{self.class.name}: status: #{response.code.to_i} - body: #{response.body}",
)
raise CompletionFailed
raise CompletionFailed, response.body
end
log =

View File

@ -124,6 +124,15 @@ module DiscourseAi
model_name = llm_model.name
dialect_klass = DiscourseAi::Completions::Dialects::Dialect.dialect_for(model_name)
if @canned_response
if @canned_llm && @canned_llm != model_name
raise "Invalid call LLM call, expected #{@canned_llm} but got #{model_name}"
end
return new(dialect_klass, nil, model_name, gateway: @canned_response)
end
gateway_klass = DiscourseAi::Completions::Endpoints::Base.endpoint_for(provider_name)
new(dialect_klass, gateway_klass, model_name, llm_model: llm_model)

View File

@ -23,7 +23,9 @@ RSpec.describe DiscourseAi::Admin::AiLlmsController do
display_name: "My cool LLM",
name: "gpt-3.5",
provider: "open_ai",
tokenizer: "DiscourseAi::Tokenizers::OpenAiTokenizer",
url: "https://test.test/v1/chat/completions",
api_key: "test",
tokenizer: "DiscourseAi::Tokenizer::OpenAiTokenizer",
max_prompt_tokens: 16_000,
}
end
@ -65,4 +67,49 @@ RSpec.describe DiscourseAi::Admin::AiLlmsController do
end
end
end
describe "GET #test" do
let(:test_attrs) do
{
name: "llama3",
provider: "hugging_face",
url: "https://test.test/v1/chat/completions",
api_key: "test",
tokenizer: "DiscourseAi::Tokenizer::Llama3Tokenizer",
max_prompt_tokens: 2_000,
}
end
context "when we can contact the model" do
it "returns a success true flag" do
DiscourseAi::Completions::Llm.with_prepared_responses(["a response"]) do
get "/admin/plugins/discourse-ai/ai-llms/test.json", params: { ai_llm: test_attrs }
expect(response).to be_successful
expect(response.parsed_body["success"]).to eq(true)
end
end
end
context "when we cannot contact the model" do
it "returns a success false flag and the error message" do
error_message = {
error:
"Input validation error: `inputs` tokens + `max_new_tokens` must be <= 1512. Given: 30 `inputs` tokens and 3984 `max_new_tokens`",
error_type: "validation",
}
WebMock.stub_request(:post, test_attrs[:url]).to_return(
status: 422,
body: error_message.to_json,
)
get "/admin/plugins/discourse-ai/ai-llms/test.json", params: { ai_llm: test_attrs }
expect(response).to be_successful
expect(response.parsed_body["success"]).to eq(false)
expect(response.parsed_body["error"]).to eq(error_message.to_json)
end
end
end
end