diff --git a/app/controllers/settings/api_services_controller.rb b/app/controllers/settings/api_services_controller.rb index ce6a3831..5ac8251a 100644 --- a/app/controllers/settings/api_services_controller.rb +++ b/app/controllers/settings/api_services_controller.rb @@ -30,6 +30,16 @@ def update end end + def test + @api_service = Current.user.api_services.find_by(id: params[:api_service_id]) + @answer = @api_service.test_api_service(params[:url], params[:token]) + + respond_to do |format| + format.html { redirect_to settings_api_services_path, notice: "Tested: #{@answer}", status: :see_other } + format.turbo_stream + end + end + def destroy @api_service.deleted! redirect_to settings_api_services_path, notice: "Deleted", status: :see_other diff --git a/app/controllers/settings/language_models_controller.rb b/app/controllers/settings/language_models_controller.rb index 5c99396f..70f6d3bf 100644 --- a/app/controllers/settings/language_models_controller.rb +++ b/app/controllers/settings/language_models_controller.rb @@ -34,6 +34,16 @@ def update end end + def test + @language_model = Current.user.language_models.find_by(id: params[:language_model_id]) + @answer = @language_model.test(params[:model]) + + respond_to do |format| + format.html { redirect_to settings_language_models_path, notice: "Tested: #{@answer}", status: :see_other } + format.turbo_stream + end + end + def destroy @language_model.deleted! redirect_to settings_language_models_path, notice: "Deleted", status: :see_other diff --git a/app/javascript/stimulus/testing_controller.js b/app/javascript/stimulus/testing_controller.js new file mode 100644 index 00000000..c9fc27de --- /dev/null +++ b/app/javascript/stimulus/testing_controller.js @@ -0,0 +1,25 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["model", "test", "url","token"] + + update_link_language_model(event) { + const link = event.currentTarget; + const href = link.href.split('?')[0]; + link.href = href + "?model=" + this.modelTarget.value + } + + update_link_api_service(event) { + const link = event.currentTarget; + const href = link.href.split('?')[0]; + link.href = href + "?url=" + this.urlTarget.value + "&token=" + this.tokenTarget.value + console.log("Link: ", link) + } + + disable_test_link() { + const link = this.testTarget; + link.addEventListener('click', function(event) { + event.preventDefault(); + }); + } +} diff --git a/app/models/api_service.rb b/app/models/api_service.rb index 9ad738fd..17798d05 100644 --- a/app/models/api_service.rb +++ b/app/models/api_service.rb @@ -39,6 +39,10 @@ def effective_token token.presence || default_llm_key end + def test_api_service(url = nil, token = nil) + ai_backend.test_api_service(self, url, token) + end + private def default_llm_key diff --git a/app/models/language_model.rb b/app/models/language_model.rb index a0e48e8d..ffd44bb0 100644 --- a/app/models/language_model.rb +++ b/app/models/language_model.rb @@ -31,6 +31,10 @@ def supports_tools? api_service.name != "Groq" # TODO: Remove this short circuit once I can debug tool use with Groq end + def test(api_name = nil) + ai_backend.test_language_model(self, api_name) + end + private def populate_position diff --git a/app/services/ai_backend.rb b/app/services/ai_backend.rb index d4204f08..5f14342b 100644 --- a/app/services/ai_backend.rb +++ b/app/services/ai_backend.rb @@ -48,6 +48,27 @@ def stream_next_conversation_message(&chunk_handler) end end + def self.test_language_model(language_model, api_name = nil) + api_name ||= language_model.api_name + url = language_model.api_service.url + token = language_model.api_service.effective_token + return "Error: API key (token) is blank" if language_model.api_service.requires_token? && token.blank? + + test_execute(url, token, api_name) + end + + def self.test_api_service(api_service, url = nil, token = nil) + url ||= api_service.url + token ||= api_service.effective_token + language_model = LanguageModel.where(best: true, api_service: api_service).first + api_name = language_model.api_name unless language_model.nil? + + return "Error: API key (token) is blank" if api_service.requires_token? && token.blank? + return "Error: API name is blank. Define a best Language Model for this API service." if api_name.blank? + + test_execute(url, token, api_name) + end + private def client_method_name diff --git a/app/services/ai_backend/anthropic.rb b/app/services/ai_backend/anthropic.rb index f8a4d5d9..638df9ea 100644 --- a/app/services/ai_backend/anthropic.rb +++ b/app/services/ai_backend/anthropic.rb @@ -14,6 +14,26 @@ def self.client end end + def self.test_execute(url, token, api_name) + Rails.logger.info "Connecting to Anthropic API server at #{url} with access token of length #{token.to_s.length}" + client = ::Anthropic::Client.new( + uri_base: url, + access_token: token + ) + + Rails.logger.info "Testing using model #{api_name}" + client.messages( + model: api_name, + messages: [ + { "role": "user", "content": "Hello!" } + ], + system: "You are a helpful assistant.", + parameters: { max_tokens: 1000 } + ).dig("content", 0, "text") + rescue => e + "Error: #{e.message}" + end + def initialize(user, assistant, conversation = nil, message = nil) super(user, assistant, conversation, message) begin diff --git a/app/services/ai_backend/gemini.rb b/app/services/ai_backend/gemini.rb index f89447cf..d125fc68 100644 --- a/app/services/ai_backend/gemini.rb +++ b/app/services/ai_backend/gemini.rb @@ -15,6 +15,27 @@ def self.client end end + def self.test_execute(url, token, api_name) + Rails.logger.info "Connecting to Gemini API server at #{url} with access token of length #{token.to_s.length}" + client = ::Gemini.new( + credentials: { + service: "generative-language-api", + api_key: token, + version: "v1beta" + }, + options: { + model: api_name, + server_sent_events: true + } + ) + + client.generate_content({ + contents: { role: "user", parts: { text: "Hello!" }} + }).dig("candidates", 0, "content", "parts", 0, "text") + rescue ::Faraday::Error => e + "Error: #{e.message}" + end + def initialize(user, assistant, conversation = nil, message = nil) super(user, assistant, conversation, message) begin diff --git a/app/services/ai_backend/open_ai.rb b/app/services/ai_backend/open_ai.rb index aa8bd600..f40c730f 100644 --- a/app/services/ai_backend/open_ai.rb +++ b/app/services/ai_backend/open_ai.rb @@ -14,6 +14,29 @@ def self.client end end + def self.test_execute(url, token, api_name) + if Rails.env.test? + client = ::TestClient::OpenAI.new( + access_token: token, + uri_base: url + ) + response = client.send(:chat, ** {parameters: {model: api_name, messages: [{ role: "user", content: "Hello!" }]}}) + else + Rails.logger.info "Connecting to OpenAI API server at #{url} with access token of length #{token.to_s.length}" + client = ::OpenAI::Client.new( + access_token: token, + uri_base: url + ) + + Rails.logger.info "Testing using model #{api_name}" + response = client.chat(parameters: {model: api_name, messages: [{ role: "user", content: "Hello!" }]}) + end + + response.dig("choices", 0, "message", "content") + rescue ::Faraday::Error => e + "Error: #{e.message}" + end + def initialize(user, assistant, conversation = nil, message = nil) super(user, assistant, conversation, message) begin diff --git a/app/views/settings/api_services/_form.html.erb b/app/views/settings/api_services/_form.html.erb index a6c9d073..30e5e8b9 100644 --- a/app/views/settings/api_services/_form.html.erb +++ b/app/views/settings/api_services/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with(model: [:settings, api_service], class: "contents") do |form| %> +<%= form_with(model: [:settings, api_service], class: "contents", data: {controller: "testing"}) do |form| %> <% if api_service.errors.any? %>