You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "jamesnetherton (via GitHub)" <gi...@apache.org> on 2026/01/30 08:30:28 UTC

[PR] Add OpenAI extension [camel-quarkus]

jamesnetherton opened a new pull request, #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227

   The `openai` component is not in the 4.17.0 catalog. This scenario happens occasionally, so I added a small 'enhancement' to the `check-extension-pages` mojo. So that it can continue to generate the expected doc resources. I intend to immediately remove the additional plugin config once this change lands on the `camel-main` branch.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745532827


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   Ok - thanks for the feedback. Lets see how it goes. If it does prove to be flaky on CI, I will rework it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "aldettinger (via GitHub)" <gi...@apache.org>.
aldettinger commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745356338


##########
docs/modules/ROOT/pages/reference/extensions/openai.adoc:
##########
@@ -0,0 +1,94 @@
+// Do not edit directly!
+// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-openai"]
+= OpenAI
+:linkattrs:
+:cq-artifact-id: camel-quarkus-openai
+:cq-native-supported: true
+:cq-status: Stable
+:cq-status-deprecation: Stable
+:cq-description: OpenAI endpoint for chat completion.
+:cq-deprecated: false
+:cq-jvm-since: 3.32.0
+:cq-native-since: 3.32.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.32.0## [.badge-key]##Native since##[.badge-supported]##3.32.0##
+endif::[]
+
+OpenAI endpoint for chat completion.
+
+[id="extensions-openai-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-openai[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-openai</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-openai-usage"]
+== Usage
+[id="extensions-openai-usage-structured-output-with-output-class-in-native-mode"]
+=== Structured output with output class in native mode
+
+When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection.
+
+This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example:

Review Comment:
   Don't we generally reference the centralized native doc part in such case ?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "aldettinger (via GitHub)" <gi...@apache.org>.
