Skip to content

Commit 465cb6c

Browse files
committed
feat: Give Namespace Context to AICopilot when generating a Flow
1 parent 428a447 commit 465cb6c

File tree

13 files changed

+252
-31
lines changed

13 files changed

+252
-31
lines changed

ui/src/components/ai/AiCopilot.vue

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@
153153
const props = defineProps<{
154154
flow: string,
155155
conversationId: string,
156-
generationType?: AiGenerationType
156+
generationType?: AiGenerationType,
157+
namespace?: string
157158
}>();
158159
159160
const error = ref<string | undefined>(undefined);
@@ -291,13 +292,24 @@
291292
let aiResponse;
292293
try {
293294
const type = props.generationType ?? aiGenerationTypes.FLOW;
294-
aiResponse = await aiStore.generate({
295-
userPrompt: prompt.value,
296-
yaml: props.flow,
297-
conversationId: props.conversationId,
298-
providerId: selectedProvider.value,
299-
type: type
300-
}) as string;
295+
if (type === aiGenerationTypes.FLOW) {
296+
aiResponse = await aiStore.generateFlow({
297+
userPrompt: prompt.value,
298+
yaml: props.flow,
299+
conversationId: props.conversationId,
300+
providerId: selectedProvider.value,
301+
namespace: props.namespace,
302+
type: type
303+
}) as string;
304+
} else {
305+
aiResponse = await aiStore.generate({
306+
userPrompt: prompt.value,
307+
yaml: props.flow,
308+
conversationId: props.conversationId,
309+
providerId: selectedProvider.value,
310+
type: type
311+
}) as string;
312+
}
301313
emit("generatedYaml", aiResponse);
302314
} catch (e: any) {
303315
error.value = e.response?.data?.message ?? e.message;

ui/src/components/inputs/EditorWrapper.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
@close="closeAiCopilot"
6363
:flow="editorContent"
6464
:conversationId="conversationId"
65+
:namespace="namespace"
6566
@generated-yaml="(yaml: string) => {draftSource = yaml; aiCopilotOpened = false}"
6667
:generationType="aiGenerationTypes.FLOW"
6768
/>

ui/src/stores/ai.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ export const useAiStore = defineStore("ai", {
1818
providerId
1919
});
2020

21+
return response.data;
22+
},
23+
24+
async generateFlow({userPrompt, yaml, conversationId, providerId, namespace, tenantId}: {userPrompt: string, yaml: string, conversationId: string, providerId?: string, namespace?: string, tenantId?: string, type: AiGenerationType}) {
25+
const response = await axios.post(`${apiUrl()}/ai/generate/flow`, {
26+
userPrompt,
27+
yaml,
28+
conversationId,
29+
providerId,
30+
namespace,
31+
tenantId
32+
});
33+
2134
return response.data;
2235
}
2336

webserver/src/main/java/io/kestra/webserver/controllers/api/AiController.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package io.kestra.webserver.controllers.api;
22

3-
import io.kestra.webserver.models.ai.FlowGenerationPrompt;
43
import io.kestra.webserver.models.ai.DashboardGenerationPrompt;
4+
import io.kestra.webserver.models.ai.FlowGenerationPrompt;
55
import io.kestra.webserver.services.ai.AiServiceInterface;
66
import io.kestra.webserver.services.ai.AiServiceManager;
7+
import io.kestra.core.tenant.TenantService;
78
import io.micronaut.context.annotation.Requires;
89
import io.micronaut.http.HttpRequest;
910
import io.micronaut.http.annotation.Body;
@@ -34,6 +35,9 @@ public class AiController {
3435
@Inject
3536
protected HttpClientAddressResolver httpClientAddressResolver;
3637

38+
@Inject
39+
protected TenantService tenantService;
40+
3741
@ExecuteOn(TaskExecutors.IO)
3842
@Post(uri = "/generate/flow", produces = "application/yaml")
3943
@Operation(tags = {"AI"}, summary = "Generate or regenerate a flow based on a prompt")
@@ -43,7 +47,7 @@ public String generateFlow(
4347
) {
4448
AiServiceInterface service = aiServiceManager.getAiService(flowGenerationPrompt.providerId());
4549

46-
return service.generateFlow(httpClientAddressResolver.resolve(httpRequest), flowGenerationPrompt);
50+
return service.generateFlow(httpClientAddressResolver.resolve(httpRequest), flowGenerationPrompt, tenantService.resolveTenant());
4751
}
4852

4953
@ExecuteOn(TaskExecutors.IO)

webserver/src/main/java/io/kestra/webserver/models/ai/FlowGenerationPrompt.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22

33
import jakarta.validation.constraints.NotNull;
44

5-
public record FlowGenerationPrompt(@NotNull String conversationId, @NotNull String userPrompt, String yaml, String providerId) {}
6-
5+
public record FlowGenerationPrompt(
6+
@NotNull String conversationId,
7+
@NotNull String userPrompt,
8+
String yaml,
9+
String providerId,
10+
String namespace
11+
) {}

webserver/src/main/java/io/kestra/webserver/services/ai/AiService.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public abstract class AiService<T extends AiConfiguration> implements AiServiceI
2424
private final T aiConfiguration;
2525
private final FlowAiCopilot flowAiCopilot;
2626
private final DashboardAiCopilot dashboardAiCopilot;
27+
private final NamespaceContextTool namespaceContextTool;
2728
private final String instanceUid;
2829
private final String aiProvider;
2930
private final String displayName;
@@ -51,14 +52,17 @@ protected FlowYamlBuilder flowYamlBuilder(String conversationId) {
5152
return AiServices.builder(FlowYamlBuilder.class)
5253
.chatModel(this.chatModel(
5354
this.listeners("FlowYamlBuilder", conversationId)
54-
)).build();
55+
))
56+
.tools(namespaceContextTool)
57+
.build();
5558
}
5659

