feat: ai code completion (#2361)
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET openai_resource_path = $1 WHERE workspace_id = $2",
|
||||
"query": "UPDATE workspace_settings SET openai_resource_path = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d7d368f9ef2587c6abd3daaec93401615a53e3d7d949b65ebf01ef8760ba46b8"
|
||||
"hash": "034583442e6f8ae38d6c4e4aac26f17c8d9d0e657f28276228fc90d3e22e1304"
|
||||
}
|
||||
@@ -37,7 +37,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -67,6 +67,11 @@
|
||||
"ordinal": 12,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "code_completion_enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -87,7 +92,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597"
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -67,6 +67,11 @@
|
||||
"ordinal": 12,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "code_completion_enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -87,7 +92,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e"
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET openai_resource_path = NULL WHERE workspace_id = $1",
|
||||
"query": "UPDATE workspace_settings SET openai_resource_path = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "28eef409e9f25e4c037dd716d6515e96e8962acb59d5e64f9cc3227e0f5fd058"
|
||||
"hash": "6268eabd561502a44e273d6391102278974623f7a7bcad6891d7abadf4d3ea03"
|
||||
}
|
||||
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
28
backend/.sqlx/query-ceb97024ebf1a1c00ea1ed4952f66b229ca4622c537cc252d8ed52b4d24270ee.json
generated
Normal file
28
backend/.sqlx/query-ceb97024ebf1a1c00ea1ed4952f66b229ca4622c537cc252d8ed52b4d24270ee.json
generated
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT openai_resource_path, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "code_completion_enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ceb97024ebf1a1c00ea1ed4952f66b229ca4622c537cc252d8ed52b4d24270ee"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE workspace_settings DROP COLUMN code_completion_enabled;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE workspace_settings ADD COLUMN code_completion_enabled BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1155,8 +1155,12 @@ paths:
|
||||
type: string
|
||||
openai_resource_path:
|
||||
type: string
|
||||
code_completion_enabled:
|
||||
type: boolean
|
||||
error_handler:
|
||||
type: string
|
||||
required:
|
||||
- code_completion_enabled
|
||||
|
||||
/w/{workspace}/workspaces/get_deploy_to:
|
||||
get:
|
||||
@@ -1309,24 +1313,28 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_openai_resource_path:
|
||||
/w/{workspace}/workspaces/edit_copilot_config:
|
||||
post:
|
||||
summary: edit OpenAI resource path
|
||||
operationId: editOpenaiResourcePath
|
||||
summary: edit copilot config
|
||||
operationId: editCopilotConfig
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: WorkspaceOpenaiResourcePath
|
||||
description: WorkspaceCopilotConfig
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- code_completion_enabled
|
||||
properties:
|
||||
openai_resource_path:
|
||||
type: string
|
||||
code_completion_enabled:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -1335,10 +1343,10 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/exists_openai_resource_path:
|
||||
/w/{workspace}/workspaces/get_copilot_info:
|
||||
get:
|
||||
summary: OpenAI resource path exists
|
||||
operationId: existsOpenaiResourcePath
|
||||
summary: get copilot info
|
||||
operationId: getCopilotInfo
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
@@ -1350,7 +1358,15 @@ paths:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: boolean
|
||||
type: object
|
||||
properties:
|
||||
exists_openai_resource_path:
|
||||
type: boolean
|
||||
code_completion_enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- exists_openai_resource_path
|
||||
- code_completion_enabled
|
||||
|
||||
/w/{workspace}/workspaces/edit_error_handler:
|
||||
post:
|
||||
@@ -6883,7 +6899,7 @@ components:
|
||||
- "workspaces.edit_deploy_to"
|
||||
- "workspaces.edit_auto_invite_domain"
|
||||
- "workspaces.edit_webhook"
|
||||
- "workspaces.edit_openai_resource_path"
|
||||
- "workspaces.edit_copilot_config"
|
||||
- "workspaces.edit_error_handler"
|
||||
- "workspaces.create"
|
||||
- "workspaces.update"
|
||||
|
||||
@@ -69,8 +69,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_resource_path", post(edit_openai_resource_path))
|
||||
.route("/exists_openai_resource_path", get(exists_openai_resource_path) )
|
||||
.route("/edit_copilot_config", post(edit_copilot_config))
|
||||
.route("/get_copilot_info", get(get_copilot_info) )
|
||||
.route("/edit_error_handler", post(edit_error_handler));
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -125,6 +125,7 @@ pub struct WorkspaceSettings {
|
||||
pub webhook: Option<String>,
|
||||
pub deploy_to: Option<String>,
|
||||
pub openai_resource_path: Option<String>,
|
||||
pub code_completion_enabled: bool,
|
||||
pub error_handler: Option<String>,
|
||||
}
|
||||
|
||||
@@ -166,8 +167,9 @@ struct EditWebhook {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditOpenaiResourcePath {
|
||||
struct EditCopilotConfig {
|
||||
openai_resource_path: Option<String>,
|
||||
code_completion_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -670,12 +672,12 @@ async fn edit_webhook(
|
||||
Ok(format!("Edit webhook for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
async fn edit_openai_resource_path(
|
||||
async fn edit_copilot_config(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
Json(eo): Json<EditOpenaiResourcePath>,
|
||||
Json(eo): Json<EditCopilotConfig>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
@@ -683,15 +685,17 @@ async fn edit_openai_resource_path(
|
||||
|
||||
if let Some(openai_resource_path) = &eo.openai_resource_path {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET openai_resource_path = $1 WHERE workspace_id = $2",
|
||||
"UPDATE workspace_settings SET openai_resource_path = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
|
||||
openai_resource_path,
|
||||
eo.code_completion_enabled,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET openai_resource_path = NULL WHERE workspace_id = $1",
|
||||
"UPDATE workspace_settings SET openai_resource_path = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
|
||||
eo.code_completion_enabled,
|
||||
&w_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
@@ -700,37 +704,43 @@ async fn edit_openai_resource_path(
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed.username,
|
||||
"workspaces.edit_openai_resource_path",
|
||||
"workspaces.edit_copilot_config",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("openai_resource_path", &format!("{:?}", eo.openai_resource_path)[..])].into()),
|
||||
Some([("openai_resource_path", &format!("{:?}", eo.openai_resource_path)[..]), ("code_completion_enabled", &format!("{:?}", eo.code_completion_enabled)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Edit openai_resource_path for workspace {}", &w_id))
|
||||
Ok(format!("Edit copilot config for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
|
||||
async fn exists_openai_resource_path(
|
||||
#[derive(Serialize)]
|
||||
struct CopilotInfo {
|
||||
pub exists_openai_resource_path: bool,
|
||||
pub code_completion_enabled: bool,
|
||||
}
|
||||
async fn get_copilot_info(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<bool> {
|
||||
) -> JsonResult<CopilotInfo> {
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
let openai_resource_path = sqlx::query_scalar!(
|
||||
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
|
||||
let record = sqlx::query!(
|
||||
"SELECT openai_resource_path, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("getting openai_resource_path: {e}")))?;
|
||||
.map_err(|e| Error::InternalErr(format!("getting openai_resource_path and code_completion_enabled: {e}")))?;
|
||||
tx.commit().await?;
|
||||
|
||||
let exists = openai_resource_path.is_some();
|
||||
|
||||
Ok(Json(exists))
|
||||
Ok(Json(CopilotInfo {
|
||||
exists_openai_resource_path: record.openai_resource_path.is_some(),
|
||||
code_completion_enabled: record.code_completion_enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
|
||||
import { CloseAction, ErrorAction, RequestType, NotificationType } from 'vscode-languageclient'
|
||||
import { MonacoBinding } from 'y-monaco'
|
||||
import { dbSchemas, type DBSchema } from '$lib/stores'
|
||||
import { dbSchemas, type DBSchema, copilotInfo } from '$lib/stores'
|
||||
|
||||
import {
|
||||
createHash as randomHash,
|
||||
@@ -47,6 +47,8 @@
|
||||
import type { Text } from 'yjs'
|
||||
import { initializeMode } from 'monaco-graphql/esm/initializeMode'
|
||||
import type { MonacoGraphQLAPI } from 'monaco-graphql/esm/api'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { editorCodeCompletion } from './copilot/completion'
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
let editor: meditor.IStandaloneCodeEditor
|
||||
@@ -354,6 +356,78 @@
|
||||
}
|
||||
}
|
||||
|
||||
let copilotCompletor: Disposable | undefined = undefined
|
||||
let copilotTs = Date.now()
|
||||
let abortController: AbortController | undefined = undefined
|
||||
function addCopilotSuggestions() {
|
||||
if (copilotCompletor) {
|
||||
copilotCompletor.dispose()
|
||||
}
|
||||
copilotCompletor = languages.registerInlineCompletionsProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
freeInlineCompletions(completions) {},
|
||||
async provideInlineCompletions(model, position, context, token) {
|
||||
abortController?.abort()
|
||||
const textUntilPosition = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
})
|
||||
|
||||
let items: languages.InlineCompletions<languages.InlineCompletion>['items'] = []
|
||||
|
||||
const lastChar = textUntilPosition[textUntilPosition.length - 1]
|
||||
if (textUntilPosition.trim().length > 5 && lastChar.match(/[\(\{\s:=]/)) {
|
||||
const textAfterPosition = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endLineNumber: model.getLineCount() + 1,
|
||||
endColumn: 1
|
||||
})
|
||||
const thisTs = Date.now()
|
||||
copilotTs = thisTs
|
||||
await sleep(500)
|
||||
if (copilotTs === thisTs) {
|
||||
abortController?.abort()
|
||||
abortController = new AbortController()
|
||||
const insertText = await editorCodeCompletion(
|
||||
textUntilPosition,
|
||||
textAfterPosition,
|
||||
lang,
|
||||
abortController
|
||||
)
|
||||
if (insertText) {
|
||||
items = [
|
||||
{
|
||||
insertText,
|
||||
range: {
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
},
|
||||
completeBracketPairs: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
commands: []
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$: $copilotInfo.exists_openai_resource_path &&
|
||||
$copilotInfo.code_completion_enabled &&
|
||||
addCopilotSuggestions()
|
||||
|
||||
const outputChannel = {
|
||||
name: 'Language Server Client',
|
||||
appendLine: (msg: string) => {
|
||||
@@ -922,6 +996,7 @@
|
||||
disposeMethod && disposeMethod()
|
||||
websocketInterval && clearInterval(websocketInterval)
|
||||
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
|
||||
copilotCompletor && copilotCompletor.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
} from '$lib/gen'
|
||||
import { initHistory, push, redo, undo } from '$lib/history'
|
||||
import {
|
||||
copilotInfo,
|
||||
enterpriseLicense,
|
||||
existsOpenaiResourcePath,
|
||||
hubScripts,
|
||||
userStore,
|
||||
workspaceStore
|
||||
@@ -638,7 +638,11 @@
|
||||
try {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
const stepSchema: Schema = JSON.parse(JSON.stringify($flowStateStore[module.id].schema)) // deep copy
|
||||
if (module.source === 'hub' && pastModule !== undefined && $existsOpenaiResourcePath) {
|
||||
if (
|
||||
module.source === 'hub' &&
|
||||
pastModule !== undefined &&
|
||||
$copilotInfo.exists_openai_resource_path
|
||||
) {
|
||||
// ask AI to set step inputs
|
||||
abortController = new AbortController()
|
||||
const inputs = await glueCopilot(
|
||||
@@ -677,7 +681,11 @@
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (module.source === 'hub' && pastModule !== undefined && !$existsOpenaiResourcePath) {
|
||||
if (
|
||||
module.source === 'hub' &&
|
||||
pastModule !== undefined &&
|
||||
!$copilotInfo.exists_openai_resource_path
|
||||
) {
|
||||
sendUserToast(
|
||||
'For better input generation, enable Windmill AI in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
WORKSPACES_EDIT_DEPLOY_TO: 'workspaces.edit_deploy_to',
|
||||
WORKSPACES_EDIT_AUTO_INVITE_DOMAIN: 'workspaces.edit_auto_invite_domain',
|
||||
WORKSPACES_EDIT_WEBHOOK: 'workspaces.edit_webhook',
|
||||
WORKSPACES_EDIT_OPENAI_RESOURCE_PATH: 'workspaces.edit_openai_resource_path',
|
||||
WORKSPACES_EDIT_COPILOT_CONFIG: 'workspaces.edit_copilot_config',
|
||||
WORKSPACES_EDIT_ERROR_HANDLER: 'workspaces.edit_error_handler',
|
||||
WORKSPACES_CREATE: 'workspaces.create',
|
||||
WORKSPACES_UPDATE: 'workspaces.update',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { FlowCopilotContext } from './flow'
|
||||
import { faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
|
||||
import { charsToNumber } from '../flows/idUtils'
|
||||
import { existsOpenaiResourcePath } from '$lib/stores'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
@@ -30,7 +30,7 @@
|
||||
$: copilotStatus && copilotPopover?.refresh()
|
||||
</script>
|
||||
|
||||
{#if $existsOpenaiResourcePath}
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
<ManualPopover bind:this={copilotPopover}>
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -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 { dbSchemas, existsOpenaiResourcePath, type DBSchema } from '$lib/stores'
|
||||
import { dbSchemas, copilotInfo, type DBSchema } from '$lib/stores'
|
||||
import type DiffEditor from '../DiffEditor.svelte'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import Popover from '../Popover.svelte'
|
||||
@@ -187,12 +187,12 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{@const fixAction = (_) => {
|
||||
if ($existsOpenaiResourcePath) {
|
||||
if ($copilotInfo.exists_openai_resource_path) {
|
||||
onFix(() => close(null))
|
||||
}
|
||||
}}
|
||||
<div use:fixAction>
|
||||
{#if $existsOpenaiResourcePath}
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
<div class="w-[42rem] min-h-[3rem] max-h-[34rem] overflow-y-scroll">
|
||||
{#if $generatedCode.length > 0}
|
||||
<div class="overflow-x-scroll">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { dbSchemas, existsOpenaiResourcePath, type DBSchema } from '$lib/stores'
|
||||
import { dbSchemas, copilotInfo, type DBSchema } 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'
|
||||
@@ -269,7 +269,7 @@
|
||||
<LoadingIcon />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $existsOpenaiResourcePath}
|
||||
{:else if $copilotInfo.exists_openai_resource_path}
|
||||
<div class="flex w-96">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { existsOpenaiResourcePath, hubScripts } from '$lib/stores'
|
||||
import { copilotInfo, hubScripts } from '$lib/stores'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import type { FlowCopilotContext, FlowCopilotModule } from './flow'
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
async function onGenerate() {
|
||||
if (!selectedCompletion && !$existsOpenaiResourcePath) {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_openai_resource_path) {
|
||||
sendUserToast(
|
||||
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
||||
true
|
||||
|
||||
61
frontend/src/lib/components/copilot/completion.ts
Normal file
61
frontend/src/lib/components/copilot/completion.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { CreateChatCompletionRequestMessage } from 'openai/resources/chat'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
|
||||
const systemPrompt =
|
||||
'You are a code completion assistant, return the code that should go inside the <completion></completion> tags. If you add a line break, take into account the indentation of the code. You can also not return anything if you think the code is already complete.'
|
||||
const prompt = `complete the following code:
|
||||
\`\`\`{language}
|
||||
{before}<completion></completion>{after}
|
||||
\`\`\``
|
||||
|
||||
const exampleUser = prompt
|
||||
.replace('{language}', 'python')
|
||||
.replace(
|
||||
'{before}',
|
||||
`def main(number):
|
||||
# print the number and divide it by 4
|
||||
`
|
||||
)
|
||||
.replace('{after}', `\n\n return number`)
|
||||
const exampleAssistant = `<completion>print(number)
|
||||
number = number / 4</completion>`
|
||||
|
||||
export async function editorCodeCompletion(
|
||||
before: string,
|
||||
after: string,
|
||||
lang: string,
|
||||
abortController: AbortController
|
||||
) {
|
||||
const messages: CreateChatCompletionRequestMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: exampleUser
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: exampleAssistant
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
.replace('{language}', lang)
|
||||
.replace('{before}', before)
|
||||
.replace('{after}', after)
|
||||
}
|
||||
]
|
||||
|
||||
try {
|
||||
const result = await getNonStreamingCompletion(messages, abortController, 'gpt-3.5-turbo')
|
||||
const completion = result.match(/<completion>(.*)<\/completion>/s)?.[1] || ''
|
||||
|
||||
return completion
|
||||
} catch (err) {
|
||||
if (err.message !== 'Request was aborted.') {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { OpenAPI } from '../../gen/core/OpenAPI'
|
||||
import { ResourceService, Script, WorkspaceService } from '../../gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
import { existsOpenaiResourcePath, workspaceStore, type DBSchema } from '$lib/stores'
|
||||
import { copilotInfo, workspaceStore, type DBSchema } from '$lib/stores'
|
||||
import { formatResourceTypes } from './utils'
|
||||
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
@@ -39,12 +39,13 @@ workspaceStore.subscribe(async (value) => {
|
||||
})
|
||||
if (value) {
|
||||
try {
|
||||
existsOpenaiResourcePath.set(
|
||||
await WorkspaceService.existsOpenaiResourcePath({ workspace: value })
|
||||
)
|
||||
copilotInfo.set(await WorkspaceService.getCopilotInfo({ workspace: value }))
|
||||
} catch (err) {
|
||||
existsOpenaiResourcePath.set(false)
|
||||
console.error('Could not get if OpenAI resource exists')
|
||||
copilotInfo.set({
|
||||
exists_openai_resource_path: false,
|
||||
code_completion_enabled: false
|
||||
})
|
||||
console.error('Could not get copilot info')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -175,7 +176,8 @@ const PROMPTS_CONFIGS = {
|
||||
|
||||
export async function getNonStreamingCompletion(
|
||||
messages: CreateChatCompletionRequestMessage[],
|
||||
abortController: AbortController
|
||||
abortController: AbortController,
|
||||
model: string = 'gpt-4'
|
||||
) {
|
||||
if (!openai) {
|
||||
throw new Error('OpenAI not initialized')
|
||||
@@ -185,13 +187,19 @@ export async function getNonStreamingCompletion(
|
||||
{
|
||||
...openaiConfig,
|
||||
messages,
|
||||
stream: false
|
||||
stream: false,
|
||||
model
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
|
||||
// if (completion.usage) {
|
||||
// const { prompt_tokens, completion_tokens } = completion.usage
|
||||
// console.log('Cost: ', (prompt_tokens * 0.0015 + completion_tokens * 0.002) / 1000)
|
||||
// }
|
||||
|
||||
return completion.choices[0]?.message.content || ''
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import InsertModuleButton from './InsertModuleButton.svelte'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import { existsOpenaiResourcePath } from '$lib/stores'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
import Menu from '$lib/components/common/menu/Menu.svelte'
|
||||
import InsertTriggerButton from './InsertTriggerButton.svelte'
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<Menu pointerDown noMinW placement="bottom-center" let:close bind:show={openNoCopilot}>
|
||||
<button
|
||||
title="AI Flow Builder"
|
||||
on:pointerdown={$existsOpenaiResourcePath
|
||||
on:pointerdown={$copilotInfo.exists_openai_resource_path
|
||||
? (ev) => {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
@@ -162,7 +162,7 @@
|
||||
>
|
||||
<Icon data={faMagicWandSparkles} scale={1} />
|
||||
</button>
|
||||
{#if !$existsOpenaiResourcePath}
|
||||
{#if !$copilotInfo.exists_openai_resource_path}
|
||||
<div class="text-primary p-4">
|
||||
<p class="text-sm w-80"
|
||||
>Enable Windmill AI in the <a
|
||||
|
||||
@@ -68,7 +68,13 @@ export const hubScripts = writable<
|
||||
}>
|
||||
| undefined
|
||||
>(undefined)
|
||||
export const existsOpenaiResourcePath = writable<boolean>(false)
|
||||
export const copilotInfo = writable<{
|
||||
exists_openai_resource_path: boolean
|
||||
code_completion_enabled: boolean
|
||||
}>({
|
||||
exists_openai_resource_path: false,
|
||||
code_completion_enabled: false
|
||||
})
|
||||
|
||||
type SQLBaseSchema = {
|
||||
[schemaKey: string]: {
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
resource_type: 'openai'
|
||||
}
|
||||
})
|
||||
await WorkspaceService.editOpenaiResourcePath({
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: id,
|
||||
requestBody: { openai_resource_path: path }
|
||||
requestBody: { openai_resource_path: path, code_completion_enabled: false }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { OauthService, Script, WorkspaceService } from '$lib/gen'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
existsOpenaiResourcePath,
|
||||
copilotInfo,
|
||||
superadmin,
|
||||
userStore,
|
||||
usersWorkspaceStore,
|
||||
@@ -29,6 +29,7 @@
|
||||
import { Slack } from 'lucide-svelte'
|
||||
|
||||
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
let initialPath: string
|
||||
let scriptPath: string
|
||||
@@ -42,6 +43,7 @@
|
||||
let errorHandlerScriptPath: string
|
||||
let errorHandlerItemKind: 'script' = 'script'
|
||||
let openaiResourceInitialPath: string | undefined = undefined
|
||||
let codeCompletionEnabled: boolean = false
|
||||
let tab =
|
||||
($page.url.searchParams.get('tab') as
|
||||
| 'users'
|
||||
@@ -111,24 +113,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function editOpenaiResourcePath(openaiResourcePath: string): Promise<void> {
|
||||
async function editCopilotConfig(openaiResourcePath: string): Promise<void> {
|
||||
// in JS, an empty string is also falsy
|
||||
openaiResourceInitialPath = openaiResourcePath
|
||||
if (openaiResourcePath) {
|
||||
await WorkspaceService.editOpenaiResourcePath({
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { openai_resource_path: openaiResourcePath }
|
||||
requestBody: {
|
||||
openai_resource_path: openaiResourcePath,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
}
|
||||
})
|
||||
copilotInfo.set({
|
||||
exists_openai_resource_path: true,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
})
|
||||
existsOpenaiResourcePath.set(true)
|
||||
sendUserToast('OpenAI resource set')
|
||||
} else {
|
||||
await WorkspaceService.editOpenaiResourcePath({
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { openai_resource_path: undefined }
|
||||
requestBody: {
|
||||
openai_resource_path: undefined,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
}
|
||||
})
|
||||
copilotInfo.set({
|
||||
exists_openai_resource_path: true,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
})
|
||||
existsOpenaiResourcePath.set(false)
|
||||
sendUserToast(`OpenAI resource removed`)
|
||||
}
|
||||
sendUserToast(`Copilot settings updated`)
|
||||
}
|
||||
|
||||
async function loadSettings(): Promise<void> {
|
||||
@@ -147,6 +160,7 @@
|
||||
openaiResourceInitialPath = settings.openai_resource_path
|
||||
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
|
||||
errorHandlerInitialPath = errorHandlerScriptPath
|
||||
codeCompletionEnabled = settings.code_completion_enabled
|
||||
}
|
||||
|
||||
$: {
|
||||
@@ -447,7 +461,8 @@
|
||||
<PageHeader title="Windmill AI" primary={false} />
|
||||
<div class="mt-2">
|
||||
<Alert type="info" title="Select an OpenAI resource to unlock Windmill AI features!">
|
||||
Windmill AI currently only supports OpenAI's GPT-4.
|
||||
Windmill AI uses OpenAI's GPT-3.5-turbo for code completion and GPT-4 for all other AI
|
||||
features.
|
||||
</Alert>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
@@ -456,11 +471,21 @@
|
||||
resourceType="openai"
|
||||
initialValue={openaiResourceInitialPath}
|
||||
on:change={(ev) => {
|
||||
editOpenaiResourcePath(ev.detail)
|
||||
editCopilotConfig(ev.detail)
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Toggle
|
||||
class="mr-2"
|
||||
bind:checked={codeCompletionEnabled}
|
||||
options={{ right: 'Enable code completion' }}
|
||||
on:change={() => {
|
||||
editCopilotConfig(openaiResourceInitialPath || '')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4" role="alert">
|
||||
|
||||
Reference in New Issue
Block a user