Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b04dccf
feat: dummy button
Dakuan Mar 9, 2026
9c917e0
fix: dummy ui
Dakuan Mar 9, 2026
9be2ec2
feat: dummy api
Dakuan Mar 9, 2026
b3f11bd
feat: log prompt
Dakuan Mar 9, 2026
51c63d2
feat: instruction gen basics
Dakuan Mar 9, 2026
dd4c560
fix: refactor
Dakuan Mar 9, 2026
da3c6cd
fix: specs
Dakuan Mar 9, 2026
7c95307
fix: review instructions step
Dakuan Mar 9, 2026
a4739f6
fix: bit more context
Dakuan Mar 9, 2026
43a0707
fix: pick up on tools better
Dakuan Mar 9, 2026
2a7ee31
fix: lint
Dakuan Mar 10, 2026
a48b1f4
fix: spec
Dakuan Mar 10, 2026
0a05f99
fix: undo spec
Dakuan Mar 10, 2026
78b74a2
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 18, 2026
71c45ab
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 18, 2026
591eb25
fix: move button
Dakuan Mar 18, 2026
a779520
fix: focus input
Dakuan Mar 18, 2026
d039f8c
fix: read only when generating
Dakuan Mar 19, 2026
b63c9e7
fix: read only when generate
Dakuan Mar 19, 2026
eb9bbb7
fix: clear state
Dakuan Mar 19, 2026
81bb0f1
fix: flag
Dakuan Mar 19, 2026
e73affb
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 19, 2026
b316464
fix: extract component
Dakuan Mar 19, 2026
da7cfed
fix: remove console log
Dakuan Mar 19, 2026
70fa380
fix: lint
Dakuan Mar 19, 2026
5d812ad
fix: svelte 5
Dakuan Mar 19, 2026
7803de7
fix: types
Dakuan Mar 20, 2026
21a8ca0
fix: actually save
Dakuan Mar 20, 2026
4005c23
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 20, 2026
422e006
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 20, 2026
db29ed4
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 20, 2026
f33a7f5
fix: move derivation
Dakuan Mar 20, 2026
9163c48
fix: refactor
Dakuan Mar 20, 2026
cb91fa4
fix: use sdk
Dakuan Mar 20, 2026
b840bf4
Merge branch 'master' into feat/generate-agent-instructions
Dakuan Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/backend-core/src/features/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ const featureFlagDefaults: Record<FeatureFlag, boolean> = {
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
[FeatureFlag.AI_AGENTS]: true,
[FeatureFlag.AI_RAG]: false,
[FeatureFlag.AI_AGENT_INSTRUCTIONS]: false,
[FeatureFlag.DEBUG_UI]: env.isDev(),
[FeatureFlag.DEV_USE_CLIENT_FROM_STORAGE]: false,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<script lang="ts">
import {
Button,
Modal,
ModalContent,
TextArea,
notifications,
} from "@budibase/bbui"
import { type EnrichedBinding } from "@budibase/types"
import { featureFlags } from "@/stores/portal"
import { API } from "@/api"
import { tick } from "svelte"
import CodeEditor from "@/components/common/CodeEditor/CodeEditor.svelte"
import { EditorModes } from "@/components/common/CodeEditor"
import { getIncludedToolRuntimeBindings } from "./toolBindingUtils"

const AI_AGENT_INSTRUCTIONS_FLAG = "AI_AGENT_INSTRUCTIONS"

export interface Props {
aiconfigId?: string
agentName?: string
goal?: string
promptInstructions?: string
promptBindings?: EnrichedBinding[]
bindingIcons?: Record<string, string | undefined>
onApplyInstructions?: (_instructions: string) => void
}

let {
aiconfigId = "",
agentName = "",
goal = "",
promptInstructions = "",
promptBindings = [],
bindingIcons = {},
onApplyInstructions = () => {},
}: Props = $props()

let readableToRuntimeBinding = $derived.by(() => {
return promptBindings.reduce<Record<string, string>>((acc, binding) => {
if (binding.readableBinding && binding.runtimeBinding) {
acc[binding.readableBinding] = binding.runtimeBinding
}
return acc
}, {})
})

let includedToolRuntimeBindings = $derived(
getIncludedToolRuntimeBindings(promptInstructions, readableToRuntimeBinding)
)

let includedToolsWithDetails = $derived(
includedToolRuntimeBindings
.map(runtimeBinding =>
promptBindings.find(
binding => binding.runtimeBinding === runtimeBinding
)
)
.filter((binding): binding is EnrichedBinding => !!binding)
)

let enabledToolReferences = $derived(
includedToolsWithDetails
.map(tool => tool.readableBinding)
.filter((binding): binding is string => !!binding)
.map(binding => `{{ ${binding} }}`)
)

let enabled = $derived(
!!($featureFlags as Record<string, boolean>)[AI_AGENT_INSTRUCTIONS_FLAG]
)
let modal = $state<Modal>()
let promptField = $state<TextArea>()
let prompt = $state("")
let generatedInstructions = $state("")
let generating = $state(false)
let requestToken = $state(0)

function resetState() {
requestToken += 1
prompt = ""
generatedInstructions = ""
generating = false
}

function hideModal() {
modal?.hide()
}

function applyGeneratedInstructions() {
onApplyInstructions(generatedInstructions)
hideModal()
notifications.success("Instructions updated successfully")
}

async function generateInstructions() {
if (generating) {
return
}

const currentRequestToken = ++requestToken
generating = true

try {
const { instructions } = await API.generateAgentInstructions({
aiconfigId,
prompt,
agentName,
goal,
toolReferences: enabledToolReferences,
})

if (currentRequestToken !== requestToken) {
return
}

notifications.success("Instructions generated successfully")
generatedInstructions = instructions
} catch (error: any) {
if (currentRequestToken !== requestToken) {
return
}

notifications.error(
error?.message ||
error?.json?.message ||
"Error generating instructions"
)
} finally {
if (currentRequestToken === requestToken) {
generating = false
}
}
}
</script>

{#if enabled}
<Button secondary size="S" icon="sparkle" on:click={() => modal?.show()}>
Generate
</Button>

<Modal
bind:this={modal}
on:show={async () => {
await tick()
promptField?.focus()
}}
on:hide={resetState}
>
<ModalContent
title={generatedInstructions
? "Review Generated Instructions"
: "Generate Instructions"}
size="M"
showCloseIcon
showConfirmButton={false}
showCancelButton={false}
>
{#if generatedInstructions}
<div class="generated-instructions-preview">
<CodeEditor
value={generatedInstructions}
bindings={promptBindings}
{bindingIcons}
mode={EditorModes.Handlebars}
renderBindingsAsTags={true}
renderMarkdownDecorations={true}
placeholder=""
on:change={event => {
generatedInstructions = event.detail || ""
}}
/>
</div>
<div class="generate-instructions-actions">
<Button secondary on:click={hideModal}>Cancel</Button>
<Button cta on:click={applyGeneratedInstructions}
>Replace current</Button
>
</div>
{:else}
<TextArea
label="Prompt"
bind:this={promptField}
bind:value={prompt}
minHeight={140}
disabled={generating}
placeholder="Describe what kind of instructions you want to generate..."
/>
<div class="generate-instructions-actions">
<Button secondary disabled={generating} on:click={hideModal}
>Cancel</Button
>
<Button
cta
icon="sparkle"
disabled={generating || !prompt.trim()}
on:click={generateInstructions}
>
{generating ? "Generating..." : "Generate"}
</Button>
</div>
{/if}
</ModalContent>
</Modal>
{/if}

<style>
.generate-instructions-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
margin-top: var(--spacing-m);
}

.generated-instructions-preview {
border: 1px solid var(--spectrum-global-color-gray-200);
border-radius: 8px;
overflow: hidden;
min-height: 220px;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import { getIntegrationIcon, type IconInfo } from "@/helpers/integrationIcons"
import ToolsDropdown from "./ToolsDropdown.svelte"
import ToolIcon from "./ToolIcon.svelte"
import GenerateInstructionsControl from "./GenerateInstructionsControl.svelte"
import type { AgentTool } from "./toolTypes"
import WebSearchConfigModal from "./WebSearchConfigModal.svelte"
import {
Expand All @@ -43,6 +44,7 @@
import { goto } from "@roxi/routify"
import BudibaseLogoSvg from "assets/bb-emblem.svg"
import { shouldAutoSelectAgentModel } from "./configUtils"
import { getIncludedToolRuntimeBindings } from "./toolBindingUtils"

$goto
// Code editor tag icons must be URL strings (see `hbsTags.ts`).
Expand Down Expand Up @@ -223,7 +225,6 @@ Any constraints the agent must follow.
)
.filter((tool): tool is AgentTool => !!tool)
)

let filteredTools = $derived.by(() => {
return availableTools.filter(tool => {
const query = toolSearch.toLowerCase()
Expand Down Expand Up @@ -560,28 +561,6 @@ Any constraints the agent must follow.
insertToolBinding(tool.readableBinding)
}

function normaliseBinding(binding: string) {
return binding
.replace(/^\s*\{\{\s*/, "")
.replace(/\s*\}\}\s*$/, "")
.trim()
}

function getIncludedToolRuntimeBindings(
prompt: string | undefined | null,
bindingsMap: Record<string, string>
) {
const matches = (prompt || "").match(/\{\{[^}]+\}\}/g) || []
return Array.from(
new Set(
matches
.map(normaliseBinding)
.map(binding => bindingsMap[binding])
.filter(Boolean)
)
)
}

function getWebSearchRuntimeBinding(
configured?: boolean,
config?: typeof webSearchConfig
Expand Down Expand Up @@ -838,6 +817,18 @@ Any constraints the agent must follow.
<span class="bindings-bar-text"
>Use <code>{`{{`}</code> to add to tools & knowledge sources</span
>
<GenerateInstructionsControl
aiconfigId={draft.aiconfig}
agentName={draft.name}
goal={draft.goal}
promptInstructions={draft.promptInstructions}
{promptBindings}
bindingIcons={readableToIcon}
onApplyInstructions={instructions => {
draft.promptInstructions = instructions
scheduleSave(true)
}}
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const normaliseBinding = (binding: string) =>
binding
.replace(/^\s*\{\{\s*/, "")
.replace(/\s*\}\}\s*$/, "")
.trim()

export const getIncludedToolRuntimeBindings = (
prompt: string | undefined | null,
bindingsMap: Record<string, string>
) => {
const matches = (prompt || "").match(/\{\{[^}]+\}\}/g) || []
return Array.from(
new Set(
matches
.map(normaliseBinding)
.map(binding => bindingsMap[binding])
.filter(Boolean)
)
)
}
12 changes: 12 additions & 0 deletions packages/frontend-core/src/api/ai.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
GenerateAgentInstructionsRequest,
GenerateAgentInstructionsResponse,
GenerateJsRequest,
GenerateJsResponse,
GenerateTablesRequest,
Expand Down Expand Up @@ -38,6 +40,9 @@ const parseSSEEventChunk = (chunk: string): TablesStreamEvent | null => {
}

export interface AIEndpoints {
generateAgentInstructions: (
req: GenerateAgentInstructionsRequest
) => Promise<GenerateAgentInstructionsResponse>
generateCronExpression: (prompt: string) => Promise<{ message: string }>
generateJs: (req: GenerateJsRequest) => Promise<GenerateJsResponse>
generateTables: (
Expand All @@ -47,6 +52,13 @@ export interface AIEndpoints {
}

export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
generateAgentInstructions: async req => {
return await API.post({
url: "/api/ai/agent-instructions",
body: req,
})
},

/**
* Generates a cron expression from a prompt
*/
Expand Down
Loading
Loading