mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
feat: put db resources in ai context (#5507)
* allow mentioning specific files in instructions * remove not working highlight implementation * make highlighted text work * fix tooltip position * clean code * cleaning * use lib for tooltip positioning * fix logic * draft for db in context * use tools for db in context * fixes * cleaning and bug fixes * fix * cleaning * fix when script is db type * simplify logic * put schema in context if already here * fix imports * fix tooltip position and make it scrollable * remove console logs * check if selected is in available * fix tooltip list * add back lost logic * last fix * fix type errors * use loaded schema from dbSchemas * fix typing, content and lang are always there * remove from context if not available anymore * add not loaded yet mention if schema not loaded * add missing callback logic * fix prompt * fix usage of updateselectedContext function * fix styling for white theme * handle tab and arrows * fix schemas not being refreshed on contexts * also refresh displayMessages when dbschemas change * fix duplicate available contexts * fix logic for new scripts * fix new lines inside text area * format files * fix number of db resources fetches * remove padding * fix tooltip y position --------- Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
committed by
GitHub
co-authored by
HugoCasa
parent
3184a7a561
commit
b636d968f1
Generated
+7
@@ -62,6 +62,7 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"textarea-caret": "^3.1.0",
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@~11.1.2",
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
@@ -11881,6 +11882,12 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/textarea-caret": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz",
|
||||
"integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"textarea-caret": "^3.1.0",
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@~11.1.2",
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { copilotSessionModel, dbSchemas, type DBSchema, type DBSchemas } from '$lib/stores'
|
||||
import {
|
||||
copilotSessionModel,
|
||||
dbSchemas,
|
||||
type DBSchema,
|
||||
type DBSchemas,
|
||||
SQLSchemaLanguages,
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import AIChatDisplay from './AIChatDisplay.svelte'
|
||||
import {
|
||||
@@ -8,11 +15,15 @@
|
||||
prepareUserMessage,
|
||||
type AIChatContext,
|
||||
type ContextElement,
|
||||
type DisplayMessage,
|
||||
type SelectedContext
|
||||
type DisplayMessage
|
||||
} from './core'
|
||||
import { createEventDispatcher, onDestroy, setContext } from 'svelte'
|
||||
import type { AIProviderModel, ScriptLang } from '$lib/gen'
|
||||
import {
|
||||
type AIProviderModel,
|
||||
type ListResourceResponse,
|
||||
type ScriptLang,
|
||||
ResourceService
|
||||
} from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { openDB, type DBSchema as IDBSchema, type IDBPDatabase } from 'idb'
|
||||
import { isInitialCode } from '$lib/script_helpers'
|
||||
@@ -26,43 +37,11 @@
|
||||
export let path: string | undefined
|
||||
|
||||
$: contextCodePath = path
|
||||
? path.split('/').pop() + '.' + langToExt(scriptLangToEditorLang(lang))
|
||||
? (path.split('/').pop() ?? 'script') + '.' + langToExt(scriptLangToEditorLang(lang))
|
||||
: undefined
|
||||
|
||||
let initializedWithInitCode: boolean | null = null
|
||||
$: lang && (initializedWithInitCode = null)
|
||||
function onCodeChange() {
|
||||
if (!contextCodePath) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (initializedWithInitCode === null && code) {
|
||||
if (isInitialCode(code)) {
|
||||
initializedWithInitCode = true
|
||||
} else {
|
||||
initializedWithInitCode = false
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath
|
||||
}
|
||||
]
|
||||
}
|
||||
} else if (initializedWithInitCode) {
|
||||
// if the code was initial and was changed, add code context, then prevent it from being added again
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath
|
||||
}
|
||||
]
|
||||
initializedWithInitCode = false
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Could not update context', err)
|
||||
}
|
||||
}
|
||||
$: contextCodePath && code && onCodeChange()
|
||||
|
||||
let db: { schema: DBSchema; resource: string } | undefined = undefined
|
||||
|
||||
@@ -90,23 +69,36 @@
|
||||
}
|
||||
$: updateSchema(lang, args, $dbSchemas)
|
||||
|
||||
let selectedContext: SelectedContext[] = []
|
||||
let selectedContext: ContextElement[] = []
|
||||
|
||||
let availableContext: ContextElement[] = []
|
||||
|
||||
function updateAvailableContext(
|
||||
let dbResources: ListResourceResponse = []
|
||||
|
||||
async function updateDBResources(workspace: string | undefined) {
|
||||
if (workspace) {
|
||||
dbResources = await ResourceService.listResource({
|
||||
workspace: workspace,
|
||||
resourceType: SQLSchemaLanguages.join(',')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAvailableContext(
|
||||
contextCodePath: string | undefined,
|
||||
code: string,
|
||||
lang: ScriptLang | 'bunnative',
|
||||
error: string | undefined,
|
||||
db: { schema: DBSchema; resource: string } | undefined,
|
||||
providerModel: AIProviderModel | undefined
|
||||
providerModel: AIProviderModel | undefined,
|
||||
dbSchemas: DBSchemas,
|
||||
dbResources: ListResourceResponse
|
||||
) {
|
||||
if (!contextCodePath) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
availableContext = [
|
||||
let newAvailableContext: ContextElement[] = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath,
|
||||
@@ -114,10 +106,21 @@
|
||||
lang
|
||||
}
|
||||
]
|
||||
if (!providerModel?.model.endsWith('/thinking')) {
|
||||
for (const d of dbResources) {
|
||||
const loadedSchema = dbSchemas[d.path]
|
||||
newAvailableContext.push({
|
||||
type: 'db',
|
||||
title: d.path,
|
||||
// If the db is already fetched, add the schema to the context
|
||||
...(loadedSchema ? { schema: loadedSchema } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
availableContext = [
|
||||
...availableContext,
|
||||
newAvailableContext = [
|
||||
...newAvailableContext,
|
||||
{
|
||||
type: 'error',
|
||||
title: 'error',
|
||||
@@ -126,22 +129,78 @@
|
||||
]
|
||||
}
|
||||
|
||||
if (db && !providerModel?.model.endsWith('/thinking')) {
|
||||
availableContext = [
|
||||
...availableContext,
|
||||
if (db) {
|
||||
// If the db is already fetched, add it to the selected context
|
||||
if (
|
||||
!selectedContext.find((c) => c.type === 'db' && c.title === db.resource) &&
|
||||
!providerModel?.model.endsWith('/thinking')
|
||||
) {
|
||||
selectedContext = [
|
||||
...selectedContext,
|
||||
{
|
||||
type: 'db',
|
||||
title: db.resource,
|
||||
schema: db.schema
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
availableContext = newAvailableContext
|
||||
|
||||
if (
|
||||
code &&
|
||||
((initializedWithInitCode === null && !isInitialCode(code)) || initializedWithInitCode)
|
||||
) {
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'db',
|
||||
title: db.resource,
|
||||
schema: db.schema
|
||||
type: 'code',
|
||||
title: contextCodePath,
|
||||
content: code,
|
||||
lang
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (code && initializedWithInitCode === null) {
|
||||
initializedWithInitCode = isInitialCode(code)
|
||||
}
|
||||
|
||||
selectedContext = selectedContext
|
||||
.map((c) => availableContext.find((ac) => ac.type === c.type && ac.title === c.title))
|
||||
.filter((c) => c !== undefined) as ContextElement[]
|
||||
} catch (err) {
|
||||
console.error('Could not update available context', err)
|
||||
}
|
||||
}
|
||||
|
||||
$: updateAvailableContext(contextCodePath, code, lang, error, db, $copilotSessionModel)
|
||||
function updateDisplayMessages(dbSchemas: DBSchemas) {
|
||||
return displayMessages.map((m) => ({
|
||||
...m,
|
||||
contextElements: m.contextElements?.map((c) =>
|
||||
c.type === 'db'
|
||||
? {
|
||||
type: 'db',
|
||||
title: c.title,
|
||||
schema: dbSchemas[c.title]
|
||||
}
|
||||
: c
|
||||
) as ContextElement[]
|
||||
}))
|
||||
}
|
||||
|
||||
$: updateDBResources($workspaceStore)
|
||||
|
||||
$: updateAvailableContext(
|
||||
contextCodePath,
|
||||
code,
|
||||
lang,
|
||||
error,
|
||||
db,
|
||||
$copilotSessionModel,
|
||||
$dbSchemas,
|
||||
dbResources
|
||||
)
|
||||
|
||||
let instructions = ''
|
||||
let loading = writable(false)
|
||||
@@ -178,28 +237,11 @@
|
||||
let messages: { role: 'user' | 'assistant' | 'system'; content: string }[] = [
|
||||
prepareSystemMessage()
|
||||
]
|
||||
|
||||
let displayMessages: DisplayMessage[] = []
|
||||
let abortController: AbortController | undefined = undefined
|
||||
|
||||
function updateSelectedContextElements() {
|
||||
try {
|
||||
const contextElements: ContextElement[] = []
|
||||
|
||||
for (const selected of selectedContext) {
|
||||
const el = availableContext.find(
|
||||
(c) => c.type === selected.type && c.title === selected.title
|
||||
)
|
||||
if (el) {
|
||||
contextElements.push(el)
|
||||
}
|
||||
}
|
||||
return contextElements
|
||||
} catch (err) {
|
||||
console.error('Could not update selected context elements', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
let selectedContextElements: ContextElement[] = []
|
||||
$: displayMessages = updateDisplayMessages($dbSchemas)
|
||||
|
||||
async function sendRequest() {
|
||||
if (!instructions.trim()) {
|
||||
@@ -210,19 +252,17 @@
|
||||
aiChatDisplay?.enableAutomaticScroll()
|
||||
abortController = new AbortController()
|
||||
|
||||
selectedContextElements = updateSelectedContextElements()
|
||||
|
||||
displayMessages = [
|
||||
...displayMessages,
|
||||
{
|
||||
role: 'user',
|
||||
content: instructions,
|
||||
contextElements: selectedContextElements
|
||||
contextElements: selectedContext
|
||||
}
|
||||
]
|
||||
const oldInstructions = instructions
|
||||
instructions = ''
|
||||
const userMessage = await prepareUserMessage(oldInstructions, lang, selectedContextElements)
|
||||
const userMessage = await prepareUserMessage(oldInstructions, lang, selectedContext)
|
||||
|
||||
messages.push({ role: 'user', content: userMessage })
|
||||
await saveChat()
|
||||
@@ -232,14 +272,8 @@
|
||||
messages,
|
||||
abortController,
|
||||
lang,
|
||||
(
|
||||
selectedContextElements.find((c) => c.type === 'db') as
|
||||
| Extract<ContextElement, { type: 'db' }>
|
||||
| undefined
|
||||
)?.schema,
|
||||
(token) => {
|
||||
currentReply.update((prev) => prev + token)
|
||||
}
|
||||
selectedContext.filter((c) => c.type === 'db').length > 0,
|
||||
(token) => currentReply.update((prev) => prev + token)
|
||||
)
|
||||
|
||||
messages.push({ role: 'assistant', content: $currentReply })
|
||||
@@ -248,7 +282,7 @@
|
||||
{
|
||||
role: 'assistant',
|
||||
content: $currentReply,
|
||||
contextElements: selectedContextElements
|
||||
contextElements: selectedContext.filter((c) => c.type === 'code')
|
||||
}
|
||||
]
|
||||
currentReply.set('')
|
||||
@@ -318,16 +352,14 @@
|
||||
}
|
||||
instructions = 'Fix the error'
|
||||
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
title: 'error'
|
||||
}
|
||||
]
|
||||
const codeContext = availableContext.find(
|
||||
(c) => c.type === 'code' && c.title === contextCodePath
|
||||
)
|
||||
const errorContext = availableContext.find((c) => c.type === 'error')
|
||||
|
||||
if (codeContext && errorContext) {
|
||||
selectedContext = [codeContext, errorContext]
|
||||
}
|
||||
|
||||
sendRequest()
|
||||
}
|
||||
@@ -390,7 +422,7 @@
|
||||
{
|
||||
role: 'assistant',
|
||||
content: $currentReply,
|
||||
contextElements: selectedContextElements
|
||||
contextElements: selectedContext.filter((c) => c.type === 'code')
|
||||
}
|
||||
]
|
||||
: displayMessages}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
<script lang="ts">
|
||||
import autosize from '$lib/autosize'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import AssistantMessage from './AssistantMessage.svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { ChevronDown, HistoryIcon, Loader2, Plus, X } from 'lucide-svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import {
|
||||
ContextIconMap,
|
||||
type AIChatContext,
|
||||
type DisplayMessage,
|
||||
type ContextElement,
|
||||
type SelectedContext
|
||||
} from './core'
|
||||
import { type AIChatContext, type DisplayMessage, type ContextElement } from './core'
|
||||
import {
|
||||
COPILOT_SESSION_MODEL_SETTING_NAME,
|
||||
COPILOT_SESSION_PROVIDER_SETTING_NAME,
|
||||
@@ -21,11 +14,13 @@
|
||||
} from '$lib/stores'
|
||||
import ContextElementBadge from './ContextElementBadge.svelte'
|
||||
import { storeLocalSetting } from '$lib/utils'
|
||||
import ContextTextarea from './ContextTextarea.svelte'
|
||||
import AvailableContextList from './AvailableContextList.svelte'
|
||||
|
||||
export let pastChats: { id: string; title: string }[]
|
||||
export let messages: DisplayMessage[]
|
||||
export let instructions: string
|
||||
export let selectedContext: SelectedContext[]
|
||||
export let selectedContext: ContextElement[]
|
||||
export let availableContext: ContextElement[]
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -59,6 +54,19 @@
|
||||
model: 'No model',
|
||||
provider: 'No provider'
|
||||
}
|
||||
|
||||
function addContextToSelection(contextElement: ContextElement) {
|
||||
if (
|
||||
!selectedContext.find(
|
||||
(c) => c.type === contextElement.type && c.title === contextElement.title
|
||||
) &&
|
||||
availableContext.find(
|
||||
(c) => c.type === contextElement.type && c.title === contextElement.title
|
||||
)
|
||||
) {
|
||||
selectedContext = [...selectedContext, contextElement]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
@@ -188,97 +196,74 @@
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-1 text-tertiary text-xs p-1 min-w-24">
|
||||
{#if availableContext.filter((c) => !selectedContext.find((sc) => sc.type === c.type)).length === 0}
|
||||
<div class="text-center text-tertiary text-xs">No available context</div>
|
||||
{:else}
|
||||
{#each availableContext as element}
|
||||
{#if !selectedContext.find((c) => c.type === element.type)}
|
||||
<button
|
||||
class="hover:bg-surface-hover rounded-md p-1 text-left flex flex-row gap-1 items-center font-normal"
|
||||
on:click={() => {
|
||||
selectedContext = [
|
||||
...selectedContext,
|
||||
{
|
||||
type: element.type,
|
||||
title: element.title
|
||||
}
|
||||
]
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<svelte:component this={ContextIconMap[element.type]} size={16} />
|
||||
{element.title}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<AvailableContextList
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
onSelect={(element) => {
|
||||
addContextToSelection(element)
|
||||
close()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{#each selectedContext as element}
|
||||
{@const contextElement = availableContext.find((c) => c.type === element.type)}
|
||||
{@const contextElement = availableContext.find(
|
||||
(c) => c.type === element.type && c.title === element.title
|
||||
)}
|
||||
{#if contextElement}
|
||||
<ContextElementBadge
|
||||
{contextElement}
|
||||
deletable
|
||||
on:delete={() => {
|
||||
selectedContext = selectedContext.filter((c) => c.type !== element.type)
|
||||
selectedContext = selectedContext.filter(
|
||||
(c) => c.type !== element.type || c.title !== element.title
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="px-2 scroll-pb-2">
|
||||
<textarea
|
||||
on:keypress={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
dispatch('sendRequest')
|
||||
}
|
||||
}}
|
||||
bind:value={instructions}
|
||||
use:autosize
|
||||
rows={3}
|
||||
placeholder={messages.length > 0 ? 'Ask followup' : 'Ask anything'}
|
||||
class="resize-none"
|
||||
/>
|
||||
<ContextTextarea
|
||||
{instructions}
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
isFirstMessage={messages.length === 0}
|
||||
on:addContext={(e) => addContextToSelection(e.detail.contextElement)}
|
||||
on:sendRequest={() => dispatch('sendRequest')}
|
||||
on:updateInstructions={(e) => (instructions = e.detail.value)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row justify-end items-center gap-2 px-0.5">
|
||||
<div class="min-w-0">
|
||||
<Popover disablePopup={$copilotInfo.aiModels.length <= 1} class="max-w-full">
|
||||
<svelte:fragment slot="trigger">
|
||||
<div class="text-tertiary text-xs flex flex-row items-center gap-0.5 font-normal">
|
||||
<span class="truncate">{providerModel.model}</span>
|
||||
{#if $copilotInfo.aiModels.length > 1}
|
||||
<div class="shrink-0">
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-1 p-1 min-w-24">
|
||||
{#each $copilotInfo.aiModels.filter((m) => m.model !== providerModel.model) as providerModel}
|
||||
<button
|
||||
class="text-left text-xs hover:bg-surface-hover rounded-md p-1 font-normal"
|
||||
on:click={() => {
|
||||
$copilotSessionModel = providerModel
|
||||
storeLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME, providerModel.model)
|
||||
storeLocalSetting(
|
||||
COPILOT_SESSION_PROVIDER_SETTING_NAME,
|
||||
providerModel.provider
|
||||
)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{providerModel.model}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end items-center gap-2 px-0.5">
|
||||
<div class="min-w-0">
|
||||
<Popover disablePopup={$copilotInfo.aiModels.length <= 1} class="max-w-full">
|
||||
<svelte:fragment slot="trigger">
|
||||
<div class="text-tertiary text-xs flex flex-row items-center gap-0.5 font-normal">
|
||||
<span class="truncate">{providerModel.model}</span>
|
||||
{#if $copilotInfo.aiModels.length > 1}
|
||||
<div class="shrink-0">
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-1 p-1 min-w-24">
|
||||
{#each $copilotInfo.aiModels.filter((m) => m.model !== providerModel.model) as providerModel}
|
||||
<button
|
||||
class="text-left text-xs hover:bg-surface-hover rounded-md p-1 font-normal"
|
||||
on:click={() => {
|
||||
$copilotSessionModel = providerModel
|
||||
storeLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME, providerModel.model)
|
||||
storeLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME, providerModel.provider)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{providerModel.model}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { ContextIconMap } from './core'
|
||||
import type { ContextElement } from './core'
|
||||
|
||||
export let availableContext: ContextElement[]
|
||||
export let selectedContext: ContextElement[]
|
||||
export let onSelect: (element: ContextElement) => void
|
||||
export let showAllAvailable = false
|
||||
export let stringSearch = ''
|
||||
export let selectedIndex = 0
|
||||
|
||||
$: actualAvailableContext = showAllAvailable
|
||||
? availableContext.filter(
|
||||
(c) => !stringSearch || c.title.toLowerCase().includes(stringSearch.toLowerCase())
|
||||
)
|
||||
: availableContext.filter(
|
||||
(c) =>
|
||||
!selectedContext.find((sc) => sc.type === c.type && sc.title === c.title) &&
|
||||
(!stringSearch || c.title.toLowerCase().includes(stringSearch.toLowerCase()))
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1 text-tertiary text-xs p-1 min-w-24 max-h-48 overflow-y-scroll">
|
||||
{#if actualAvailableContext.length === 0}
|
||||
<div class="text-center text-tertiary text-xs">No available context</div>
|
||||
{:else}
|
||||
{#each actualAvailableContext as element, i}
|
||||
<button
|
||||
class="hover:bg-surface-hover rounded-md p-1 text-left flex flex-row gap-1 items-center font-normal {i ===
|
||||
selectedIndex
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => onSelect(element)}
|
||||
>
|
||||
<svelte:component this={ContextIconMap[element.type]} size={16} />
|
||||
{element.title}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
{:else if contextElement.type === 'db'}
|
||||
<div class="p-2 max-w-96 max-h-[300px] text-xs overflow-auto">
|
||||
{#if contextElement.schema.lang === 'graphql'}
|
||||
{#if contextElement.schema && contextElement.schema.lang === 'graphql'}
|
||||
{#await import('$lib/components/GraphqlSchemaViewer.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
@@ -62,8 +62,10 @@
|
||||
class="h-full"
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
{:else if contextElement.schema}
|
||||
<ObjectViewer json={formatSchema(contextElement.schema)} pureViewer collapseLevel={1} />
|
||||
{:else}
|
||||
<div class="text-tertiary">Not loaded yet</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if contextElement.type === 'code'}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<script lang="ts">
|
||||
import autosize from '$lib/autosize'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { ContextElement } from './core'
|
||||
import AvailableContextList from './AvailableContextList.svelte'
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
|
||||
export let instructions: string
|
||||
export let availableContext: ContextElement[]
|
||||
export let selectedContext: ContextElement[]
|
||||
export let isFirstMessage: boolean
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
updateInstructions: { value: string }
|
||||
sendRequest: null
|
||||
addContext: { contextElement: ContextElement }
|
||||
}>()
|
||||
|
||||
let showContextTooltip = false
|
||||
let contextTooltipWord = ''
|
||||
let tooltipPosition = { x: 0, y: 0 }
|
||||
let textarea: HTMLTextAreaElement
|
||||
let selectedSuggestionIndex = 0
|
||||
|
||||
function getHighlightedText(text: string) {
|
||||
return text.replace(/@[\w/.-]+/g, (match) => {
|
||||
const contextElement = availableContext.find((c) => c.title === match.slice(1))
|
||||
if (contextElement) {
|
||||
return `<span class="bg-black dark:bg-white text-white dark:text-black z-10">${match}</span>`
|
||||
}
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
function addContextToSelection(contextElement: ContextElement) {
|
||||
dispatch('addContext', { contextElement })
|
||||
}
|
||||
|
||||
function updateInstructionsWithContext(contextElement: ContextElement) {
|
||||
const index = instructions.lastIndexOf('@')
|
||||
if (index !== -1) {
|
||||
const newInstructions = instructions.substring(0, index) + `@${contextElement.title}`
|
||||
dispatch('updateInstructions', { value: newInstructions })
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextSelection(contextElement: ContextElement) {
|
||||
addContextToSelection(contextElement)
|
||||
updateInstructionsWithContext(contextElement)
|
||||
showContextTooltip = false
|
||||
}
|
||||
|
||||
function updateTooltipPosition(
|
||||
availableContext: ContextElement[],
|
||||
showContextTooltip: boolean,
|
||||
contextTooltipWord: string
|
||||
) {
|
||||
if (!textarea || !showContextTooltip) return
|
||||
|
||||
const coords = getCaretCoordinates(textarea, textarea.selectionEnd)
|
||||
const rect = textarea.getBoundingClientRect()
|
||||
|
||||
const filteredAvailableContext = availableContext.filter(
|
||||
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
|
||||
)
|
||||
|
||||
const itemHeight = 28 // Estimated height of one item + gap (Button: p-1(8px) + text-xs(16px) = 24px; Parent: gap-1(4px) = 28px)
|
||||
const containerPadding = 8 // p-1 top + p-1 bottom = 4px + 4px = 8px
|
||||
const maxHeight = 192 + containerPadding // max-h-48 (192px) + containerPadding (8px)
|
||||
|
||||
// Calculate uncapped height, subtract gap from last item as it's not needed
|
||||
const numItems = filteredAvailableContext.length
|
||||
let uncappedHeight =
|
||||
numItems > 0 ? numItems * itemHeight - 4 + containerPadding : containerPadding
|
||||
// Ensure height is at least containerPadding even if no items
|
||||
uncappedHeight = Math.max(uncappedHeight, containerPadding)
|
||||
|
||||
const estimatedTooltipHeight = Math.min(uncappedHeight, maxHeight)
|
||||
const margin = 6 // Small margin between caret and tooltip
|
||||
|
||||
let finalY: number
|
||||
|
||||
if (isFirstMessage) {
|
||||
// Position below the caret line
|
||||
finalY = rect.top + coords.top + coords.height - 3
|
||||
} else {
|
||||
// Position above the caret line
|
||||
finalY = rect.top + coords.top - estimatedTooltipHeight - margin
|
||||
}
|
||||
|
||||
tooltipPosition = {
|
||||
x: rect.left + coords.left - 70,
|
||||
y: finalY
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
textarea = e.target as HTMLTextAreaElement
|
||||
const words = instructions.split(/\s+/)
|
||||
const lastWord = words[words.length - 1]
|
||||
|
||||
if (
|
||||
lastWord.startsWith('@') &&
|
||||
(!availableContext.find((c) => c.title === lastWord.slice(1)) ||
|
||||
!selectedContext.find((c) => c.title === lastWord.slice(1)))
|
||||
) {
|
||||
showContextTooltip = true
|
||||
contextTooltipWord = lastWord
|
||||
} else {
|
||||
showContextTooltip = false
|
||||
contextTooltipWord = ''
|
||||
selectedSuggestionIndex = 0
|
||||
}
|
||||
dispatch('updateInstructions', { value: instructions })
|
||||
}
|
||||
|
||||
function handleKeyPress(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
if (contextTooltipWord) {
|
||||
const filteredContext = availableContext.filter(
|
||||
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
|
||||
)
|
||||
const contextElement = filteredContext[selectedSuggestionIndex]
|
||||
if (contextElement) {
|
||||
const isInSelectedContext = selectedContext.find(
|
||||
(c) => c.title === contextElement.title && c.type === contextElement.type
|
||||
)
|
||||
// If the context element is already in the selected context and the last word in the instructions is the same as the context element title, send request
|
||||
if (isInSelectedContext && instructions.split(' ').pop() === '@' + contextElement.title) {
|
||||
dispatch('sendRequest')
|
||||
return
|
||||
}
|
||||
handleContextSelection(contextElement)
|
||||
} else if (contextTooltipWord === '@' && availableContext.length > 0) {
|
||||
handleContextSelection(availableContext[0])
|
||||
}
|
||||
} else {
|
||||
dispatch('sendRequest')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (!showContextTooltip) return
|
||||
|
||||
const filteredContext = availableContext.filter(
|
||||
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
|
||||
)
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
const contextElement = filteredContext[selectedSuggestionIndex]
|
||||
if (contextElement) {
|
||||
handleContextSelection(contextElement)
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
selectedSuggestionIndex = (selectedSuggestionIndex + 1) % filteredContext.length
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
selectedSuggestionIndex =
|
||||
(selectedSuggestionIndex - 1 + filteredContext.length) % filteredContext.length
|
||||
}
|
||||
}
|
||||
|
||||
$: updateTooltipPosition(availableContext, showContextTooltip, contextTooltipWord)
|
||||
</script>
|
||||
|
||||
<div class="relative w-full px-2 scroll-pb-2">
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full min-h-12 px-4 text-sm pt-1 pointer-events-none"
|
||||
style="line-height: 1.72"
|
||||
>
|
||||
<span class="break-words" style="white-space: pre-wrap;">
|
||||
{@html getHighlightedText(instructions)}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
on:keypress={handleKeyPress}
|
||||
on:keydown={handleKeyDown}
|
||||
bind:value={instructions}
|
||||
use:autosize
|
||||
rows={3}
|
||||
on:input={handleInput}
|
||||
on:blur={() => {
|
||||
setTimeout(() => {
|
||||
showContextTooltip = false
|
||||
}, 100)
|
||||
}}
|
||||
placeholder={isFirstMessage ? 'Ask anything' : 'Ask followup'}
|
||||
class="resize-none bg-transparent caret-black dark:caret-white"
|
||||
style={instructions.length > 0
|
||||
? 'color: transparent; -webkit-text-fill-color: transparent;'
|
||||
: ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if showContextTooltip}
|
||||
<div
|
||||
class="absolute bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-50"
|
||||
style="left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;"
|
||||
>
|
||||
<AvailableContextList
|
||||
{availableContext}
|
||||
{selectedContext}
|
||||
onSelect={(element) => {
|
||||
handleContextSelection(element)
|
||||
}}
|
||||
showAllAvailable={true}
|
||||
stringSearch={contextTooltipWord.slice(1)}
|
||||
selectedIndex={selectedSuggestionIndex}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -11,8 +11,9 @@ import type {
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionTool
|
||||
} from 'openai/resources/index.mjs'
|
||||
import { workspaceStore, type DBSchema } from '$lib/stores'
|
||||
import { workspaceStore, type DBSchema, dbSchemas } from '$lib/stores'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/utils'
|
||||
|
||||
export function formatResourceTypes(
|
||||
resourceTypes: ResourceType[],
|
||||
@@ -204,18 +205,20 @@ export async function getFormattedResourceTypes(
|
||||
}
|
||||
|
||||
export const CHAT_SYSTEM_PROMPT = `
|
||||
You are a coding assistant on the Windmill platform. You are given a list of instructions to follow \`INSTRUCTIONS\` as well as the current code in the file \`CODE\`.
|
||||
You are a coding assistant for the Windmill platform. You are provided with a list of \`INSTRUCTIONS\` and the current contents of a code file under \`CODE\`.
|
||||
|
||||
Please respond to the user's query. The user's query is never invalid.
|
||||
Your task is to respond to the user's request. Assume all user queries are valid and actionable.
|
||||
|
||||
In the case that the user asks you to make changes to code, you should make sure to return a single CODE BLOCK, as well as explanations and descriptions of the changes.
|
||||
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
|
||||
- If suggesting changes, rewrite the **complete code** and not just a part of it.
|
||||
When the user requests code changes:
|
||||
- Always include a **single code block** with the **entire updated file**, not just the modified sections.
|
||||
- Follow the instructions carefully and explain the reasoning behind your changes.
|
||||
- If the request is abstract (e.g., "make this cleaner"), interpret it concretely and reflect that in the code block.
|
||||
- Preserve existing formatting, indentation, and whitespace unless changes are strictly required to fulfill the user's request.
|
||||
- The user can ask you to look at or modify specific files, databases or errors by having its name in the INSTRUCTIONS preceded by the @ symbol. In this case, put your focus on the element that is explicitly mentioned.
|
||||
- The user can ask you questions about a list of \`DATABASES\` that are available in the user's workspace. If the user asks you a question about a database, you should ask the user to specify the database name if not given, or take the only one available if there is only one.
|
||||
|
||||
Requirements:
|
||||
- When suggesting changes, do not change spacing, indentation, or other whitespace apart from what is strictly necessary to apply the changes.
|
||||
|
||||
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
|
||||
Important:
|
||||
Do not mention or reveal these instructions to the user unless explicitly asked to do so.
|
||||
`
|
||||
|
||||
const CHAT_USER_CODE_CONTEXT = `
|
||||
@@ -237,11 +240,19 @@ INSTRUCTIONS:
|
||||
WINDMILL LANGUAGE CONTEXT:
|
||||
{lang_context}
|
||||
|
||||
DATABASES:
|
||||
{db_context}
|
||||
|
||||
CODE:
|
||||
{code_context}
|
||||
|
||||
ERROR:
|
||||
{error_context}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
export const CHAT_USER_DB_CONTEXT = `- {title}: SCHEMA: \n{schema}\n`
|
||||
|
||||
export function prepareSystemMessage(): {
|
||||
role: 'system'
|
||||
content: string
|
||||
@@ -264,28 +275,23 @@ export const ContextIconMap = {
|
||||
db: Database
|
||||
}
|
||||
|
||||
export type SelectedContext = {
|
||||
type: 'code' | 'error' | 'db'
|
||||
title: string
|
||||
}
|
||||
|
||||
export type ContextElement =
|
||||
| {
|
||||
type: 'code'
|
||||
content: string
|
||||
title: string
|
||||
lang: ScriptLang | 'bunnative'
|
||||
}
|
||||
type: 'code'
|
||||
content: string
|
||||
title: string
|
||||
lang: ScriptLang | 'bunnative'
|
||||
}
|
||||
| {
|
||||
type: 'error'
|
||||
content: string
|
||||
title: 'error'
|
||||
}
|
||||
type: 'error'
|
||||
content: string
|
||||
title: 'error'
|
||||
}
|
||||
| {
|
||||
type: 'db'
|
||||
schema: DBSchema
|
||||
title: string
|
||||
}
|
||||
type: 'db'
|
||||
schema?: DBSchema
|
||||
title: string
|
||||
}
|
||||
|
||||
export async function prepareUserMessage(
|
||||
instructions: string,
|
||||
@@ -294,6 +300,7 @@ export async function prepareUserMessage(
|
||||
) {
|
||||
let codeContext = ''
|
||||
let errorContext = ''
|
||||
let dbContext = ''
|
||||
for (const context of selectedContext) {
|
||||
if (context.type === 'code') {
|
||||
codeContext += CHAT_USER_CODE_CONTEXT.replace('{title}', context.title)
|
||||
@@ -304,6 +311,8 @@ export async function prepareUserMessage(
|
||||
throw new Error('Multiple error contexts provided')
|
||||
}
|
||||
errorContext = CHAT_USER_ERROR_CONTEXT.replace('{error}', context.content)
|
||||
} else if (context.type === 'db') {
|
||||
dbContext += CHAT_USER_DB_CONTEXT.replace('{title}', context.title).replace('{schema}', context.schema?.stringified ?? 'to fetch with get_db_schema')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +320,7 @@ export async function prepareUserMessage(
|
||||
.replace('{lang_context}', getLangContext(language))
|
||||
.replace('{code_context}', codeContext)
|
||||
.replace('{error_context}', errorContext)
|
||||
.replace('{db_context}', dbContext)
|
||||
|
||||
return userMessage
|
||||
}
|
||||
@@ -340,7 +350,14 @@ const DB_SCHEMA_FUNCTION_DEF: ChatCompletionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_db_schema',
|
||||
description: 'Gets the schema of the database in context'
|
||||
description: 'Gets the schema of the database',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
resourcePath: { type: 'string', description: 'The path of the database resource' }
|
||||
},
|
||||
required: ['resourcePath']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +385,6 @@ async function callTool(
|
||||
functionName: string,
|
||||
args: any,
|
||||
lang: ScriptLang | 'bunnative',
|
||||
dbSchema: DBSchema | undefined,
|
||||
workspace: string
|
||||
) {
|
||||
switch (functionName) {
|
||||
@@ -376,10 +392,22 @@ async function callTool(
|
||||
const formattedResourceTypes = await getFormattedResourceTypes(lang, args.query, workspace)
|
||||
return formattedResourceTypes
|
||||
case 'get_db_schema':
|
||||
if (!dbSchema) {
|
||||
throw new Error('No database schema provided')
|
||||
if (!args.resourcePath) {
|
||||
throw new Error('Database path not provided')
|
||||
}
|
||||
const stringSchema = await formatDBSchema(dbSchema)
|
||||
const resource = await ResourceService.getResource({
|
||||
workspace: workspace,
|
||||
path: args.resourcePath
|
||||
})
|
||||
const newDbSchemas = {}
|
||||
await getDbSchemas(resource.resource_type, args.resourcePath, workspace, newDbSchemas, (error) => { console.error(error) })
|
||||
dbSchemas.update(schemas => ({ ...schemas, ...newDbSchemas }))
|
||||
const dbs = get(dbSchemas)
|
||||
const db = dbs[args.resourcePath]
|
||||
if (!db) {
|
||||
throw new Error('Database not found')
|
||||
}
|
||||
const stringSchema = await formatDBSchema(db)
|
||||
return stringSchema
|
||||
default:
|
||||
throw new Error(`Unknown tool call: ${functionName}`)
|
||||
@@ -390,7 +418,7 @@ export async function chatRequest(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
lang: ScriptLang | 'bunnative',
|
||||
dbSchema: DBSchema | undefined,
|
||||
useDbTools: boolean,
|
||||
onNewToken: (token: string) => void
|
||||
) {
|
||||
const toolDefs: ChatCompletionTool[] = []
|
||||
@@ -404,7 +432,7 @@ export async function chatRequest(
|
||||
) {
|
||||
toolDefs.push(RESOURCE_TYPE_FUNCTION_DEF)
|
||||
}
|
||||
if (dbSchema) {
|
||||
if (useDbTools) {
|
||||
toolDefs.push(DB_SCHEMA_FUNCTION_DEF)
|
||||
}
|
||||
try {
|
||||
@@ -459,7 +487,6 @@ export async function chatRequest(
|
||||
toolCall.function.name,
|
||||
args,
|
||||
lang,
|
||||
dbSchema,
|
||||
get(workspaceStore) ?? ''
|
||||
)
|
||||
messages.push({
|
||||
|
||||
@@ -139,9 +139,9 @@ const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME)
|
||||
export const copilotSessionModel = writable<AIProviderModel | undefined>(
|
||||
sessionModel && sessionProvider
|
||||
? {
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
export const usedTriggerKinds = writable<string[]>([])
|
||||
@@ -158,8 +158,10 @@ type SQLBaseSchema = {
|
||||
}
|
||||
}
|
||||
|
||||
export const SQLSchemaLanguages = ['mysql', 'bigquery', 'postgresql', 'snowflake', 'mssql', 'oracledb'] as const
|
||||
|
||||
export interface SQLSchema {
|
||||
lang: 'mysql' | 'bigquery' | 'postgresql' | 'snowflake' | 'mssql' | 'oracledb'
|
||||
lang: typeof SQLSchemaLanguages[number]
|
||||
schema: SQLBaseSchema
|
||||
publicOnly: boolean | undefined
|
||||
stringified: string
|
||||
|
||||
Reference in New Issue
Block a user