2024-01-29 16:04:25 -03:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "enum_site_setting"
|
|
|
|
|
|
|
|
module DiscourseAi
|
|
|
|
module Configuration
|
|
|
|
class LlmEnumerator < ::EnumSiteSetting
|
2024-10-22 11:16:02 +11:00
|
|
|
def self.global_usage
|
|
|
|
rval = Hash.new { |h, k| h[k] = [] }
|
|
|
|
|
|
|
|
if SiteSetting.ai_bot_enabled
|
|
|
|
LlmModel
|
|
|
|
.where("enabled_chat_bot = ?", true)
|
|
|
|
.pluck(:id)
|
|
|
|
.each { |llm_id| rval[llm_id] << { type: :ai_bot } }
|
|
|
|
end
|
|
|
|
|
2025-04-24 17:22:19 +11:00
|
|
|
# this is unconditional, so it is clear that we always signal configuration
|
|
|
|
AiPersona
|
|
|
|
.where("default_llm_id IS NOT NULL")
|
|
|
|
.pluck(:default_llm_id, :name, :id)
|
|
|
|
.each { |llm_id, name, id| rval[llm_id] << { type: :ai_persona, name: name, id: id } }
|
|
|
|
|
2024-10-22 11:16:02 +11:00
|
|
|
if SiteSetting.ai_helper_enabled
|
|
|
|
model_id = SiteSetting.ai_helper_model.split(":").last.to_i
|
2025-04-24 17:22:19 +11:00
|
|
|
rval[model_id] << { type: :ai_helper } if model_id != 0
|
|
|
|
end
|
|
|
|
|
|
|
|
if SiteSetting.ai_helper_image_caption_model
|
|
|
|
model_id = SiteSetting.ai_helper_image_caption_model.split(":").last.to_i
|
|
|
|
rval[model_id] << { type: :ai_helper_image_caption } if model_id != 0
|
2024-10-22 11:16:02 +11:00
|
|
|
end
|
|
|
|
|
|
|
|
if SiteSetting.ai_summarization_enabled
|
2025-04-02 12:54:47 -03:00
|
|
|
summarization_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)
|
|
|
|
model_id = summarization_persona.default_llm_id || LlmModel.last&.id
|
|
|
|
|
2024-10-22 11:16:02 +11:00
|
|
|
rval[model_id] << { type: :ai_summarization }
|
|
|
|
end
|
|
|
|
|
|
|
|
if SiteSetting.ai_embeddings_semantic_search_enabled
|
|
|
|
model_id = SiteSetting.ai_embeddings_semantic_search_hyde_model.split(":").last.to_i
|
|
|
|
rval[model_id] << { type: :ai_embeddings_semantic_search }
|
|
|
|
end
|
|
|
|
|
2024-12-27 07:12:29 +09:00
|
|
|
if SiteSetting.ai_spam_detection_enabled && AiModerationSetting.spam.present?
|
2024-12-12 09:17:25 +11:00
|
|
|
model_id = AiModerationSetting.spam[:llm_model_id]
|
|
|
|
rval[model_id] << { type: :ai_spam }
|
|
|
|
end
|
|
|
|
|
2025-04-24 17:22:19 +11:00
|
|
|
if defined?(DiscourseAutomation::Automation)
|
|
|
|
DiscourseAutomation::Automation
|
|
|
|
.joins(:fields)
|
|
|
|
.where(script: %w[llm_report llm_triage])
|
|
|
|
.where("discourse_automation_fields.name = ?", "model")
|
|
|
|
.pluck(
|
|
|
|
"metadata ->> 'value', discourse_automation_automations.name, discourse_automation_automations.id",
|
|
|
|
)
|
|
|
|
.each do |model_text, name, id|
|
|
|
|
next if model_text.blank?
|
|
|
|
model_id = model_text.split("custom:").last.to_i
|
|
|
|
if model_id.present?
|
|
|
|
if model_text =~ /custom:(\d+)/
|
|
|
|
rval[model_id] << { type: :automation, name: name, id: id }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-10-22 11:16:02 +11:00
|
|
|
rval
|
|
|
|
end
|
|
|
|
|
2024-01-29 16:04:25 -03:00
|
|
|
def self.valid_value?(val)
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
FEATURE: PDF support for rag pipeline (#1118)
This PR introduces several enhancements and refactorings to the AI Persona and RAG (Retrieval-Augmented Generation) functionalities within the discourse-ai plugin. Here's a breakdown of the changes:
**1. LLM Model Association for RAG and Personas:**
- **New Database Columns:** Adds `rag_llm_model_id` to both `ai_personas` and `ai_tools` tables. This allows specifying a dedicated LLM for RAG indexing, separate from the persona's primary LLM. Adds `default_llm_id` and `question_consolidator_llm_id` to `ai_personas`.
- **Migration:** Includes a migration (`20250210032345_migrate_persona_to_llm_model_id.rb`) to populate the new `default_llm_id` and `question_consolidator_llm_id` columns in `ai_personas` based on the existing `default_llm` and `question_consolidator_llm` string columns, and a post migration to remove the latter.
- **Model Changes:** The `AiPersona` and `AiTool` models now `belong_to` an `LlmModel` via `rag_llm_model_id`. The `LlmModel.proxy` method now accepts an `LlmModel` instance instead of just an identifier. `AiPersona` now has `default_llm_id` and `question_consolidator_llm_id` attributes.
- **UI Updates:** The AI Persona and AI Tool editors in the admin panel now allow selecting an LLM for RAG indexing (if PDF/image support is enabled). The RAG options component displays an LLM selector.
- **Serialization:** The serializers (`AiCustomToolSerializer`, `AiCustomToolListSerializer`, `LocalizedAiPersonaSerializer`) have been updated to include the new `rag_llm_model_id`, `default_llm_id` and `question_consolidator_llm_id` attributes.
**2. PDF and Image Support for RAG:**
- **Site Setting:** Introduces a new hidden site setting, `ai_rag_pdf_images_enabled`, to control whether PDF and image files can be indexed for RAG. This defaults to `false`.
- **File Upload Validation:** The `RagDocumentFragmentsController` now checks the `ai_rag_pdf_images_enabled` setting and allows PDF, PNG, JPG, and JPEG files if enabled. Error handling is included for cases where PDF/image indexing is attempted with the setting disabled.
- **PDF Processing:** Adds a new utility class, `DiscourseAi::Utils::PdfToImages`, which uses ImageMagick (`magick`) to convert PDF pages into individual PNG images. A maximum PDF size and conversion timeout are enforced.
- **Image Processing:** A new utility class, `DiscourseAi::Utils::ImageToText`, is included to handle OCR for the images and PDFs.
- **RAG Digestion Job:** The `DigestRagUpload` job now handles PDF and image uploads. It uses `PdfToImages` and `ImageToText` to extract text and create document fragments.
- **UI Updates:** The RAG uploader component now accepts PDF and image file types if `ai_rag_pdf_images_enabled` is true. The UI text is adjusted to indicate supported file types.
**3. Refactoring and Improvements:**
- **LLM Enumeration:** The `DiscourseAi::Configuration::LlmEnumerator` now provides a `values_for_serialization` method, which returns a simplified array of LLM data (id, name, vision_enabled) suitable for use in serializers. This avoids exposing unnecessary details to the frontend.
- **AI Helper:** The `AiHelper::Assistant` now takes optional `helper_llm` and `image_caption_llm` parameters in its constructor, allowing for greater flexibility.
- **Bot and Persona Updates:** Several updates were made across the codebase, changing the string based association to a LLM to the new model based.
- **Audit Logs:** The `DiscourseAi::Completions::Endpoints::Base` now formats raw request payloads as pretty JSON for easier auditing.
- **Eval Script:** An evaluation script is included.
**4. Testing:**
- The PR introduces a new eval system for LLMs, this allows us to test how functionality works across various LLM providers. This lives in `/evals`
2025-02-14 12:15:07 +11:00
|
|
|
# returns an array of hashes (id: , name:, vision_enabled:)
|
|
|
|
def self.values_for_serialization(allowed_seeded_llm_ids: nil)
|
|
|
|
builder = DB.build(<<~SQL)
|
|
|
|
SELECT id, display_name AS name, vision_enabled
|
|
|
|
FROM llm_models
|
|
|
|
/*where*/
|
|
|
|
SQL
|
|
|
|
|
|
|
|
if allowed_seeded_llm_ids.is_a?(Array) && !allowed_seeded_llm_ids.empty?
|
|
|
|
builder.where(
|
|
|
|
"id > 0 OR id IN (:allowed_seeded_llm_ids)",
|
|
|
|
allowed_seeded_llm_ids: allowed_seeded_llm_ids,
|
|
|
|
)
|
|
|
|
else
|
|
|
|
builder.where("id > 0")
|
|
|
|
end
|
|
|
|
|
|
|
|
builder.query_hash.map(&:symbolize_keys)
|
|
|
|
end
|
|
|
|
|
2024-12-12 09:17:25 +11:00
|
|
|
def self.values(allowed_seeded_llms: nil)
|
2024-06-21 16:35:31 -03:00
|
|
|
values = DB.query_hash(<<~SQL).map(&:symbolize_keys)
|
2024-06-19 18:01:35 -03:00
|
|
|
SELECT display_name AS name, id AS value
|
|
|
|
FROM llm_models
|
|
|
|
SQL
|
2024-01-29 16:04:25 -03:00
|
|
|
|
2024-12-12 09:17:25 +11:00
|
|
|
if allowed_seeded_llms.is_a?(Array)
|
|
|
|
values =
|
|
|
|
values.filter do |value_h|
|
2024-12-23 14:42:26 +09:00
|
|
|
value_h[:value] > 0 || allowed_seeded_llms.include?("#{value_h[:value]}")
|
2024-12-12 09:17:25 +11:00
|
|
|
end
|
|
|
|
end
|
2024-05-13 12:46:42 -03:00
|
|
|
|
2024-12-12 09:17:25 +11:00
|
|
|
values.each { |value_h| value_h[:value] = "custom:#{value_h[:value]}" }
|
2024-06-19 18:01:35 -03:00
|
|
|
values
|
2024-01-29 16:04:25 -03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|