mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
feat(ai-chat): expand chat question answers (#9310)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { CircleHelp } from 'lucide-svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { aiChatManager } from './AIChatManager.svelte'
|
||||
import type { UserQuestionDisplay } from './shared'
|
||||
|
||||
@@ -13,6 +14,8 @@
|
||||
let { toolCallId, userQuestion }: Props = $props()
|
||||
|
||||
let choiceButtons = $state<(HTMLButtonElement | undefined)[]>([])
|
||||
let customAnswer = $state('')
|
||||
let canSubmitCustomAnswer = $derived(customAnswer.trim().length > 0)
|
||||
|
||||
onMount(() => {
|
||||
if (userQuestion.choices.length === 0) {
|
||||
@@ -32,6 +35,15 @@
|
||||
aiChatManager.handleUserQuestionAnswer(toolCallId, choice)
|
||||
}
|
||||
|
||||
function submitCustomAnswer() {
|
||||
const answer = customAnswer.trim()
|
||||
if (!answer) {
|
||||
return
|
||||
}
|
||||
|
||||
aiChatManager.handleUserQuestionAnswer(toolCallId, answer)
|
||||
}
|
||||
|
||||
function handleChoiceKeydown(event: KeyboardEvent, choice: string, index: number) {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
@@ -53,6 +65,16 @@
|
||||
selectChoice(choice)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomAnswerKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Enter') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
submitCustomAnswer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -81,5 +103,27 @@
|
||||
</span>
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
<div class="flex min-w-0 gap-2 pt-1">
|
||||
<TextInput
|
||||
bind:value={customAnswer}
|
||||
class="min-w-0 flex-1"
|
||||
size="sm"
|
||||
inputProps={{
|
||||
'aria-label': 'Custom answer',
|
||||
placeholder: 'Custom answer',
|
||||
onkeydown: handleCustomAnswerKeydown
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
disabled={!canSubmitCustomAnswer}
|
||||
onClick={submitCustomAnswer}
|
||||
btnClasses="shrink-0"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1104,7 +1104,7 @@ describe('global AI tools', () => {
|
||||
expect(item.value.value).toBeUndefined()
|
||||
})
|
||||
|
||||
it('asks the user a multiple-choice question and returns the selected answer', async () => {
|
||||
it('asks the user a question and returns the selected answer', async () => {
|
||||
const callbacks: ToolCallbacks = {
|
||||
setToolStatus: vi.fn(),
|
||||
removeToolStatus: vi.fn(),
|
||||
@@ -1138,6 +1138,79 @@ describe('global AI tools', () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('allows up to ten proposed answers', async () => {
|
||||
const choices = Array.from({ length: 10 }, (_, index) => `choice-${index + 1}`)
|
||||
const callbacks: ToolCallbacks = {
|
||||
setToolStatus: vi.fn(),
|
||||
removeToolStatus: vi.fn(),
|
||||
requestUserQuestion: vi.fn(async (_toolId, question) => question.choices[9])
|
||||
}
|
||||
|
||||
const raw = await callGlobalTool(
|
||||
'askUserQuestion',
|
||||
{
|
||||
question: 'Which option should be used?',
|
||||
choices
|
||||
},
|
||||
callbacks
|
||||
)
|
||||
|
||||
expect(raw).toBe('choice-10')
|
||||
expect(callbacks.requestUserQuestion).toHaveBeenCalledWith(
|
||||
'test-askUserQuestion',
|
||||
expect.objectContaining({
|
||||
choices
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects more than ten proposed answers', async () => {
|
||||
const callbacks: ToolCallbacks = {
|
||||
setToolStatus: vi.fn(),
|
||||
removeToolStatus: vi.fn(),
|
||||
requestUserQuestion: vi.fn()
|
||||
}
|
||||
|
||||
await expect(
|
||||
callGlobalTool(
|
||||
'askUserQuestion',
|
||||
{
|
||||
question: 'Which option should be used?',
|
||||
choices: Array.from({ length: 11 }, (_, index) => `choice-${index + 1}`)
|
||||
},
|
||||
callbacks
|
||||
)
|
||||
).rejects.toThrow()
|
||||
expect(callbacks.requestUserQuestion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns a custom answer that is not one of the proposed answers', async () => {
|
||||
const callbacks: ToolCallbacks = {
|
||||
setToolStatus: vi.fn(),
|
||||
removeToolStatus: vi.fn(),
|
||||
requestUserQuestion: vi.fn(async () => 'use deno instead')
|
||||
}
|
||||
|
||||
const raw = await callGlobalTool(
|
||||
'askUserQuestion',
|
||||
{
|
||||
question: 'Which script language should be used?',
|
||||
choices: ['bun', 'python3']
|
||||
},
|
||||
callbacks
|
||||
)
|
||||
|
||||
expect(raw).toBe('use deno instead')
|
||||
expect(callbacks.setToolStatus).toHaveBeenLastCalledWith(
|
||||
'test-askUserQuestion',
|
||||
expect.objectContaining({
|
||||
content: 'User answered question: use deno instead',
|
||||
result: 'use deno instead',
|
||||
userQuestion: expect.objectContaining({ selectedChoice: 'use deno instead' })
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareGlobalSystemMessage', () => {
|
||||
|
||||
@@ -134,10 +134,10 @@ const askUserQuestionSchema = z.object({
|
||||
.min(1)
|
||||
.describe('The concise question to show to the user before continuing.'),
|
||||
choices: z
|
||||
.array(z.string().min(1).describe('Short answer text shown to the user and returned as-is.'))
|
||||
.array(z.string().min(1).describe('Proposed answer text shown to the user and returned as-is.'))
|
||||
.min(2)
|
||||
.max(6)
|
||||
.describe('Two to six mutually exclusive answer strings.')
|
||||
.max(10)
|
||||
.describe('Two to ten mutually exclusive proposed answer strings.')
|
||||
})
|
||||
|
||||
const listWorkspaceItemsSchema = z.object({
|
||||
@@ -503,7 +503,7 @@ Rules:
|
||||
- Variable values are never readable. For secrets, create a secret variable and reference it from resources as "$var:path/to/variable".
|
||||
- Use search_resource_types before write_resource.
|
||||
- Use get_instructions before writing scripts, flows, resources, or apps. For scripts, pass the target language.
|
||||
- Ask the user when a required decision is ambiguous.
|
||||
- When a required decision is ambiguous, use askUserQuestion with two to ten clear proposed answer strings instead of guessing. The user can also type a custom answer when none of the proposed answers fit.
|
||||
- Keep context targeted.
|
||||
|
||||
Flows:
|
||||
@@ -1275,7 +1275,7 @@ export const globalTools: Tool<{}>[] = [
|
||||
def: createToolDef(
|
||||
askUserQuestionSchema,
|
||||
'askUserQuestion',
|
||||
'Ask the user a multiple-choice question.'
|
||||
'Ask the user a question with proposed answers and wait for their selected or custom answer before continuing.'
|
||||
),
|
||||
fn: async ({ args, toolId, toolCallbacks }) => {
|
||||
const parsed = askUserQuestionSchema.parse(args)
|
||||
|
||||
Reference in New Issue
Block a user