mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-21 08:03:33 +00:00
feat: add instance-level AI settings (#8453)
* feat: add instance-level AI settings with workspace fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add AI step to onboarding setup wizard Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: thread workspace prop through resource editor and disable chat offset Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "fix: thread workspace prop through resource editor and disable chat offset" This reverts commit 9fea9cc0c239f6432d1fef1487c45e74ab752e21. * fix: set workspace store and disable chat offset during AI setup step Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: thread workspace and disableChatOffset props through resource editors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: populate workspace and user stores for AI step path component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: initialize AI clients for test key during onboarding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract AI config state into InstanceAISettings component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move AI config state ownership into AISettings component Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Persist instance AI settings before navigation * Reload effective workspace AI state after save * Scope AI key tests to the rendered workspace * Add post-create AI onboarding for new workspaces * Unify instance AI settings header * Fix instance AI drawer offset on workspace selection * Add instance AI fallback settings behavior * Update sqlx metadata * Update sqlx metadata * Clarify active instance AI in workspace settings * Refresh workspace AI state after instance AI save * Declare instance AI summary in API schema * Normalize empty instance AI config handling * Clean up workspace AI settings UI * Unify AI config provider checks * Split AI settings metadata from effective config * Propagate instance AI cache invalidation across servers * Fix AI settings dirty state tracking * Update sqlx metadata --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a26a2e8092
commit
db5e03610d
@@ -8,8 +8,9 @@
|
||||
|
||||
// Re-export everything from windmill-api-workspaces
|
||||
pub use windmill_api_workspaces::workspaces::*;
|
||||
use windmill_api_workspaces::workspaces::{build_copilot_settings_state, InstanceAISummary};
|
||||
|
||||
use crate::ai::{AIConfig, AI_REQUEST_CACHE};
|
||||
use crate::ai::{invalidate_ai_request_cache_for_workspace, AIConfig};
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::teams_oss::{
|
||||
connect_teams, edit_teams_command, run_teams_message_test_job,
|
||||
@@ -24,7 +25,7 @@ use axum::{
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{
|
||||
error::{Error, JsonResult, Result},
|
||||
error::{Error, JsonResult},
|
||||
utils::require_admin,
|
||||
DB,
|
||||
};
|
||||
@@ -34,6 +35,9 @@ use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use axum::extract::Query;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::error::Result;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::utils::require_admin_or_devops;
|
||||
|
||||
@@ -83,7 +87,7 @@ async fn edit_copilot_config(
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
Json(ai_config): Json<AIConfig>,
|
||||
) -> Result<String> {
|
||||
) -> JsonResult<EditCopilotConfigResponse> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
if let Some(ref custom_prompts) = ai_config.custom_prompts {
|
||||
@@ -109,11 +113,7 @@ async fn edit_copilot_config(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(ref providers) = ai_config.providers {
|
||||
for provider in providers.keys() {
|
||||
AI_REQUEST_CACHE.remove(&(w_id.clone(), provider.clone()));
|
||||
}
|
||||
}
|
||||
invalidate_ai_request_cache_for_workspace(&w_id);
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
@@ -139,37 +139,66 @@ async fn edit_copilot_config(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Edit copilot config for workspace {}", &w_id))
|
||||
let workspace_has_config = ai_config.has_providers();
|
||||
let instance_ai_config =
|
||||
sqlx::query_scalar!("SELECT value FROM global_settings WHERE name = 'ai_config'")
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
let settings_state =
|
||||
build_copilot_settings_state(workspace_has_config, instance_ai_config.as_ref());
|
||||
let effective_ai_config = if workspace_has_config {
|
||||
ai_config
|
||||
} else if let Some(instance_ai_config) = instance_ai_config {
|
||||
serde_json::from_value::<AIConfig>(instance_ai_config).unwrap_or_default()
|
||||
} else {
|
||||
AIConfig::default()
|
||||
};
|
||||
|
||||
Ok(Json(EditCopilotConfigResponse {
|
||||
effective_ai_config,
|
||||
has_instance_ai_config: settings_state.has_instance_ai_config,
|
||||
uses_instance_ai_config: settings_state.uses_instance_ai_config,
|
||||
instance_ai_summary: settings_state.instance_ai_summary,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditCopilotConfigResponse {
|
||||
effective_ai_config: AIConfig,
|
||||
has_instance_ai_config: bool,
|
||||
uses_instance_ai_config: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instance_ai_summary: Option<InstanceAISummary>,
|
||||
}
|
||||
|
||||
async fn get_copilot_info(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<AIConfig> {
|
||||
let mut tx = db.begin().await?;
|
||||
let copilot_info = sqlx::query_scalar!(
|
||||
let workspace_ai_config = sqlx::query_scalar!(
|
||||
"SELECT ai_config as \"ai_config: sqlx::types::Json<AIConfig>\" FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!(
|
||||
"getting ai config: {e:#}"
|
||||
))
|
||||
})?;
|
||||
tx.commit().await?;
|
||||
|
||||
if let Some(sqlx::types::Json(copilot_info)) = copilot_info {
|
||||
Ok(Json(copilot_info))
|
||||
if let Some(workspace_ai_config) = workspace_ai_config.filter(|c| c.0.has_providers()) {
|
||||
Ok(Json(workspace_ai_config.0))
|
||||
} else if let Some(instance_config) =
|
||||
sqlx::query_scalar!("SELECT value FROM global_settings WHERE name = 'ai_config'")
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
{
|
||||
Ok(Json(
|
||||
serde_json::from_value::<AIConfig>(instance_config).unwrap_or_default(),
|
||||
))
|
||||
} else {
|
||||
Ok(Json(AIConfig {
|
||||
providers: None,
|
||||
default_model: None,
|
||||
code_completion_model: None,
|
||||
custom_prompts: None,
|
||||
max_tokens_per_model: None,
|
||||
}))
|
||||
Ok(Json(AIConfig::default()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user