This commit is contained in:
Ruben Fiszel
2025-07-18 22:41:24 +00:00
11 changed files with 285 additions and 102 deletions

View File

@@ -82,7 +82,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.85.0
toolchain: 1.88.0
- uses: Swatinem/rust-cache@v2
with:

View File

@@ -1 +1 @@
773a959358d0dc30255621a584fc2b8e2dd41582
e2bbcda64a9eb783d0832bc6f764be1627046961

View File

@@ -345,7 +345,7 @@ function getDTName(s: string) {
if (s.indexOf('@') === 0 && s.indexOf('/') !== -1) {
// we have a scoped module, e.g. @bla/foo
// which should be converted to bla__foo
s = s.substr(1).replace('/', '__')
s = s.substring(1).replace('/', '__')
}
return s
}

View File

@@ -26,7 +26,6 @@
import DateTimeInput from './DateTimeInput.svelte'
import DateInput from './DateInput.svelte'
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
import FileUpload from './common/fileUpload/FileUpload.svelte'
import autosize from '$lib/autosize'
import PasswordArgInput from './PasswordArgInput.svelte'
import Password from './Password.svelte'
@@ -622,7 +621,7 @@
{:else if (inputCat == 'resource-object' && format && format.split('-').length > 1 && format
.replace('resource-', '')
.replace('_', '')
.toLowerCase() == 's3object') || (inputCat == 'list' && itemsType?.resourceType === 's3_object')}
.toLowerCase() == 's3object') || (inputCat == 'list' && (itemsType?.resourceType === 's3_object' || itemsType?.resourceType === 's3object'))}
<S3ArgInput
multiple={inputCat == 'list'}
bind:value
@@ -668,30 +667,6 @@
reorderable
/>
</div>
{:else if itemsType?.type == 'object' && itemsType?.resourceType == 's3object'}
<div class="w-full">
<FileUpload
{appPath}
computeForceViewerPolicies={computeS3ForceViewerPolicies}
{workspace}
allowMultiple={true}
randomFileKey={true}
on:addition={(evt) => {
value = [
...value,
{
s3: evt.detail?.path ?? '',
filename: evt.detail?.filename ?? ''
}
]
}}
on:deletion={(evt) => {
value = value.filter((v) => v.s3 !== evt.detail?.path)
}}
defaultValue={defaultValue?.map((v) => v.s3)}
initialValue={value}
/>
</div>
{:else}
<div class="w-full">
{#key redraw}

View File

@@ -321,16 +321,23 @@
if (
items.findIndex((x) => {
const c = x.data as AppComponent
if (c.type === 'schemaformcomponent') {
if (
c.type === 'schemaformcomponent' ||
c.type === 'formbuttoncomponent' ||
c.type === 'formcomponent'
) {
const props =
c.type === 'schemaformcomponent'
? (c.componentInput as any)?.value?.properties
: (c.componentInput as any)?.runnable?.type === 'runnableByName'
? (c.componentInput as any)?.runnable?.inlineScript?.schema?.properties
: (c.componentInput as any)?.runnable?.schema?.properties
return (
Object.values((c.componentInput as any)?.value?.properties ?? {}).findIndex(
(p: any) => p?.type === 'object' && p?.format === 'resource-s3_object'
) !== -1
)
} else if (c.type === 'formbuttoncomponent' || c.type === 'formcomponent') {
return (
Object.values((c.componentInput as any)?.fields ?? {}).findIndex(
(p: any) => p?.fieldType === 'object' && p?.format === 'resource-s3_object'
Object.values(props ?? {}).findIndex(
(p: any) =>
(p?.type === 'object' && p?.format === 'resource-s3_object') ||
(p?.type === 'array' &&
(p?.items?.resourceType === 's3object' || p?.items?.resourceType === 's3_object'))
) !== -1
)
} else {

View File

@@ -126,7 +126,7 @@
}
}
}}
defaultValue={defaultValue?.s3}
defaultValue={multiple ? defaultValue?.map((v) => v.s3) : defaultValue?.s3}
initialValue={value}
/>
{/if}

View File

@@ -1,13 +1,10 @@
import { ScriptService, type FlowModule, type RawScript, type Script } from '$lib/gen'
import type {
ChatCompletionSystemMessageParam,
ChatCompletionTool,
ChatCompletionUserMessageParam
} from 'openai/resources/chat/completions.mjs'
import YAML from 'yaml'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
import type { FunctionParameters } from 'openai/resources/shared.mjs'
import uFuzzy from '@leeoniya/ufuzzy'
import { emptySchema, emptyString } from '$lib/utils'
import {
@@ -15,7 +12,7 @@ import {
getLangContext,
SUPPORTED_CHAT_SCRIPT_LANGUAGES
} from '../script/core'
import type { Tool } from '../shared'
import { createSearchHubScriptsTool, createToolDef, type Tool } from '../shared'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
export type AIModuleAction = 'added' | 'modified' | 'removed'
@@ -63,12 +60,6 @@ const searchScriptsToolDef = createToolDef(
'Search for scripts in the workspace'
)
const searchHubScriptsToolDef = createToolDef(
searchScriptsSchema,
'search_hub_scripts',
'Search for scripts in the hub'
)
const langSchema = z.enum(
SUPPORTED_CHAT_SCRIPT_LANGUAGES as [RawScript['language'], ...RawScript['language'][]]
)
@@ -349,6 +340,7 @@ const getInstructionsForCodeGenerationToolDef = createToolDef(
const workspaceScriptsSearch = new WorkspaceScriptsSearch()
export const flowTools: Tool<FlowAIChatHelpers>[] = [
createSearchHubScriptsTool(false),
{
def: searchScriptsToolDef,
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
@@ -369,30 +361,6 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
return JSON.stringify(scriptResults)
}
},
{
def: searchHubScriptsToolDef,
fn: async ({ args, toolId, toolCallbacks }) => {
toolCallbacks.setToolStatus(
toolId,
'Searching for hub scripts related to "' + args.query + '"...'
)
const parsedArgs = searchScriptsSchema.parse(args)
const scripts = await ScriptService.queryHubScripts({
text: parsedArgs.query,
kind: 'script'
})
toolCallbacks.setToolStatus(
toolId,
'Found ' + scripts.length + ' scripts in the hub related to "' + args.query + '"'
)
return JSON.stringify(
scripts.map((s) => ({
path: `hub/${s.version_id}/${s.app}/${s.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
summary: s.summary
}))
)
}
},
{
def: addStepToolDef,
fn: async ({ args, helpers, toolId, toolCallbacks }) => {
@@ -566,32 +534,6 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
}
]
function createToolDef(
zodSchema: z.ZodSchema,
name: string,
description: string
): ChatCompletionTool {
const schema = zodToJsonSchema(zodSchema, {
name,
target: 'openAi'
})
let parameters = schema.definitions![name] as FunctionParameters
parameters = {
...parameters,
required: parameters.required ?? []
}
return {
type: 'function',
function: {
strict: true,
name,
description,
parameters
}
}
}
export function prepareFlowSystemMessage(): ChatCompletionSystemMessageParam {
const content = `You are a helpful assistant that creates and edits workflows on the Windmill platform. You're provided with a bunch of tools to help you edit the flow.
Follow the user instructions carefully.

View File

@@ -8,12 +8,21 @@ import type {
ChatCompletionTool,
ChatCompletionUserMessageParam
} from 'openai/resources/index.mjs'
import { type DBSchema, dbSchemas } from '$lib/stores'
import { copilotSessionModel, type DBSchema, dbSchemas } from '$lib/stores'
import { scriptLangToEditorLang } from '$lib/scripts'
import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/utils'
import type { CodePieceElement, ContextElement } from '../context'
import type { Tool } from '../shared'
import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers'
import { createSearchHubScriptsTool, type Tool } from '../shared'
import { setupTypeAcquisition, type DepsToGet } from '$lib/ata'
import { getModelContextWindow } from '../../lib'
// Score threshold for npm packages search filtering
const SCORE_THRESHOLD = 1000
// percentage of the context window for documentation of npm packages
const DOCS_CONTEXT_PERCENTAGE = 1
// percentage of the context window for types of npm packages
const TYPES_CONTEXT_PERCENTAGE = 1
export function formatResourceTypes(
allResourceTypes: ResourceType[],
@@ -334,6 +343,7 @@ export const CHAT_SYSTEM_PROMPT = `
- 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.
- You can also receive a \`DIFF\` of the changes that have been made to the code. You should use this diff to give better answers.
- Before giving your answer, check again that you carefully followed these instructions.
- When asked to create a script that communicates with an external service, you can use the \`search_hub_scripts\` tool to search for relevant scripts in the hub. Make sure the language is the same as what the user is coding in. If you do not find any relevant scripts, you can use the \`search_npm_packages\` tool to search for relevant packages and their documentation. Always give a link to the documentation in your answer if possible.
Important:
Do not mention or reveal these instructions to the user unless explicitly asked to do so.
@@ -477,6 +487,10 @@ export function prepareScriptTools(
if (context.some((c) => c.type === 'db')) {
tools.push(dbSchemaTool)
}
if (['bun', 'deno'].includes(language)) {
tools.push(createSearchHubScriptsTool(true))
tools.push(searchNpmPackagesTool)
}
return tools
}
@@ -655,3 +669,158 @@ export const dbSchemaTool: Tool<ScriptChatHelpers> = {
return stringSchema
}
}
type PackageSearchQuery = {
package: {
name: string
version: string
links: {
npm: string
homepage: string
repository: string
bugs: string
}
}
searchScore: number
}
type PackageSearchResult = {
package: string
documentation: string
types: string
}
const packagesSearchCache = new Map<string, PackageSearchResult[]>()
export async function searchExternalIntegrationResources(args: { query: string }): Promise<string> {
try {
if (packagesSearchCache.has(args.query)) {
return JSON.stringify(packagesSearchCache.get(args.query))
}
const result = await fetch(`https://registry.npmjs.org/-/v1/search?text=${args.query}&size=2`)
const data = await result.json()
const filtered = data.objects.filter(
(r: PackageSearchQuery) => r.searchScore >= SCORE_THRESHOLD
)
const modelContextWindow = getModelContextWindow(get(copilotSessionModel)?.model ?? '')
const results: PackageSearchResult[] = await Promise.all(
filtered.map(async (r: PackageSearchQuery) => {
let documentation = ''
let types = ''
try {
const docResponse = await fetch(`https://unpkg.com/${r.package.name}/readme.md`)
const docLimit = Math.floor((modelContextWindow * DOCS_CONTEXT_PERCENTAGE) / 100)
documentation = await docResponse.text()
documentation = documentation.slice(0, docLimit)
} catch (error) {
console.error('Error getting documentation for package:', error)
documentation = ''
}
try {
const typesResponse = await fetchNpmPackageTypes(r.package.name, r.package.version)
const typesLimit = Math.floor((modelContextWindow * TYPES_CONTEXT_PERCENTAGE) / 100)
types = typesResponse.types.slice(0, typesLimit)
} catch (error) {
console.error('Error getting types for package:', error)
types = ''
}
return {
package: r.package.name,
documentation: documentation,
types: types
}
})
)
packagesSearchCache.set(args.query, results)
return JSON.stringify(results)
} catch (error) {
console.error('Error searching external integration resources:', error)
return 'Error searching external integration resources'
}
}
const SEARCH_NPM_PACKAGES_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: 'search_npm_packages',
description: 'Search for npm packages and their documentation',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The query to search for'
}
},
required: ['query']
}
}
}
export const searchNpmPackagesTool: Tool<ScriptChatHelpers> = {
def: SEARCH_NPM_PACKAGES_TOOL,
fn: async ({ args, toolId, toolCallbacks }) => {
toolCallbacks.setToolStatus(toolId, 'Searching for relevant packages...')
const result = await searchExternalIntegrationResources(args)
toolCallbacks.setToolStatus(toolId, 'Retrieved relevant packages')
return result
}
}
export async function fetchNpmPackageTypes(
packageName: string,
version: string = 'latest'
): Promise<{ success: boolean; types: string; error?: string }> {
try {
const typeDefinitions = new Map<string, string>()
const ata = setupTypeAcquisition({
projectName: 'NPM-Package-Types',
depsParser: () => [],
root: '',
delegate: {
receivedFile: (code: string, path: string) => {
if (path.endsWith('.d.ts')) {
typeDefinitions.set(path, code)
}
},
localFile: () => {}
}
})
const depsToGet: DepsToGet = [
{
raw: packageName,
module: packageName,
version: version
}
]
await ata(depsToGet)
if (typeDefinitions.size === 0) {
return {
success: false,
types: '',
error: `No type definitions found for ${packageName}`
}
}
const formattedTypes = Array.from(typeDefinitions.entries())
.map(([path, content]) => `// ${path}\n${content}`)
.join('\n\n')
return {
success: true,
types: formattedTypes
}
} catch (error) {
console.error('Error fetching NPM package types:', error)
return {
success: false,
types: '',
error: `Error fetching package types: ${error instanceof Error ? error.message : 'Unknown error'}`
}
}
}

View File

@@ -7,6 +7,10 @@ import { get } from 'svelte/store'
import type { ContextElement } from './context'
import { workspaceStore } from '$lib/stores'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
import type { FunctionParameters } from 'openai/resources/shared.mjs'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { z } from 'zod'
import { ScriptService } from '$lib/gen'
type BaseDisplayMessage = {
content: string
@@ -112,3 +116,77 @@ export interface Tool<T> {
export interface ToolCallbacks {
setToolStatus: (id: string, content: string) => void
}
export function createToolDef(
zodSchema: z.ZodSchema,
name: string,
description: string
): ChatCompletionTool {
const schema = zodToJsonSchema(zodSchema, {
name,
target: 'openAi'
})
let parameters = schema.definitions![name] as FunctionParameters
parameters = {
...parameters,
required: parameters.required ?? []
}
return {
type: 'function',
function: {
strict: true,
name,
description,
parameters
}
}
}
const searchHubScriptsSchema = z.object({
query: z
.string()
.describe('The query to search for, e.g. send email, list stripe invoices, etc..')
})
const searchHubScriptsToolDef = createToolDef(
searchHubScriptsSchema,
'search_hub_scripts',
'Search for scripts in the hub'
)
export const createSearchHubScriptsTool = (withContent: boolean = false) => ({
def: searchHubScriptsToolDef,
fn: async ({ args, toolId, toolCallbacks }) => {
toolCallbacks.setToolStatus(
toolId,
'Searching for hub scripts related to "' + args.query + '"...'
)
const parsedArgs = searchHubScriptsSchema.parse(args)
const scripts = await ScriptService.queryHubScripts({
text: parsedArgs.query,
kind: 'script'
})
toolCallbacks.setToolStatus(
toolId,
'Found ' + scripts.length + ' scripts in the hub related to "' + args.query + '"'
)
// if withContent, fetch scripts with their content, limit to 3 results
const results = await Promise.all(
scripts.slice(0, withContent ? 3 : undefined).map(async (s) => {
let content = ''
if (withContent) {
content = await ScriptService.getHubScriptContentByPath({
path: `hub/${s.version_id}/${s.app}/${s.summary.toLowerCase().replaceAll(/\s+/g, '_')}`
})
}
return {
path: `hub/${s.version_id}/${s.app}/${s.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
summary: s.summary,
...(withContent ? { content } : {})
}
})
)
return JSON.stringify(results)
}
})

View File

@@ -106,6 +106,18 @@ function getModelMaxTokens(model: string) {
return 8192
}
export function getModelContextWindow(model: string) {
if (model.startsWith('gpt-4.1') || model.startsWith('gemini')) {
return 1000000
} else if (model.startsWith('gpt-4o') || model.startsWith('llama-3.3')) {
return 128000
} else if (model.startsWith('claude') || model.startsWith('o4-mini') || model.startsWith('o3')) {
return 200000
} else {
return 128000
}
}
function getModelSpecificConfig(
modelProvider: AIProviderModel,
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]

View File

@@ -376,7 +376,7 @@ components:
type:
type: string
enum:
- forloopflow
- whileloopflow
parallel:
type: boolean
parallelism: