mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-23 01:08:33 +02:00
feat: support for other providers of ai models
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,13 +1,20 @@
|
||||
use async_openai::types::ListModelResponse;
|
||||
use axum::{extract::State, response::IntoResponse, Form};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use common::storage::types::{
|
||||
analytics::Analytics,
|
||||
conversation::Conversation,
|
||||
system_prompts::{DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT},
|
||||
system_settings::SystemSettings,
|
||||
user::User,
|
||||
use common::{
|
||||
error::AppError,
|
||||
storage::types::{
|
||||
analytics::Analytics,
|
||||
conversation::Conversation,
|
||||
knowledge_entity::KnowledgeEntity,
|
||||
system_prompts::{DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT},
|
||||
system_settings::SystemSettings,
|
||||
text_chunk::TextChunk,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
html_state::HtmlState,
|
||||
@@ -25,6 +32,7 @@ pub struct AdminPanelData {
|
||||
users: i64,
|
||||
default_query_prompt: String,
|
||||
conversation_archive: Vec<Conversation>,
|
||||
available_models: ListModelResponse,
|
||||
}
|
||||
|
||||
pub async fn show_admin_panel(
|
||||
@@ -35,6 +43,12 @@ pub async fn show_admin_panel(
|
||||
let analytics = Analytics::get_current(&state.db).await?;
|
||||
let users_count = Analytics::get_users_amount(&state.db).await?;
|
||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||
let available_models = state
|
||||
.openai_client
|
||||
.models()
|
||||
.list()
|
||||
.await
|
||||
.map_err(|_e| AppError::InternalError("OpenAI error".to_string()))?;
|
||||
|
||||
Ok(TemplateResponse::new_template(
|
||||
"admin/base.html",
|
||||
@@ -42,6 +56,7 @@ pub async fn show_admin_panel(
|
||||
user,
|
||||
settings,
|
||||
analytics,
|
||||
available_models,
|
||||
users: users_count,
|
||||
default_query_prompt: DEFAULT_QUERY_SYSTEM_PROMPT.to_string(),
|
||||
conversation_archive,
|
||||
@@ -103,11 +118,14 @@ pub async fn toggle_registration_status(
|
||||
pub struct ModelSettingsInput {
|
||||
query_model: String,
|
||||
processing_model: String,
|
||||
embedding_model: String,
|
||||
embedding_dimensions: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ModelSettingsData {
|
||||
settings: SystemSettings,
|
||||
available_models: ListModelResponse,
|
||||
}
|
||||
|
||||
pub async fn update_model_settings(
|
||||
@@ -122,19 +140,77 @@ pub async fn update_model_settings(
|
||||
|
||||
let current_settings = SystemSettings::get_current(&state.db).await?;
|
||||
|
||||
// --- Determine if re-embedding is required ---
|
||||
let reembedding_needed = input
|
||||
.embedding_dimensions
|
||||
.is_some_and(|new_dims| new_dims != current_settings.embedding_dimensions);
|
||||
|
||||
// --- Create the new settings object immutably ---
|
||||
let new_settings = SystemSettings {
|
||||
query_model: input.query_model,
|
||||
processing_model: input.processing_model,
|
||||
..current_settings
|
||||
embedding_model: input.embedding_model,
|
||||
// Use new dimensions if provided, otherwise retain the current ones.
|
||||
embedding_dimensions: input
|
||||
.embedding_dimensions
|
||||
.unwrap_or(current_settings.embedding_dimensions),
|
||||
// Copy all other fields from the current settings.
|
||||
..current_settings.clone()
|
||||
};
|
||||
|
||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
||||
|
||||
if reembedding_needed {
|
||||
info!("Embedding dimensions changed. Spawning background re-embedding task...");
|
||||
|
||||
let db_for_task = state.db.clone();
|
||||
let openai_for_task = state.openai_client.clone();
|
||||
let new_model_for_task = new_settings.embedding_model.clone();
|
||||
let new_dims_for_task = new_settings.embedding_dimensions;
|
||||
|
||||
tokio::spawn(async move {
|
||||
// First, update all text chunks
|
||||
if let Err(e) = TextChunk::update_all_embeddings(
|
||||
&db_for_task,
|
||||
&openai_for_task,
|
||||
&new_model_for_task,
|
||||
new_dims_for_task,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Background re-embedding task failed for TextChunks: {}", e);
|
||||
}
|
||||
|
||||
// Second, update all knowledge entities
|
||||
if let Err(e) = KnowledgeEntity::update_all_embeddings(
|
||||
&db_for_task,
|
||||
&openai_for_task,
|
||||
&new_model_for_task,
|
||||
new_dims_for_task,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Background re-embedding task failed for KnowledgeEntities: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let available_models = state
|
||||
.openai_client
|
||||
.models()
|
||||
.list()
|
||||
.await
|
||||
.map_err(|_e| AppError::InternalError("Failed to get models".to_string()))?;
|
||||
|
||||
Ok(TemplateResponse::new_partial(
|
||||
"admin/base.html",
|
||||
"model_settings_form",
|
||||
ModelSettingsData {
|
||||
settings: new_settings,
|
||||
available_models,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ pub async fn delete_job(
|
||||
let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db).await?;
|
||||
|
||||
Ok(TemplateResponse::new_partial(
|
||||
"index/signed_in/active_jobs.html",
|
||||
"dashboard/active_jobs.html",
|
||||
"active_jobs_section",
|
||||
ActiveJobsData {
|
||||
user: user.clone(),
|
||||
|
||||
@@ -50,36 +50,94 @@
|
||||
<legend class="fieldset-legend">AI Models</legend>
|
||||
{% block model_settings_form %}
|
||||
<form hx-patch="/update-model-settings" hx-swap="outerHTML">
|
||||
<!-- Query Model -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Query Model</span>
|
||||
</label>
|
||||
<select name="query_model" class="select select-bordered w-full">
|
||||
<option value="gpt-4o-mini" {% if settings.query_model=="gpt-4o-mini" %}selected{% endif %}>GPT-4o Mini
|
||||
</option>
|
||||
<option value="gpt-4.1" {% if settings.query_model=="gpt-4.1" %}selected{% endif %}>GPT-4.1</option>
|
||||
<option value="gpt-4.1-mini" {% if settings.query_model=="gpt-4.1-mini" %}selected{% endif %}>GPT-4.1-mini
|
||||
{% for model in available_models.data %}
|
||||
<option value="{{model.id}}" {% if settings.query_model==model.id %} selected {% endif %}>{{model.id}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Model used for answering user queries</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control my-4">
|
||||
<!-- Processing Model -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Processing Model</span>
|
||||
</label>
|
||||
<select name="processing_model" class="select select-bordered w-full">
|
||||
<option value="gpt-4o-mini" {% if settings.query_model=="gpt-4o-mini" %}selected{% endif %}>GPT-4o Mini
|
||||
</option>
|
||||
<option value="gpt-4.1" {% if settings.query_model=="gpt-4.1" %}selected{% endif %}>GPT-4.1</option>
|
||||
<option value="gpt-4.1-mini" {% if settings.query_model=="gpt-4.1-mini" %}selected{% endif %}>GPT-4.1-mini
|
||||
{% for model in available_models.data %}
|
||||
<option value="{{model.id}}" {% if settings.processing_model==model.id %} selected {% endif %}>{{model.id}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Model used for content processing and ingestion</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save Model Settings</button>
|
||||
<!-- Embedding Model -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Embedding Model</span>
|
||||
</label>
|
||||
<select name="embedding_model" class="select select-bordered w-full">
|
||||
{% for model in available_models.data %}
|
||||
<option value="{{model.id}}" {% if settings.embedding_model==model.id %} selected {% endif %}>{{model.id}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Current used:
|
||||
<span class="font-mono">{{settings.embedding_model}} ({{settings.embedding_dimensions}} dims)</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Embedding Dimensions (Always Visible) -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label" for="embedding_dimensions">
|
||||
<span class="label-text">Embedding Dimensions</span>
|
||||
</label>
|
||||
<input type="number" id="embedding_dimensions" name="embedding_dimensions" class="input input-bordered w-full"
|
||||
value="{{ settings.embedding_dimensions }}" required />
|
||||
</div>
|
||||
|
||||
<!-- Conditional Alert -->
|
||||
<div id="embedding-change-alert" role="alert" class="alert alert-warning mt-2 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span><strong>Warning:</strong> Changing dimensions will require re-creating all embeddings. Make sure you
|
||||
look up what dimensions the model uses or use a model that allows specifying embedding dimensions</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-sm mt-4">Save Model Settings</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Use a self-executing function to avoid polluting the global scope
|
||||
// and to ensure it runs correctly after an HTMX swap.
|
||||
(() => {
|
||||
const dimensionInput = document.getElementById('embedding_dimensions');
|
||||
const alertElement = document.getElementById('embedding-change-alert');
|
||||
// The initial value is read directly from the template each time this script runs.
|
||||
const initialDimensions = '{{ settings.embedding_dimensions }}';
|
||||
|
||||
if (dimensionInput && alertElement) {
|
||||
// Use the 'input' event for immediate feedback as the user types.
|
||||
dimensionInput.addEventListener('input', (event) => {
|
||||
// Show alert if the current value is not the initial value. Hide it otherwise.
|
||||
if (event.target.value !== initialDimensions) {
|
||||
alertElement.classList.remove('hidden');
|
||||
} else {
|
||||
alertElement.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
</fieldset>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user