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 76a0de66ebd8 CAMEL-26932: camel-jbang - the AI tools give the lines of
source code as they are in the file (#29939)
76a0de66ebd8 is described below
commit 76a0de66ebd8e7cb16a07363dac7d6a51e5d1d35
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 19:29:46 2026 +0100
CAMEL-26932: camel-jbang - the AI tools give the lines of source code as
they are in the file (#29939)
The dev consoles escape each line of a code list, and the CLI commands undo
it when they print
the lines, but the runtime tools (route_source, route_dump,
route_structure, processor_detail,
config_drift, history) gave the lines as they came, so the agent saw \"
that is not in the file.
ToolContext unescapes the lines of the answers of the actions, and of the
history and error files.
CAMEL-26932: camel-jbang - shorter sentences in the Javadoc of
ToolContext.unescapeCodeLines
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../dsl/jbang/core/commands/ai/ToolContext.java | 47 +++++++++++-
.../core/commands/ai/ToolContextCodeLinesTest.java | 85 ++++++++++++++++++++++
2 files changed, 130 insertions(+), 2 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
index 16d271524594..d2670d694aac 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
@@ -19,7 +19,9 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collection;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -31,6 +33,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.dsl.jbang.core.common.ActionClient;
import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
import org.apache.camel.dsl.jbang.run.common.CatalogLoader;
+import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
@@ -360,18 +363,58 @@ public class ToolContext {
if (!result.isOk()) {
throw new ToolExecutionException(result.error());
}
+ if (unescapeCodeLines(json)) {
+ return json.toJson();
+ }
}
return answer;
}
public JsonObject readErrorFile() {
requireProcess();
- return RuntimeHelper.readErrorFile(pid);
+ JsonObject errors = RuntimeHelper.readErrorFile(pid);
+ unescapeCodeLines(errors);
+ return errors;
}
public JsonObject readHistoryFile() {
requireProcess();
- return RuntimeHelper.readHistoryFile(pid);
+ JsonObject history = RuntimeHelper.readHistoryFile(pid);
+ unescapeCodeLines(history);
+ return history;
+ }
+
+ /**
+ * Unescapes the lines of source code, so they are as they are in the
file. The dev consoles escape each line of a
+ * {@code code} list as JSON text in JSON. The CLI commands undo this when
they print the lines, but the AI tools
+ * gave the lines as they came, so the agent saw {@code \"} that is not in
the file (CAMEL-26932).
+ *
+ * @return whether a line was changed
+ */
+ static boolean unescapeCodeLines(Object node) {
+ boolean changed = false;
+ if (node instanceof JsonObject jo) {
+ for (Map.Entry<String, Object> e : jo.entrySet()) {
+ if ("code".equals(e.getKey()) && e.getValue() instanceof
JsonArray lines) {
+ for (Object line : lines) {
+ if (line instanceof JsonObject l && l.get("code")
instanceof String code) {
+ String text = Jsoner.unescape(code);
+ if (!text.equals(code)) {
+ l.put("code", text);
+ changed = true;
+ }
+ }
+ }
+ } else {
+ changed |= unescapeCodeLines(e.getValue());
+ }
+ }
+ } else if (node instanceof Collection<?> list) {
+ for (Object o : list) {
+ changed |= unescapeCodeLines(o);
+ }
+ }
+ return changed;
}
public String stopApplication() {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContextCodeLinesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContextCodeLinesTest.java
new file mode 100644
index 000000000000..5ff708bd4ec0
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContextCodeLinesTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26932: the AI tools give the lines of source code as they are in the
file. The dev consoles escape each line of
+ * a code list, and the agent saw \" that is not in the file.
+ */
+class ToolContextCodeLinesTest {
+
+ private static final String LINE = " - setBody: { simple: \"Hello
${header.name}\" } # C:\\temp";
+
+ @Test
+ void theLinesOfARouteDumpAreAsInTheFile() throws Exception {
+ // route-dump, source and route-structure: routes, each with a list of
lines
+ JsonObject answer = new JsonObject();
+ answer.put("routes", new JsonArray(List.of(route(LINE))));
+
+ assertThat(ToolContext.unescapeCodeLines(answer)).isTrue();
+
+ JsonObject route = (JsonObject) ((JsonArray)
answer.get("routes")).get(0);
+ List<JsonObject> code = route.getCollection("code");
+ assertThat(code.get(0).getString("code")).isEqualTo(LINE);
+ assertThat(code.get(0).getInteger("line")).isEqualTo(7);
+ // what the agent reads is the file, also after a JSON round trip
+ JsonObject parsed = (JsonObject) Jsoner.deserialize(answer.toJson());
+ JsonObject again = (JsonObject) ((JsonArray)
parsed.get("routes")).get(0);
+ assertThat(((JsonObject) ((JsonArray)
again.get("code")).get(0)).getString("code")).isEqualTo(LINE);
+ }
+
+ @Test
+ void aLineWithoutEscapesIsNoChange() {
+ JsonObject answer = new JsonObject();
+ answer.put("routes", new JsonArray(List.of(route("- from:
timer:tick"))));
+
+ assertThat(ToolContext.unescapeCodeLines(answer)).isFalse();
+ }
+
+ @Test
+ void otherTextIsAsItWas() {
+ // a message body or header is not a line of code, and is left as the
integration wrote it
+ JsonObject message = new JsonObject();
+ message.put("body", Jsoner.escape("{\"code\":\"x\"}"));
+ message.put("code", "A\\\"B");
+ JsonObject answer = new JsonObject();
+ answer.put("message", message);
+
+ assertThat(ToolContext.unescapeCodeLines(answer)).isFalse();
+ assertThat(message.getString("code")).isEqualTo("A\\\"B");
+ }
+
+ private static JsonObject route(String text) {
+ JsonObject line = new JsonObject();
+ line.put("line", 7);
+ // as the dev consoles write a line
+ line.put("code", Jsoner.escape(text));
+ JsonObject route = new JsonObject();
+ route.put("routeId", "orders");
+ route.put("code", new JsonArray(List.of(line)));
+ return route;
+ }
+}