diff --git a/modules/llm/app/components/llm_connections/embedding_sets/box_component.html.erb b/modules/llm/app/components/llm_connections/embedding_sets/box_component.html.erb new file mode 100644 index 000000000000..96e9568f027c --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/box_component.html.erb @@ -0,0 +1,57 @@ +<%#-- 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. + +++#%> + +<%= render(Primer::Box.new(border: true, border_radius: 2, p: 3, mb: 3, test_selector: "llm-embedding-set--box")) do %> + <%= + flex_layout(justify_content: :space_between, align_items: :flex_start, mb: 3) do |header| + header.with_column do + concat(render(Primer::Beta::Text.new(tag: :h3, font_size: 4, font_weight: :bold, mb: 1)) { t("admin.embedding_sets.box.title") }) + concat(render(Primer::Beta::Text.new(tag: :p, color: :muted, mb: 0)) { t("admin.embedding_sets.box.caption") }) + end + + header.with_column(ml: 3) do + render( + Primer::Beta::Button.new( + scheme: :primary, + size: :small, + tag: :a, + href: new_dialog_embedding_sets_path, + data: { controller: "async-dialog" }, + test_selector: "llm-embedding-set--new-button" + ) + ) do |button| + button.with_leading_visual_icon(icon: :plus) + t("admin.embedding_sets.box.new") + end + end + end + %> + + <%= render(LlmConnections::EmbeddingSets::TableComponent.new(rows: embedding_sets)) %> +<% end %> diff --git a/modules/llm/app/components/llm_connections/embedding_sets/box_component.rb b/modules/llm/app/components/llm_connections/embedding_sets/box_component.rb new file mode 100644 index 000000000000..04ab92ec4149 --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/box_component.rb @@ -0,0 +1,39 @@ +# 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 + module EmbeddingSets + class BoxComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + options :embedding_sets + end + end +end diff --git a/modules/llm/app/components/llm_connections/embedding_sets/delete_dialog_component.html.erb b/modules/llm/app/components/llm_connections/embedding_sets/delete_dialog_component.html.erb new file mode 100644 index 000000000000..7e691ff17848 --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/delete_dialog_component.html.erb @@ -0,0 +1,44 @@ +<%#-- 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. + +++#%> + +<%= + render( + Primer::OpenProject::DangerDialog.new( + title: t("admin.embedding_sets.destroy.title"), + confirm_button_text: t(:button_delete), + form_arguments:, + test_selector: TEST_SELECTOR + ) + ) do |dialog| + dialog.with_confirmation_message do |message| + message.with_heading(tag: :h2) { t("admin.embedding_sets.destroy.heading", title: embedding_set.title) } + message.with_description_content(t("admin.embedding_sets.destroy.description")) + end + end +%> diff --git a/modules/llm/app/components/llm_connections/embedding_sets/delete_dialog_component.rb b/modules/llm/app/components/llm_connections/embedding_sets/delete_dialog_component.rb new file mode 100644 index 000000000000..f6a5f58faf8e --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/delete_dialog_component.rb @@ -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 + module EmbeddingSets + class DeleteDialogComponent < ApplicationComponent + include OpTurbo::Streamable + + TEST_SELECTOR = "llm-embedding-set--delete-dialog" + + alias_method :embedding_set, :model + + def form_arguments + { action: embedding_set_path(embedding_set), method: :delete } + end + end + end +end diff --git a/modules/llm/app/components/llm_connections/embedding_sets/dialog_component.html.erb b/modules/llm/app/components/llm_connections/embedding_sets/dialog_component.html.erb new file mode 100644 index 000000000000..e9bd52ca71ea --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/dialog_component.html.erb @@ -0,0 +1,66 @@ +<%#-- 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. + +++#%> + +<%= + render( + Primer::Alpha::Dialog.new( + title:, + size: :medium_portrait, + id: DIALOG_ID, + test_selector: DIALOG_ID + ) + ) do |d| + d.with_header(variant: :large) + + d.with_body do + render(LlmConnections::EmbeddingSets::FormComponent.new(embedding_set, connection:)) + end + + d.with_footer do + component_collection do |buttons| + buttons.with_component( + Primer::Beta::Button.new(data: { close_dialog_id: DIALOG_ID }) + ) do + t(:button_cancel) + end + + buttons.with_component( + Primer::Beta::Button.new( + scheme: :primary, + form: LlmConnections::EmbeddingSets::FormComponent::FORM_ID, + type: :submit, + test_selector: "llm-embedding-set--submit" + ) + ) do + button_caption + end + end + end + end +%> diff --git a/modules/llm/app/components/llm_connections/embedding_sets/dialog_component.rb b/modules/llm/app/components/llm_connections/embedding_sets/dialog_component.rb new file mode 100644 index 000000000000..3eb402595270 --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/dialog_component.rb @@ -0,0 +1,58 @@ +# 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 + module EmbeddingSets + class DialogComponent < ApplicationComponent + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + DIALOG_ID = "llm-embedding-set-dialog" + + alias_method :embedding_set, :model + + options :connection + + private + + def title + if embedding_set.persisted? + t("admin.embedding_sets.dialog.edit_title", title: embedding_set.title_was) + else + t("admin.embedding_sets.dialog.new_title") + end + end + + def button_caption + embedding_set.persisted? ? t(:button_save) : t(:button_create) + end + end + end +end diff --git a/modules/llm/app/components/llm_connections/embedding_sets/form_component.html.erb b/modules/llm/app/components/llm_connections/embedding_sets/form_component.html.erb new file mode 100644 index 000000000000..2fca976ac9a4 --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/form_component.html.erb @@ -0,0 +1,43 @@ +<%#-- 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. + +++#%> + +<%= + component_wrapper do + primer_form_with( + id: FORM_ID, + model:, + scope: :embedding_set, + url: form_url, + method: http_method, + data: { turbo: true } + ) do |f| + render(::EmbeddingSets::Form.new(f, model_options:)) + end + end +%> diff --git a/modules/llm/app/components/llm_connections/embedding_sets/form_component.rb b/modules/llm/app/components/llm_connections/embedding_sets/form_component.rb new file mode 100644 index 000000000000..c2fab9b26f14 --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/form_component.rb @@ -0,0 +1,58 @@ +# 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 + module EmbeddingSets + class FormComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + FORM_ID = "llm-embedding-set-form" + + options :connection + + private + + def http_method + model.persisted? ? :patch : :post + end + + def form_url + model.persisted? ? embedding_set_path(model) : embedding_sets_path + end + + def model_options + LlmConnections::SelectableModelsQuery + .new(connection, OpenProject::Llm::Features[:semantic_search], bound_model_id: model.model_id_in_database) + .call + end + end + end +end diff --git a/modules/llm/app/components/llm_connections/embedding_sets/row_component.rb b/modules/llm/app/components/llm_connections/embedding_sets/row_component.rb new file mode 100644 index 000000000000..14aaa337cd89 --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/row_component.rb @@ -0,0 +1,98 @@ +# 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 + module EmbeddingSets + class RowComponent < OpPrimer::BorderBoxRowComponent + alias_method :embedding_set, :model + + def row_data + { test_selector: "llm-embedding-set--row-#{embedding_set.id}" } + end + + def title + safe_join( + [ + render(Primer::Beta::Text.new(font_weight: :bold)) { embedding_set.title }, + description + ].compact + ) + end + + delegate :model_id, :context_token_length, :embedding_vector_dimensions, to: :embedding_set + + def state + render(Primer::Beta::Text.new(color: :subtle)) do + I18n.t("admin.embedding_sets.states.#{embedding_set.state}") + end + end + + def button_links + [action_menu] + end + + private + + def description + return if embedding_set.description.blank? + + render(Primer::Beta::Text.new(tag: :div, color: :subtle, font_size: :small, classes: "ellipsis")) do + embedding_set.description + end + end + + def action_menu + render(Primer::Alpha::ActionMenu.new(anchor_align: :end, + test_selector: "llm-embedding-set--menu-#{embedding_set.id}")) do |menu| + menu.with_show_button(icon: "kebab-horizontal", "aria-label": I18n.t(:label_more), scheme: :invisible) + + menu.with_item( + label: I18n.t(:button_edit), + tag: :a, + href: edit_dialog_embedding_set_path(embedding_set), + content_arguments: { data: { controller: "async-dialog" } } + ) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + + menu.with_item( + label: I18n.t("admin.embedding_sets.menu.delete"), + scheme: :danger, + tag: :a, + href: deletion_dialog_embedding_set_path(embedding_set), + content_arguments: { data: { controller: "async-dialog" } } + ) do |item| + item.with_leading_visual_icon(icon: :trash) + end + end + end + end + end +end diff --git a/modules/llm/app/components/llm_connections/embedding_sets/table_component.rb b/modules/llm/app/components/llm_connections/embedding_sets/table_component.rb new file mode 100644 index 000000000000..5099eb0b1bfb --- /dev/null +++ b/modules/llm/app/components/llm_connections/embedding_sets/table_component.rb @@ -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 + module EmbeddingSets + class TableComponent < OpPrimer::BorderBoxTableComponent + columns :title, :model_id, :context_token_length, :embedding_vector_dimensions, :state + + main_column :title + + mobile_columns :title, :state + + mobile_labels :state + + def has_actions? = true + + def has_footer? = false + + def mobile_title = I18n.t("admin.embedding_sets.box.title") + + def headers + [ + [:title, { caption: EmbeddingSet.human_attribute_name(:title) }], + [:model_id, { caption: EmbeddingSet.human_attribute_name(:model_id) }], + [:context_token_length, { caption: EmbeddingSet.human_attribute_name(:context_token_length) }], + [:embedding_vector_dimensions, { caption: EmbeddingSet.human_attribute_name(:embedding_vector_dimensions) }], + [:state, { caption: EmbeddingSet.human_attribute_name(:state) }] + ] + end + + def blank_title = I18n.t("admin.embedding_sets.box.blank_title") + + def blank_description = I18n.t("admin.embedding_sets.box.blank_description") + + def blank_icon = :database + end + end +end diff --git a/modules/llm/app/contracts/embedding_sets/base_contract.rb b/modules/llm/app/contracts/embedding_sets/base_contract.rb new file mode 100644 index 000000000000..091f46d65ff7 --- /dev/null +++ b/modules/llm/app/contracts/embedding_sets/base_contract.rb @@ -0,0 +1,41 @@ +# 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 EmbeddingSets + class BaseContract < ::ModelContract + include RequiresAdminGuard + + attribute :title + attribute :description + attribute :model_id + attribute :embedding_vector_dimensions + attribute :context_token_length + end +end diff --git a/modules/llm/app/contracts/embedding_sets/create_contract.rb b/modules/llm/app/contracts/embedding_sets/create_contract.rb new file mode 100644 index 000000000000..f897ce8328d0 --- /dev/null +++ b/modules/llm/app/contracts/embedding_sets/create_contract.rb @@ -0,0 +1,34 @@ +# 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 EmbeddingSets + class CreateContract < BaseContract + end +end diff --git a/modules/llm/app/contracts/embedding_sets/delete_contract.rb b/modules/llm/app/contracts/embedding_sets/delete_contract.rb new file mode 100644 index 000000000000..d38d6e40e2fc --- /dev/null +++ b/modules/llm/app/contracts/embedding_sets/delete_contract.rb @@ -0,0 +1,35 @@ +# 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 EmbeddingSets + class DeleteContract < ::DeleteContract + delete_permission :admin + end +end diff --git a/modules/llm/app/contracts/embedding_sets/update_contract.rb b/modules/llm/app/contracts/embedding_sets/update_contract.rb new file mode 100644 index 000000000000..c5fb1ef35791 --- /dev/null +++ b/modules/llm/app/contracts/embedding_sets/update_contract.rb @@ -0,0 +1,34 @@ +# 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 EmbeddingSets + class UpdateContract < BaseContract + end +end diff --git a/modules/llm/app/controllers/admin/embedding_sets_controller.rb b/modules/llm/app/controllers/admin/embedding_sets_controller.rb new file mode 100644 index 000000000000..f604e954e84a --- /dev/null +++ b/modules/llm/app/controllers/admin/embedding_sets_controller.rb @@ -0,0 +1,127 @@ +# 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 EmbeddingSetsController < ApplicationController + include OpTurbo::ComponentStream + + layout "admin" + menu_item :llm_connection + + before_action :require_feature + before_action :require_admin + before_action :set_connection + before_action :require_enabled_connection + before_action :find_embedding_set, only: %i[edit_dialog update deletion_dialog destroy] + + def new_dialog + respond_with_dialog LlmConnections::EmbeddingSets::DialogComponent.new(EmbeddingSet.new, connection: @connection) + end + + def edit_dialog + respond_with_dialog LlmConnections::EmbeddingSets::DialogComponent.new(@embedding_set, connection: @connection) + end + + def create + result = ::EmbeddingSets::CreateService + .new(user: current_user) + .call(embedding_set_params) + + respond_to_save(result, notice: t(:notice_successful_create)) + end + + def update + result = ::EmbeddingSets::UpdateService + .new(user: current_user, model: @embedding_set) + .call(embedding_set_params) + + respond_to_save(result, notice: t(:notice_successful_update)) + end + + def deletion_dialog + respond_with_dialog LlmConnections::EmbeddingSets::DeleteDialogComponent.new(@embedding_set) + end + + def destroy + result = ::EmbeddingSets::DeleteService + .new(user: current_user, model: @embedding_set) + .call + + if result.success? + flash[:notice] = t(:notice_successful_delete) + else + flash[:error] = result.errors.full_messages + end + + redirect_to llm_feature_bindings_path, status: :see_other + end + + private + + def respond_to_save(result, notice:) + result.on_success do + flash[:notice] = notice + redirect_to llm_feature_bindings_path, status: :see_other + end + + result.on_failure do + update_via_turbo_stream( + component: LlmConnections::EmbeddingSets::FormComponent.new(result.result, connection: @connection), + status: :unprocessable_entity + ) + respond_with_turbo_streams + end + end + + def require_feature + render_404 unless OpenProject::FeatureDecisions.llm_connection_active? && + OpenProject::FeatureDecisions.semantic_search_active? + end + + def set_connection + @connection = LlmConnection.active_connection + end + + def require_enabled_connection + return if Setting.llm_features_enabled? && @connection.configured? + + flash[:notice] = t("admin.llm_connections.disabled_notice") + redirect_to llm_connection_path, status: :see_other + end + + def find_embedding_set + @embedding_set = EmbeddingSet.find(params.expect(:id)) + end + + def embedding_set_params + params.expect(embedding_set: %i[title description model_id embedding_vector_dimensions context_token_length]) + end + end +end diff --git a/modules/llm/app/forms/embedding_sets/form.rb b/modules/llm/app/forms/embedding_sets/form.rb new file mode 100644 index 000000000000..1c7dd509f546 --- /dev/null +++ b/modules/llm/app/forms/embedding_sets/form.rb @@ -0,0 +1,106 @@ +# 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 EmbeddingSets + class Form < ApplicationForm + # Primer::Forms::Base.new assigns the builder itself and calls this with the + # remaining keywords, so the builder must not appear in the signature. + def initialize(model_options:) + super() + @model_options = model_options + end + + form do |f| + f.text_field( + name: :title, + label: EmbeddingSet.human_attribute_name(:title), + required: true, + autofocus: true + ) + + f.text_area( + name: :description, + label: EmbeddingSet.human_attribute_name(:description), + rows: 3 + ) + + f.autocompleter( + name: :model_id, + label: EmbeddingSet.human_attribute_name(:model_id), + caption: I18n.t("admin.embedding_sets.form.model_caption"), + required: true, + autocomplete_options: { + decorated: true, + inputValue: selected_model_id, + append_to: "##{LlmConnections::EmbeddingSets::DialogComponent::DIALOG_ID}", + data: { test_selector: "llm-embedding-set--model" } + } + ) do |list| + model_options.each do |option| + list.option(label: option_label(option), + value: option.model_id, + selected: selected_model_id == option.model_id) + end + end + + f.text_field( + name: :context_token_length, + type: :number, + min: 1, + label: EmbeddingSet.human_attribute_name(:context_token_length), + caption: I18n.t("admin.embedding_sets.form.context_token_length_caption"), + required: true, + input_width: :small + ) + + f.text_field( + name: :embedding_vector_dimensions, + type: :number, + min: 1, + label: EmbeddingSet.human_attribute_name(:embedding_vector_dimensions), + caption: I18n.t("admin.embedding_sets.form.embedding_vector_dimensions_caption"), + required: true, + input_width: :small + ) + end + + private + + attr_reader :model_options + + def selected_model_id = model.model_id + + def option_label(option) + return option.model_id if option.qualifies + + I18n.t("admin.embedding_sets.form.option_unqualified", model: option.model_id) + end + end +end diff --git a/modules/llm/app/models/embedding_set.rb b/modules/llm/app/models/embedding_set.rb new file mode 100644 index 000000000000..def43dc57b8f --- /dev/null +++ b/modules/llm/app/models/embedding_set.rb @@ -0,0 +1,53 @@ +# 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. +#++ + +class EmbeddingSet < ApplicationRecord + enum :state, { + created: "created", + ingesting: "ingesting", + error: "error", + ready: "ready" + } + + validates :title, presence: true + validates :model_id, presence: true, length: { maximum: LlmModel::MAX_EXTERNAL_ID_LENGTH } + validates :embedding_vector_dimensions, :context_token_length, + numericality: { only_integer: true, greater_than: 0 } + + validate :model_offered, if: -> { model_id.present? && model_id_changed? } + + private + + def model_offered + return if LlmConnection.active_connection.embedding_model_ids.include?(model_id) + + errors.add(:model_id, :not_offered) + end +end diff --git a/modules/llm/app/services/embedding_sets/create_service.rb b/modules/llm/app/services/embedding_sets/create_service.rb new file mode 100644 index 000000000000..ebbdadb1defa --- /dev/null +++ b/modules/llm/app/services/embedding_sets/create_service.rb @@ -0,0 +1,34 @@ +# 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 EmbeddingSets + class CreateService < ::BaseServices::Create + end +end diff --git a/modules/llm/app/services/embedding_sets/delete_service.rb b/modules/llm/app/services/embedding_sets/delete_service.rb new file mode 100644 index 000000000000..0936ff85431d --- /dev/null +++ b/modules/llm/app/services/embedding_sets/delete_service.rb @@ -0,0 +1,34 @@ +# 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 EmbeddingSets + class DeleteService < ::BaseServices::Delete + end +end diff --git a/modules/llm/app/services/embedding_sets/set_attributes_service.rb b/modules/llm/app/services/embedding_sets/set_attributes_service.rb new file mode 100644 index 000000000000..6967395bfaa7 --- /dev/null +++ b/modules/llm/app/services/embedding_sets/set_attributes_service.rb @@ -0,0 +1,34 @@ +# 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 EmbeddingSets + class SetAttributesService < ::BaseServices::SetAttributes + end +end diff --git a/modules/llm/app/services/embedding_sets/update_service.rb b/modules/llm/app/services/embedding_sets/update_service.rb new file mode 100644 index 000000000000..d68d80a93354 --- /dev/null +++ b/modules/llm/app/services/embedding_sets/update_service.rb @@ -0,0 +1,34 @@ +# 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 EmbeddingSets + class UpdateService < ::BaseServices::Update + end +end diff --git a/modules/llm/app/views/admin/llm_feature_bindings/index.html.erb b/modules/llm/app/views/admin/llm_feature_bindings/index.html.erb index 7e38cac13451..2354d2e7e640 100644 --- a/modules/llm/app/views/admin/llm_feature_bindings/index.html.erb +++ b/modules/llm/app/views/admin/llm_feature_bindings/index.html.erb @@ -57,3 +57,7 @@ See COPYRIGHT and LICENSE files for more details. ) ) %> <% end %> + +<% if OpenProject::FeatureDecisions.semantic_search_active? %> + <%= render(LlmConnections::EmbeddingSets::BoxComponent.new(embedding_sets: EmbeddingSet.order(:created_at))) %> +<% end %> diff --git a/modules/llm/config/locales/en.yml b/modules/llm/config/locales/en.yml index 89706f5ccd71..7d7dd43508db 100644 --- a/modules/llm/config/locales/en.yml +++ b/modules/llm/config/locales/en.yml @@ -2,6 +2,14 @@ en: activerecord: attributes: + embedding_set: + context_token_length: "Context length" + description: "Description" + embedding_vector_dimensions: "Dimensions" + # ActiveRecord::Base.human_attribute_name strips the _id suffix. + model: "Model" + state: "Status" + title: "Display name" llm_capability_verdict: capability: "Capability" model_id: "Model" @@ -27,6 +35,10 @@ en: external: "Model name" errors: models: + embedding_set: + attributes: + model_id: + not_offered: "is not an embedding model offered by the configured LLM server." llm_connection: attributes: api_format: @@ -67,6 +79,32 @@ en: external_id: conflicting_capabilities: "has capability assertions stored under it that conflict with those of this model." admin: + embedding_sets: + box: + blank_description: "Create an embedding set to choose the model, context length and dimensions used to index work packages." + blank_title: "No embedding sets yet" + caption: "Each set describes one way of indexing work packages for semantic search." + new: "New embedding set" + title: "Embedding sets" + destroy: + description: "The set and its settings will be removed. This cannot be undone." + heading: "Delete %{title}?" + title: "Delete embedding set" + dialog: + edit_title: "Edit \"%{title}\"" + new_title: "New embedding set" + form: + context_token_length_caption: "Maximum number of tokens embedded per work package." + embedding_vector_dimensions_caption: "How many numbers each vector has." + model_caption: "Only models known to create embeddings are offered." + option_unqualified: "%{model} (no longer eligible)" + menu: + delete: "Delete…" + states: + created: "Not ingested" + error: "Failed" + ingesting: "Ingesting" + ready: "Ready" llm_connections: delete_api_key: configured_from_env: "This connection is configured through the environment and cannot be changed here." diff --git a/modules/llm/config/routes.rb b/modules/llm/config/routes.rb index d69c73306cfe..d98612fb7e06 100644 --- a/modules/llm/config/routes.rb +++ b/modules/llm/config/routes.rb @@ -59,6 +59,17 @@ # of a registered feature, and a feature may not have a row yet. resources :llm_feature_bindings, only: %i[index update], controller: "admin/llm_feature_bindings" + resources :embedding_sets, only: %i[create update destroy], controller: "admin/embedding_sets" do + collection do + get :new_dialog, defaults: { format: :turbo_stream } + end + + member do + get :edit_dialog, defaults: { format: :turbo_stream } + get :deletion_dialog, defaults: { format: :turbo_stream } + end + end + resources :work_package_embeddings, only: [], controller: "admin/work_package_embeddings" do collection do post :reindex diff --git a/modules/llm/db/migrate/20261014120000_create_embedding_sets.rb b/modules/llm/db/migrate/20261014120000_create_embedding_sets.rb new file mode 100644 index 000000000000..1b718d894978 --- /dev/null +++ b/modules/llm/db/migrate/20261014120000_create_embedding_sets.rb @@ -0,0 +1,44 @@ +# 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. +#++ + +class CreateEmbeddingSets < ActiveRecord::Migration[8.1] + def change + create_table :embedding_sets do |t| + t.string :title, null: false + t.text :description + t.string :model_id, null: false, limit: 512 + t.integer :embedding_vector_dimensions, null: false + t.integer :context_token_length, null: false + t.string :state, null: false, default: "created" + + t.timestamps null: false + end + end +end diff --git a/modules/llm/spec/contracts/embedding_sets/create_contract_spec.rb b/modules/llm/spec/contracts/embedding_sets/create_contract_spec.rb new file mode 100644 index 000000000000..021b2a6bac5d --- /dev/null +++ b/modules/llm/spec/contracts/embedding_sets/create_contract_spec.rb @@ -0,0 +1,47 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper +require "contracts/shared/model_contract_shared_context" + +RSpec.describe EmbeddingSets::CreateContract do + include_context "ModelContract shared context" + include_context "with an embedding model offered" + + let(:embedding_set) { build(:embedding_set) } + let(:contract) { described_class.new(embedding_set, current_user) } + + it_behaves_like "contract is valid for active admins and invalid for regular users" + + include_examples "contract reuses the model errors" do + let(:current_user) { build_stubbed(:admin) } + end +end diff --git a/modules/llm/spec/contracts/embedding_sets/delete_contract_spec.rb b/modules/llm/spec/contracts/embedding_sets/delete_contract_spec.rb new file mode 100644 index 000000000000..cb142b90fae6 --- /dev/null +++ b/modules/llm/spec/contracts/embedding_sets/delete_contract_spec.rb @@ -0,0 +1,43 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper +require "contracts/shared/model_contract_shared_context" + +RSpec.describe EmbeddingSets::DeleteContract do + include_context "ModelContract shared context" + include_context "with an embedding model offered" + + let(:embedding_set) { create(:embedding_set) } + let(:contract) { described_class.new(embedding_set, current_user) } + + it_behaves_like "contract is valid for active admins and invalid for regular users" +end diff --git a/modules/llm/spec/contracts/embedding_sets/update_contract_spec.rb b/modules/llm/spec/contracts/embedding_sets/update_contract_spec.rb new file mode 100644 index 000000000000..939815e5ec38 --- /dev/null +++ b/modules/llm/spec/contracts/embedding_sets/update_contract_spec.rb @@ -0,0 +1,47 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper +require "contracts/shared/model_contract_shared_context" + +RSpec.describe EmbeddingSets::UpdateContract do + include_context "ModelContract shared context" + include_context "with an embedding model offered" + + let(:embedding_set) { create(:embedding_set) } + let(:contract) { described_class.new(embedding_set, current_user) } + + it_behaves_like "contract is valid for active admins and invalid for regular users" + + include_examples "contract reuses the model errors" do + let(:current_user) { build_stubbed(:admin) } + end +end diff --git a/modules/llm/spec/factories/embedding_set_factory.rb b/modules/llm/spec/factories/embedding_set_factory.rb new file mode 100644 index 000000000000..44f2045fa8c8 --- /dev/null +++ b/modules/llm/spec/factories/embedding_set_factory.rb @@ -0,0 +1,39 @@ +# 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. +#++ + +FactoryBot.define do + factory :embedding_set do + sequence(:title) { |n| "Embedding set #{n}" } + description { nil } + model_id { "bge-m3" } + embedding_vector_dimensions { 1024 } + context_token_length { 8192 } + end +end diff --git a/modules/llm/spec/features/admin/llm_connection_spec.rb b/modules/llm/spec/features/admin/llm_connection_spec.rb index 58cc9f9956e3..e18371d89b35 100644 --- a/modules/llm/spec/features/admin/llm_connection_spec.rb +++ b/modules/llm/spec/features/admin/llm_connection_spec.rb @@ -261,5 +261,63 @@ def choose_action(item) expect(page).to have_no_test_selector("llm-feature-binding--dimensions-description_assistant") expect(page).to be_axe_clean.within("#content") end + + describe "embedding sets" do + include Components::Autocompleter::NgSelectAutocompleteHelpers + + before do + connection.capability_verdicts.create!(model_id: "bge-m3", capability: "embeddings", + state: "supported", source: "probe", checked_at: Time.current) + end + + def within_embedding_set_menu(embedding_set, &) + find_test_selector("llm-embedding-set--menu-#{embedding_set.id}").click_link_or_button + within("anchored-position", &) + end + + it "creates, edits and deletes a set" do + visit llm_feature_bindings_path + + find_test_selector("llm-embedding-set--new-button").click + + within_test_selector("llm-embedding-set-dialog") do + fill_in "Display name", with: " " + fill_in "Context length", with: "2048" + fill_in "Dimensions", with: "768" + select_autocomplete(find_test_selector("llm-embedding-set--model"), + query: "bge-m3", + results_selector: "#llm-embedding-set-dialog") + expect(page).to be_axe_clean + + click_on "Create" + + expect(page).to have_text("can't be blank") + fill_in "Display name", with: "Nomic small" + click_on "Create" + end + + expect_flash(message: "Successful creation.") + embedding_set = EmbeddingSet.find_by!(title: "Nomic small") + within_test_selector("llm-embedding-set--row-#{embedding_set.id}") do + expect(page).to have_text("bge-m3") + expect(page).to have_text("Not ingested") + end + + within_embedding_set_menu(embedding_set) { click_on "Edit" } + within_test_selector("llm-embedding-set-dialog") do + fill_in "Display name", with: "Nomic large" + click_on "Save" + end + + expect_flash(message: "Successful update.") + expect(page).to have_test_selector("llm-embedding-set--row-#{embedding_set.id}", text: "Nomic large") + + within_embedding_set_menu(embedding_set) { click_on "Delete…" } + within_test_selector("llm-embedding-set--delete-dialog") { click_on "Delete" } + + expect_flash(message: "Successful deletion.") + expect(page).to have_no_test_selector("llm-embedding-set--row-#{embedding_set.id}") + end + end end end diff --git a/modules/llm/spec/models/embedding_set_spec.rb b/modules/llm/spec/models/embedding_set_spec.rb new file mode 100644 index 000000000000..4919c8e9cab1 --- /dev/null +++ b/modules/llm/spec/models/embedding_set_spec.rb @@ -0,0 +1,75 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe EmbeddingSet do + include_context "with an embedding model offered" + + subject(:embedding_set) { build(:embedding_set) } + + it "starts out as created" do + expect(embedding_set.save).to be(true) + expect(embedding_set.reload).to be_created + end + + it "requires a title" do + embedding_set.title = "" + + expect(embedding_set).not_to be_valid + expect(embedding_set.errors.details[:title]).to include(error: :blank) + end + + %i[embedding_vector_dimensions context_token_length].each do |attribute| + it "requires a positive integer #{attribute}" do + embedding_set.public_send(:"#{attribute}=", 0) + + expect(embedding_set).not_to be_valid + expect(embedding_set.errors.details[attribute]).to include(a_hash_including(error: :greater_than)) + end + end + + it "rejects a chat model" do + embedding_set.model_id = "qwen3.6-27b" + + expect(embedding_set).not_to be_valid + expect(embedding_set.errors.details[:model_id]).to include(error: :not_offered) + end + + it "keeps a stored model that is no longer offered" do + embedding_set.save! + connection.capability_verdicts.destroy_all + + embedding_set.title = "Renamed" + + expect(embedding_set).to be_valid + end +end diff --git a/modules/llm/spec/requests/admin/embedding_sets_spec.rb b/modules/llm/spec/requests/admin/embedding_sets_spec.rb new file mode 100644 index 000000000000..9c3c220a2425 --- /dev/null +++ b/modules/llm/spec/requests/admin/embedding_sets_spec.rb @@ -0,0 +1,153 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe "Admin embedding sets", :skip_csrf, + type: :rails_request, with_flag: { llm_connection: true, semantic_search: true }, + with_settings: { llm_features_enabled: true } do + include_context "with an embedding model offered" + + shared_let(:admin) { create(:admin) } + + let(:turbo_headers) { { "Accept" => "text/vnd.turbo-stream.html" } } + let(:valid_params) do + { embedding_set: { title: "Nomic small", description: "Trial", model_id: "bge-m3", + embedding_vector_dimensions: 768, context_token_length: 2048 } } + end + + before { login_as admin } + + describe "with the semantic search flag off", with_flag: { llm_connection: true, semantic_search: false } do + it "does not expose the endpoints" do + get new_dialog_embedding_sets_path + expect(response).to have_http_status(:not_found) + + post embedding_sets_path, params: valid_params + expect(response).to have_http_status(:not_found) + expect(EmbeddingSet.count).to eq(0) + end + end + + describe "as a non-admin" do + before { login_as create(:user) } + + it "refuses to create a set" do + post embedding_sets_path, params: valid_params + + expect(response).to have_http_status(:forbidden) + expect(EmbeddingSet.count).to eq(0) + end + end + + describe "GET /admin/llm_feature_bindings" do + it "lists the embedding sets below the features" do + create(:embedding_set, title: "Qwen3 baseline") + + get llm_feature_bindings_path + + expect(response).to have_http_status(:ok) + expect(page).to have_test_selector("llm-embedding-set--box", text: "Qwen3 baseline") + expect(page).to have_test_selector("llm-embedding-set--box", text: "Not ingested") + end + + it "hides the box without the semantic search flag", with_flag: { llm_connection: true, semantic_search: false } do + get llm_feature_bindings_path + + expect(page).to have_no_test_selector("llm-embedding-set--box") + end + end + + describe "GET new_dialog" do + it "offers only embedding models" do + get new_dialog_embedding_sets_path, headers: turbo_headers + + expect(response).to have_http_status(:ok) + expect(response.body).to include("New embedding set") + + items = Nokogiri::HTML(response.parsed_body).at_css("opce-autocompleter")["data-items"] + expect(JSON.parse(items).pluck("id")).to contain_exactly("bge-m3") + end + end + + describe "POST /admin/embedding_sets" do + it "creates the set and returns to the feature configuration" do + post embedding_sets_path, params: valid_params + + expect(response).to redirect_to(llm_feature_bindings_path) + expect(EmbeddingSet.sole).to have_attributes(title: "Nomic small", model_id: "bge-m3", + embedding_vector_dimensions: 768, + context_token_length: 2048, state: "created") + end + + it "re-renders the form with errors" do + post embedding_sets_path, params: { embedding_set: valid_params[:embedding_set].merge(model_id: "qwen3.6-27b") }, + headers: turbo_headers + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to include("is not an embedding model offered by the configured LLM server.") + expect(EmbeddingSet.count).to eq(0) + end + end + + describe "existing set" do + let!(:embedding_set) { create(:embedding_set, title: "Qwen3 baseline") } + + it "renders the edit dialog" do + get edit_dialog_embedding_set_path(embedding_set), headers: turbo_headers + + expect(response).to have_http_status(:ok) + expect(response.body).to include("Edit "Qwen3 baseline"") + end + + it "updates the set" do + patch embedding_set_path(embedding_set), + params: { embedding_set: { title: "Qwen3 production", context_token_length: 4096 } } + + expect(response).to redirect_to(llm_feature_bindings_path) + expect(embedding_set.reload).to have_attributes(title: "Qwen3 production", context_token_length: 4096) + end + + it "renders the deletion dialog" do + get deletion_dialog_embedding_set_path(embedding_set), headers: turbo_headers + + expect(response).to have_http_status(:ok) + expect(response.body).to include("Delete Qwen3 baseline?") + end + + it "deletes the set" do + delete embedding_set_path(embedding_set) + + expect(response).to redirect_to(llm_feature_bindings_path) + expect(EmbeddingSet.count).to eq(0) + end + end +end diff --git a/modules/llm/spec/support/shared/embedding_model_context.rb b/modules/llm/spec/support/shared/embedding_model_context.rb new file mode 100644 index 000000000000..eef92ebfc9c6 --- /dev/null +++ b/modules/llm/spec/support/shared/embedding_model_context.rb @@ -0,0 +1,38 @@ +# 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. +#++ + +RSpec.shared_context "with an embedding model offered" do + let!(:connection) { create(:llm_connection, :with_models) } + + before do + connection.capability_verdicts.create!(model_id: "bge-m3", capability: "embeddings", + state: "supported", source: "probe", checked_at: Time.current) + end +end