feat: use openai resource for windmill AI (#1902)

* feat: add inline code gen flow

* feat(frontend): add script gen to flow and app builders

* fix(backend): allow all users to use openai

* feat: use openai resource for windmill AI

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
This commit is contained in:
HugoCasa
2023-07-19 13:44:16 +02:00
committed by GitHub
parent 1a7dc8837e
commit f7201bff0e
11 changed files with 166 additions and 104 deletions

View File

@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE workspace_settings DROP COLUMN openai_resource_path;
ALTER TABLE workspace_settings ADD COLUMN openai_key VARCHAR(255);

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN openai_resource_path VARCHAR(1000);
ALTER TABLE workspace_settings DROP COLUMN openai_key;

View File

@@ -971,7 +971,7 @@ paths:
type: string
deploy_to:
type: string
openai_key:
openai_resource_path:
type: string
error_handler:
type: string
@@ -1126,24 +1126,24 @@ paths:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/edit_openai_key:
/w/{workspace}/workspaces/edit_openai_resource_path:
post:
summary: edit OpenAI key
operationId: editOpenaiKey
summary: edit OpenAI resource path
operationId: editOpenaiResourcePath
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: WorkspaceOpenAIKey
description: WorkspaceOpenaiResourcePath
required: true
content:
application/json:
schema:
type: object
properties:
openai_key:
openai_resource_path:
type: string
responses:
"200":
@@ -1153,10 +1153,10 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/exists_openai_key:
/w/{workspace}/workspaces/exists_openai_resource_path:
get:
summary: OpenAI key exists
operationId: existsOpenaiKey
summary: OpenAI resource path exists
operationId: existsOpenaiResourcePath
tags:
- workspace
parameters:

View File

@@ -1,4 +1,4 @@
use crate::{db::DB, users::Authed, HTTP_CLIENT};
use crate::{db::DB, users::Authed, variables::build_crypt, HTTP_CLIENT};
use axum::{
body::{Bytes, StreamBody},
@@ -8,19 +8,23 @@ use axum::{
routing::post,
Router,
};
use magic_crypt::MagicCryptTrait;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::error::{to_anyhow, Error};
use serde::Deserialize;
pub fn workspaced_service() -> Router {
let router = Router::new().route("/proxy/*openai_path", post(proxy));
router
}
struct OpenAIKey {
openai_key: Option<String>,
#[derive(Deserialize)]
struct OpenaiResource {
api_key: String,
organisation: Option<String>,
}
async fn proxy(
authed: Authed,
Extension(db): Extension<DB>,
@@ -28,35 +32,80 @@ async fn proxy(
body: Bytes,
) -> impl IntoResponse {
let mut tx = db.begin().await?;
let settings = sqlx::query_as!(
OpenAIKey,
"SELECT openai_key FROM workspace_settings WHERE workspace_id = $1",
let openai_resource_path = sqlx::query_scalar!(
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await
.map_err(|e| Error::InternalErr(format!("getting openai_key: {e}")))?;
.await?;
tx.commit().await?;
let openai_key = match settings.openai_key {
Some(key) => key,
None => {
return Err(Error::BadRequest(
"openai_key is not set for this workspace".to_string(),
))
}
if openai_resource_path.is_none() {
return Err(Error::InternalErr(
"OpenAI resource not configured".to_string(),
));
}
let openai_resource_path = openai_resource_path.unwrap();
tx = db.begin().await?;
let resource = sqlx::query_scalar!(
"SELECT value
FROM resource
WHERE path = $1 AND workspace_id = $2",
&openai_resource_path,
&w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
if resource.is_none() {
return Err(Error::InternalErr(
"OpenAI resource missing value".to_string(),
));
}
let mut resource: OpenaiResource = serde_json::from_value(resource.unwrap())
.map_err(|e| Error::InternalErr(format!("validating openai resource {e}")))?;
let openai_api_key_path = if resource.api_key.starts_with("$var:") {
resource.api_key.strip_prefix("$var:").unwrap().to_string()
} else {
return Err(Error::InternalErr(
"OpenAI resource api key must be a variable".to_string(),
));
};
let resp = HTTP_CLIENT
tx = db.begin().await?;
resource.api_key = sqlx::query_scalar!(
"SELECT value
FROM variable
WHERE path = $1 AND workspace_id = $2",
&openai_api_key_path,
&w_id
)
.fetch_one(&mut *tx)
.await?;
let mc = build_crypt(&mut tx, &w_id).await?;
tx.commit().await?;
resource.api_key = mc
.decrypt_base64_to_string(resource.api_key)
.map_err(|e| Error::InternalErr(e.to_string()))?;
let mut request = HTTP_CLIENT
.post(String::from("https://api.openai.com/v1/") + &openai_path)
.header("content-type", "application/json")
.header("authorization", format!("Bearer {}", openai_key))
.body(body)
.send()
.await
.map_err(to_anyhow)?;
.header("authorization", format!("Bearer {}", resource.api_key))
.body(body);
let mut tx = db.begin().await?;
if resource.organisation.is_some() {
request = request.header("OpenAI-Organization", resource.organisation.unwrap());
}
let resp = request.send().await.map_err(to_anyhow)?;
tx = db.begin().await?;
audit_log(
&mut *tx,
&authed.username,

View File

@@ -67,8 +67,8 @@ pub fn workspaced_service() -> Router {
.route("/edit_deploy_to", post(edit_deploy_to))
.route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info))
.route("/edit_openai_key", post(edit_openai_key))
.route("/exists_openai_key", get(exists_openai_key) )
.route("/edit_openai_resource_path", post(edit_openai_resource_path))
.route("/exists_openai_resource_path", get(exists_openai_resource_path) )
.route("/edit_error_handler", post(edit_error_handler));
#[cfg(feature = "enterprise")]
@@ -116,7 +116,7 @@ pub struct WorkspaceSettings {
pub plan: Option<String>,
pub webhook: Option<String>,
pub deploy_to: Option<String>,
pub openai_key: Option<String>,
pub openai_resource_path: Option<String>,
pub error_handler: Option<String>,
}
@@ -158,8 +158,8 @@ struct EditWebhook {
}
#[derive(Deserialize)]
struct EditOpenAIKey {
openai_key: Option<String>,
struct EditOpenaiResourcePath {
openai_resource_path: Option<String>,
}
#[derive(Deserialize)]
@@ -661,28 +661,28 @@ async fn edit_webhook(
Ok(format!("Edit webhook for workspace {}", &w_id))
}
async fn edit_openai_key(
async fn edit_openai_resource_path(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(eo): Json<EditOpenAIKey>,
Json(eo): Json<EditOpenaiResourcePath>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
if let Some(openai_key) = &eo.openai_key {
if let Some(openai_resource_path) = &eo.openai_resource_path {
sqlx::query!(
"UPDATE workspace_settings SET openai_key = $1 WHERE workspace_id = $2",
openai_key,
"UPDATE workspace_settings SET openai_resource_path = $1 WHERE workspace_id = $2",
openai_resource_path,
&w_id
)
.execute(&mut *tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET openai_key = NULL WHERE workspace_id = $1",
"UPDATE workspace_settings SET openai_resource_path = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut *tx)
@@ -691,35 +691,35 @@ async fn edit_openai_key(
audit_log(
&mut *tx,
&authed.username,
"workspaces.edit_openai_key",
"workspaces.edit_openai_resource_path",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("openai_key", &format!("{:?}", eo.openai_key)[..])].into()),
Some([("openai_resource_path", &format!("{:?}", eo.openai_resource_path)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!("Edit openai_key for workspace {}", &w_id))
Ok(format!("Edit openai_resource_path for workspace {}", &w_id))
}
async fn exists_openai_key(
async fn exists_openai_resource_path(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<bool> {
let mut tx = db.begin().await?;
let openai_key = sqlx::query_scalar!(
"SELECT openai_key FROM workspace_settings WHERE workspace_id = $1",
let openai_resource_path = sqlx::query_scalar!(
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut *tx)
.await
.map_err(|e| Error::InternalErr(format!("getting openai_key: {e}")))?;
.map_err(|e| Error::InternalErr(format!("getting openai_resource_path: {e}")))?;
tx.commit().await?;
let exists = openai_key.is_some();
let exists = openai_resource_path.is_some();
Ok(Json(exists))
}

View File

@@ -30,7 +30,7 @@
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle" type="checkbox" />
<input class="!hidden js-password-toggle" id="toggle" type="checkbox" />
<label
class="bg-gray-300 hover:bg-gray-400 rounded px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label"
for="toggle">show</label

View File

@@ -6,7 +6,7 @@
import { sendUserToast } from '$lib/toast'
import type Editor from '../Editor.svelte'
import { faCheck, faClose, faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
import { existsOpenaiKeyStore } from '$lib/stores'
import { existsOpenaiResourcePath } from '$lib/stores'
import type DiffEditor from '../DiffEditor.svelte'
import { scriptLangToEditorLang } from '$lib/scripts'
@@ -18,7 +18,7 @@
// state
let genLoading: boolean = false
let openAIAvailable: boolean | undefined = undefined
let openaiAvailable: boolean | undefined = undefined
let generatedCode = ''
async function onFix() {
@@ -57,14 +57,11 @@
generatedCode = ''
}
async function checkIfOpenAIAvailable(lang: SupportedLanguage) {
try {
const exists = $existsOpenaiKeyStore
openAIAvailable = exists && SUPPORTED_LANGUAGES.has(lang)
} catch (err) {
console.error(err)
sendUserToast('Failed to check if OpenAI is available', true)
}
function checkIfOpenaiAvailable(
lang: SupportedLanguage | 'frontend',
existsOpenaiResourcePath: boolean
) {
openaiAvailable = existsOpenaiResourcePath && SUPPORTED_LANGUAGES.has(lang)
}
function showDiff() {
@@ -78,7 +75,7 @@
diffEditor?.hide()
}
$: checkIfOpenAIAvailable(lang)
$: checkIfOpenaiAvailable(lang, $existsOpenaiResourcePath)
$: lang && (generatedCode = '')
@@ -87,7 +84,7 @@
</script>
{#if error}
{#if openAIAvailable}
{#if openaiAvailable}
<div class="mt-2">
{#if generatedCode}
<div class="flex gap-1">

View File

@@ -9,7 +9,7 @@
import Popup from '../common/popup/Popup.svelte'
import { fade } from 'svelte/transition'
import { Icon } from 'svelte-awesome'
import { existsOpenaiKeyStore } from '$lib/stores'
import { existsOpenaiResourcePath } from '$lib/stores'
import type DiffEditor from '../DiffEditor.svelte'
import { scriptLangToEditorLang } from '$lib/scripts'
import type { Selection } from 'monaco-editor/esm/vs/editor/editor.api'
@@ -25,7 +25,7 @@
// state
let funcDesc: string = ''
let genLoading: boolean = false
let openAIAvailable: boolean | undefined = undefined
let openaiAvailable: boolean | undefined = undefined
let button: HTMLButtonElement | undefined
let input: HTMLInputElement | undefined
let generatedCode = ''
@@ -33,6 +33,9 @@
let isEdit = false
async function onGenerate() {
if (funcDesc.length <= 0) {
return
}
try {
// close popup ^^
const elem = document.activeElement as HTMLElement
@@ -75,14 +78,11 @@
generatedCode = ''
}
async function checkIfOpenAIAvailable(lang: SupportedLanguage | 'frontend') {
try {
const exists = $existsOpenaiKeyStore
openAIAvailable = exists && SUPPORTED_LANGUAGES.has(lang)
} catch (err) {
console.error(err)
sendUserToast('Failed to check if OpenAI is available', true)
}
function checkIfOpenaiAvailable(
lang: SupportedLanguage | 'frontend',
existsOpenaiResourcePath: boolean
) {
openaiAvailable = existsOpenaiResourcePath && SUPPORTED_LANGUAGES.has(lang)
}
function showDiff() {
@@ -106,7 +106,7 @@
})
}
$: checkIfOpenAIAvailable(lang)
$: checkIfOpenaiAvailable(lang, $existsOpenaiResourcePath)
$: input?.focus()
@@ -118,7 +118,7 @@
$: selection && (isEdit = !selection.isEmpty())
</script>
{#if openAIAvailable}
{#if openaiAvailable}
{#if generatedCode}
{#if inlineScript}
<div class="flex gap-1">
@@ -210,7 +210,7 @@
bind:value={funcDesc}
class="!w-auto grow"
on:keypress={({ key }) => {
if (key === 'Enter') {
if (key === 'Enter' && funcDesc.length > 0) {
onGenerate()
}
}}
@@ -225,6 +225,7 @@
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Generate"
on:click={onGenerate}
disabled={funcDesc.length <= 0}
>
<Icon data={faMagicWandSparkles} />
</Button>

View File

@@ -2,7 +2,7 @@ import { OpenAI } from 'openai'
import { OpenAPI } from '../../gen/core/OpenAPI'
import { ResourceService, Script, WorkspaceService } from '../../gen'
import { existsOpenaiKeyStore, workspaceStore } from '$lib/stores'
import { existsOpenaiResourcePath, workspaceStore } from '$lib/stores'
import { formatResourceTypes } from './utils'
import { scriptLangToEditorLang } from '$lib/scripts'
@@ -45,10 +45,10 @@ workspaceStore.subscribe(async (value) => {
workspace = value
if (workspace) {
try {
existsOpenaiKeyStore.set(await WorkspaceService.existsOpenaiKey({ workspace }))
existsOpenaiResourcePath.set(await WorkspaceService.existsOpenaiResourcePath({ workspace }))
} catch (err) {
existsOpenaiKeyStore.set(false)
console.error('Could not get if openai key exists')
existsOpenaiResourcePath.set(false)
console.error('Could not get if OpenAI resource exists')
}
}
})

View File

@@ -65,7 +65,7 @@ export const hubScripts = writable<
}>
| undefined
>(undefined)
export const existsOpenaiKeyStore = writable<boolean>(false)
export const existsOpenaiResourcePath = writable<boolean>(false)
export function switchWorkspace(workspace: string | undefined) {
localStorage.removeItem('flow')

View File

@@ -10,6 +10,7 @@
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
import InviteUser from '$lib/components/InviteUser.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import SearchItems from '$lib/components/SearchItems.svelte'
import Slider from '$lib/components/Slider.svelte'
@@ -27,7 +28,7 @@
} from '$lib/gen'
import {
enterpriseLicense,
existsOpenaiKeyStore,
existsOpenaiResourcePath,
superadmin,
userStore,
usersWorkspaceStore,
@@ -54,10 +55,10 @@
let customer_id: string | undefined = undefined
let webhook: string | undefined = undefined
let workspaceToDeployTo: string | undefined = undefined
let openAIKey: string | undefined = undefined
let errorHandlerInitialPath: string
let errorHandlerScriptPath: string
let errorHandlerItemKind: 'script' = 'script'
let openaiResourceInitialPath: string | undefined = undefined
let tab =
($page.url.searchParams.get('tab') as
| 'users'
@@ -127,22 +128,23 @@
}
}
async function editOpenAIKey(): Promise<void> {
async function editOpenaiResourcePath(openaiResourcePath: string): Promise<void> {
// in JS, an empty string is also falsy
if (openAIKey) {
await WorkspaceService.editOpenaiKey({
openaiResourceInitialPath = openaiResourcePath
if (openaiResourcePath) {
await WorkspaceService.editOpenaiResourcePath({
workspace: $workspaceStore!,
requestBody: { openai_key: openAIKey }
requestBody: { openai_resource_path: openaiResourcePath }
})
existsOpenaiKeyStore.set(true)
sendUserToast('OpenAI key set')
existsOpenaiResourcePath.set(true)
sendUserToast('OpenAI resource set')
} else {
await WorkspaceService.editOpenaiKey({
await WorkspaceService.editOpenaiResourcePath({
workspace: $workspaceStore!,
requestBody: { openai_key: undefined }
requestBody: { openai_resource_path: undefined }
})
existsOpenaiKeyStore.set(false)
sendUserToast(`OpenAI key removed`)
existsOpenaiResourcePath.set(false)
sendUserToast(`OpenAI resource removed`)
}
}
@@ -160,7 +162,7 @@
customer_id = settings.customer_id
workspaceToDeployTo = settings.deploy_to
webhook = settings.webhook
openAIKey = settings.openai_key
openaiResourceInitialPath = settings.openai_resource_path
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
errorHandlerInitialPath = errorHandlerScriptPath
}
@@ -300,7 +302,7 @@
<Tab size="md" value="openai">
<div class="flex gap-2 items-center my-1"
>OpenAI Credentials <span class="text-white px-2 py-1 rounded-full text-xs bg-red-500"
>Windmill AI <span class="text-white px-2 py-1 rounded-full text-xs bg-red-500"
>Beta</span
></div
>
@@ -888,15 +890,22 @@
</div>
</div>
{:else if tab == 'openai'}
<PageHeader title="OpenAI Credentials" primary={false} />
<PageHeader title="Windmill AI" primary={false} />
<div class="mt-2"
><Alert type="info" title="Experimental feature"
>Enter your OpenAI api key to unlock Windmill's AI features!</Alert
>
</div>
<div class="flex gap-2 mt-5">
<input type="text" placeholder="Secret GPT-4 API key" bind:value={openAIKey} />
<Button size="md" on:click={editOpenAIKey}>Save</Button>
>Select an OpenAI resource to unlock Windmill AI features!</Alert
></div
>
<div class="mt-5">
{#key openaiResourceInitialPath}
<ResourcePicker
resourceType="openai"
initialValue={openaiResourceInitialPath}
on:change={(ev) => {
editOpenaiResourcePath(ev.detail)
}}
/>
{/key}
</div>
{/if}
{:else}