5760
protected DashboardYamlBuilder dashboardYamlBuilder(String conversationId) {
5861
return AiServices.builder(DashboardYamlBuilder.class)
5962
.chatModel(this.chatModel(
6063
this.listeners("DashboardYamlBuilder", conversationId)
61-
)).build();
64+
))
65+
.build();
6266
}
6367

6468
public AiService(
@@ -67,6 +71,7 @@ public AiService(
6771
final VersionProvider versionProvider,
6872
final InstanceService instanceService,
6973
final PosthogService postHogService,
74+
final NamespaceContextTool namespaceContextTool,
7075
final String aiProvider,
7176
final String displayName,
7277
final List<ChatModelListener> listeners,
@@ -78,13 +83,14 @@ public AiService(
7883
this.displayName = displayName;
7984
this.listeners = listeners;
8085
this.aiConfiguration = aiConfiguration;
86+
this.namespaceContextTool = namespaceContextTool;
8187

8288
this.flowAiCopilot = new FlowAiCopilot(jsonSchemaGenerator, pluginRegistry, versionProvider.getVersion());
8389
this.dashboardAiCopilot = new DashboardAiCopilot(jsonSchemaGenerator, pluginRegistry, versionProvider.getVersion());
8490
}
8591

8692
@Override
87-
public String generateFlow(String ip, FlowGenerationPrompt flowGenerationPrompt) {
93+
public String generateFlow(String ip, FlowGenerationPrompt flowGenerationPrompt, String tenantId) {
8894
AiService.GenerationContext ctx = this.beforeGeneration(ip, flowGenerationPrompt.conversationId(), "FlowGeneration", Map.of(
8995
"flowYaml", flowGenerationPrompt.yaml(),
9096
"userPrompt", flowGenerationPrompt.userPrompt()
@@ -93,7 +99,8 @@ public String generateFlow(String ip, FlowGenerationPrompt flowGenerationPrompt)
9399
String generatedFlow = flowAiCopilot.generateFlow(
94100
this.pluginFinder(flowGenerationPrompt.conversationId()),
95101
this.flowYamlBuilder(flowGenerationPrompt.conversationId()),
96-
flowGenerationPrompt
102+
flowGenerationPrompt,
103+
tenantId
97104
);
98105

99106
return this.afterGeneration(ctx, "FlowGenerationResult", Map.of("generatedFlow", generatedFlow), generatedFlow, "generatedFlow");

webserver/src/main/java/io/kestra/webserver/services/ai/AiServiceInterface.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
@WebServerEnabled
1313
public interface AiServiceInterface {
14-
String generateFlow(String ip, FlowGenerationPrompt flowGenerationPrompt);
14+
String generateFlow(String ip, FlowGenerationPrompt flowGenerationPrompt, String tenantId);
1515

1616
String generateDashboard(String ip, DashboardGenerationPrompt dashboardGenerationPrompt);
1717

webserver/src/main/java/io/kestra/webserver/services/ai/AiServiceManager.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class AiServiceManager {
2222
private final Map<String, AiServiceInterface> aiServices = new HashMap<>();
2323
private final AiProvidersConfiguration providersConfiguration;
2424
private String defaultProviderId;
25+
protected final NamespaceContextTool namespaceContextTool;
2526

2627
public AiServiceManager(
2728
AiProvidersConfiguration providersConfiguration,
@@ -32,9 +33,11 @@ public AiServiceManager(
3233
VersionProvider versionProvider,
3334
InstanceService instanceService,
3435
PosthogService posthogService,
35-
List<dev.langchain4j.model.chat.listener.ChatModelListener> listeners
36+
List<dev.langchain4j.model.chat.listener.ChatModelListener> listeners,
37+
NamespaceContextTool namespaceContextTool
3638
) {
3739
this.providersConfiguration = providersConfiguration;
40+
this.namespaceContextTool = namespaceContextTool;
3841

3942
List<AiProviderConfiguration> configs = new java.util.ArrayList<>(
4043
providersConfiguration.providers() != null ? providersConfiguration.providers() : List.of()
@@ -95,7 +98,7 @@ protected AiServiceInterface createAiService(
9598
try {
9699
if (type.equals("gemini")) {
97100
GeminiConfiguration geminiConfig = JacksonMapper.ofJson().convertValue(configMap, GeminiConfiguration.class);
98-
return new GeminiAiService(pluginRegistry, jsonSchemaGenerator, versionProvider, instanceService, posthogService, provider.displayName(), listeners, geminiConfig);
101+
return new GeminiAiService(pluginRegistry, jsonSchemaGenerator, versionProvider, instanceService, posthogService, namespaceContextTool, provider.displayName(), listeners, geminiConfig);
99102
}
100103
log.warn("Unknown AI type: {}", type);
101104
return null;
@@ -131,6 +134,3 @@ public String getDefaultProviderId() {
131134
return defaultProviderId;
132135
}
133136
}
134-
135-
136-

webserver/src/main/java/io/kestra/webserver/services/ai/FlowAiCopilot.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ protected List<String> excludedPluginTypes() {
5656
return EXCLUDED_PLUGIN_TYPES;
5757
}
5858

59-
public String generateFlow(PluginFinder pluginFinder, FlowYamlBuilder flowYamlBuilder, FlowGenerationPrompt flowGenerationPrompt) {
59+
public String generateFlow(PluginFinder pluginFinder, FlowYamlBuilder flowYamlBuilder, FlowGenerationPrompt flowGenerationPrompt, String tenantId) {
6060
String enhancedPrompt = String.format("Current Flow YAML:\n```yaml\n%s\n```\n\nUser's prompt:\n``\n%s\n```", java.util.Optional.ofNullable(flowGenerationPrompt.yaml()).orElse(""), flowGenerationPrompt.userPrompt());
6161

6262
List<String> mostRelevantPlugins = this.mostRelevantPlugins(pluginFinder, enhancedPrompt, this.excludedPluginTypes());
6363

6464
return this.generateYaml(
65-
flowYamlBuilder::buildFlow,
65+
(schemaJson, genErr, userPr) -> flowYamlBuilder.buildFlow(schemaJson, genErr, java.util.Optional.ofNullable(flowGenerationPrompt.yaml()).orElse(""), flowGenerationPrompt.namespace(), tenantId, userPr),
6666
Flow.class,
6767
mostRelevantPlugins,
6868
NON_REQUEST_ERROR,

webserver/src/main/java/io/kestra/webserver/services/ai/FlowYamlBuilder.java

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
public interface FlowYamlBuilder {
88
// Note, there is a newline within a pebble example because {{...}} are parsed by langchain4j that tries to find a variable. This is a hack to workaround the regex and avoid errors.
99
@SystemMessage("""
10-
You are an expert in generating Kestra Flow YAML. Your task is to generate a valid Kestra Flow YAML that follows user's requirements strictly following the following json schema:
10+
You are an expert in generating Kestra Flow YAML. Your task is to generate a valid Kestra Flow YAML that follows user's requirements strictly following the following json schema (when provided):
1111
```
1212
{_{flowSchema}_}
1313
```
14-
14+
15+
Additional runtime inputs available to you as variables (preferred over embedding data inside the schema):
16+
- {_{namespace}_}: An explicit namespace string provided separately. If present, prefer this over any namespace found in the YAML snippet.
17+
- {_{tenantId}_}: An explicit tenant identifier (may be null for single-tenant deployments). If present, prefer this over any tenantId found in the YAML snippet.
18+
1519
Here are the rules:
1620
- Use examples, properties, and outputs only as specified in the schema.
1721
- If the user asks for troubleshooting, try to fix any related expression or task.
@@ -37,21 +41,62 @@ Avoid duplicating existing intent (e.g., if the Flow logs "hi" and the user want
3741
- Triggers expose some variables that can be accessed through `trigger.outputName` in expressions. The only variables available are those defined in the trigger's outputs.
3842
- Unless specified by the user, never assume a local port to serve any content, always use a remote URL (like a public HTTP server) to fetch content.
3943
- Unless specified by the user, do not use any authenticated API, always use public APIs or those that don't require authentication.
40-
- To avoid escaping quotes, use double quotes first and if you need quotes inside, use single ones. Only escape them if you have 3+ level quotes, for example: `message: "Hello {{inputs.userJson | jq('.name')}}"` is preferred but `message: "Hello \\"Bob\\""` may still be used.
44+
- To avoid escaping quotes, use double quotes first and if you need quotes inside, use single ones. Only escape them if you have 3+ level quotes, for example: `message: "Hello {{inputs.userJson | jq('.name')}}"` is preferred but `message: "Hello \"Bob\""` may still be used.
4145
- A property key is unique within each type.
4246
- When fetching data from the JDBC plugin, always use fetchType: STORE.
4347
- Manipulating date in pebble expressions can be done through `dateAdd` (`{{now()|dateAdd(-1,'DAYS')}}`) and `date` filters (`{{"July 24, 2001"|date("yyyy-MM-dd",existingFormat="MMMM dd, yyyy")}}`). Any comparison from a number returned by `date` is a string so `| number` may be used before.
4448
- Current date is `{{current_date_time}}`.
4549
- Always preserve root-level `id` and `namespace` if provided.
4650
- Don't add any Schedule trigger unless a regular occurrence is asked.
47-
- If the user uses vague references ("it", "that"), infer context from the current Flow YAML.
51+
- If the user uses vague references ("it", "that"), infer context from the current Flow YAML or the explicit `namespace`/`tenantId` variables.
4852
- Except for error scenarios, output only the raw YAML, with no explanation or additional text.
49-
53+
- If you have any other information to share to the user, add them as comments in the YAML using `#` at the beginning of the raw YAML.
54+
- Never add raw text in the response
55+
56+
Available Tools for Context Retrieval:
57+
58+
You have access to tools that retrieve namespace configuration on-demand. Prefer explicit variables `{_{namespace}_}` and `{_{tenantId}_}` when provided. Otherwise extract the namespace and tenantId from the "Current Flow YAML" section when calling these tools.
59+
60+
Namespace Context Tools :
61+
62+
1. KV Store Keys (getKvStoreKeys):
63+
- Call when user wants to interact with KV Store (read/write/list keys)
64+
- Returns: JSON with list of existing KV keys, descriptions, and update dates
65+
- Usage in flows: {{kv('keyName')}} to read, KV tasks (Get, Put, Delete) to manage
66+
67+
2. Plugin Defaults (getPluginDefaults) [EE Only]:
68+
- Call when user asks to integrate with specific technology (e.g., MongoDB, PostgreSQL)
69+
- Returns: JSON mapping plugin types to their default values
70+
- Apply defaults in generated tasks
71+
72+
3. Namespace Variables (getNamespaceVariables) [EE Only]:
73+
- Call when user references variables
74+
- Returns: JSON mapping variable names to values
75+
- Prefer namespace variables using {{vars.variableName}} over hardcoded values
76+
77+
4. Secret Names (getSecretNames) [EE Only]:
78+
- Call when user mentions secrets or credentials, or the technology being integrated typically requires credentials
79+
- Returns: JSON array of secret names (NOT values)
80+
- Reference using {{secret('secretName')}} for sensitive data
81+
82+
5. Complete Context (getAllNamespaceContext) [EE Only]:
83+
- Call for comprehensive namespace information
84+
- Returns: Combined context including KV keys, plugin defaults, variables, and secrets
85+
86+
Tool Calling Guidelines:
87+
- Use explicit `{_{namespace}_}` and `{_{tenantId}_}` placeholders, if not provided, do not call the tool.
88+
- Call tools BEFORE generating YAML to provide accurate suggestions
89+
- Only call tools relevant to the user's request
90+
- If namespace is not in YAML and flow is new, inform user namespace is required
91+
5092
IMPORTANT: If the user prompt cannot be fulfilled with the schema, instead of generating a Flow, reply: `{_{flowGenerationError}_}`.
5193
Do not invent properties or types. Strictly follow the provided schema.""")
5294
String buildFlow(
5395
@V("flowSchema") String flowSchema,
5496
@V("flowGenerationError") String flowGenerationError,
97+
@V("currentFlowYaml") String currentFlowYaml,
98+
@V("namespace") String namespace,
99+
@V("tenantId") String tenantId,
55100
@UserMessage String userPrompt
56101
);
57102
}

0 commit comments

Comments
 (0)