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 8b71b79af6dd CAMEL-26213: camel-chatscript - send the first message 
after starting the conversation, keep the headers, and reset the user of the 
message (#29060)
8b71b79af6dd is described below

commit 8b71b79af6ddb62f492e85335371b2f6956aef4f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Nov 3 12:13:27 2026 +0100

    CAMEL-26213: camel-chatscript - send the first message after starting the 
conversation, keep the headers, and reset the user of the message (#29060)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/chatscript-component.adoc   |   8 +-
 .../src/main/docs/chatscript-component.adoc        |   8 +-
 .../camel/component/chatscript/ChatScriptBot.java  |  36 ++++-
 .../component/chatscript/ChatScriptMessage.java    |   7 +-
 .../component/chatscript/ChatScriptProducer.java   |  11 +-
 .../chatscript/ChatScriptConversationTest.java     | 171 +++++++++++++++++++++
 .../component/chatscript/FakeChatScriptServer.java |  99 ++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 8 files changed, 336 insertions(+), 17 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chatscript-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chatscript-component.adoc
index 3bd8501ad969..6f17742a3ee5 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chatscript-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chatscript-component.adoc
@@ -42,7 +42,13 @@ This component expects a `ChatScriptMessage`, or its JSON as 
a String with the f
 ----
 
 The bot name comes from the endpoint, and the reply is the `reply` field of 
the `ChatScriptMessage` the producer returns
-(`response` in its JSON form).
+(`response` in its JSON form). The headers of the message are kept.
+
+The first message of the endpoint starts the conversation of its user with an 
empty message, as the ChatScript
+protocol requires, and is then sent to the bot. A first message with an empty 
text only starts the conversation, and its
+reply is the greeting of the bot. With `resetChat=true` every message first 
resets the conversation of its user (the
+`username` of the message, or the `chatUserName` option when the message has 
none) and then starts a new one.
+
 The following route sends the message body to the bot `Harry` as the user in 
the `user` header:
 
 [source,yaml]
diff --git 
a/components/camel-ai/camel-chatscript/src/main/docs/chatscript-component.adoc 
b/components/camel-ai/camel-chatscript/src/main/docs/chatscript-component.adoc
index 3bd8501ad969..6f17742a3ee5 100644
--- 
a/components/camel-ai/camel-chatscript/src/main/docs/chatscript-component.adoc
+++ 
b/components/camel-ai/camel-chatscript/src/main/docs/chatscript-component.adoc
@@ -42,7 +42,13 @@ This component expects a `ChatScriptMessage`, or its JSON as 
a String with the f
 ----
 
 The bot name comes from the endpoint, and the reply is the `reply` field of 
the `ChatScriptMessage` the producer returns
-(`response` in its JSON form).
+(`response` in its JSON form). The headers of the message are kept.
+
+The first message of the endpoint starts the conversation of its user with an 
empty message, as the ChatScript
+protocol requires, and is then sent to the bot. A first message with an empty 
text only starts the conversation, and its
+reply is the greeting of the bot. With `resetChat=true` every message first 
resets the conversation of its user (the
+`username` of the message, or the `chatUserName` option when the message has 
none) and then starts a new one.
+
 The following route sends the message body to the bot `Harry` as the user in 
the `user` header:
 
 [source,yaml]
diff --git 
a/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptBot.java
 
b/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptBot.java
index 0b21d39a9089..125474c8e818 100644
--- 
a/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptBot.java
+++ 
b/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptBot.java
@@ -46,16 +46,21 @@ public class ChatScriptBot {
     }
 
     public String sendChat(String input) throws Exception {
-        if (!initialized) {
-            return init(null);
-        }
-        ChatScriptMessage g = new ChatScriptMessage(this.userName, 
this.botName, input);
-        return doMessage(g.toCSFormat());
+        return sendChat(new ChatScriptMessage(this.userName, this.botName, 
input));
     }
 
+    /**
+     * Sends the message and returns the reply of the bot. The first message 
starts the conversation of its user with an
+     * empty message, as the ChatScript protocol requires, and is then sent 
too; when the message has no text the reply
+     * is the greeting of the bot.
+     */
     public String sendChat(ChatScriptMessage input) throws Exception {
         if (!initialized) {
-            return init(input);
+            String greeting = init(input);
+            if (input.getBody() == null || input.getBody().isEmpty()) {
+                // the empty message that started the conversation was the 
message itself
+                return greeting;
+            }
         }
         return doMessage(input.toCSFormat());
     }
@@ -84,8 +89,12 @@ public class ChatScriptBot {
 
     }
 
+    /**
+     * Starts the conversation of the user of the message by sending an empty 
message, and returns the greeting of the
+     * bot. The text of the message is not sent.
+     */
     public String init(ChatScriptMessage input) throws Exception {
-        ChatScriptMessage g = new ChatScriptMessage(input.getUserName(), 
this.botName, null);
+        ChatScriptMessage g = new ChatScriptMessage(input.getUserName(), 
this.botName, "");
         String response = doMessage(g);
         LOG.info("Conversation started between the bot {} and {}", 
this.botName, input.getUserName());
         initialized = true;
@@ -96,9 +105,20 @@ public class ChatScriptBot {
         return "ChatSCript";
     }
 
+    /**
+     * Resets the conversation of the user of this bot (the chatUserName 
option of the endpoint).
+     */
     public void reset() {
+        reset(this.userName);
+    }
+
+    /**
+     * Resets the conversation of the given user with the ChatScript :reset 
command, so the next message starts a new
+     * conversation.
+     */
+    public void reset(String user) {
         try {
-            doMessage(new ChatScriptMessage(this.userName, this.botName, 
":reset").toCSFormat());
+            doMessage(new ChatScriptMessage(user, this.botName, 
":reset").toCSFormat());
             initialized = false;
         } catch (Exception e) {
             LOG.warn("Failed to reset the ChatScript conversation: {}", 
e.getMessage(), e);
diff --git 
a/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptMessage.java
 
b/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptMessage.java
index 33156425eb9d..b8268210c9f6 100644
--- 
a/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptMessage.java
+++ 
b/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptMessage.java
@@ -73,10 +73,15 @@ public class ChatScriptMessage {
     public String toCSFormat() {
         String s;
         final char nullChar = (char) 0;
-        s = this.username + nullChar + this.botname + nullChar + this.body + 
nullChar;
+        // a missing field goes out as empty text, not as the text null
+        s = emptyIfNull(this.username) + nullChar + emptyIfNull(this.botname) 
+ nullChar + emptyIfNull(this.body) + nullChar;
         return s;
     }
 
+    private static String emptyIfNull(String value) {
+        return value != null ? value : "";
+    }
+
     @Override
     public String toString() {
         return "ChatScriptMessage [username=" + username + ", botname=" + 
botname + ", message=" + body + ", reply=" + reply
diff --git 
a/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptProducer.java
 
b/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptProducer.java
index 44f7aa48094b..0fbcab9eb2cf 100644
--- 
a/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptProducer.java
+++ 
b/components/camel-ai/camel-chatscript/src/main/java/org/apache/camel/component/chatscript/ChatScriptProducer.java
@@ -35,11 +35,6 @@ public class ChatScriptProducer extends DefaultProducer {
 
     @Override
     public void process(Exchange exchange) throws Exception {
-        // Start a fresh conversation on every exchange when requested 
(resetChat).
-        if (endpoint.isResetChat()) {
-            endpoint.getBot().reset();
-        }
-
         Object body = exchange.getIn().getBody();
         ChatScriptMessage inputMessage;
         /* use can pass message object as json or the object it self */
@@ -53,9 +48,13 @@ public class ChatScriptProducer extends DefaultProducer {
         if (ObjectHelper.isEmpty(inputMessage.getUserName())) {
             inputMessage.setUserName(endpoint.getBot().getUserName());
         }
+        // Start a fresh conversation of the user of the message on every 
exchange when requested (resetChat).
+        if (endpoint.isResetChat()) {
+            endpoint.getBot().reset(inputMessage.getUserName());
+        }
         String response = this.endpoint.getBot().sendChat(inputMessage);
         inputMessage.setReply(response);
-        exchange.getOut().setBody(inputMessage);
+        exchange.getMessage().setBody(inputMessage);
     }
 
     private ChatScriptMessage buildMessage(Object body) throws Exception {
diff --git 
a/components/camel-ai/camel-chatscript/src/test/java/org/apache/camel/component/chatscript/ChatScriptConversationTest.java
 
b/components/camel-ai/camel-chatscript/src/test/java/org/apache/camel/component/chatscript/ChatScriptConversationTest.java
new file mode 100644
index 000000000000..753489bf8469
--- /dev/null
+++ 
b/components/camel-ai/camel-chatscript/src/test/java/org/apache/camel/component/chatscript/ChatScriptConversationTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.component.chatscript;
+
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Talks to a fake ChatScript server and checks the messages the component 
sends on the wire.
+ */
+class ChatScriptConversationTest {
+
+    private FakeChatScriptServer server;
+    private CamelContext context;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        server = new FakeChatScriptServer();
+        context = new DefaultCamelContext();
+        context.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        context.stop();
+        server.close();
+    }
+
+    @Test
+    void firstMessageStartsTheConversationAndIsThenSent() throws Exception {
+        Producer producer = producer("");
+
+        Exchange exchange = process(producer, "{\"username\": \"bob\", 
\"message\": \"Hello\"}");
+
+        assertEquals(List.of("bob", "Harry", ""), server.nextMessage(),
+                "the conversation must start with an empty message");
+        assertEquals(List.of("bob", "Harry", "Hello"), server.nextMessage(),
+                "the first message must be sent after the conversation 
started");
+        assertTrue(server.hasNoMoreMessages());
+        assertEquals("You said Hello", 
exchange.getMessage().getBody(ChatScriptMessage.class).getReply());
+    }
+
+    @Test
+    void nextMessagesAreSentOnce() throws Exception {
+        Producer producer = producer("");
+
+        process(producer, new ChatScriptMessage("bob", null, "Hello"));
+        Exchange exchange = process(producer, new ChatScriptMessage("bob", 
null, "How are you"));
+
+        assertEquals(List.of("bob", "Harry", ""), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", "Hello"), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", "How are you"), 
server.nextMessage());
+        assertTrue(server.hasNoMoreMessages());
+        assertEquals("You said How are you", 
exchange.getMessage().getBody(ChatScriptMessage.class).getReply());
+    }
+
+    @Test
+    void emptyFirstMessageStartsTheConversationAndReturnsTheGreeting() throws 
Exception {
+        Producer producer = producer("");
+
+        Exchange exchange = process(producer, "{\"username\": \"bob\", 
\"message\": \"\"}");
+
+        assertEquals(List.of("bob", "Harry", ""), server.nextMessage());
+        assertTrue(server.hasNoMoreMessages(), "the empty message must be sent 
once");
+        assertEquals("Welcome bob", 
exchange.getMessage().getBody(ChatScriptMessage.class).getReply());
+    }
+
+    @Test
+    void resetChatResetsTheConversationOfTheUserOfTheMessage() throws 
Exception {
+        Producer producer = producer("?resetChat=true");
+
+        process(producer, "{\"username\": \"bob\", \"message\": \"Hello\"}");
+        Exchange exchange = process(producer, "{\"username\": \"bob\", 
\"message\": \"Hello again\"}");
+
+        assertEquals(List.of("bob", "Harry", ":reset"), server.nextMessage(),
+                "the conversation of the user of the message must be reset");
+        assertEquals(List.of("bob", "Harry", ""), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", "Hello"), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", ":reset"), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", ""), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", "Hello again"), 
server.nextMessage());
+        assertTrue(server.hasNoMoreMessages());
+        assertEquals("You said Hello again", 
exchange.getMessage().getBody(ChatScriptMessage.class).getReply());
+    }
+
+    @Test
+    void resetChatResetsTheChatUserNameWhenTheMessageHasNoUser() throws 
Exception {
+        Producer producer = producer("?resetChat=true&chatUserName=alice");
+
+        Exchange exchange = process(producer, "{\"message\": \"Hello\"}");
+
+        assertEquals(List.of("alice", "Harry", ":reset"), 
server.nextMessage());
+        assertEquals(List.of("alice", "Harry", ""), server.nextMessage());
+        assertEquals(List.of("alice", "Harry", "Hello"), server.nextMessage());
+        assertTrue(server.hasNoMoreMessages());
+        assertEquals("You said Hello", 
exchange.getMessage().getBody(ChatScriptMessage.class).getReply());
+    }
+
+    @Test
+    void headersOfTheMessageAreKept() throws Exception {
+        Producer producer = producer("");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setHeader("user", "bob");
+        exchange.getIn().setHeader("conversation", 42);
+        exchange.getIn().setBody("{\"username\": \"bob\", \"message\": 
\"Hello\"}");
+
+        producer.process(exchange);
+
+        assertEquals("bob", exchange.getMessage().getHeader("user"));
+        assertEquals(42, exchange.getMessage().getHeader("conversation"));
+        assertEquals("You said Hello", 
exchange.getMessage().getBody(ChatScriptMessage.class).getReply());
+    }
+
+    @Test
+    void sendChatWithTextStartsTheConversationOfTheBotUser() throws Exception {
+        ChatScriptBot bot = new ChatScriptBot(FakeChatScriptServer.HOST, 
server.getPort(), "Harry", "bob");
+
+        assertEquals("You said Hello", bot.sendChat("Hello"));
+        assertEquals("You said Bye", bot.sendChat("Bye"));
+
+        assertEquals(List.of("bob", "Harry", ""), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", "Hello"), server.nextMessage());
+        assertEquals(List.of("bob", "Harry", "Bye"), server.nextMessage());
+        assertTrue(server.hasNoMoreMessages());
+    }
+
+    @Test
+    void missingFieldsAreSentAsEmptyText() {
+        assertEquals("bob\0Harry\0\0", new ChatScriptMessage("bob", "Harry", 
null).toCSFormat());
+        assertEquals("\0Harry\0Hello\0", new ChatScriptMessage(null, "Harry", 
"Hello").toCSFormat());
+    }
+
+    private Producer producer(String options) throws Exception {
+        ChatScriptEndpoint endpoint = context.getEndpoint(
+                "chatscript:" + FakeChatScriptServer.HOST + ":" + 
server.getPort() + "/Harry" + options,
+                ChatScriptEndpoint.class);
+        return endpoint.createProducer();
+    }
+
+    private Exchange process(Producer producer, Object body) throws Exception {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody(body);
+        producer.process(exchange);
+        return exchange;
+    }
+}
diff --git 
a/components/camel-ai/camel-chatscript/src/test/java/org/apache/camel/component/chatscript/FakeChatScriptServer.java
 
b/components/camel-ai/camel-chatscript/src/test/java/org/apache/camel/component/chatscript/FakeChatScriptServer.java
new file mode 100644
index 000000000000..7b22fc656754
--- /dev/null
+++ 
b/components/camel-ai/camel-chatscript/src/test/java/org/apache/camel/component/chatscript/FakeChatScriptServer.java
@@ -0,0 +1,99 @@
+/*
+ * 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.component.chatscript;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A ChatScript server that records every message (user, bot and text, the 
three null terminated strings of the
+ * ChatScript protocol) and answers one line per connection: the greeting for 
an empty text, an empty line for
+ * {@code :reset}, and {@code You said <text>} otherwise.
+ */
+final class FakeChatScriptServer implements AutoCloseable {
+
+    static final String HOST = "127.0.0.1";
+
+    private final ServerSocket serverSocket;
+    private final BlockingQueue<List<String>> messages = new 
LinkedBlockingQueue<>();
+
+    FakeChatScriptServer() throws IOException {
+        serverSocket = new ServerSocket(0, 50, InetAddress.getByName(HOST));
+        Thread thread = new Thread(this::serve, "fake-chatscript-server");
+        thread.setDaemon(true);
+        thread.start();
+    }
+
+    int getPort() {
+        return serverSocket.getLocalPort();
+    }
+
+    /**
+     * The next message the server received, as user, bot and text, or null 
when none arrives within 10 seconds.
+     */
+    List<String> nextMessage() throws InterruptedException {
+        return messages.poll(10, TimeUnit.SECONDS);
+    }
+
+    boolean hasNoMoreMessages() {
+        return messages.isEmpty();
+    }
+
+    private void serve() {
+        while (!serverSocket.isClosed()) {
+            try (Socket socket = serverSocket.accept();
+                 BufferedReader in = new BufferedReader(new 
InputStreamReader(socket.getInputStream()));
+                 PrintWriter out = new PrintWriter(socket.getOutputStream(), 
true)) {
+                String line = in.readLine();
+                if (line == null) {
+                    continue;
+                }
+                String[] fields = line.split("\0", -1);
+                List<String> message = List.of(fields[0], fields[1], 
fields[2]);
+                messages.add(message);
+                out.println(reply(message));
+            } catch (IOException e) {
+                // the server socket was closed
+            }
+        }
+    }
+
+    private static String reply(List<String> message) {
+        String text = message.get(2);
+        if (text.isEmpty()) {
+            return "Welcome " + message.get(0);
+        }
+        if (":reset".equals(text)) {
+            return "";
+        }
+        return "You said " + text;
+    }
+
+    @Override
+    public void close() throws IOException {
+        serverSocket.close();
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 7350bf33ef9b..0c4884b4f85f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -4294,6 +4294,19 @@ lists the file). A file deleted after the listing is 
skipped as well; before, it
 Before, any other failure failed the poll, and as every poll lists the file 
system again, one such file stopped the
 consumer from consuming any file. The poll still fails when none of the listed 
files could be got.
 
+=== camel-chatscript - the first message of a conversation is sent, and the 
headers are kept
+
+The first message of a ChatScript endpoint, and every message with 
`resetChat=true`, used to start the conversation
+with the text `null` and return the greeting of the bot as the reply, so the 
message itself was never sent. The
+conversation now starts with an empty message, as the ChatScript protocol 
requires, and the message is then sent, so
+the reply is the answer of the bot to it. A first message with an empty text 
only starts the conversation and still
+returns the greeting.
+
+With `resetChat=true` the `:reset` command is now sent for the user of the 
message (or for the `chatUserName` option
+when the message has no user), not for the `chatUserName` option alone.
+
+The producer now keeps the headers of the message, which it used to drop.
+
 === camel-milo - a Bad or Uncertain status is delivered by default
 
 A `milo-client` consumer now delivers a data change with a Bad or Uncertain 
status by default, even when its value is

Reply via email to