aldettinger commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745484347


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   I was more fearing a kind of race condition with wireTap thread pool but nothing demonstrated. If we could not simply switch to non order expectations now, we are not taking a big risk anyway. Even if flaky, it would not happen a lot :crossed_fingers: .



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#issuecomment-3822915090

   I need to fix things up for Windows where I added test file deletion:
   
   ```
   The process cannot access the file because it is being used by another process 
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "apupier (via GitHub)" <gi...@apache.org>.
apupier commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745508972


##########
docs/modules/ROOT/pages/reference/extensions/openai.adoc:
##########
@@ -0,0 +1,94 @@
+// Do not edit directly!
+// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-openai"]
+= OpenAI
+:linkattrs:
+:cq-artifact-id: camel-quarkus-openai
+:cq-native-supported: true
+:cq-status: Stable
+:cq-status-deprecation: Stable
+:cq-description: OpenAI endpoint for chat completion.
+:cq-deprecated: false
+:cq-jvm-since: 3.32.0
+:cq-native-since: 3.32.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.32.0## [.badge-key]##Native since##[.badge-supported]##3.32.0##
+endif::[]
+
+OpenAI endpoint for chat completion.
+
+[id="extensions-openai-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-openai[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-openai</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-openai-usage"]
+== Usage
+[id="extensions-openai-usage-structured-output-with-output-class-in-native-mode"]
+=== Structured output with output class in native mode
+
+When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection.
+
+This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example:
+
+[source,java]
+----
+@RegisterForReflection
+public class MyStructuredOutputClass {
+    ...
+}
+----
+
+[source,java]
+----
+public class MyJQRoutes extends RouteBuilder {

Review Comment:
   `MyJQRoutes`? A copy-paste from JQ doc?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745467713


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   We do rely on ordering for this test...
   
   Unless the LLM gets the order of numbers wrong (unlikely), won't the streamed chunks come in the expected 1-10 order?
   
   Or do you think the messages arriving at the `seda` endpoint might be out of order (I've not seen it happen so far in local testing)?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "aldettinger (via GitHub)" <gi...@apache.org>.
aldettinger commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745441975


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   Do we rely on message ordering for expectation ? If yes, not sure the route would preserve order, not sure of the opposite though.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745375250


##########
docs/modules/ROOT/pages/reference/extensions/openai.adoc:
##########
@@ -0,0 +1,94 @@
+// Do not edit directly!
+// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-openai"]
+= OpenAI
+:linkattrs:
+:cq-artifact-id: camel-quarkus-openai
+:cq-native-supported: true
+:cq-status: Stable
+:cq-status-deprecation: Stable
+:cq-description: OpenAI endpoint for chat completion.
+:cq-deprecated: false
+:cq-jvm-since: 3.32.0
+:cq-native-since: 3.32.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.32.0## [.badge-key]##Native since##[.badge-supported]##3.32.0##
+endif::[]
+
+OpenAI endpoint for chat completion.
+
+[id="extensions-openai-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-openai[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-openai</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-openai-usage"]
+== Usage
+[id="extensions-openai-usage-structured-output-with-output-class-in-native-mode"]
+=== Structured output with output class in native mode
+
+When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection.
+
+This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example:

Review Comment:
   Most extension docs give some native mode examples and then link to the native mode guide at the end. Which is the pattern I followed here.  



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745697101


##########
docs/modules/ROOT/pages/reference/extensions/openai.adoc:
##########
@@ -0,0 +1,94 @@
+// Do not edit directly!
+// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-openai"]
+= OpenAI
+:linkattrs:
+:cq-artifact-id: camel-quarkus-openai
+:cq-native-supported: true
+:cq-status: Stable
+:cq-status-deprecation: Stable
+:cq-description: OpenAI endpoint for chat completion.
+:cq-deprecated: false
+:cq-jvm-since: 3.32.0
+:cq-native-since: 3.32.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.32.0## [.badge-key]##Native since##[.badge-supported]##3.32.0##
+endif::[]
+
+OpenAI endpoint for chat completion.
+
+[id="extensions-openai-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-openai[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-openai</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-openai-usage"]
+== Usage
+[id="extensions-openai-usage-structured-output-with-output-class-in-native-mode"]
+=== Structured output with output class in native mode
+
+When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection.
+
+This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example:
+
+[source,java]
+----
+@RegisterForReflection
+public class MyStructuredOutputClass {
+    ...
+}
+----
+
+[source,java]
+----
+public class MyJQRoutes extends RouteBuilder {

Review Comment:
   Should be fixed in the latest commit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "aldettinger (via GitHub)" <gi...@apache.org>.
aldettinger commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2753402164


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   Looks good, well done :clap: 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton merged PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "aldettinger (via GitHub)" <gi...@apache.org>.
aldettinger commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2746712685


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   Ok, that said the probability should have been quite low. Anyway, no order in the route is not a big deal, just need to relax assertion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2752967192


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   I reworked the test assertion(s) so that it can deal with results coming out of sequence.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "aldettinger (via GitHub)" <gi...@apache.org>.
aldettinger commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745484347


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   I was more fearing a kind of race condition with wireTap thread pool but nothing sure. If we could not simply switch to non order expectations now, we are not taking a big risk anyway. Even if flaky, it would not happen a lot :crossed_fingers: .



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2745535530


##########
docs/modules/ROOT/pages/reference/extensions/openai.adoc:
##########
@@ -0,0 +1,94 @@
+// Do not edit directly!
+// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-openai"]
+= OpenAI
+:linkattrs:
+:cq-artifact-id: camel-quarkus-openai
+:cq-native-supported: true
+:cq-status: Stable
+:cq-status-deprecation: Stable
+:cq-description: OpenAI endpoint for chat completion.
+:cq-deprecated: false
+:cq-jvm-since: 3.32.0
+:cq-native-since: 3.32.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.32.0## [.badge-key]##Native since##[.badge-supported]##3.32.0##
+endif::[]
+
+OpenAI endpoint for chat completion.
+
+[id="extensions-openai-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-openai[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-openai</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-openai-usage"]
+== Usage
+[id="extensions-openai-usage-structured-output-with-output-class-in-native-mode"]
+=== Structured output with output class in native mode
+
+When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection.
+
+This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example:
+
+[source,java]
+----
+@RegisterForReflection
+public class MyStructuredOutputClass {
+    ...
+}
+----
+
+[source,java]
+----
+public class MyJQRoutes extends RouteBuilder {

Review Comment:
   Whoops - I thought I already fixed that. I'll change it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Add OpenAI extension [camel-quarkus]

Posted by "jamesnetherton (via GitHub)" <gi...@apache.org>.
jamesnetherton commented on code in PR #8227:
URL: https://github.com/apache/camel-quarkus/pull/8227#discussion_r2746597656


##########
integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java:
##########
@@ -0,0 +1,179 @@
+/*
+ * 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.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiTest {
+    @Test
+    void simpleChat() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("In one sentence, what is Apache Camel?")
+                .post("/openai/chat")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("integration framework"));
+    }
+
+    @Test
+    void chatWithImage() throws IOException {
+        Path path = Paths.get("target/camel-log.png");
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading camel-logo.png");
+            }
+
+            stream.transferTo(new FileOutputStream(path.toFile()));
+
+            RestAssured.given()
+                    .queryParam("userMessage", "Describe what you see in this image")
+                    .body("target/camel-log.png")
+                    .post("/openai/chat/image")
+                    .then()
+                    .statusCode(200)
+                    .body(
+                            containsStringIgnoringCase("camel"),
+                            containsStringIgnoringCase("silhouette"),
+                            containsStringIgnoringCase("logo"));
+        } finally {
+            Files.deleteIfExists(path);
+        }
+    }
+
+    @Test
+    void chatInitiatedFromFileConsumer() throws IOException {
+        Path prompts = Paths.get("target/prompts");
+        Path prompt = prompts.resolve("whatis-camel-prompt.txt");
+        Files.createDirectories(prompts);
+
+        try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) {
+            if (stream == null) {
+                throw new IllegalStateException("Failed loading whatis-camel-prompt.txt");
+            }
+
+            stream.transferTo(new FileOutputStream(prompt.toFile()));
+
+            // Start the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/start")
+                    .then()
+                    .statusCode(204);
+
+            Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> {
+                RestAssured.given()
+                        .queryParam("endpointUri", "seda:filePromptResults")
+                        .get("/openai/chat/results")
+                        .then()
+                        .statusCode(200)
+                        .body(containsStringIgnoringCase("integration framework"));
+            });
+        } finally {
+            // Stop the file-prompts route
+            RestAssured.given()
+                    .post("/openai/routes/file-prompts/stop")
+                    .then()
+                    .statusCode(204);
+
+            Files.deleteIfExists(prompt);
+        }
+    }
+
+    @Test
+    void chatWithMemory() {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("I am a Camel and my species is Camelus Dromedarius.")
+                .post("/openai/chat/memory")
+                .then()
+                .statusCode(200)
+                .body(containsStringIgnoringCase("Camelus Dromedarius"));
+    }
+
+    @Test
+    void streamingChat() {
+        // Send the streaming request
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body("Stream the numbers 1 to 10 on a new line each time and nothing else.")
+                .post("/openai/chat/streaming")
+                .then()
+                .statusCode(204);
+
+        // Assert the streamed results
+        IntStream.rangeClosed(1, 10)

Review Comment:
   @aldettinger I think you are right about this!
   
   Seems the latest Windows failure is due to unexpected ordering.
   
   I'll try to rework the test to make it more reliable.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org