feat: more AI models (#5207)

* feat: more AI models

* nits

* groq + openrouter

* nit
This commit is contained in:
HugoCasa
2025-02-04 22:17:30 +01:00
committed by GitHub
parent 8e0647833a
commit 49fedf12cd
41 changed files with 1884 additions and 720 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value\n FROM resource\n WHERE path = $1 AND workspace_id = $2",
"query": "SELECT value\n FROM resource\n WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
true
]
},
"hash": "286fa00c088df146c08c1934556f06a4243c66787ab8759a8045e60effd2fb77"
"hash": "0a9dd1addaf48eeb46eed59abb6daf9819d07b08cf7ca442ea7ec78a9b2b63b2"
}
@@ -1,21 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ai_resource, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
"query": "SELECT value, resource_type FROM resource WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ai_resource",
"name": "value",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "code_completion_enabled",
"type_info": "Bool"
"name": "resource_type",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
@@ -24,5 +25,5 @@
false
]
},
"hash": "0230bd64d2b6719c3536a106e18a68c7b43b3a9a9efa92b5517132e9c0d8a25b"
"hash": "103ef3cf5cf4d25d780e4aefd5b290d810a5e8ea6458d9f9fd484ced549ea82e"
}
@@ -70,68 +70,73 @@
},
{
"ordinal": 13,
"name": "code_completion_enabled",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 15,
"ordinal": 14,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 15,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 16,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"ordinal": 17,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 19,
"ordinal": 18,
"name": "auto_add",
"type_info": "Bool"
},
{
"ordinal": 20,
"ordinal": 19,
"name": "automatic_billing",
"type_info": "Bool"
},
{
"ordinal": 21,
"ordinal": 20,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 21,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 23,
"ordinal": 22,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 24,
"ordinal": 23,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 25,
"ordinal": 24,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 25,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 26,
"name": "code_completion_model",
"type_info": "Varchar"
}
],
"parameters": {
@@ -153,7 +158,6 @@
true,
true,
true,
false,
true,
false,
true,
@@ -165,6 +169,8 @@
true,
true,
true,
true,
false,
true
]
},
@@ -1,15 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET ai_resource = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
"query": "UPDATE workspace_settings SET ai_resource = NULL, code_completion_model = $1, ai_models = '{}' WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "6940ddc5ee8d2a1048213d46f52d964b199604ba66dedbe9f89cea727d726a2d"
"hash": "1bbef6baa5b8e2522d685df2979bb1e4b9022f5e841afd9eeb08a81688f6c0c8"
}
@@ -1,16 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET ai_resource = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
"query": "UPDATE workspace_settings SET ai_resource = $1, code_completion_model = $2, ai_models = $3 WHERE workspace_id = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Bool",
"Varchar",
"VarcharArray",
"Text"
]
},
"nullable": []
},
"hash": "1610c79b8d238e3a35d795f34a2a0100b933b7f199d41cc2f554d57c811d55f3"
"hash": "4edf05cbf35325d444de0e74ee070aafd27ef61c940daba186e7e66f668c31ed"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n ai_resource, \n code_completion_enabled, \n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n ai_resource, \n ai_models,\n code_completion_model,\n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
@@ -40,41 +40,46 @@
},
{
"ordinal": 7,
"name": "code_completion_enabled",
"type_info": "Bool"
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 8,
"name": "code_completion_model",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "name",
"type_info": "Varchar"
}
@@ -94,6 +99,7 @@
true,
false,
true,
true,
false,
true,
true,
@@ -102,5 +108,5 @@
false
]
},
"hash": "0331a81262e2d3c1bcfaeb64617b11eb68ab4599d64e5e5af639a9ac5d791fd0"
"hash": "51648e377d47815d0b15694572d5c9cc0a303d70980346e1f3c4096a8922d7d5"
}
@@ -70,68 +70,73 @@
},
{
"ordinal": 13,
"name": "code_completion_enabled",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 15,
"ordinal": 14,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 15,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 16,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"ordinal": 17,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 19,
"ordinal": 18,
"name": "auto_add",
"type_info": "Bool"
},
{
"ordinal": 20,
"ordinal": 19,
"name": "automatic_billing",
"type_info": "Bool"
},
{
"ordinal": 21,
"ordinal": 20,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 21,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 23,
"ordinal": 22,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 24,
"ordinal": 23,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 25,
"ordinal": 24,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 25,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 26,
"name": "code_completion_model",
"type_info": "Varchar"
}
],
"parameters": {
@@ -153,7 +158,6 @@
true,
true,
true,
false,
true,
false,
true,
@@ -165,6 +169,8 @@
true,
true,
true,
true,
false,
true
]
},
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ai_resource, code_completion_model, ai_models FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ai_resource",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "code_completion_model",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "ai_models",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true,
false
]
},
"hash": "b186efe51e7bb1924efbd7e9b36085502a1e77e30e59c7310c78976f77a810a3"
}
@@ -0,0 +1,5 @@
ALTER TABLE workspace_settings ADD COLUMN code_completion_enabled BOOLEAN DEFAULT FALSE NOT NULL;
UPDATE workspace_settings SET code_completion_enabled = TRUE WHERE code_completion_model IS NOT NULL;
ALTER TABLE workspace_settings DROP COLUMN ai_models, DROP COLUMN code_completion_model;
@@ -0,0 +1,24 @@
ALTER TABLE workspace_settings
ADD COLUMN ai_models varchar(255)[] DEFAULT '{}' NOT NULL,
ADD COLUMN code_completion_model varchar(255);
UPDATE workspace_settings
SET ai_models = CASE
WHEN ai_resource->>'provider' = 'openai' THEN ARRAY['gpt-4o']
WHEN ai_resource->>'provider' = 'anthropic' THEN ARRAY['claude-3-5-sonnet-latest']
WHEN ai_resource->>'provider' = 'mistral' THEN ARRAY['codestral-latest']
ELSE ai_models
END
WHERE ai_resource->>'path' IS NOT NULL;
UPDATE workspace_settings
SET code_completion_model = CASE
WHEN ai_resource->>'provider' = 'openai' THEN 'gpt-4o'
WHEN ai_resource->>'provider' = 'anthropic' THEN 'claude-3-5-sonnet-latest'
WHEN ai_resource->>'provider' = 'mistral' THEN 'codestral-latest'
ELSE code_completion_model
END
WHERE code_completion_enabled IS TRUE;
ALTER TABLE workspace_settings DROP COLUMN code_completion_enabled;
+31 -15
View File
@@ -1745,9 +1745,13 @@ paths:
deploy_to:
type: string
ai_resource:
$ref: "#/components/schemas/AiResource"
code_completion_enabled:
type: boolean
$ref: "#/components/schemas/AIResource"
code_completion_model:
type: string
ai_models:
type: array
items:
type: string
error_handler:
type: string
error_handler_extra_args:
@@ -1771,7 +1775,7 @@ paths:
operator_settings:
$ref: "#/components/schemas/OperatorSettings"
required:
- code_completion_enabled
- ai_models
- automatic_billing
- error_handler_muted_on_cancel
@@ -2084,12 +2088,16 @@ paths:
schema:
type: object
required:
- code_completion_enabled
- ai_models
properties:
ai_resource:
$ref: "#/components/schemas/AiResource"
code_completion_enabled:
type: boolean
$ref: "#/components/schemas/AIResource"
code_completion_model:
type: string
ai_models:
type: array
items:
type: string
responses:
"200":
description: status
@@ -2116,15 +2124,18 @@ paths:
type: object
properties:
ai_provider:
type: string
$ref: "#/components/schemas/AIProvider"
exists_ai_resource:
type: boolean
code_completion_enabled:
type: boolean
code_completion_model:
type: string
ai_models:
type: array
items:
type: string
required:
- ai_provider
- exists_ai_resource
- code_completion_enabled
- ai_models
/w/{workspace}/workspaces/edit_error_handler:
post:
@@ -11546,16 +11557,21 @@ components:
schemas:
$ref: "../../openflow.openapi.yaml#/components/schemas"
AiResource:
AIProvider:
type: string
enum: [openai, anthropic, mistral, deepseek, groq, openrouter, customai]
AIResource:
type: object
properties:
path:
type: string
provider:
type: string
$ref: "#/components/schemas/AIProvider"
required:
- path
- provider
Script:
type: object
properties:
+195 -115
View File
@@ -4,27 +4,21 @@ use crate::{
};
use anthropic::AnthropicCache;
use axum::{
body::Bytes,
extract::{Path, Query},
response::IntoResponse,
routing::post,
Extension, Router,
};
use anyhow::Context;
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
use http::HeaderMap;
use lazy_static::lazy_static;
use mistral::MistralCache;
use openai::OpenaiCache;
use quick_cache::sync::Cache;
use reqwest::{Client, RequestBuilder};
use serde::{Deserialize, Deserializer};
use windmill_audit::audit_ee::audit_log;
use windmill_audit::ActionKind;
use windmill_common::error::{to_anyhow, Result};
use windmill_common::error::Error;
use serde::{Deserialize, Serialize};
use serde_json::value::{RawValue, Value};
use std::collections::HashMap;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::error::{to_anyhow, Error, Result};
use mistral::MistralCache;
use openai::OpenaiCache;
use openai_api_compatible::OpenaiApiCompatibleCache;
lazy_static::lazy_static! {
static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
@@ -33,16 +27,63 @@ lazy_static::lazy_static! {
.build().unwrap();
}
trait AiRequest {
fn prepare_request(self, path: &str, body: Bytes) -> Result<RequestBuilder>;
mod openai_api_compatible {
use super::*;
#[derive(Deserialize, Clone, Debug)]
pub struct OpenaiApiCompatibleCache {
pub base_url: String,
pub api_key: Option<String>,
}
impl OpenaiApiCompatibleCache {
pub fn prepare_request(self, path: &str, body: Bytes) -> Result<RequestBuilder> {
let url = format!("{}/{}", self.base_url, path);
let mut request = HTTP_CLIENT
.post(url)
.header("content-type", "application/json")
.body(body);
if let Some(api_key) = self.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
Ok(request)
}
}
pub async fn get_cached_value(
db: &DB,
w_id: &str,
resource: Value,
base_url: Option<String>,
) -> Result<KeyCache> {
let mut resource: OpenaiApiCompatibleCache = if let Some(base_url) = base_url {
let api_key = match resource {
Value::Object(mut obj) => obj
.remove("api_key")
.map(|v| serde_json::from_value::<String>(v.clone()).ok())
.flatten(),
_ => None,
};
OpenaiApiCompatibleCache { base_url, api_key }
} else {
serde_json::from_value(resource).with_context(|| "validating custom AI resource")?
};
if let Some(api_key) = resource.api_key {
resource.api_key = Some(get_variable_or_self(api_key, db, w_id).await?);
}
Ok(KeyCache::OpenaiApiCompatible(resource))
}
}
mod openai {
use super::*;
use super::{get_variable_or_self, KeyCache};
const API_VERSION: &str = "2023-05-15";
const API_VERSION: &str = "2024-10-21";
#[derive(Deserialize, Debug)]
struct OpenaiResource {
@@ -94,8 +135,8 @@ mod openai {
}
const BASE_URL: &str = "https://api.openai.com/v1";
impl AiRequest for OpenaiCache {
fn prepare_request(self, openai_path: &str, mut body: Bytes) -> Result<RequestBuilder> {
impl OpenaiCache {
pub fn prepare_request(self, openai_path: &str, mut body: Bytes) -> Result<RequestBuilder> {
let OpenaiCache { api_key, azure_base_path, organization_id, user } = self;
if user.is_some() {
tracing::debug!("Adding user to request body");
@@ -245,15 +286,9 @@ mod anthropic {
const API_VERSION: &str = "2023-06-01";
impl AnthropicCache {
pub fn new(api_key: String) -> Self {
Self { api_key }
}
}
const BASE_URL: &str = "https://api.anthropic.com";
impl AiRequest for AnthropicCache {
fn prepare_request(self, anthropic_path: &str, body: Bytes) -> Result<RequestBuilder> {
impl AnthropicCache {
pub fn prepare_request(self, anthropic_path: &str, body: Bytes) -> Result<RequestBuilder> {
let AnthropicCache { api_key } = self;
let url = format!("{}/{}", BASE_URL, anthropic_path);
let request = HTTP_CLIENT
@@ -270,8 +305,7 @@ mod anthropic {
let mut resource: AnthropicCache = serde_json::from_value(resource)
.map_err(|e| Error::InternalErr(format!("validating anthropic resource {e:#}")))?;
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
let workspace_cache = AnthropicCache::new(resource.api_key);
Ok(KeyCache::Anthropic(workspace_cache))
Ok(KeyCache::Anthropic(resource))
}
}
@@ -283,15 +317,9 @@ mod mistral {
pub api_key: String,
}
impl MistralCache {
pub fn new(api_key: String) -> Self {
Self { api_key }
}
}
const BASE_URL: &str = "https://api.mistral.ai";
impl AiRequest for MistralCache {
fn prepare_request(self, mistral_path: &str, body: Bytes) -> Result<RequestBuilder> {
impl MistralCache {
pub fn prepare_request(self, mistral_path: &str, body: Bytes) -> Result<RequestBuilder> {
let MistralCache { api_key } = self;
let url = format!("{}/{}", BASE_URL, mistral_path);
@@ -309,9 +337,7 @@ mod mistral {
let mut resource: MistralCache = serde_json::from_value(resource)
.map_err(|e| Error::InternalErr(format!("validating mistral resource {e:#}")))?;
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
let workspace_cache = MistralCache::new(resource.api_key);
Ok(KeyCache::Mistral(workspace_cache))
Ok(KeyCache::Mistral(resource))
}
}
@@ -320,16 +346,17 @@ pub enum KeyCache {
Openai(OpenaiCache),
Anthropic(AnthropicCache),
Mistral(MistralCache),
OpenaiApiCompatible(OpenaiApiCompatibleCache),
}
#[derive(Clone, Debug)]
pub struct AiCache {
pub struct AICache {
pub path: String,
pub cached_key: KeyCache,
pub expires_at: std::time::Instant,
}
impl AiCache {
impl AICache {
pub fn new(path: String, cached_key: KeyCache) -> Self {
Self {
path,
@@ -343,35 +370,57 @@ impl AiCache {
}
lazy_static! {
pub static ref AI_KEY_CACHE: Cache<String, AiCache> = Cache::new(500);
pub static ref AI_KEY_CACHE: Cache<String, AICache> = Cache::new(500);
}
#[derive(Deserialize, Debug)]
struct ProxyQueryParams {
no_cache: Option<bool>,
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum AIProvider {
OpenAI,
Anthropic,
Mistral,
DeepSeek,
Groq,
OpenRouter,
CustomAI,
}
#[derive(Deserialize, Debug)]
pub struct AiResource {
pub path: String,
#[serde(deserialize_with = "check_if_valid_ai_provider")]
pub provider: String,
}
fn check_if_valid_ai_provider<'de, D>(provider: D) -> std::result::Result<String, D::Error>
where
D: Deserializer<'de>,
{
let provider = String::deserialize(provider)?;
match provider.as_str() {
"anthropic" | "openai" | "mistral" => Ok(provider),
_ => Err(serde::de::Error::custom(
"Only the following Ai providers are supported: openai, anthropic and mistral"
.to_string(),
)),
impl AIProvider {
pub fn get_openai_compatible_base_url(&self) -> Result<Option<String>> {
match self {
AIProvider::DeepSeek => Ok(Some("https://api.deepseek.com/v1".to_string())),
AIProvider::Groq => Ok(Some("https://api.groq.com/openai/v1".to_string())),
AIProvider::OpenRouter => Ok(Some("https://openrouter.ai/api/v1".to_string())),
AIProvider::CustomAI => Ok(None),
_ => Err(Error::BadRequest(
"Please use the specific provider instead of the OpenAI compatible one".to_string(),
)),
}
}
}
impl TryFrom<&str> for AIProvider {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
match s {
"openai" => Ok(AIProvider::OpenAI),
"anthropic" => Ok(AIProvider::Anthropic),
"mistral" => Ok(AIProvider::Mistral),
"groq" => Ok(AIProvider::Groq),
"openrouter" => Ok(AIProvider::OpenRouter),
"deepseek" => Ok(AIProvider::DeepSeek),
"customai" => Ok(AIProvider::CustomAI),
_ => Err(Error::BadRequest(format!("Invalid AI provider: {}", s))),
}
}
}
#[derive(Deserialize, Debug)]
pub struct AIResource {
pub path: String,
pub provider: AIProvider,
}
pub fn workspaced_service() -> Router {
let router = Router::new().route("/proxy/*ai", post(proxy));
@@ -382,74 +431,105 @@ async fn proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, ai_path)): Path<(String, String)>,
Query(query_params): Query<ProxyQueryParams>,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
let workspace_cache = AI_KEY_CACHE.get(&w_id);
let forced_resource_path = headers
.get("X-Resource-Path")
.map(|v| v.to_str().unwrap_or("").to_string());
let ai_cache = match workspace_cache {
Some(cache) if !cache.is_expired() && !query_params.no_cache.unwrap_or(false) => {
cache.cached_key
}
Some(cache) if !cache.is_expired() && forced_resource_path.is_none() => cache.cached_key,
_ => {
let ai_resource = sqlx::query_scalar!(
"SELECT ai_resource FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?;
let (resource, resource_path, ai_provider) = if let Some(resource_path) =
forced_resource_path
{
// guess the provider from the resource type
let record = sqlx::query!(
"SELECT value, resource_type FROM resource WHERE path = $1 AND workspace_id = $2",
&resource_path,
&w_id
)
.fetch_optional(&db)
.await?
.ok_or_else(|| {
Error::NotFound(format!(
"Could not find the resource {}, update the resource path in the workspace settings", resource_path
))
})?;
if ai_resource.is_none() {
return Err(Error::InternalErr("AI resource not configured".to_string()));
}
(
record.value,
resource_path,
AIProvider::try_from(record.resource_type.as_str())?,
)
} else {
let ai_resource = sqlx::query_scalar!(
"SELECT ai_resource FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?;
let ai_resource = serde_json::from_value::<AiResource>(ai_resource.unwrap())
.map_err(|e| Error::BadRequest(e.to_string()))?;
let ai_resource_path = ai_resource.path;
if ai_resource.is_none() {
return Err(Error::InternalErr("AI resource not configured".to_string()));
}
let resource = sqlx::query_scalar!(
"SELECT value
FROM resource
WHERE path = $1 AND workspace_id = $2",
&ai_resource_path,
&w_id
)
.fetch_optional(&db)
.await?
.ok_or_else(|| {
Error::InternalErr(format!(
"Could not find the {} resource at path {ai_resource_path}, update the resource path in the workspace settings", ai_resource.provider
))
})?;
let ai_resource = serde_json::from_value::<AIResource>(ai_resource.unwrap())
.map_err(|e| Error::BadRequest(e.to_string()))?;
let resource = sqlx::query_scalar!(
"SELECT value
FROM resource
WHERE path = $1 AND workspace_id = $2",
&ai_resource.path,
&w_id
)
.fetch_optional(&db)
.await?
.ok_or_else(|| {
Error::NotFound(format!(
"Could not find the {:?} resource at path {}, update the resource path in the workspace settings", ai_resource.provider, ai_resource.path
))
})?;
(resource, ai_resource.path, ai_resource.provider)
};
if resource.is_none() {
return Err(Error::InternalErr(format!(
"{} resource missing value",
ai_resource.provider
"{:?} resource missing value",
ai_provider
)));
}
let resource = resource.unwrap();
let ai_cache = match ai_resource.provider.as_str() {
"openai" => openai::get_cached_value(&db, &w_id, resource).await,
"anthropic" => anthropic::get_cached_value(&db, &w_id, resource).await,
"mistral" => mistral::get_cached_value(&db, &w_id, resource).await,
provider => {
return Err(Error::BadRequest(format!("{} is not supported", provider)))
let ai_cache = match ai_provider {
AIProvider::OpenAI => openai::get_cached_value(&db, &w_id, resource).await,
AIProvider::Anthropic => anthropic::get_cached_value(&db, &w_id, resource).await,
AIProvider::Mistral => mistral::get_cached_value(&db, &w_id, resource).await,
_ => {
openai_api_compatible::get_cached_value(
&db,
&w_id,
resource,
ai_provider.get_openai_compatible_base_url()?,
)
.await
}
};
let ai_cache = ai_cache?;
AI_KEY_CACHE.insert(
w_id.clone(),
AiCache::new(ai_resource_path, ai_cache.clone()),
);
AI_KEY_CACHE.insert(w_id.clone(), AICache::new(resource_path, ai_cache.clone()));
ai_cache
}
};
let (path, request) = match ai_cache {
KeyCache::Openai(cached) => ("openai_path", cached.prepare_request(&ai_path, body)),
KeyCache::Anthropic(cached) => ("anthropic_path", cached.prepare_request(&ai_path, body)),
KeyCache::Mistral(cached) => ("mistral_path", cached.prepare_request(&ai_path, body)),
let request = match ai_cache {
KeyCache::Openai(cached) => cached.prepare_request(&ai_path, body),
KeyCache::Anthropic(cached) => cached.prepare_request(&ai_path, body),
KeyCache::Mistral(cached) => cached.prepare_request(&ai_path, body),
KeyCache::OpenaiApiCompatible(cached) => cached.prepare_request(&ai_path, body),
};
let response = request?.send().await.map_err(to_anyhow)?;
@@ -463,7 +543,7 @@ async fn proxy(
ActionKind::Execute,
&w_id,
Some(&authed.email),
Some([(path, &format!("{:?}", ai_path)[..])].into()),
Some([("ai_resource_path", &format!("{:?}", ai_path)[..])].into()),
)
.await?;
tx.commit().await?;
+38 -29
View File
@@ -8,7 +8,7 @@
use std::collections::HashMap;
use crate::ai::{AiResource, AI_KEY_CACHE};
use crate::ai::{AIProvider, AIResource, AI_KEY_CACHE};
use crate::db::ApiAuthed;
use crate::users_ee::send_email_if_possible;
use crate::utils::get_instance_username_or_create_pending;
@@ -177,7 +177,9 @@ pub struct WorkspaceSettings {
pub webhook: Option<String>,
pub deploy_to: Option<String>,
pub ai_resource: Option<serde_json::Value>,
pub code_completion_enabled: bool,
pub ai_models: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_completion_model: Option<String>,
pub error_handler: Option<String>,
pub error_handler_extra_args: Option<serde_json::Value>,
pub error_handler_muted_on_cancel: Option<bool>,
@@ -246,7 +248,8 @@ struct EditWebhook {
#[derive(Deserialize)]
struct EditCopilotConfig {
ai_resource: Option<serde_json::Value>,
code_completion_enabled: bool,
code_completion_model: Option<String>,
ai_models: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug)]
@@ -660,27 +663,37 @@ async fn edit_copilot_config(
let mut tx = db.begin().await?;
if let Some(ai_resource) = &eo.ai_resource {
let path = serde_json::from_value::<AiResource>(ai_resource.clone())
.map_err(|e| Error::BadRequest(e.to_string()))?
.path;
let parsed_ai_resource = serde_json::from_value::<AIResource>(ai_resource.clone())
.map_err(|e| Error::BadRequest(e.to_string()))?;
#[cfg(not(feature = "enterprise"))]
{
if matches!(parsed_ai_resource.provider, AIProvider::CustomAI) {
return Err(Error::BadRequest(
"Custom AI is only available on EE".to_string(),
));
}
}
sqlx::query!(
"UPDATE workspace_settings SET ai_resource = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
"UPDATE workspace_settings SET ai_resource = $1, code_completion_model = $2, ai_models = $3 WHERE workspace_id = $4",
ai_resource,
eo.code_completion_enabled,
eo.code_completion_model,
eo.ai_models.as_slice(),
&w_id
)
.execute(&mut *tx)
.await?;
if let Some(cached) = AI_KEY_CACHE.get(&w_id) {
if cached.path != path {
if cached.path != parsed_ai_resource.path {
AI_KEY_CACHE.remove(&w_id);
}
}
} else {
sqlx::query!(
"UPDATE workspace_settings SET ai_resource = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
eo.code_completion_enabled,
"UPDATE workspace_settings SET ai_resource = NULL, code_completion_model = $1, ai_models = '{}' WHERE workspace_id = $2",
eo.code_completion_model,
&w_id,
)
.execute(&mut *tx)
@@ -698,8 +711,8 @@ async fn edit_copilot_config(
[
("ai_resource", &format!("{:?}", eo.ai_resource)[..]),
(
"code_completion_enabled",
&format!("{:?}", eo.code_completion_enabled)[..],
"code_completion_model",
&format!("{:?}", eo.code_completion_model)[..],
),
]
.into(),
@@ -713,9 +726,12 @@ async fn edit_copilot_config(
#[derive(Serialize)]
struct CopilotInfo {
pub ai_provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_provider: Option<AIProvider>,
pub exists_ai_resource: bool,
pub code_completion_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_completion_model: Option<String>,
pub ai_models: Vec<String>,
}
async fn get_copilot_info(
Extension(db): Extension<DB>,
@@ -723,33 +739,26 @@ async fn get_copilot_info(
) -> JsonResult<CopilotInfo> {
let mut tx = db.begin().await?;
let record = sqlx::query!(
"SELECT ai_resource, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
"SELECT ai_resource, code_completion_model, ai_models FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await
.map_err(|e| Error::InternalErr(format!("getting ai_resource and code_completion_enabled: {e:#}")))?;
.map_err(|e| Error::InternalErr(format!("getting ai_resource and code_completion_model: {e:#}")))?;
tx.commit().await?;
let (ai_provider, exists_ai_resource) = if let Some(ai_resource) = record.ai_resource {
let ai_resource = serde_json::from_value::<AiResource>(ai_resource);
let exist = ai_resource.is_ok();
(
if exist {
ai_resource.unwrap().provider
} else {
"".to_string()
},
exist,
)
let ai_resource = serde_json::from_value::<AIResource>(ai_resource)?;
(Some(ai_resource.provider), true)
} else {
("".to_string(), false)
(None, false)
};
Ok(Json(CopilotInfo {
ai_provider,
exists_ai_resource,
code_completion_enabled: record.code_completion_enabled,
code_completion_model: record.code_completion_model,
ai_models: record.ai_models,
}))
}
@@ -241,7 +241,9 @@ struct SimplifiedSettings {
error_handler_extra_args: Option<Value>,
error_handler_muted_on_cancel: bool,
ai_resource: Option<serde_json::Value>,
code_completion_enabled: bool,
ai_models: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
code_completion_model: Option<String>,
large_file_storage: Option<Value>,
git_sync: Option<Value>,
default_app: Option<String>,
@@ -617,7 +619,8 @@ pub(crate) async fn tarball_workspace(
deploy_to,
error_handler,
ai_resource,
code_completion_enabled,
ai_models,
code_completion_model,
error_handler_extra_args,
error_handler_muted_on_cancel,
large_file_storage,
+1 -1
View File
@@ -54,7 +54,7 @@ export const OpenAPI: OpenAPIConfig = {
PASSWORD: undefined,
TOKEN: getEnv("WM_TOKEN"),
USERNAME: undefined,
VERSION: '1.447.5',
VERSION: '1.454.1',
WITH_CREDENTIALS: true,
interceptors: {
request: new Interceptors(),
+445 -1
View File
File diff suppressed because one or more lines are too long
+422 -35
View File
@@ -1,8 +1,10 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AiResource = {
export type AIProvider = 'openai' | 'anthropic' | 'mistral' | 'deepseek' | 'customai';
export type AIResource = {
path: string;
provider: string;
provider: AIProvider;
};
export type Script = {
@@ -30,7 +32,7 @@ export type Script = {
};
lock?: string;
lock_error_logs?: string;
language: 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
language: ScriptLang;
kind: 'script' | 'failure' | 'trigger' | 'command' | 'approval' | 'preprocessor';
starred: boolean;
tag?: string;
@@ -54,8 +56,6 @@ export type Script = {
on_behalf_of_email?: string;
};
export type language = 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
export type kind = 'script' | 'failure' | 'trigger' | 'command' | 'approval' | 'preprocessor';
export type NewScript = {
@@ -69,7 +69,7 @@ export type NewScript = {
};
is_template?: boolean;
lock?: string;
language: 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
language: ScriptLang;
kind?: 'script' | 'failure' | 'trigger' | 'command' | 'approval' | 'preprocessor';
tag?: string;
draft_only?: boolean;
@@ -159,7 +159,7 @@ export type QueuedJob = {
flow_status?: FlowStatus;
raw_flow?: FlowValue;
is_flow_step: boolean;
language?: 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
language?: ScriptLang;
email: string;
visible_to_owner: boolean;
mem_peak?: number;
@@ -202,7 +202,7 @@ export type CompletedJob = {
flow_status?: FlowStatus;
raw_flow?: FlowValue;
is_flow_step: boolean;
language?: 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
language?: ScriptLang;
is_skipped: boolean;
email: string;
visible_to_owner: boolean;
@@ -372,11 +372,13 @@ export type MainArgSignature = {
export type type2 = 'Valid' | 'Invalid';
export type ScriptLang = 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
export type Preview = {
content?: string;
path?: string;
args: ScriptArgs;
language?: 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
language?: ScriptLang;
tag?: string;
kind?: 'code' | 'identity' | 'http';
dedicated_worker?: boolean;
@@ -550,10 +552,18 @@ export type EditSchedule = {
cron_version?: string;
};
export type HttpTrigger = {
path: string;
export type TriggerExtraProperty = {
email: string;
extra_perms: {
[key: string]: (boolean);
};
workspace_id: string;
edited_by: string;
edited_at: string;
};
export type HttpTrigger = TriggerExtraProperty & {
path: string;
script_path: string;
route_path: string;
static_asset_config?: {
@@ -562,11 +572,6 @@ export type HttpTrigger = {
filename?: string;
};
is_flow: boolean;
extra_perms: {
[key: string]: (boolean);
};
email: string;
workspace_id: string;
http_method: 'get' | 'post' | 'put' | 'delete' | 'patch';
is_async: boolean;
requires_auth: boolean;
@@ -613,22 +618,16 @@ export type TriggersCount = {
webhook_count?: number;
email_count?: number;
websocket_count?: number;
postgres_count?: number;
kafka_count?: number;
nats_count?: number;
};
export type WebsocketTrigger = {
export type WebsocketTrigger = TriggerExtraProperty & {
path: string;
edited_by: string;
edited_at: string;
script_path: string;
url: string;
is_flow: boolean;
extra_perms: {
[key: string]: (boolean);
};
email: string;
workspace_id: string;
server_id?: string;
last_server_ping?: string;
error?: string;
@@ -639,6 +638,7 @@ export type WebsocketTrigger = {
}>;
initial_messages?: Array<WebsocketTriggerInitialMessage>;
url_runnable_args?: ScriptArgs;
can_return_message: boolean;
};
export type NewWebsocketTrigger = {
@@ -653,6 +653,7 @@ export type NewWebsocketTrigger = {
}>;
initial_messages?: Array<WebsocketTriggerInitialMessage>;
url_runnable_args?: ScriptArgs;
can_return_message: boolean;
};
export type EditWebsocketTrigger = {
@@ -666,6 +667,7 @@ export type EditWebsocketTrigger = {
}>;
initial_messages?: Array<WebsocketTriggerInitialMessage>;
url_runnable_args?: ScriptArgs;
can_return_message: boolean;
};
export type WebsocketTriggerInitialMessage = {
@@ -678,6 +680,74 @@ export type WebsocketTriggerInitialMessage = {
};
};
export type Slot = {
name?: string;
};
export type SlotList = {
slot_name?: string;
active?: boolean;
};
export type PublicationData = {
table_to_track?: Array<Relations>;
transaction_to_track: Array<(string)>;
};
export type TableToTrack = Array<{
table_name: string;
columns_name?: Array<(string)>;
where_clause?: string;
}>;
export type Relations = {
schema_name: string;
table_to_track: TableToTrack;
};
export type Language = 'Typescript';
export type TemplateScript = {
postgres_resource_path: string;
relations: Array<Relations>;
language: Language;
};
export type PostgresTrigger = TriggerExtraProperty & {
path: string;
script_path: string;
is_flow: boolean;
enabled: boolean;
postgres_resource_path: string;
publication_name: string;
server_id?: string;
replication_slot_name: string;
error?: string;
last_server_ping?: string;
};
export type NewPostgresTrigger = {
replication_slot_name?: string;
publication_name?: string;
path: string;
script_path: string;
is_flow: boolean;
enabled: boolean;
postgres_resource_path: string;
publication?: PublicationData;
};
export type EditPostgresTrigger = {
replication_slot_name: string;
publication_name: string;
path: string;
script_path: string;
is_flow: boolean;
enabled: boolean;
postgres_resource_path: string;
publication?: PublicationData;
};
export type KafkaTrigger = {
path: string;
edited_by: string;
@@ -817,6 +887,7 @@ export type UserWorkspaceList = {
name: string;
username: string;
color: string;
operator_settings?: OperatorSettings;
}>;
};
@@ -1111,7 +1182,7 @@ export type MetricDataPoint = {
export type RawScriptForDependencies = {
raw_code: string;
path: string;
language: 'python3' | 'deno' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'bun' | 'php' | 'rust' | 'ansible' | 'csharp';
language: ScriptLang;
};
export type ConcurrencyGroup = {
@@ -1223,6 +1294,79 @@ export type CaptureConfig = {
last_server_ping?: string;
};
export type OperatorSettings = {
/**
* Whether operators can view runs
*/
runs: boolean;
/**
* Whether operators can view schedules
*/
schedules: boolean;
/**
* Whether operators can view resources
*/
resources: boolean;
/**
* Whether operators can view variables
*/
variables: boolean;
/**
* Whether operators can view audit logs
*/
audit_logs: boolean;
/**
* Whether operators can view triggers
*/
triggers: boolean;
/**
* Whether operators can view groups page
*/
groups: boolean;
/**
* Whether operators can view folders page
*/
folders: boolean;
/**
* Whether operators can view workers page
*/
workers: boolean;
} | null;
export type TeamInfo = {
/**
* The unique identifier of the Microsoft Teams team
*/
team_id: string;
/**
* The display name of the Microsoft Teams team
*/
team_name: string;
/**
* List of channels within the team
*/
channels: Array<ChannelInfo>;
};
export type ChannelInfo = {
/**
* The unique identifier of the channel
*/
channel_id: string;
/**
* The display name of the channel
*/
channel_name: string;
/**
* The Microsoft Teams tenant identifier
*/
tenant_id: string;
/**
* The service URL for the channel
*/
service_url: string;
};
export type OpenFlow = {
summary: string;
description?: string;
@@ -1331,7 +1475,7 @@ export type RawScript = {
is_trigger?: boolean;
};
export type language2 = 'deno' | 'bun' | 'python3' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'php';
export type language = 'deno' | 'bun' | 'python3' | 'go' | 'bash' | 'powershell' | 'postgresql' | 'mysql' | 'bigquery' | 'snowflake' | 'mssql' | 'oracledb' | 'graphql' | 'nativets' | 'php';
export type PathScript = {
input_transforms: {
@@ -1441,10 +1585,14 @@ export type FlowStatusModule = {
export type type4 = 'WaitingForPriorSteps' | 'WaitingForEvents' | 'WaitingForExecutor' | 'InProgress' | 'Success' | 'Failure';
export type ParameterId = string;
export type ParameterKey = string;
export type ParameterWorkspaceId = string;
export type ParameterPublicationName = string;
export type ParameterVersionId = number;
export type ParameterToken = string;
@@ -2239,6 +2387,13 @@ export type WhoisData = {
export type WhoisResponse = (User);
export type UpdateOperatorSettingsData = {
requestBody: OperatorSettings;
workspace: string;
};
export type UpdateOperatorSettingsResponse = (string);
export type ExistsEmailData = {
email: string;
};
@@ -2285,8 +2440,9 @@ export type GetSettingsResponse = ({
customer_id?: string;
webhook?: string;
deploy_to?: string;
ai_resource?: AiResource;
code_completion_enabled: boolean;
ai_resource?: AIResource;
code_completion_model?: string;
ai_models: Array<(string)>;
error_handler?: string;
error_handler_extra_args?: ScriptArgs;
error_handler_muted_on_cancel: boolean;
@@ -2297,6 +2453,7 @@ export type GetSettingsResponse = ({
default_scripts?: WorkspaceDefaultScripts;
mute_critical_alerts?: boolean;
color?: string;
operator_settings?: OperatorSettings;
});
export type GetDeployToData = {
@@ -2427,8 +2584,9 @@ export type EditCopilotConfigData = {
* WorkspaceCopilotConfig
*/
requestBody: {
ai_resource?: AiResource;
code_completion_enabled: boolean;
ai_resource?: AIResource;
code_completion_model?: string;
ai_models: Array<(string)>;
};
workspace: string;
};
@@ -2440,9 +2598,10 @@ export type GetCopilotInfoData = {
};
export type GetCopilotInfoResponse = ({
ai_provider: string;
ai_provider: AIProvider;
exists_ai_resource: boolean;
code_completion_enabled: boolean;
code_completion_model?: string;
ai_models: Array<(string)>;
});
export type EditErrorHandlerData = {
@@ -2586,6 +2745,7 @@ export type GetUsedTriggersResponse = ({
websocket_used: boolean;
kafka_used: boolean;
nats_used: boolean;
postgres_used: boolean;
});
export type ListUsersData = {
@@ -2916,6 +3076,8 @@ export type GetOauthConnectResponse = ({
scopes?: Array<(string)>;
});
export type SyncTeamsResponse = (Array<TeamInfo>);
export type CreateResourceData = {
/**
* new resource
@@ -5490,6 +5652,20 @@ export type SetWebsocketTriggerEnabledData = {
export type SetWebsocketTriggerEnabledResponse = (string);
export type TestWebsocketConnectionData = {
/**
* test websocket connection
*/
requestBody: {
url: string;
url_runnable_args?: ScriptArgs;
can_return_message: boolean;
};
workspace: string;
};
export type TestWebsocketConnectionResponse = (string);
export type CreateKafkaTriggerData = {
/**
* new kafka trigger
@@ -5565,6 +5741,20 @@ export type SetKafkaTriggerEnabledData = {
export type SetKafkaTriggerEnabledResponse = (string);
export type TestKafkaConnectionData = {
/**
* test kafka connection
*/
requestBody: {
connection: {
[key: string]: unknown;
};
};
workspace: string;
};
export type TestKafkaConnectionResponse = (string);
export type CreateNatsTriggerData = {
/**
* new nats trigger
@@ -5640,6 +5830,195 @@ export type SetNatsTriggerEnabledData = {
export type SetNatsTriggerEnabledResponse = (string);
export type TestNatsConnectionData = {
/**
* test nats connection
*/
requestBody: {
connection: {
[key: string]: unknown;
};
};
workspace: string;
};
export type TestNatsConnectionResponse = (string);
export type IsValidPostgresConfigurationData = {
path: string;
workspace: string;
};
export type IsValidPostgresConfigurationResponse = (boolean);
export type CreateTemplateScriptData = {
/**
* template script
*/
requestBody: TemplateScript;
workspace: string;
};
export type CreateTemplateScriptResponse = (string);
export type GetTemplateScriptData = {
id: string;
workspace: string;
};
export type GetTemplateScriptResponse = (string);
export type ListPostgresReplicationSlotData = {
path: string;
workspace: string;
};
export type ListPostgresReplicationSlotResponse = (Array<SlotList>);
export type CreatePostgresReplicationSlotData = {
path: string;
/**
* new slot for postgres
*/
requestBody: Slot;
workspace: string;
};
export type CreatePostgresReplicationSlotResponse = (string);
export type DeletePostgresReplicationSlotData = {
path: string;
/**
* replication slot of postgres
*/
requestBody: Slot;
workspace: string;
};
export type DeletePostgresReplicationSlotResponse = (string);
export type ListPostgresPublicationData = {
path: string;
workspace: string;
};
export type ListPostgresPublicationResponse = (Array<(string)>);
export type GetPostgresPublicationData = {
path: string;
publication: string;
workspace: string;
};
export type GetPostgresPublicationResponse = (PublicationData);
export type CreatePostgresPublicationData = {
path: string;
publication: string;
/**
* new publication for postgres
*/
requestBody: PublicationData;
workspace: string;
};
export type CreatePostgresPublicationResponse = (string);
export type UpdatePostgresPublicationData = {
path: string;
publication: string;
/**
* update publication for postgres
*/
requestBody: PublicationData;
workspace: string;
};
export type UpdatePostgresPublicationResponse = (string);
export type DeletePostgresPublicationData = {
path: string;
publication: string;
workspace: string;
};
export type DeletePostgresPublicationResponse = (string);
export type CreatePostgresTriggerData = {
/**
* new postgres trigger
*/
requestBody: NewPostgresTrigger;
workspace: string;
};
export type CreatePostgresTriggerResponse = (string);
export type UpdatePostgresTriggerData = {
path: string;
/**
* updated trigger
*/
requestBody: EditPostgresTrigger;
workspace: string;
};
export type UpdatePostgresTriggerResponse = (string);
export type DeletePostgresTriggerData = {
path: string;
workspace: string;
};
export type DeletePostgresTriggerResponse = (string);
export type GetPostgresTriggerData = {
path: string;
workspace: string;
};
export type GetPostgresTriggerResponse = (PostgresTrigger);
export type ListPostgresTriggersData = {
isFlow?: boolean;
/**
* which page to return (start at 1, default 1)
*/
page?: number;
/**
* filter by path
*/
path?: string;
pathStart?: string;
/**
* number of items to return for a given page (default 30, max 100)
*/
perPage?: number;
workspace: string;
};
export type ListPostgresTriggersResponse = (Array<PostgresTrigger>);
export type ExistsPostgresTriggerData = {
path: string;
workspace: string;
};
export type ExistsPostgresTriggerResponse = (boolean);
export type SetPostgresTriggerEnabledData = {
path: string;
/**
* updated postgres trigger enable
*/
requestBody: {
enabled: boolean;
};
workspace: string;
};
export type SetPostgresTriggerEnabledResponse = (string);
export type ListInstanceGroupsResponse = (Array<InstanceGroup>);
export type GetInstanceGroupData = {
@@ -5987,7 +6366,7 @@ export type ListAutoscalingEventsData = {
export type ListAutoscalingEventsResponse = (Array<AutoscalingEvent>);
export type GetGranularAclsData = {
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger';
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger';
path: string;
workspace: string;
};
@@ -5997,7 +6376,7 @@ export type GetGranularAclsResponse = ({
});
export type AddGranularAclsData = {
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger';
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger';
path: string;
/**
* acl to add
@@ -6012,7 +6391,7 @@ export type AddGranularAclsData = {
export type AddGranularAclsResponse = (string);
export type RemoveGranularAclsData = {
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger';
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger';
path: string;
/**
* acl to add
@@ -6076,6 +6455,13 @@ export type ListCapturesData = {
export type ListCapturesResponse = (Array<Capture>);
export type GetCaptureData = {
id: number;
workspace: string;
};
export type GetCaptureResponse = (Capture);
export type DeleteCaptureData = {
id: number;
workspace: string;
@@ -6200,6 +6586,7 @@ export type DuckdbConnectionSettingsV2Data = {
export type DuckdbConnectionSettingsV2Response = ({
connection_settings_str: string;
azure_container_path?: string;
});
export type PolarsConnectionSettingsData = {
+18 -18
View File
@@ -1,15 +1,11 @@
import { yamlStringify } from "./deps.ts";
import { Confirm } from "./deps.ts";
import { colors } from "./deps.ts";
import { yamlParseFile } from "./deps.ts";
import { log } from "./deps.ts";
import process from "node:process";
import { colors, Confirm, log, yamlParseFile, yamlStringify } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { AiResource, Config, GlobalSetting } from "./gen/types.gen.ts";
import { compareInstanceObjects, InstanceSyncOptions } from "./instance.ts";
import { isSuperset } from "./types.ts";
import { deepEqual } from "./utils.ts";
import * as wmill from "./gen/services.gen.ts";
import { AiResource, Config, GlobalSetting } from "./gen/types.gen.ts";
import { removeWorkerPrefix } from "./worker_groups.ts";
import process from "node:process";
export interface SimplifiedSettings {
// slack_team_id?: string;
@@ -25,7 +21,8 @@ export interface SimplifiedSettings {
error_handler_extra_args?: any;
error_handler_muted_on_cancel?: boolean;
ai_resource?: AiResource;
code_completion_enabled: boolean;
code_completion_model?: string;
ai_models: string[];
large_file_storage?: any;
git_sync?: any;
default_app?: string;
@@ -81,7 +78,8 @@ export async function pushWorkspaceSettings(
error_handler_muted_on_cancel:
remoteSettings.error_handler_muted_on_cancel,
ai_resource: remoteSettings.ai_resource,
code_completion_enabled: remoteSettings.code_completion_enabled,
code_completion_model: remoteSettings.code_completion_model,
ai_models: remoteSettings.ai_models,
large_file_storage: remoteSettings.large_file_storage,
git_sync: remoteSettings.git_sync,
default_app: remoteSettings.default_app,
@@ -153,15 +151,17 @@ export async function pushWorkspaceSettings(
}
}
if (
localSettings.ai_resource !== settings.ai_resource ||
localSettings.code_completion_enabled !== settings.code_completion_enabled
localSettings.ai_resource != settings.ai_resource ||
localSettings.code_completion_model != settings.code_completion_model ||
!deepEqual(localSettings.ai_models, settings.ai_models)
) {
log.debug(`Updating openai settings...`);
log.debug(`Updating copilot settings...`);
await wmill.editCopilotConfig({
workspace,
requestBody: {
ai_resource: localSettings.ai_resource,
code_completion_enabled: localSettings.code_completion_enabled,
code_completion_model: localSettings.code_completion_model,
ai_models: localSettings.ai_models,
},
});
}
@@ -282,7 +282,9 @@ export async function readInstanceSettings(opts: InstanceSyncOptions) {
await checkInstanceSettingsPath(opts);
try {
localSettings = (await yamlParseFile(instanceSettingsPath)) as GlobalSetting[];
localSettings = (await yamlParseFile(
instanceSettingsPath
)) as GlobalSetting[];
} catch {
log.warn(`No ${instanceSettingsPath} found`);
}
@@ -445,9 +447,7 @@ export async function pushInstanceSettings(
}
}
export async function readLocalConfigs(
opts: InstanceSyncOptions
) {
export async function readLocalConfigs(opts: InstanceSyncOptions) {
let localConfigs: Config[] = [];
await checkInstanceConfigPath(opts);
+10 -5
View File
@@ -48,7 +48,7 @@
} from '$lib/relative_imports'
import Tooltip from './Tooltip.svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
import { initAllAiWorkspace } from './copilot/lib'
import { workspaceAIClients } from './copilot/lib'
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
import type { PickableProperties } from './flows/previousResults'
$: token = $page.url.searchParams.get('wm_token') ?? undefined
@@ -110,14 +110,19 @@
async function setCopilotInfo() {
if (workspace) {
initAllAiWorkspace(workspace)
workspaceAIClients.init(workspace)
try {
copilotInfo.set(await WorkspaceService.getCopilotInfo({ workspace }))
const info = await WorkspaceService.getCopilotInfo({ workspace })
copilotInfo.set({
...info,
ai_provider: info.ai_provider ?? 'openai'
})
} catch (err) {
copilotInfo.set({
ai_provider: '',
ai_provider: 'openai',
exists_ai_resource: false,
code_completion_enabled: false
code_completion_model: undefined,
ai_models: []
})
console.error('Could not get copilot info')
+2 -3
View File
@@ -172,7 +172,6 @@
import { initVim } from './monaco_keybindings'
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
import { parseTypescriptDeps } from '$lib/relative_imports'
import type { AiProviderTypes } from './copilot/lib'
// import EditorTheme from './EditorTheme.svelte'
@@ -640,7 +639,7 @@
textAfterPosition,
lang,
abortController,
aiProvider as AiProviderTypes
aiProvider
)
if (insertText) {
items = [
@@ -668,7 +667,7 @@
}
$: $copilotInfo.exists_ai_resource &&
$copilotInfo.code_completion_enabled &&
$copilotInfo.code_completion_model &&
$codeCompletionSessionEnabled &&
initialized &&
addCopilotSuggestions()
@@ -86,7 +86,6 @@
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
import { type TriggerContext, type ScheduleTrigger } from './triggers'
import type { AiProviderTypes } from './copilot/lib'
export let initialPath: string = ''
export let pathStoreInit: string | undefined = undefined
@@ -821,7 +820,7 @@
try {
push(history, $flowStore)
let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx]
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
const aiProvider = $copilotInfo.ai_provider
copilotLoading = true
copilotStatus = "Generating code for step '" + module.id + "'..."
@@ -12,7 +12,7 @@
}
</script>
{#if $copilotInfo.exists_ai_resource && $copilotInfo.code_completion_enabled}
{#if $copilotInfo.exists_ai_resource && $copilotInfo.code_completion_model}
<Toggle
size="xs"
bind:checked={$codeCompletionSessionEnabled}
@@ -1,13 +1,14 @@
<script lang="ts">
import { ExternalLink, Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import { getNonStreamingCompletion } from './lib'
import Popup from '../common/popup/Popup.svelte'
import { sendUserToast } from '$lib/toast'
import { copilotInfo } from '$lib/stores'
import { base } from '$lib/base'
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
import type { AIProvider } from '$lib/gen'
export let schedule: string
export let cronVersion: string
@@ -56,7 +57,7 @@
const response = await getNonStreamingCompletion(
messages,
abortController,
aiProvider as AiProviderTypes
aiProvider as AIProvider
)
if (response.startsWith('ERROR:')) {
@@ -1,7 +1,7 @@
<script lang="ts">
import { Check, Loader2, Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import type { Flow, InputTransform } from '$lib/gen'
import ManualPopover from '../ManualPopover.svelte'
@@ -71,7 +71,7 @@ Only output the expression, do not explain or discuss.`
}
],
abortController,
aiProvider as AiProviderTypes
aiProvider
)
} catch (err) {
if (!abortController.signal.aborted) {
@@ -1,5 +1,5 @@
<script lang="ts">
import { getCompletion, getResponseFromEvent, type AiProviderTypes } from './lib'
import { getCompletion, getResponseFromEvent } from './lib'
import { isInitialCode } from '$lib/script_helpers'
import { Check, Loader2, Wand2 } from 'lucide-svelte'
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
@@ -118,7 +118,7 @@ Generate a description for the flow below:
content: config.user.replace(`{${config.placeholderName}}`, placeholderContent)
}
]
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
const aiProvider = $copilotInfo.ai_provider
const response = await getCompletion(messages, abortController, aiProvider)
generatedContent = ''
for await (const chunk of response) {
@@ -1,7 +1,7 @@
<script lang="ts">
import { Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from '../flows/types'
@@ -68,7 +68,7 @@ Only return the expression without any wrapper. Do not explain or discuss.`
}
],
abortController,
aiProvider as AiProviderTypes
aiProvider
)
dispatch('setExpr', result)
@@ -2,7 +2,7 @@
import { base } from '$lib/base'
import { Button } from '../common'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import Popup from '../common/popup/Popup.svelte'
import { copilotInfo } from '$lib/stores'
@@ -40,7 +40,7 @@
}
],
abortController,
aiProvider as AiProviderTypes
aiProvider
)
dispatch('gen', { res: res, prompt: funcDesc })
funcDesc = ''
@@ -2,7 +2,7 @@
import { base } from '$lib/base'
import { Button } from '../common'
import { SUPPORTED_LANGUAGES, copilot, type AiProviderTypes } from './lib'
import { SUPPORTED_LANGUAGES, copilot } from './lib'
import type { SupportedLanguage } from '$lib/common'
import { sendUserToast } from '$lib/toast'
import type Editor from '../Editor.svelte'
@@ -40,7 +40,7 @@
try {
genLoading = true
abortController = new AbortController()
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
const aiProvider = $copilotInfo.ai_provider
await copilot(
{
language: lang,
@@ -53,7 +53,7 @@
generatedCode,
abortController,
aiProvider,
generatedExplanation,
generatedExplanation
)
setupDiff()
diffEditor?.setModified($generatedCode)
@@ -2,18 +2,18 @@
import { base } from '$lib/base'
import { Button } from '../common'
import {
MAX_SCHEMA_LENGTH,
SUPPORTED_LANGUAGES,
addThousandsSeparator,
copilot,
type AiProviderTypes
} from './lib'
import { MAX_SCHEMA_LENGTH, SUPPORTED_LANGUAGES, addThousandsSeparator, copilot } from './lib'
import type { SupportedLanguage } from '$lib/common'
import { sendUserToast } from '$lib/toast'
import type Editor from '../Editor.svelte'
import Popup from '../common/popup/Popup.svelte'
import { dbSchemas, copilotInfo, type DBSchema, workspaceStore } from '$lib/stores'
import {
dbSchemas,
copilotInfo,
type DBSchema,
workspaceStore,
copilotSessionModel
} from '$lib/stores'
import type DiffEditor from '../DiffEditor.svelte'
import { scriptLangToEditorLang } from '$lib/scripts'
import type SimpleEditor from '../SimpleEditor.svelte'
@@ -25,16 +25,7 @@
import LoadingIcon from '../apps/svelte-select/lib/LoadingIcon.svelte'
import { sleep } from '$lib/utils'
import { autoPlacement } from '@floating-ui/core'
import {
AlertTriangle,
Ban,
Bot,
Check,
ExternalLink,
HistoryIcon,
Wand2,
X
} from 'lucide-svelte'
import { AlertTriangle, Ban, Check, ExternalLink, HistoryIcon, Wand2, X } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { isInitialCode } from '$lib/script_helpers'
import { twMerge } from 'tailwind-merge'
@@ -74,7 +65,7 @@
return
}
savePrompt()
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
const aiProvider = $copilotInfo.ai_provider
try {
genLoading = true
blockPopupOpen = true
@@ -248,6 +239,16 @@
}
}
function checkForInvalidModel() {
if (
!$copilotSessionModel ||
($copilotSessionModel && !$copilotInfo.ai_models.includes($copilotSessionModel))
) {
$copilotSessionModel = $copilotInfo.ai_models[0]
}
}
$: $copilotInfo && checkForInvalidModel()
onDestroy(() => {
abortController?.abort()
})
@@ -399,21 +400,27 @@
</div>
{:else if $copilotInfo.exists_ai_resource}
<div class="flex flex-col gap-4">
<div class="flex flex-row justify-between items-center">
<div class="flex flex-row justify-between items-center w-96 gap-2">
<ToggleButtonGroup class="w-auto shrink-0" bind:selected={mode}>
<ToggleButton value={'gen'} label="Generate from scratch" small light />
<ToggleButton value={'edit'} label="Edit existing code" small light />
</ToggleButtonGroup>
<div class="text-[0.6rem] text-secondary opacity-60 flex flex-row items-center gap-0.5">
{#if $copilotInfo.ai_provider === 'openai'}
GPT-4o
{:else if $copilotInfo.ai_provider === 'anthropic'}
Claude-3.5
{:else}
Codestral
<div class="min-w-0">
{#if $copilotInfo.ai_models.length > 1}
<select
bind:value={$copilotSessionModel}
class="!text-xs !pr-5 !bg-[right_center] overflow-ellipsis text-right !border-none !shadow-none"
>
{#each $copilotInfo.ai_models as model}
<option value={model} class="pr-4">{model}</option>
{/each}
</select>
{:else if $copilotInfo.ai_models.length === 1}
<div class="text-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
{$copilotInfo.ai_models[0]}
</div>
{/if}
<Bot size={14} />
</div>
</div>
<div class="flex w-96 items-start">
@@ -1,7 +1,7 @@
<script lang="ts">
import { Check, Loader2, Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import type { Flow, InputTransform } from '$lib/gen'
import ManualPopover from '../ManualPopover.svelte'
@@ -116,7 +116,7 @@ Only return the expression without any wrapper.`
}
],
abortController,
aiProvider as AiProviderTypes
aiProvider
)
if (
@@ -7,7 +7,7 @@
import type { FlowEditorContext } from '../flows/types'
import type { PickableProperties } from '../flows/previousResults'
import { getContext } from 'svelte'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import { getNonStreamingCompletion } from './lib'
import { sendUserToast } from '$lib/toast'
import Button from '../common/button/Button.svelte'
import type { FlowCopilotContext } from './flow'
@@ -92,7 +92,7 @@ input_name2: expression2
}
],
abortController,
aiProvider as AiProviderTypes
aiProvider
)
parsedInputs = generatedContent.split('\n').map((x) => x.split(': '))
@@ -1,10 +1,13 @@
<script lang="ts">
import type { AIProvider } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import Button from '../common/button/Button.svelte'
import { testKey, type AiProviderTypes } from './lib'
import { testKey } from './lib'
export let disabled = false
export let apiKey: string | undefined = undefined
export let aiProvider: AiProviderTypes
export let resourcePath: string | undefined = undefined
export let aiProvider: AIProvider
export let model: string
let loading = false
</script>
@@ -21,8 +24,10 @@
setTimeout(() => {
abortController.abort()
}, 10000)
await testKey({
apiKey,
resourcePath,
messages: [
{
role: 'user',
@@ -30,7 +35,8 @@
}
],
abortController,
aiProvider
aiProvider,
model
})
sendUserToast('Valid key')
} catch (err) {
@@ -42,5 +48,11 @@
} finally {
loading = false
}
}}>Test key</Button
}}
>
{#if apiKey}
Test key
{:else}
Test
{/if}
</Button>
@@ -1,6 +1,7 @@
import type { ChatCompletionMessageParam } from 'openai/resources/chat/index.mjs'
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
import type { AIProvider } from '$lib/gen'
import { codeCompletionLoading } from '$lib/stores'
import type { ChatCompletionMessageParam } from 'openai/resources/chat/index.mjs'
import { getNonStreamingCompletion } from './lib'
const systemPrompt = `You are a code completion assistant, return the code that should go instead of the <completion_tokens>.
@@ -74,7 +75,7 @@ export async function editorCodeCompletion(
after: string,
lang: string,
abortController: AbortController,
aiProvider: AiProviderTypes
aiProvider: AIProvider
) {
codeCompletionLoading.set(true)
const messages: ChatCompletionMessageParam[] = [
+8 -12
View File
@@ -1,21 +1,17 @@
import {
type FlowModule,
ScriptService,
type RawScript,
type PathScript,
type AIProvider,
type FlowModule,
type InputTransform,
type PathScript,
type RawScript,
type Script
} from '$lib/gen'
import {
addResourceTypes,
deltaCodeCompletion,
getNonStreamingCompletion,
type AiProviderTypes
} from './lib'
import { scriptLangToEditorLang } from '$lib/scripts'
import type { Writable } from 'svelte/store'
import type Editor from '../Editor.svelte'
import type { Drawer } from '../common'
import { scriptLangToEditorLang } from '$lib/scripts'
import { addResourceTypes, deltaCodeCompletion, getNonStreamingCompletion } from './lib'
export type FlowCopilotModule = {
id: string
@@ -259,7 +255,7 @@ export async function stepCopilot(
| undefined,
isFirstInLoop: boolean,
abortController: AbortController,
aiProvider: AiProviderTypes
aiProvider: AIProvider
) {
if (module.source !== 'custom') {
throw new Error('Not a custom module')
@@ -328,7 +324,7 @@ export async function glueCopilot(
},
isFirstInLoop: boolean,
abortController: AbortController,
aiProvider: AiProviderTypes
aiProvider: AIProvider
) {
const { prevCode, prevLang } = await getPreviousStepContent(pastModule, workspace)
+253 -248
View File
@@ -1,56 +1,130 @@
import { OpenAI } from 'openai'
import { OpenAPI, ResourceService, type Script } from '../../gen'
import type { Writable } from 'svelte/store'
import type { AIProvider } from '$lib/gen'
import {
copilotInfo,
copilotSessionModel,
type DBSchema,
type GraphqlSchema,
type SQLSchema
} from '$lib/stores'
import { Anthropic } from '@anthropic-ai/sdk'
import type { DBSchema, GraphqlSchema, SQLSchema } from '$lib/stores'
import { formatResourceTypes } from './utils'
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
import { Mistral } from '@mistralai/mistralai'
import { buildClientSchema, printSchema } from 'graphql'
import { OpenAI } from 'openai'
import type {
ChatCompletionCreateParamsStreaming,
ChatCompletionMessageParam
} from 'openai/resources/index.mjs'
import { get, type Writable } from 'svelte/store'
import { OpenAPI, ResourceService, type Script } from '../../gen'
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
import { formatResourceTypes } from './utils'
import type { MessageCreateParams, MessageParam } from '@anthropic-ai/sdk/resources/messages.mjs'
import type { ChatCompletionRequest } from '@mistralai/mistralai/models/components/chatcompletionrequest'
import type {
SystemMessage,
UserMessage,
AssistantMessage,
ToolMessage,
CompletionEvent,
ContentChunk
ContentChunk,
SystemMessage,
ToolMessage,
UserMessage
} from '@mistralai/mistralai/models/components'
import type { ChatCompletionRequest } from '@mistralai/mistralai/models/components/chatcompletionrequest'
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
export type AiProviderTypes = 'openai' | 'anthropic' | 'mistral'
interface AiProvider {
init: (workspace: string, updateClient: boolean, token?: string) => void
export const AI_DEFAULT_MODELS: Record<AIProvider, string[]> = {
openai: ['gpt-4o', 'gpt-4o-mini'],
anthropic: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'],
mistral: ['codestral-latest'],
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
openrouter: ['meta-llama/llama-3.2-3b-instruct:free'],
customai: []
}
class WorkspacedMistral implements AiProvider {
private client: Mistral | undefined
export const OPENAI_COMPATIBLE_BASE_URLS = {
groq: 'https://api.groq.com/openai/v1',
openrouter: 'https://openrouter.ai/api/v1',
deepseek: 'https://api.deepseek.com/v1'
} as const
init(workspace: string, updateClient: boolean, token?: string) {
if (!this.client || updateClient) {
this.client = initWorkspaceAiProvider(workspace, 'mistral', token) as unknown as Mistral
}
class WorkspacedAIClients {
private openaiClient: OpenAI | undefined
private anthropicClient: Anthropic | undefined
private mistralClient: Mistral | undefined
init(workspace: string) {
this.initOpenai(workspace)
this.initAnthropic(workspace)
this.initMistral(workspace)
}
getClient() {
if (!this.client) {
throw new Error('AnthropicAi not initialized')
private getBaseURL(workspace: string) {
return `${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy`
}
private initOpenai(workspace: string) {
const baseURL = this.getBaseURL(workspace)
this.openaiClient = new OpenAI({
baseURL,
apiKey: 'fake-key',
defaultHeaders: {
Authorization: '' // a non empty string will be unable to access Windmill backend proxy
},
dangerouslyAllowBrowser: true
})
}
private initAnthropic(workspace: string) {
const baseURL = this.getBaseURL(workspace)
this.anthropicClient = new Anthropic({
baseURL,
apiKey: 'fake-key',
dangerouslyAllowBrowser: true
})
}
private initMistral(workspace: string) {
const baseURL = this.getBaseURL(workspace)
this.mistralClient = new Mistral({
serverURL: baseURL
})
}
getOpenaiClient() {
if (!this.openaiClient) {
throw new Error('OpenAI not initialized')
}
return this.client
return this.openaiClient
}
getAnthropicClient() {
if (!this.anthropicClient) {
throw new Error('Anthropic not initialized')
}
return this.anthropicClient
}
getMistralClient() {
if (!this.mistralClient) {
throw new Error('Mistral not initialized')
}
return this.mistralClient
}
}
export namespace MistralAi {
export let workspace = new WorkspacedMistral()
export const workspaceAIClients = new WorkspacedAIClients()
const DEFAULT_COMPLETION_CONFIG: ChatCompletionCreateParamsStreaming = {
model: '',
max_tokens: 8000, //TODO: make this dynamic
temperature: 0,
seed: 42,
stream: true,
messages: []
}
namespace MistralAI {
export const mistralConfig: ChatCompletionRequest = {
temperature: 0,
model: null,
@@ -78,30 +152,11 @@ export namespace MistralAi {
}
}
class WorkspacedAnthropic implements AiProvider {
private client: Anthropic | undefined
init(workspace: string, updateClient: boolean, token: string | undefined = undefined) {
if (!this.client || updateClient) {
this.client = initWorkspaceAiProvider(workspace, 'anthropic', token) as unknown as Anthropic
}
}
getClient() {
if (!this.client) {
throw new Error('AnthropicAi not initialized')
}
return this.client
}
}
export namespace AnthropicAi {
export let workspace = new WorkspacedAnthropic()
export namespace AnthropicAI {
export const config: MessageCreateParams = {
temperature: 0,
max_tokens: 8192,
model: 'claude-3-5-sonnet-20241022',
model: '',
messages: []
}
@@ -135,30 +190,11 @@ export namespace AnthropicAi {
}
}
class WorkspacedOpenai implements AiProvider {
private client: OpenAI | undefined
init(workspace: string, updateClient: boolean, token: string | undefined = undefined) {
if (!this.client || updateClient) {
this.client = initWorkspaceAiProvider(workspace, 'openai', token) as unknown as OpenAI
}
}
getClient() {
if (!this.client) {
throw new Error('OpenAI not initialized')
}
return this.client
}
}
export namespace OpenAi {
export let workspace = new WorkspacedOpenai()
namespace OpenAi {
export const openaiConfig: ChatCompletionCreateParamsStreaming = {
temperature: 0,
max_tokens: 16384,
model: 'gpt-4o-2024-08-06',
model: '',
seed: 42,
stream: true,
messages: []
@@ -169,125 +205,38 @@ export namespace OpenAi {
}
}
export function initAllAiWorkspace(workspace: string, updateClient: boolean = false) {
OpenAi.workspace.init(workspace, updateClient)
AnthropicAi.workspace.init(workspace, updateClient)
MistralAi.workspace.init(workspace, updateClient)
}
function initWorkspaceAiProvider(
workspace: string,
aiProvider: AiProviderTypes,
token: string | undefined = undefined
): Anthropic | OpenAI | Mistral {
const baseURL = `${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy`
let client
switch (aiProvider) {
case 'openai': {
client = new OpenAI({
baseURL,
apiKey: 'fake-key',
defaultHeaders: {
Authorization: token ? `Bearer ${token}` : ''
},
dangerouslyAllowBrowser: true
})
break
}
case 'anthropic': {
client = new Anthropic({
baseURL,
apiKey: 'fake-key',
defaultHeaders: {
Authorization: token ? `Bearer ${token}` : ''
},
dangerouslyAllowBrowser: true
})
break
}
case 'mistral': {
client = new Mistral({
serverURL: baseURL
})
}
}
return client
}
export async function testKey({
apiKey,
resourcePath,
model,
abortController,
messages,
aiProvider
}: {
apiKey?: string
resourcePath?: string
model: string | undefined
messages: ChatCompletionMessageParam[]
abortController: AbortController
aiProvider: AiProviderTypes
aiProvider: AIProvider
}) {
if (apiKey) {
switch (aiProvider) {
case 'openai': {
const openai = new OpenAI({
apiKey,
dangerouslyAllowBrowser: true
})
await openai.chat.completions.create(
{
...OpenAi.openaiConfig,
messages,
stream: false
},
{
signal: abortController.signal
}
)
break
}
case 'anthropic': {
const anthropic = new Anthropic({
apiKey,
dangerouslyAllowBrowser: true
})
const [, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
await anthropic.messages.create(
{
...AnthropicAi.config,
messages: anthropicMessages,
stream: false
},
{
signal: abortController.signal
}
)
break
}
case 'mistral': {
const mistral = new Mistral({
apiKey
})
await mistral.chat.complete(
{
...MistralAi.mistralConfig,
model: 'codestral-latest',
stream: false,
messages: messages as MistralAi.MistralParamsMessage[]
},
{
fetchOptions: {
signal: abortController.signal,
headers: {
'content-type': 'application/json'
}
}
}
)
break
}
}
} else {
await getNonStreamingCompletion(messages, abortController, aiProvider, undefined, true)
if (!apiKey && !resourcePath) {
throw new Error('API key or resource path is required')
}
const modelToTest = model ?? AI_DEFAULT_MODELS[aiProvider][0]
if (!modelToTest) {
throw new Error('Missing a model to test')
}
await getNonStreamingCompletion(
messages,
abortController,
aiProvider,
apiKey,
resourcePath,
modelToTest
)
}
interface BaseOptions {
@@ -410,7 +359,9 @@ function addDBSChema(scriptOptions: CopilotOptions, prompt: string) {
const { dbSchema, language } = scriptOptions
if (
dbSchema &&
['postgresql', 'mysql', 'snowflake', 'bigquery', 'mssql', 'graphql', 'oracledb'].includes(language) && // make sure we are using a SQL/query language
['postgresql', 'mysql', 'snowflake', 'bigquery', 'mssql', 'graphql', 'oracledb'].includes(
language
) && // make sure we are using a SQL/query language
language === dbSchema.lang // make sure we are using the same language as the schema
) {
let { stringified } = dbSchema
@@ -467,66 +418,109 @@ const PROMPTS_CONFIGS = {
export async function getNonStreamingCompletion(
messages: ChatCompletionMessageParam[],
abortController: AbortController,
aiProvider: AiProviderTypes,
model = OpenAi.openaiConfig.model,
noCache?: boolean
aiProvider: AIProvider,
apiKey?: string, // testing API KEY directly from the frontend
resourcePath?: string, // testing resource path passed as a header to the backend proxy
forceModel?: string
) {
let response: string | undefined = ''
const queryOptions = {
query: {
no_cache: noCache
},
let model = forceModel
if (!model) {
model = get(copilotSessionModel)
let info = get(copilotInfo)
const { ai_models: aiModels } = info
if (!model || !aiModels.includes(model)) {
console.warn('Invalid model, using default model:', aiModels[0])
model = aiModels[0]
}
}
if (!model) {
throw new Error('No model found')
}
const fetchOptions: {
signal: AbortSignal
headers?: Record<string, string>
} = {
signal: abortController.signal
}
switch (aiProvider) {
case 'openai': {
const openaiClient = OpenAi.workspace.getClient()
const completion = await openaiClient.chat.completions.create(
{
...OpenAi.openaiConfig,
messages,
stream: false,
model
},
queryOptions
)
response = completion.choices[0]?.message.content || ''
break
if (resourcePath) {
fetchOptions.headers = {
'X-Resource-Path': resourcePath
}
}
switch (aiProvider) {
case 'anthropic': {
const anthropicClient = AnthropicAi.workspace.getClient()
const [system, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
const anthropicClient = apiKey
? new Anthropic({
apiKey,
dangerouslyAllowBrowser: true
})
: workspaceAIClients.getAnthropicClient()
const [system, anthropicMessages] = AnthropicAI.getSystemPromptAndArrayMessages(messages)
const message = await anthropicClient.messages.create(
{
...AnthropicAi.config,
...AnthropicAI.config,
system,
model,
messages: anthropicMessages,
stream: false
},
queryOptions
fetchOptions
)
response = message.content[0].type === 'text' ? message.content[0].text : ''
break
}
case 'mistral': {
const mistralClient = MistralAi.workspace.getClient()
const mistralClient = apiKey
? new Mistral({
apiKey
})
: workspaceAIClients.getMistralClient()
const message = await mistralClient.chat.complete(
{
...MistralAi.mistralConfig,
model: 'codestral-latest',
...MistralAI.mistralConfig,
model,
stream: false,
messages: messages as MistralAi.MistralParamsMessage[]
messages: messages as MistralAI.MistralParamsMessage[]
},
{
fetchOptions: {
signal: abortController.signal,
cache: 'no-store'
}
fetchOptions: fetchOptions
}
)
response = MistralAi.retrieveTextValue(message.choices && message.choices[0].message.content)
response = MistralAI.retrieveTextValue(message.choices && message.choices[0].message.content)
break
}
default: {
if (aiProvider === 'customai' && apiKey) {
throw new Error('Cannot test API key for Custom AI, only resource path is supported')
}
const baseURL = OPENAI_COMPATIBLE_BASE_URLS[aiProvider]
if (apiKey && aiProvider !== 'openai' && !baseURL) {
throw new Error('No base URL for this provider: ' + aiProvider)
}
const openaiClient = apiKey
? new OpenAI({
apiKey,
baseURL,
dangerouslyAllowBrowser: true
})
: workspaceAIClients.getOpenaiClient()
const completion = await openaiClient.chat.completions.create(
{
...(aiProvider === 'openai' ? OpenAi.openaiConfig : DEFAULT_COMPLETION_CONFIG),
messages,
model,
stream: false
},
fetchOptions
)
response = completion.choices[0]?.message.content || ''
}
}
return response
}
@@ -534,17 +528,30 @@ export async function getNonStreamingCompletion(
export async function getCompletion(
messages: ChatCompletionMessageParam[],
abortController: AbortController,
aiProvider: AiProviderTypes,
model = OpenAi.openaiConfig.model
aiProvider: AIProvider
) {
let model = get(copilotSessionModel)
let info = get(copilotInfo)
const { ai_models: aiModels } = info
if (!model || !aiModels.includes(model)) {
console.warn('Invalid model, using default model:', aiModels[0])
model = aiModels[0]
}
if (!model) {
throw new Error('No model found')
}
switch (aiProvider) {
case 'anthropic': {
const anthropicClient = AnthropicAi.workspace.getClient()
const [system, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
const anthropicClient = workspaceAIClients.getAnthropicClient()
const [system, anthropicMessages] = AnthropicAI.getSystemPromptAndArrayMessages(messages)
const completion = await anthropicClient.messages.create(
{
...AnthropicAi.config,
...AnthropicAI.config,
model,
system,
messages: anthropicMessages,
stream: true
@@ -553,13 +560,29 @@ export async function getCompletion(
)
return completion
}
case 'openai': {
const openaiClient = OpenAi.workspace.getClient()
case 'mistral': {
const mistralClient = workspaceAIClients.getMistralClient()
const message = await mistralClient.chat.stream(
{
...MistralAI.mistralConfig,
model,
messages: messages as MistralAI.MistralParamsMessage[]
},
{
fetchOptions: {
signal: abortController.signal
}
}
)
return message
}
default: {
const openaiClient = workspaceAIClients.getOpenaiClient()
const completion = await openaiClient.chat.completions.create(
{
...OpenAi.openaiConfig,
messages,
model
...(aiProvider === 'openai' ? OpenAi.openaiConfig : DEFAULT_COMPLETION_CONFIG),
model,
messages
},
{
signal: abortController.signal
@@ -567,23 +590,6 @@ export async function getCompletion(
)
return completion
}
case 'mistral': {
const mistralClient = MistralAi.workspace.getClient()
const message = await mistralClient.chat.stream(
{
...MistralAi.mistralConfig,
model: 'codestral-latest',
messages: messages as MistralAi.MistralParamsMessage[]
},
{
fetchOptions: {
signal: abortController.signal,
cache: 'no-store'
}
}
)
return message
}
}
}
@@ -592,21 +598,20 @@ export function getResponseFromEvent(
| Anthropic.Messages.RawMessageStreamEvent
| OpenAI.Chat.Completions.ChatCompletionChunk
| CompletionEvent,
aiProvider: AiProviderTypes
aiProvider: AIProvider
): string {
switch (aiProvider) {
case 'openai': {
const messages = part as OpenAI.Chat.Completions.ChatCompletionChunk
return OpenAi.retrieveTextValue(messages)
}
case 'anthropic': {
const messages = part as Anthropic.Messages.RawMessageStreamEvent
return AnthropicAi.retrieveTextValue(messages)
return AnthropicAI.retrieveTextValue(messages)
}
case 'mistral': {
const messages = part as CompletionEvent
return MistralAi.retrieveTextValue(messages.data.choices[0].delta.content)
return MistralAI.retrieveTextValue(messages.data.choices[0].delta.content)
}
default:
const messages = part as OpenAI.Chat.Completions.ChatCompletionChunk
return OpenAi.retrieveTextValue(messages)
}
}
@@ -614,7 +619,7 @@ export async function copilot(
scriptOptions: CopilotOptions,
generatedCode: Writable<string>,
abortController: AbortController,
aiProvider: AiProviderTypes,
aiProvider: AIProvider,
generatedExplanation?: Writable<string>
) {
const { prompt, systemPrompt } = await getPrompts(scriptOptions)
@@ -704,7 +709,7 @@ export async function deltaCodeCompletion(
messages: ChatCompletionMessageParam[],
generatedCodeDelta: Writable<string>,
abortController: AbortController,
aiProvider: AiProviderTypes
aiProvider: AIProvider
) {
const completion = await getCompletion(messages, abortController, aiProvider)
+30 -29
View File
@@ -8,11 +8,11 @@ export interface Setting {
key: string
// If value is not specified for first element, it will automatcally use undefined
select_items?: {
label: string,
tooltip?: string,
label: string
tooltip?: string
// If not specified, label will be used
value?: any,
}[],
value?: any
}[]
fieldType:
| 'text'
| 'number'
@@ -41,7 +41,7 @@ export interface Setting {
isValid?: (value: any) => boolean
error?: string
defaultValue?: () => any
codeAreaLang?: string,
codeAreaLang?: string
}
export type SettingStorage = 'setting'
@@ -81,9 +81,9 @@ export const settings: Record<string, Setting[]> = {
isValid: (value: string | undefined) =>
value
? value?.startsWith('http') &&
value.includes('://') &&
!value?.endsWith('/') &&
!value?.endsWith(' ')
value.includes('://') &&
!value?.endsWith('/') &&
!value?.endsWith(' ')
: false
},
{
@@ -182,7 +182,7 @@ export const settings: Record<string, Setting[]> = {
{
label: 'Azure OpenAI base path',
description:
'All Windmill AI features will run on the specified deployed model. Format: https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}. <a href="https://www.windmill.dev/docs/core_concepts/ai_generation#azure-openai-advanced-models">Learn more</a>',
'All workspaces using an OpenAI resource for Windmill AI will run on the specified deployed model. Format: https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}. <a href="https://www.windmill.dev/docs/core_concepts/ai_generation#azure-openai-advanced-models">Learn more</a>',
key: 'openai_azure_base_path',
fieldType: 'text',
storage: 'setting',
@@ -236,24 +236,26 @@ export const settings: Record<string, Setting[]> = {
// 2. Change LATEST_STABLE_PY in dockerfile
// 3. Change #[default] annotation for PyVersion in backend
placeholder: '3.10,3.11,3.12,3.13',
select_items: [{
label: "Latest Stable",
value: "default",
tooltip: "python-3.11",
},
{
label: "3.10",
},
{
label: "3.11",
},
{
label: "3.12",
},
{
label: "3.13",
}],
storage: 'setting',
select_items: [
{
label: 'Latest Stable',
value: 'default',
tooltip: 'python-3.11'
},
{
label: '3.10'
},
{
label: '3.11'
},
{
label: '3.12'
},
{
label: '3.13'
}
],
storage: 'setting'
},
{
label: 'Pip index url',
@@ -294,8 +296,7 @@ export const settings: Record<string, Setting[]> = {
},
{
label: 'Nuget Config',
description:
'Write a nuget.config file to set custom package sources and credentials',
description: 'Write a nuget.config file to set custom package sources and credentials',
key: 'nuget_config',
fieldType: 'codearea',
codeAreaLang: 'xml',
@@ -19,7 +19,7 @@
import MenuButton from './MenuButton.svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
import { isCloudHosted } from '$lib/cloud'
import { initAllAiWorkspace } from '../copilot/lib'
import { workspaceAIClients } from '../copilot/lib'
import { twMerge } from 'tailwind-merge'
export let isCollapsed: boolean = false
@@ -31,7 +31,7 @@
if ($workspaceStore === id) {
return
}
initAllAiWorkspace(id, true)
workspaceAIClients.init(id)
const editPages = [
'/scripts/edit/',
'/flows/edit/',
@@ -82,8 +82,11 @@
>
<div class="flex items-center justify-between min-w-0 w-full">
<div>
<div class="text-primary pl-4 truncate text-left text-[1.2em]">{workspace.name}</div>
<div class="text-tertiary font-mono pl-4 text-2xs whitespace-nowrap truncate text-left">
<div class="text-primary pl-4 truncate text-left text-[1.2em]">{workspace.name}</div
>
<div
class="text-tertiary font-mono pl-4 text-2xs whitespace-nowrap truncate text-left"
>
{workspace.id}
</div>
</div>
@@ -91,7 +94,7 @@
<div
class="w-5 h-5 mr-2 rounded border border-gray-300 dark:border-gray-600"
style="background-color: {workspace.color}"
></div>
/>
{/if}
</div>
</button>
+12 -7
View File
@@ -1,13 +1,14 @@
import { BROWSER } from 'esm-env'
import { derived, type Readable, writable } from 'svelte/store'
import type { IntrospectionQuery } from 'graphql'
import {
type WorkspaceDefaultScripts,
type AIProvider,
type OperatorSettings,
type TokenResponse,
type UserWorkspaceList,
type OperatorSettings
type WorkspaceDefaultScripts
} from './gen'
import type { IntrospectionQuery } from 'graphql'
import { getLocalSetting } from './utils'
export interface UserExt {
@@ -82,13 +83,14 @@ export const userWorkspaces: Readable<
}
})
export const copilotInfo = writable<{
ai_provider: string
ai_provider: AIProvider
exists_ai_resource: boolean
code_completion_enabled: boolean
code_completion_model?: string
ai_models: string[]
}>({
ai_provider: '',
ai_provider: 'openai',
exists_ai_resource: false,
code_completion_enabled: false
ai_models: []
})
export const codeCompletionLoading = writable<boolean>(false)
export const metadataCompletionEnabled = writable<boolean>(true)
@@ -103,6 +105,9 @@ export const vimMode = writable<boolean>(getLocalSetting(VIM_MODE_SETTING_NAME)
export const codeCompletionSessionEnabled = writable<boolean>(
getLocalSetting(CODE_COMPLETION_SETTING_NAME) != 'false'
)
export const copilotSessionModel = writable<string | undefined>(
getLocalSetting(CODE_COMPLETION_SETTING_NAME) ?? undefined
)
export const usedTriggerKinds = writable<string[]>([])
type SQLBaseSchema = {
@@ -44,7 +44,7 @@
import { syncTutorialsTodos } from '$lib/tutorialUtils'
import { ArrowLeft, Search } from 'lucide-svelte'
import { getUserExt } from '$lib/user'
import { initAllAiWorkspace } from '$lib/components/copilot/lib'
import { workspaceAIClients } from '$lib/components/copilot/lib'
import { twMerge } from 'tailwind-merge'
import OperatorMenu from '$lib/components/sidebar/OperatorMenu.svelte'
import GlobalSearchModal from '$lib/components/search/GlobalSearchModal.svelte'
@@ -192,11 +192,9 @@
async function loadUsedTriggerKinds() {
let usedKinds: string[] = []
const { http_routes_used, websocket_used, kafka_used, postgres_used, nats_used } =
await WorkspaceService.getUsedTriggers(
{
workspace: $workspaceStore ?? ''
}
)
await WorkspaceService.getUsedTriggers({
workspace: $workspaceStore ?? ''
})
if (http_routes_used) {
usedKinds.push('http')
}
@@ -242,15 +240,19 @@
let devOnly = $page.url.pathname.startsWith(base + '/scripts/dev')
async function loadCopilot(workspace: string) {
initAllAiWorkspace(workspace)
workspaceAIClients.init(workspace)
try {
copilotInfo.set(await WorkspaceService.getCopilotInfo({ workspace }))
} catch (err) {
console.log(err)
const info = await WorkspaceService.getCopilotInfo({ workspace })
copilotInfo.set({
ai_provider: '',
...info,
ai_provider: info.ai_provider ?? 'openai'
})
} catch (err) {
copilotInfo.set({
ai_provider: 'openai',
exists_ai_resource: false,
code_completion_enabled: false
code_completion_model: undefined,
ai_models: []
})
console.error('Could not get copilot info')
}
@@ -294,7 +296,13 @@
setContext('openSearchWithPrefilledText', openSearchModal)
$: {
if ($enterpriseLicense && $workspaceStore && $userStore && $devopsRole !== undefined && ($devopsRole || $userStore.is_admin)) {
if (
$enterpriseLicense &&
$workspaceStore &&
$userStore &&
$devopsRole !== undefined &&
($devopsRole || $userStore.is_admin)
) {
mountModal = true
loadCriticalAlertsMuted()
}
@@ -6,7 +6,8 @@
SettingService,
UserService,
VariableService,
WorkspaceService
WorkspaceService,
type AIProvider
} from '$lib/gen'
import { validateUsername } from '$lib/utils'
import { logoutWithRedirect } from '$lib/logout'
@@ -18,12 +19,12 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import { onMount } from 'svelte'
import { sendUserToast } from '$lib/toast'
import TestAiKey from '$lib/components/copilot/TestAiKey.svelte'
import TestAIKey from '$lib/components/copilot/TestAIKey.svelte'
import { switchWorkspace } from '$lib/storeUtils'
import { isCloudHosted } from '$lib/cloud'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import type { AiProviderTypes } from '$lib/components/copilot/lib'
import { AI_DEFAULT_MODELS } from '$lib/components/copilot/lib'
const rd = $page.url.searchParams.get('rd')
@@ -41,8 +42,12 @@
let colorEnabled = false
function generateRandomColor() {
const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
workspaceColor = randomColor;
const randomColor =
'#' +
Math.floor(Math.random() * 16777215)
.toString(16)
.padStart(6, '0')
workspaceColor = randomColor
}
$: id = name.toLowerCase().replace(/\s/gi, '-')
@@ -111,7 +116,8 @@
workspace: id,
requestBody: {
ai_resource: { path, provider: selected },
code_completion_enabled: codeCompletionEnabled
ai_models: aiKey ? AI_DEFAULT_MODELS[selected].slice(0, 1) : [],
code_completion_model: codeCompletionEnabled ? AI_DEFAULT_MODELS[selected][0] : undefined
}
})
}
@@ -178,7 +184,7 @@
let auto_invite = false
let operatorOnly = false
let selected: AiProviderTypes = 'openai'
let selected: Exclude<AIProvider, 'customai'> = 'openai'
</script>
<CenteredModal title="New Workspace">
@@ -198,11 +204,23 @@
</label>
<label class="block pb-4">
<span class="text-secondary text-sm">Workspace color</span>
<span class="ml-5 text-tertiary text-xs">Color to identify the current workspace in the list of workspaces</span>
<span class="ml-5 text-tertiary text-xs"
>Color to identify the current workspace in the list of workspaces</span
>
<div class="flex items-center gap-2">
<Toggle bind:checked={colorEnabled} options={{ right: 'Enable' }} />
{#if colorEnabled}<input class="w-10" type="color" bind:value={workspaceColor} disabled={!colorEnabled} />{/if}
<input type="text" class="w-24 text-sm" bind:value={workspaceColor} disabled={!colorEnabled} />
{#if colorEnabled}<input
class="w-10"
type="color"
bind:value={workspaceColor}
disabled={!colorEnabled}
/>{/if}
<input
type="text"
class="w-24 text-sm"
bind:value={workspaceColor}
disabled={!colorEnabled}
/>
<Button on:click={generateRandomColor} size="xs" disabled={!colorEnabled}>Random</Button>
</div>
</label>
@@ -230,9 +248,12 @@
</span>
<div class="pb-2">
<ToggleButtonGroup bind:selected>
<ToggleButton value="openai" label="OpenAi" />
<ToggleButton value="openai" label="OpenAI" />
<ToggleButton value="anthropic" label="Anthropic" />
<ToggleButton value="mistral" label="Mistral" />
<ToggleButton value="deepseek" label="DeepSeek" />
<ToggleButton value="groq" label="Groq" />
<ToggleButton value="openrouter" label="OpenRouter" />
</ToggleButtonGroup>
</div>
</div>
@@ -243,7 +264,12 @@
bind:value={aiKey}
on:keyup={handleKeyUp}
/>
<TestAiKey apiKey={aiKey} disabled={!aiKey} aiProvider={selected} />
<TestAIKey
apiKey={aiKey}
disabled={!aiKey}
aiProvider={selected}
model={AI_DEFAULT_MODELS[selected][0]}
/>
</div>
{#if aiKey}
<Toggle
@@ -3,7 +3,7 @@
import { page } from '$app/stores'
import { isCloudHosted } from '$lib/cloud'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Badge, Button, Tab, Tabs } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton, Tab, Tabs } from '$lib/components/common'
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
@@ -20,7 +20,8 @@
WorkspaceService,
JobService,
ResourceService,
SettingService
SettingService,
type AIProvider
} from '$lib/gen'
import {
enterpriseLicense,
@@ -50,7 +51,7 @@
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import TestAiKey from '$lib/components/copilot/TestAiKey.svelte'
import TestAIKey from '$lib/components/copilot/TestAIKey.svelte'
import Portal from '$lib/components/Portal.svelte'
import { fade } from 'svelte/transition'
@@ -66,8 +67,11 @@
import { hubPaths } from '$lib/hub'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import type { AiProviderTypes } from '$lib/components/copilot/lib'
import { AI_DEFAULT_MODELS } from '$lib/components/copilot/lib'
import Description from '$lib/components/Description.svelte'
import MultiSelect from 'svelte-multiselect'
import Label from '$lib/components/Label.svelte'
import ArgEnum from '$lib/components/ArgEnum.svelte'
type GitSyncTypeMap = {
scripts: boolean
@@ -113,8 +117,11 @@
let errorHandlerMutedOnCancel: boolean | undefined = undefined
let criticalAlertUIMuted: boolean | undefined = undefined
let initialCriticalAlertUIMuted: boolean | undefined = undefined
let aiResourceInitialPath: string | undefined = undefined
let aiResourceInitialProvider: string | undefined = undefined
let aiResourcePath: string | undefined = undefined
let aiProvider: AIProvider = 'openai'
let aiModels: string[] = []
let codeCompletionModel: string | undefined = undefined
let s3ResourceSettings: S3ResourceSettings = {
resourceType: 's3',
@@ -142,8 +149,6 @@
let editedWorkspaceEncryptionKey: string | undefined = undefined
let workspaceReencryptionInProgress: boolean = false
let encryptionKeyRegex = /^[a-zA-Z0-9]{64}$/
let codeCompletionEnabled: boolean = false
let selected: AiProviderTypes = 'openai'
let tab =
($page.url.searchParams.get('tab') as
| 'users'
@@ -191,10 +196,7 @@
}
}
async function editCopilotConfig(aiResourcePath: string, aiProvider: string): Promise<void> {
// in JS, an empty string is also falsy
aiResourceInitialPath = aiResourcePath
aiResourceInitialProvider = aiProvider
async function editCopilotConfig(): Promise<void> {
if (aiResourcePath) {
await WorkspaceService.editCopilotConfig({
workspace: $workspaceStore!,
@@ -203,26 +205,30 @@
path: aiResourcePath,
provider: aiProvider
},
code_completion_enabled: codeCompletionEnabled
code_completion_model: codeCompletionModel,
ai_models: aiModels
}
})
copilotInfo.set({
ai_provider: aiProvider,
exists_ai_resource: true,
code_completion_enabled: codeCompletionEnabled
code_completion_model: codeCompletionModel,
ai_models: aiModels
})
} else {
await WorkspaceService.editCopilotConfig({
workspace: $workspaceStore!,
requestBody: {
ai_resource: undefined,
code_completion_enabled: codeCompletionEnabled
code_completion_model: codeCompletionModel,
ai_models: []
}
})
copilotInfo.set({
ai_provider: '',
ai_provider: 'openai',
exists_ai_resource: false,
code_completion_enabled: codeCompletionEnabled
code_completion_model: codeCompletionModel,
ai_models: []
})
}
sendUserToast(`Copilot settings updated`)
@@ -382,6 +388,7 @@
}, 1000 - (timeEnd - timeStart))
}
let loadedSettings = false
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
team_name = settings.slack_name
@@ -395,9 +402,12 @@
customer_id = settings.customer_id
workspaceToDeployTo = settings.deploy_to
webhook = settings.webhook
aiResourceInitialPath = settings.ai_resource?.path
aiResourceInitialProvider = settings.ai_resource?.provider
selected = (aiResourceInitialProvider as AiProviderTypes) ?? 'openai'
aiResourcePath = settings.ai_resource?.path
aiProvider = settings.ai_resource?.provider ?? 'openai'
codeCompletionModel = settings.code_completion_model
aiModels = settings.ai_models
errorHandlerItemKind = settings.error_handler?.split('/')[0] as 'flow' | 'script'
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
errorHandlerInitialScriptPath = errorHandlerScriptPath
@@ -415,7 +425,6 @@
: 'custom'
}
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
codeCompletionEnabled = settings.code_completion_enabled
workspaceDefaultAppPath = settings.default_app
s3ResourceSettings = convertBackendSettingsToFrontendSettings(settings.large_file_storage)
@@ -508,6 +517,8 @@
workspace: $workspaceStore!,
path: 'openai_client_credentials_oauth'
})
loadedSettings = true
}
let deployUiSettings: {
@@ -698,7 +709,9 @@
</Tab>
</Tabs>
</div>
{#if tab == 'users'}
{#if !loadedSettings}
<Skeleton layout={[1, [40]]} />
{:else if tab == 'users'}
<WorkspaceUserSettings />
{:else if tab == 'deploy_to'}
<div class="flex flex-col gap-4 my-8">
@@ -1044,43 +1057,104 @@
</Description>
</div>
</div>
<ToggleButtonGroup
bind:selected
on:selected={() => {
aiResourceInitialPath = ''
aiResourceInitialProvider = ''
}}
>
<ToggleButton value="openai" label="OpenAI" />
<ToggleButton value="anthropic" label="Anthropic" />
<ToggleButton value="mistral" label="Mistral" />
</ToggleButtonGroup>
<div class="mt-5 flex gap-1">
{#key [aiResourceInitialPath, aiResourceInitialProvider, usingOpenaiClientCredentialsOauth, selected]}
<ResourcePicker
resourceType={usingOpenaiClientCredentialsOauth
? 'openai_client_credentials_oauth'
: selected}
initialValue={aiResourceInitialPath}
on:change={(ev) => {
editCopilotConfig(ev.detail, selected)
}}
/>
<TestAiKey
disabled={!aiResourceInitialPath || aiResourceInitialProvider != selected}
aiProvider={selected}
/>
{/key}
</div>
<div class="mt-3">
<Toggle
class="mr-2"
bind:checked={codeCompletionEnabled}
options={{ right: 'Enable code completion' }}
on:change={() => {
editCopilotConfig(aiResourceInitialPath || '', aiResourceInitialProvider || '')
<div class="flex flex-col gap-4">
<ToggleButtonGroup
bind:selected={aiProvider}
on:selected={() => {
aiResourcePath = ''
aiModels = []
codeCompletionModel = undefined
}}
/>
>
<ToggleButton value="openai" label="OpenAI" />
<ToggleButton value="anthropic" label="Anthropic" />
<ToggleButton value="mistral" label="Mistral" />
<ToggleButton value="deepseek" label="DeepSeek" />
<ToggleButton value="groq" label="Groq" />
<ToggleButton value="openrouter" label="OpenRouter" />
<ToggleButton
value="customai"
label={'Custom AI' + ($enterpriseLicense ? '' : ' (EE)')}
disabled={!$enterpriseLicense}
tooltip="Configure a custom AI provider that is OpenAI API compatible"
showTooltipIcon
/>
</ToggleButtonGroup>
<div class="flex gap-1">
{#key aiProvider}
<ResourcePicker
resourceType={usingOpenaiClientCredentialsOauth
? 'openai_client_credentials_oauth'
: aiProvider}
initialValue={aiResourcePath}
bind:value={aiResourcePath}
on:change={() => {
if (aiResourcePath && aiModels.length === 0) {
if (aiProvider !== 'customai') {
aiModels = AI_DEFAULT_MODELS[aiProvider].slice(0, 1)
}
}
}}
/>
<TestAIKey
disabled={!aiResourcePath || (aiProvider === 'customai' && aiModels.length === 0)}
resourcePath={aiResourcePath}
{aiProvider}
model={aiProvider === 'customai' ? aiModels[0] : AI_DEFAULT_MODELS[aiProvider][0]}
/>
{/key}
</div>
{#if aiResourcePath}
<Label label="Enabled models">
<MultiSelect
options={AI_DEFAULT_MODELS[aiProvider]}
ulOptionsClass={'!bg-surface-secondary'}
allowUserOptions="append"
bind:selected={aiModels}
/>
</Label>
<div class="flex flex-col gap-2">
<Toggle
on:change={() => {
if (codeCompletionModel != undefined) {
codeCompletionModel = undefined
} else {
codeCompletionModel = AI_DEFAULT_MODELS[aiProvider][0] ?? ''
}
}}
checked={codeCompletionModel != undefined}
options={{
right: 'Code completion'
}}
/>
{#if codeCompletionModel != undefined}
<Label label="Code completion model">
<ArgEnum
enum_={AI_DEFAULT_MODELS[aiProvider]}
bind:value={codeCompletionModel}
disabled={false}
autofocus={false}
defaultValue={undefined}
valid={true}
create={true}
required={false}
/>
</Label>
{/if}
</div>
{/if}
<Button
disabled={(aiResourcePath && aiModels.length === 0) ||
(codeCompletionModel != undefined && codeCompletionModel.length === 0)}
size="sm"
on:click={editCopilotConfig}
>
Save
</Button>
</div>
{:else if tab == 'windmill_lfs'}
<div class="flex flex-col gap-4 my-8">