This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 025f7cb97232 CAMEL-24744: camel-jbang - MCP scaffold tools, camel init
templates and bundled examples generate canonical YAML
025f7cb97232 is described below
commit 025f7cb9723205bc7573a2f61f83e0738790f678
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 15 06:51:33 2026 +0200
CAMEL-24744: camel-jbang - MCP scaffold tools, camel init templates and
bundled examples generate canonical YAML
Follow-up to CAMEL-24722 / CAMEL-24723: the YAML that camel-jbang generates
is the
starting point for users and AI agents, yet it was still written in the
deprecated
compact notation, so the first `camel run` warned about a Camel-generated
file.
The `camel init` templates (yaml, integration, kamelet-source,
kamelet-action), the
bundled examples and the `camel_openapi_scaffold` /
`camel_ai_pipeline_scaffold` MCP
tools now emit the canonical form (`expression:` wrapper, `log: {message:
...}`,
`to: {uri: ...}`). Running the canonical validator over the AI pipeline
scaffold also
surfaced two bugs where the generated route never loaded (`steps:` at the
wrong
level); both fixed. The stale `message-size/message-size.camel.yaml` copy
is removed.
Tests validate every template, bundled example and scaffold output against
the
canonical schema, and the upgrade guide documents the changed generated
output.
Closes #26444
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 10 ++
.../beginner/tui-hello-world/tui-hello-world.yaml | 14 +-
.../content-based-router.camel.yaml | 10 +-
.../message-size/message-size.camel.yaml | 102 ---------------
.../observability/message-size/orders.camel.yaml | 40 ++++--
.../main/resources/templates/integration.yaml.ftl | 7 +-
.../resources/templates/kamelet-action.yaml.ftl | 4 +-
.../resources/templates/kamelet-source.yaml.ftl | 11 +-
.../src/main/resources/templates/yaml.ftl | 7 +-
.../jbang/core/commands/DependencyUpdateTest.java | 16 ++-
.../jbang/core/common/ExampleRoutesLoadTest.java | 21 +++
.../dsl/jbang/core/common/TemplateHelperTest.java | 50 +++++++
.../test/resources/dependency-update/route.yaml | 7 +-
.../core/commands/mcp/AiPipelineScaffoldTools.java | 145 +++++++++++----------
.../dsl/jbang/core/commands/mcp/OpenApiTools.java | 12 +-
.../commands/mcp/AiPipelineScaffoldToolsTest.java | 22 ++++
.../jbang/core/commands/mcp/OpenApiToolsTest.java | 37 +++++-
17 files changed, 308 insertions(+), 207 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 2fcbbec5e74c..e973045d1446 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -663,6 +663,16 @@ Running an existing Maven project (`camel run pom.xml`) is
unchanged, and still
from the `pom.xml`. The other commands that take `--runtime` (such as `camel
export`,
`camel dependency list` and `camel version list`) accept `jbang` as an alias
for `main`.
+The YAML that `camel init` writes (`camel init foo.yaml`, and the Integration
and Kamelet templates), the
+bundled examples (`camel init --example`) and the routes the
`camel_ai_pipeline_scaffold` and
+`camel_openapi_scaffold` MCP tools generate are now written in the canonical
YAML DSL format, with an
+expression under `expression:` and a step as a map of its options
+(`setBody: {expression: {simple: {expression: "..."}}}`, `log: {message:
"..."}`, `to: {uri: "..."}`).
+The compact notation they used to be written in (`setBody: {simple: "..."}`,
`log: "..."`) is deprecated:
+`camel run` warns about it, `camel validate yaml --canonical` reports it, and
`camel validate normalize`
+rewrites a file in the canonical format. Existing files keep working; only the
generated starting point
+changed. Scripts that post-process the generated YAML by matching the old text
need to be updated.
+
=== camel-jbang (MCP servers)
The Camel authoring tools for AI agents are now defined once, in
`camel-jbang-core`, and exposed under the
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
index 28db17370b76..b5bce65695db 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
@@ -6,14 +6,20 @@
repeatCount: 1
steps:
- setBody:
- constant: "Welcome! Open the TUI (camel tui) and press F2 → Send
Message to say hello."
- - log: "${body}"
+ expression:
+ constant:
+ expression: "Welcome! Open the TUI (camel tui) and press F2 →
Send Message to say hello."
+ - log:
+ message: "${body}"
- route:
id: greet
from:
uri: direct:greet
steps:
- - log: "Received: ${body}"
+ - log:
+ message: "Received: ${body}"
- setBody:
- simple: "Hello ${body}!"
+ expression:
+ simple:
+ expression: "Hello ${body}!"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
index 698c2b1c39dc..d451fcfd93bf 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
@@ -14,8 +14,9 @@
type: int
- choice:
when:
- - simple:
- expression: "${body} >= 30"
+ - expression:
+ simple:
+ expression: "${body} >= 30"
steps:
- setHeader:
name: level
@@ -24,8 +25,9 @@
expression: hot
- log:
message: "Hot alert: ${body} C"
- - simple:
- expression: "${body} >= 15"
+ - expression:
+ simple:
+ expression: "${body} >= 15"
steps:
- setHeader:
name: level
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml
deleted file mode 100644
index 9b2e0361c3a4..000000000000
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml
+++ /dev/null
@@ -1,102 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-# Producers: simulate different payload sizes using Content-Length header
-- route:
- id: small-producer
- from:
- uri: timer:small
- parameters:
- period: 2000
- steps:
- - setHeader:
- name: Content-Length
- simple: "${random(100,1023)}"
- - setHeader:
- name: source
- simple: small-producer
- - to:
- uri: seda:small
-
-- route:
- id: medium-producer
- from:
- uri: timer:medium
- parameters:
- period: 3000
- steps:
- - setHeader:
- name: Content-Length
- simple: "${random(8192,18432)}"
- - setHeader:
- name: source
- simple: medium-producer
- - setHeader:
- name: tracking-id
- simple: "TRK-${random(10000,99999)}"
- - to:
- uri: seda:medium
-
-- route:
- id: large-producer
- from:
- uri: timer:large
- parameters:
- period: 5000
- steps:
- - setHeader:
- name: Content-Length
- simple: "${random(10485760,20971520)}"
- - setHeader:
- name: source
- simple: large-producer
- - setHeader:
- name: tracking-id
- simple: "TRK-${random(10000,99999)}"
- - setHeader:
- name: batch-id
- simple: "BATCH-${random(100,999)}"
- - setHeader:
- name: priority
- simple: "${random(1,5)}"
- - to:
- uri: seda:large
-
-# Consumers: process messages from each seda endpoint
-- route:
- id: process-small
- from:
- uri: seda:small
- steps:
- - log:
- message: "Small: Content-Length=${header.Content-Length}"
-
-- route:
- id: process-medium
- from:
- uri: seda:medium
- steps:
- - log:
- message: "Medium: Content-Length=${header.Content-Length}"
-
-- route:
- id: process-large
- from:
- uri: seda:large
- steps:
- - log:
- message: "Large: Content-Length=${header.Content-Length}"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
index 10930065eb1d..c7d84fa0b866 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
@@ -8,10 +8,14 @@
steps:
- setHeader:
name: Content-Length
- simple: "${random(100,1023)}"
+ expression:
+ simple:
+ expression: "${random(100,1023)}"
- setHeader:
name: source
- simple: small-producer
+ expression:
+ simple:
+ expression: small-producer
- to:
uri: seda:small
@@ -24,13 +28,19 @@
steps:
- setHeader:
name: Content-Length
- simple: "${random(8192,18432)}"
+ expression:
+ simple:
+ expression: "${random(8192,18432)}"
- setHeader:
name: source
- simple: medium-producer
+ expression:
+ simple:
+ expression: medium-producer
- setHeader:
name: tracking-id
- simple: "TRK-${random(10000,99999)}"
+ expression:
+ simple:
+ expression: "TRK-${random(10000,99999)}"
- to:
uri: seda:medium
@@ -43,19 +53,29 @@
steps:
- setHeader:
name: Content-Length
- simple: "${random(10485760,20971520)}"
+ expression:
+ simple:
+ expression: "${random(10485760,20971520)}"
- setHeader:
name: source
- simple: large-producer
+ expression:
+ simple:
+ expression: large-producer
- setHeader:
name: tracking-id
- simple: "TRK-${random(10000,99999)}"
+ expression:
+ simple:
+ expression: "TRK-${random(10000,99999)}"
- setHeader:
name: batch-id
- simple: "BATCH-${random(100,999)}"
+ expression:
+ simple:
+ expression: "BATCH-${random(100,999)}"
- setHeader:
name: priority
- simple: "${random(1,5)}"
+ expression:
+ simple:
+ expression: "${random(1,5)}"
- to:
uri: seda:large
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
index 50f8e8a2d2b4..3c200771ad7e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
@@ -28,5 +28,8 @@ spec:
period: "1000"
steps:
- setBody:
- constant: "Hello Camel from yaml"
- - log: "${body}"
+ expression:
+ constant:
+ expression: "Hello Camel from yaml"
+ - log:
+ message: "${body}"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
index 26355e87bccd..959f39ead507 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
@@ -42,4 +42,6 @@ spec:
uri: "kamelet:source"
steps:
- delay:
- constant: "{{milliseconds}}"
+ expression:
+ constant:
+ expression: "{{milliseconds}}"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
index d40d80a6a827..fb2cd7b18f0d 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
@@ -48,8 +48,13 @@ spec:
period: "{{period}}"
steps:
- setBody:
- constant: "{{message}}"
+ expression:
+ constant:
+ expression: "{{message}}"
- setHeader:
name: "Content-Type"
- constant: "text/plain"
- - to: "kamelet:sink"
+ expression:
+ constant:
+ expression: "text/plain"
+ - to:
+ uri: "kamelet:sink"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
index 48d5a88d02bf..808f5b7e79bd 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
@@ -23,5 +23,8 @@
period: "1000"
steps:
- setBody:
- simple: Hello Camel from ${routeId}
- - log: ${body}
+ expression:
+ simple:
+ expression: "Hello Camel from ${routeId}"
+ - log:
+ message: "${body}"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
index 2951d17f221c..9a0328045ffd 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
@@ -389,11 +389,17 @@ class DependencyUpdateTest extends
CamelCommandBaseTestSupport {
private void addArangodbToCamelFile() throws Exception {
File camelFile = new File(workingDir,
"src/main/resources/camel/my.camel.yaml");
String content = Files.readString(camelFile.toPath());
- content = content.replace("- log: ${body}", """
- - to:
- uri: arangodb
- parameters:
- database: demo
+ // the generated route ends with a canonical log step; swap it for an
arangodb producer
+ String log = """
+ - log:
+ message: "${body}"
+ """;
+ assertThat(content).contains(log);
+ content = content.replace(log, """
+ - to:
+ uri: arangodb
+ parameters:
+ database: demo
""");
Files.writeString(camelFile.toPath(), content);
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
index abaa20f65836..003c76299bb8 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
@@ -23,11 +23,13 @@ import java.nio.file.Path;
import java.util.Properties;
import java.util.stream.Stream;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.model.Model;
import org.apache.camel.spi.Resource;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.ResourceHelper;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -47,9 +49,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* fixture and the example's {@code application.properties} are required),
whereas endpoints and their producers and
* consumers are only created at start. Not starting therefore avoids opening
external endpoints (JMS, Kafka, SQL, ...)
* but also means endpoint wiring is not exercised.
+ *
+ * Every example is also validated against the canonical YAML schema: the
examples are what a user or an AI agent starts
+ * from, so they must be written in the canonical format and not in the
deprecated compact notation, which
+ * {@code camel run} warns about.
*/
class ExampleRoutesLoadTest {
+ private static YamlValidator canonicalValidator;
+
+ @BeforeAll
+ static void setUpValidator() throws Exception {
+ canonicalValidator = new YamlValidator(true);
+ canonicalValidator.init();
+ }
+
static Stream<Arguments> exampleRouteFiles() throws Exception {
// resolve the examples from the classpath (target/classes/examples)
so the test does not depend on the working
// directory being the module base, which differs between Maven
Surefire and IDE run configurations
@@ -100,6 +114,13 @@ class ExampleRoutesLoadTest {
}
}
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("exampleRouteFiles")
+ void shouldBeCanonicalYaml(Path file) throws Exception {
+ var report = canonicalValidator.validate(file.toFile());
+ assertTrue(report.isEmpty(), "Example is not in the canonical YAML
format: " + file + "\n" + report);
+ }
+
private static void loadExampleProperties(DefaultCamelContext context,
Path file) throws Exception {
Path properties = file.resolveSibling("application.properties");
if (!Files.exists(properties)) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
index 41ba25f1e2ea..a7f91b4ef0d9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
@@ -23,15 +23,29 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.apache.camel.util.IOHelper;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.yaml.snakeyaml.Yaml;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+@SuppressWarnings("unchecked")
class TemplateHelperTest {
+ private static YamlValidator canonicalValidator;
+
+ @BeforeAll
+ static void setUpValidator() throws Exception {
+ canonicalValidator = new YamlValidator(true);
+ canonicalValidator.init();
+ }
+
// ===== TemplateHelper.processTemplate() tests (FreeMarker-processed
templates) =====
@Test
@@ -485,6 +499,42 @@ class TemplateHelperTest {
/**
* Loads a template the same way Init.java does: raw text + header
stripping.
*/
+ // ===== the YAML the templates generate is what a user or an AI agent
starts from: it must be canonical =====
+
+ @ParameterizedTest
+ @ValueSource(strings = { "yaml.ftl", "rest-dsl.yaml.ftl" })
+ void testInitYamlTemplatesAreCanonical(String name) throws Exception {
+ String content = loadInitTemplate(name).replace("[=Spec]",
"petstore.json");
+
+ assertCanonical(name, content);
+ }
+
+ @Test
+ void testInitIntegrationTemplateIsCanonical() throws Exception {
+ String content =
loadInitTemplate("integration.yaml.ftl").replace("[=Name]", "my-integration");
+
+ // the Integration CR wraps the routes in spec.flows
+ Map<String, Object> cr = new Yaml().load(content);
+ Map<String, Object> spec = (Map<String, Object>) cr.get("spec");
+ assertCanonical("integration.yaml.ftl", new
Yaml().dump(spec.get("flows")));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "kamelet-source.yaml.ftl",
"kamelet-sink.yaml.ftl", "kamelet-action.yaml.ftl" })
+ void testInitKameletTemplatesAreCanonical(String name) throws Exception {
+ String content = loadInitTemplate(name).replace("[=Name]",
"my-kamelet");
+
+ // the Kamelet CR wraps the route in spec.template as its from
+ Map<String, Object> cr = new Yaml().load(content);
+ Map<String, Object> spec = (Map<String, Object>) cr.get("spec");
+ assertCanonical(name, new Yaml().dump(List.of(spec.get("template"))));
+ }
+
+ private static void assertCanonical(String name, String yaml) throws
Exception {
+ var report = canonicalValidator.validate(yaml);
+ assertTrue(report.isEmpty(), name + " is not in the canonical YAML
format:\n" + yaml + "\n" + report);
+ }
+
private String loadInitTemplate(String name) throws IOException {
try (InputStream is =
getClass().getClassLoader().getResourceAsStream("templates/" + name)) {
String content = IOHelper.loadText(is);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
index df0f91ed45f8..cb3a75391e6b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
@@ -22,5 +22,8 @@
period: "1000"
steps:
- setBody:
- simple: Hello Camel from my
- - log: ${body}
+ expression:
+ simple:
+ expression: "Hello Camel from my"
+ - log:
+ message: "${body}"
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
index 1a392ff181dd..18de55b1206f 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
@@ -114,13 +114,13 @@ public class AiPipelineScaffoldTools {
sb.append(" id: ai-summarization\n");
sb.append(" from:\n");
appendSourceEndpoint(sb, source);
- sb.append(" steps:\n");
+ sb.append(" steps:\n");
appendDocumentProcessing(sb, processor);
- sb.append(" - process:\n");
- sb.append(" ref: \"#buildSummarizationPrompt\"\n");
+ sb.append(" - process:\n");
+ sb.append(" ref: \"#buildSummarizationPrompt\"\n");
appendBedrockConverse(sb, model, region);
- sb.append(" - log:\n");
- sb.append(" message: \"Summary generated: ${body}\"\n");
+ sb.append(" - log:\n");
+ sb.append(" message: \"Summary generated: ${body}\"\n");
return sb.toString();
}
@@ -132,16 +132,16 @@ public class AiPipelineScaffoldTools {
sb.append(" id: ai-extraction\n");
sb.append(" from:\n");
appendSourceEndpoint(sb, source);
- sb.append(" steps:\n");
+ sb.append(" steps:\n");
appendDocumentProcessing(sb, processor);
- sb.append(" - process:\n");
- sb.append(" ref: \"#buildExtractionPrompt\"\n");
+ sb.append(" - process:\n");
+ sb.append(" ref: \"#buildExtractionPrompt\"\n");
appendBedrockConverse(sb, model, region);
- sb.append(" - unmarshal:\n");
- sb.append(" json:\n");
- sb.append(" unmarshalType: java.util.Map\n");
- sb.append(" - log:\n");
- sb.append(" message: \"Extracted data: ${body}\"\n");
+ sb.append(" - unmarshal:\n");
+ sb.append(" json:\n");
+ sb.append(" unmarshalType: java.util.Map\n");
+ sb.append(" - log:\n");
+ sb.append(" message: \"Extracted data: ${body}\"\n");
return sb.toString();
}
@@ -159,34 +159,40 @@ public class AiPipelineScaffoldTools {
sb.append(" id: rag-ingestion\n");
sb.append(" from:\n");
appendSourceEndpoint(sb, source);
- sb.append(" steps:\n");
+ sb.append(" steps:\n");
appendDocumentProcessing(sb, processor);
- sb.append(" # Chunk the extracted text for embedding\n");
- sb.append(" - split:\n");
- sb.append(" tokenize: \"\\n\\n\"\n");
- sb.append(" streaming: true\n");
- sb.append(" steps:\n");
- sb.append(" - to:\n");
- sb.append(" uri: \"langchain4j-embeddings:embed\"\n");
- sb.append(" parameters:\n");
- sb.append(" embeddingModelId: \"#bedrockEmbedding\"\n");
- sb.append(" # TODO: Configure your vector store endpoint\n");
- sb.append(" - to:
\"log:ingested?showBody=false&showHeaders=true\"\n\n");
+ sb.append(" # Chunk the extracted text for embedding\n");
+ sb.append(" - split:\n");
+ sb.append(" expression:\n");
+ sb.append(" tokenize:\n");
+ sb.append(" token: \"\\n\\n\"\n");
+ sb.append(" streaming: true\n");
+ sb.append(" steps:\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"langchain4j-embeddings:embed\"\n");
+ sb.append(" parameters:\n");
+ sb.append(" embeddingModelId:
\"#bedrockEmbedding\"\n");
+ sb.append(" # TODO: Configure your vector store
endpoint\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"log:ingested\"\n");
+ sb.append(" parameters:\n");
+ sb.append(" showBody: false\n");
+ sb.append(" showHeaders: true\n\n");
// Query route
sb.append("- route:\n");
sb.append(" id: rag-query\n");
sb.append(" from:\n");
sb.append(" uri: \"direct:query\"\n");
- sb.append(" steps:\n");
- sb.append(" # TODO: Retrieve relevant chunks from vector
store\n");
- sb.append(" # - to: \"langchain4j-embeddings:embed\" # embed the
query\n");
- sb.append(" # - to: \"qdrant:search\" # search
vector store\n");
- sb.append(" - process:\n");
- sb.append(" ref: \"#buildRagPrompt\"\n");
+ sb.append(" steps:\n");
+ sb.append(" # TODO: Retrieve relevant chunks from vector
store\n");
+ sb.append(" # - to: \"langchain4j-embeddings:embed\" # embed
the query\n");
+ sb.append(" # - to: \"qdrant:search\" # search
vector store\n");
+ sb.append(" - process:\n");
+ sb.append(" ref: \"#buildRagPrompt\"\n");
appendBedrockConverse(sb, model, region);
- sb.append(" - log:\n");
- sb.append(" message: \"RAG answer: ${body}\"\n");
+ sb.append(" - log:\n");
+ sb.append(" message: \"RAG answer: ${body}\"\n");
return sb.toString();
}
@@ -198,22 +204,29 @@ public class AiPipelineScaffoldTools {
sb.append(" id: ai-classification\n");
sb.append(" from:\n");
appendSourceEndpoint(sb, source);
- sb.append(" steps:\n");
+ sb.append(" steps:\n");
appendDocumentProcessing(sb, processor);
- sb.append(" - process:\n");
- sb.append(" ref: \"#buildClassificationPrompt\"\n");
+ sb.append(" - process:\n");
+ sb.append(" ref: \"#buildClassificationPrompt\"\n");
appendBedrockConverse(sb, model, region);
- sb.append(" - choice:\n");
- sb.append(" when:\n");
- sb.append(" - simple: \"${body} contains 'invoice'\"\n");
- sb.append(" steps:\n");
- sb.append(" - to: \"direct:handle-invoice\"\n");
- sb.append(" - simple: \"${body} contains 'contract'\"\n");
+ sb.append(" - choice:\n");
+ sb.append(" when:\n");
+ sb.append(" - expression:\n");
+ sb.append(" simple:\n");
+ sb.append(" expression: \"${body} contains
'invoice'\"\n");
+ sb.append(" steps:\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"direct:handle-invoice\"\n");
+ sb.append(" - expression:\n");
+ sb.append(" simple:\n");
+ sb.append(" expression: \"${body} contains
'contract'\"\n");
+ sb.append(" steps:\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"direct:handle-contract\"\n");
+ sb.append(" otherwise:\n");
sb.append(" steps:\n");
- sb.append(" - to: \"direct:handle-contract\"\n");
- sb.append(" otherwise:\n");
- sb.append(" steps:\n");
- sb.append(" - to: \"direct:handle-other\"\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"direct:handle-other\"\n");
return sb.toString();
}
@@ -249,39 +262,39 @@ public class AiPipelineScaffoldTools {
case "textract" -> appendTextractStep(sb);
case "combined" -> {
appendDoclingStep(sb);
- sb.append(" # Also extract tables/forms via Textract for
structured data\n");
+ sb.append(" # Also extract tables/forms via Textract
for structured data\n");
appendTextractStep(sb);
- sb.append(" - process:\n");
- sb.append(" ref: \"#mergeDoclingAndTextract\"\n");
+ sb.append(" - process:\n");
+ sb.append(" ref: \"#mergeDoclingAndTextract\"\n");
}
default -> appendDoclingStep(sb);
}
}
private void appendDoclingStep(StringBuilder sb) {
- sb.append(" - to:\n");
- sb.append(" uri: \"docling:convert\"\n");
- sb.append(" parameters:\n");
- sb.append(" operation: CONVERT_TO_MARKDOWN\n");
- sb.append(" useDoclingServe: true\n");
- sb.append(" doclingServeUrl: \"{{docling.server.url}}\"\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"docling:convert\"\n");
+ sb.append(" parameters:\n");
+ sb.append(" operation: CONVERT_TO_MARKDOWN\n");
+ sb.append(" useDoclingServe: true\n");
+ sb.append(" doclingServeUrl:
\"{{docling.server.url}}\"\n");
}
private void appendTextractStep(StringBuilder sb) {
- sb.append(" - to:\n");
- sb.append(" uri: \"aws2-textract:detect\"\n");
- sb.append(" parameters:\n");
- sb.append(" operation: detectDocumentText\n");
- sb.append(" region: \"{{aws.region}}\"\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"aws2-textract:detect\"\n");
+ sb.append(" parameters:\n");
+ sb.append(" operation: detectDocumentText\n");
+ sb.append(" region: \"{{aws.region}}\"\n");
}
private void appendBedrockConverse(StringBuilder sb, String model, String
region) {
- sb.append(" - to:\n");
- sb.append(" uri: \"aws-bedrock:label\"\n");
- sb.append(" parameters:\n");
- sb.append(" operation: converse\n");
- sb.append(" modelId: \"").append(model).append("\"\n");
- sb.append(" region: \"").append(region).append("\"\n");
+ sb.append(" - to:\n");
+ sb.append(" uri: \"aws-bedrock:label\"\n");
+ sb.append(" parameters:\n");
+ sb.append(" operation: converse\n");
+ sb.append(" modelId: \"").append(model).append("\"\n");
+ sb.append(" region: \"").append(region).append("\"\n");
}
// ---- Properties generation ----
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
index 8e50784a4beb..6a84882c8625 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
@@ -177,18 +177,24 @@ public class OpenApiTools {
if (stub.contentType() != null) {
yaml.append(" - setHeader:\n");
yaml.append(" name: Content-Type\n");
- yaml.append(" constant:
").append(stub.contentType()).append("\n");
+ yaml.append(" expression:\n");
+ yaml.append(" constant:\n");
+ yaml.append(" expression:
").append(stub.contentType()).append("\n");
}
// Set response code if we know it
if (stub.responseCode() != null) {
yaml.append(" - setHeader:\n");
yaml.append(" name: CamelHttpResponseCode\n");
- yaml.append(" constant:
").append(stub.responseCode()).append("\n");
+ yaml.append(" expression:\n");
+ yaml.append(" constant:\n");
+ yaml.append(" expression:
").append(stub.responseCode()).append("\n");
}
yaml.append(" - setBody:\n");
- yaml.append(" constant: \"TODO: implement
").append(stub.operationId()).append("\"\n");
+ yaml.append(" expression:\n");
+ yaml.append(" constant:\n");
+ yaml.append(" expression: \"TODO: implement
").append(stub.operationId()).append("\"\n");
}
return new ScaffoldResult(yaml.toString(), stubs.size(), filename,
mode, apiTitle);
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
index ef736764e4bf..84974e51e027 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
@@ -17,6 +17,8 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -27,8 +29,16 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
class AiPipelineScaffoldToolsTest {
+ private static YamlValidator canonicalValidator;
+
private AiPipelineScaffoldTools tools;
+ @BeforeAll
+ static void setUpValidator() throws Exception {
+ canonicalValidator = new YamlValidator(true);
+ canonicalValidator.init();
+ }
+
@BeforeEach
void setUp() {
tools = new AiPipelineScaffoldTools();
@@ -76,6 +86,18 @@ class AiPipelineScaffoldToolsTest {
assertThat(result.description()).isNotBlank();
}
+ @ParameterizedTest
+ @ValueSource(strings = { "summarization", "extraction", "rag",
"classification" })
+ void shouldGenerateCanonicalYamlForEachPipelineType(String type) throws
Exception {
+ AiPipelineScaffoldTools.ScaffoldResult result
+ = tools.camel_ai_pipeline_scaffold(type, "docling", "file",
null, null);
+
+ // the scaffold is the starting point of a user or an AI agent: it
must not be in the deprecated compact notation
+ assertThat(canonicalValidator.validate(result.yamlRoute()))
+ .as("canonical validation of the %s scaffold:%n%s", type,
result.yamlRoute())
+ .isEmpty();
+ }
+
@Test
void shouldGenerateSummarizationWithDocling() {
AiPipelineScaffoldTools.ScaffoldResult result
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
index 7fcb93850c67..8dfc29830956 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -286,15 +287,45 @@ class OpenApiToolsTest {
void scaffoldResponseCodesFromSpec() {
OpenApiTools.ScaffoldResult result =
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
- assertThat(result.yaml()).contains("constant: 200");
- assertThat(result.yaml()).contains("constant: 201");
+ assertThat(result.yaml()).contains("""
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: 200
+ """);
+ assertThat(result.yaml()).contains("""
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: 201
+ """);
}
@Test
void scaffoldContentTypeHeaders() {
OpenApiTools.ScaffoldResult result =
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
- assertThat(result.yaml()).contains("constant: application/json");
+ assertThat(result.yaml()).contains("""
+ - setHeader:
+ name: Content-Type
+ expression:
+ constant:
+ expression: application/json
+ """);
+ }
+
+ @Test
+ void scaffoldIsCanonicalYaml() throws Exception {
+ OpenApiTools.ScaffoldResult result =
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
+
+ // the scaffold is the starting point of a user or an AI agent: it
must not be in the deprecated compact notation
+ YamlValidator canonicalValidator = new YamlValidator(true);
+ canonicalValidator.init();
+ assertThat(canonicalValidator.validate(result.yaml()))
+ .as("canonical validation of the scaffold:%n%s", result.yaml())
+ .isEmpty();
}
@Test