-
Notifications
You must be signed in to change notification settings - Fork 3.5k
[AI-3] Add the LLM connection administration page #24880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <%= | ||
| component_wrapper(tag: "turbo-frame", **wrapper_options) do | ||
| concat( | ||
| settings_primer_form_with(**form_options) do |f| | ||
| render(LlmConnections::ConnectionForm.new(f)) | ||
| end | ||
| ) | ||
|
|
||
| concat( | ||
| render( | ||
| Primer::Beta::Text.new( | ||
| tag: :div, | ||
| mt: 3, | ||
| hidden: true, | ||
| display: :flex, | ||
| align_items: :center, | ||
| color: :muted, | ||
| data: { "admin--llm-connection-form-target": "progressBanner" } | ||
| ) | ||
| ) do | ||
| concat(render(Primer::Beta::Spinner.new(size: :small, mr: 2))) | ||
| concat(t("admin.llm_connections.form.label_connecting")) | ||
| end | ||
| ) | ||
| end | ||
| %> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License | ||
| # as published by the Free Software Foundation; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module LlmConnections | ||
| class FormComponent < ApplicationComponent | ||
| include ApplicationHelper | ||
| include OpPrimer::ComponentHelpers | ||
| include OpTurbo::Streamable | ||
|
|
||
| def self.wrapper_key = :llm_connection_form | ||
|
|
||
| alias_method :connection, :model | ||
|
|
||
| private | ||
|
|
||
| def wrapper_options | ||
| { | ||
| data: { | ||
| controller: "admin--llm-connection-form", | ||
| test_selector: "llm-connection--form" | ||
| } | ||
| } | ||
| end | ||
|
|
||
| def form_options | ||
| { | ||
| model: connection, | ||
| url: llm_connection_path, | ||
| method: :patch | ||
| } | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License | ||
| # as published by the Free Software Foundation; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module LlmConnections | ||
| # Validations that hold for every write, including provisioning from the | ||
| # environment. Deliberately makes no network request -- see UpdateContract. | ||
| class BaseContract < ModelContract | ||
| attribute :enabled | ||
| attribute :api_format | ||
| attribute :base_url | ||
| attribute :api_key | ||
| attribute :default_chat_model_id | ||
| attribute :default_embedding_model_id | ||
|
|
||
| validates :base_url, presence: true | ||
| validates :api_format, inclusion: { in: Llm::Adapters::FORMATS } | ||
| # Bedrock and Vertex AI can be discovered against, but not called: they need | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicated comment, let's keep only the one at the source. |
||
| # a secret key and region, or a project and location, and there is one | ||
| # api_key column. Offering them would only fail at request time. | ||
| validates :api_format, exclusion: { in: Llm::Session::UNSUPPORTED_FORMATS, message: :not_supported }, | ||
| unless: -> { api_format.blank? } | ||
| # Resolves to the validate_url gem, which defaults to http and https. Plain | ||
| # http is deliberately allowed: an on-premise LLM server on an internal | ||
| # network commonly terminates TLS elsewhere, or not at all. | ||
| validates :base_url, url: { message: :invalid_url }, unless: -> { base_url.blank? } | ||
|
|
||
| validate :enabled_requires_connection | ||
|
|
||
| private | ||
|
|
||
| def enabled_requires_connection | ||
| return unless model.enabled? | ||
| return if model.base_url.present? | ||
|
|
||
| errors.add :enabled, :requires_connection | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License | ||
| # as published by the Free Software Foundation; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module LlmConnections | ||
| # The contract used when an administrator saves the form. | ||
| # | ||
| # This is the only contract that talks to the remote server, so that a failed | ||
| # probe persists nothing. It must NOT move to BaseContract: provisioning from | ||
| # the environment reuses that base, and a validates line cannot be un-declared | ||
| # by a subclass -- an outbound request during seeding would fail the boot of a | ||
| # container whose LLM server has not started yet. | ||
| # | ||
| # The probe itself is guarded on changed attributes, so toggling +enabled+ or | ||
| # picking a default model contacts nothing. | ||
| class UpdateContract < BaseContract | ||
| validates :base_url, llm_server: true, unless: -> { errors.include?(:base_url) } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License | ||
| # as published by the Free Software Foundation; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module Admin | ||
| class LlmConnectionsController < ApplicationController | ||
| include OpTurbo::ComponentStream | ||
| include PaginationHelper | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 |
||
|
|
||
| layout "admin" | ||
| menu_item :llm_connection | ||
|
|
||
| before_action :require_feature | ||
| before_action :require_admin | ||
| before_action :set_connection | ||
|
|
||
| def show; end | ||
|
|
||
| def update | ||
| result = ::LlmConnections::UpdateService | ||
| .new(user: current_user, model: @connection) | ||
| .call(**llm_connection_params) | ||
|
|
||
| result.on_success { redirect_after_save } | ||
| result.on_failure { render_form_with_errors } | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def set_connection | ||
| @connection = LlmConnection.instance | ||
| end | ||
|
|
||
| # The flag gates the endpoints, not only the menu entry: an unfinished page | ||
| # must not accept writes just because somebody knows the URL. | ||
| def require_feature | ||
| render_404 unless OpenProject::FeatureDecisions.llm_connection_active? | ||
| end | ||
|
|
||
| def redirect_after_save | ||
| redirect_with_notice(t(".success")) | ||
| end | ||
|
|
||
| def render_form_with_errors | ||
| update_via_turbo_stream(component: ::LlmConnections::FormComponent.new(@connection)) | ||
| # The HTML fallback re-renders the whole page, which needs everything the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 This comment describes a |
||
| # show action assigns -- not just the form that failed. | ||
| respond_with_turbo_streams do |format| | ||
| format.html do | ||
| show | ||
| render :show | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def redirect_with_notice(message) | ||
| flash[:notice] = message | ||
| redirect_to llm_connection_path, status: :see_other | ||
| end | ||
|
|
||
| def redirect_with_error(message) | ||
| flash[:error] = message | ||
| redirect_to llm_connection_path, status: :see_other | ||
| end | ||
|
|
||
| # A blank API key means "keep the stored one": the form never renders the | ||
| # saved value, so submitting it unchanged posts an empty string. | ||
| def llm_connection_params | ||
| permitted = params.expect( | ||
| llm_connection: %i[enabled api_format base_url api_key] | ||
| ) | ||
| permitted.delete(:api_key) if permitted[:api_key].blank? | ||
| permitted.to_h.symbolize_keys | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this next to the other one :)