From 5681380ed6d556d30210e666d570690aa67a601a Mon Sep 17 00:00:00 2001 From: Jiri Ondrusek Date: Mon, 27 Jul 2026 16:55:48 +0200 Subject: [PATCH 1/2] Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag filtering for langchain4j Bridges Camel's AiToolRegistry to langchain4j's ToolProvider SPI in extensions-support/langchain4j. When both camel-ai-tool and quarkus-langchain4j are on the classpath, the deployment processor auto-registers CamelAiToolProvider as a CDI bean, making Camel AI tools available to @RegisterAiService AI services without explicit wiring. Introduces @CamelAiTools("tag") annotation for per-AI-service tool filtering via a CDI interceptor + ThreadLocal pattern. Tag resolution: ThreadLocal present -> getToolsByTag(), absent -> getAllTools(). Services without @CamelAiTools always see all tools. Includes integration tests with mock ToolCallingChatModels verifying end-to-end tool invocation and tag isolation across weather + admin AI services, plus an Ollama-based test behind -Pollama. Document LangChain4j integration and @CamelAiTools tag filtering in ai-tool usage.adoc Co-Authored-By: Claude Opus 4.6 --- .../pages/reference/extensions/ai-tool.adoc | 56 +++ .../ROOT/pages/reference/extensions/core.adoc | 406 ------------------ .../langchain4j/deployment/pom.xml | 5 + .../langchain4j/deployment/AiToolPresent.java | 36 ++ .../SupportQuarkusLangchain4jProcessor.java | 111 ++++- .../langchain4j/runtime/pom.xml | 10 + .../langchain4j/AiToolSpecConverter.java | 29 ++ .../langchain4j/CamelAiToolProvider.java | 129 ++++++ .../support/langchain4j/CamelAiTools.java | 48 +++ .../langchain4j/CamelAiToolsInterceptor.java | 73 ++++ .../QuarkusLangchain4jRecorder.java | 5 + .../ai-tool/runtime/src/main/doc/usage.adoc | 54 +++ .../ai-tool-langchain4j/README.adoc | 15 + integration-tests/ai-tool-langchain4j/pom.xml | 222 ++++++++++ .../it/AiToolLangchain4jOllamaResource.java | 54 +++ .../it/AiToolLangchain4jResource.java | 90 ++++ .../it/AiToolLangchain4jRoutes.java | 40 ++ .../model/AbstractToolCallingChatModel.java | 93 ++++ .../it/model/AdminToolCallingChatModel.java | 45 ++ .../it/model/WeatherToolCallingChatModel.java | 45 ++ .../model/WrongTagToolCallingChatModel.java | 78 ++++ .../it/service/AdminAiService.java | 31 ++ .../it/service/WeatherAiService.java | 31 ++ .../it/service/WeatherAiServiceOllama.java | 30 ++ .../WeatherAiServiceOllamaWrongTag.java | 30 ++ .../it/service/WeatherAiServiceWrongTag.java | 31 ++ .../src/main/resources/application.properties | 18 + .../langchain4j/it/AiToolLangchain4jIT.java | 23 + .../it/AiToolLangchain4jOllamaIT.java | 25 ++ .../it/AiToolLangchain4jOllamaTest.java | 68 +++ .../langchain4j/it/AiToolLangchain4jTest.java | 97 +++++ integration-tests/pom.xml | 1 + tooling/scripts/test-categories.yaml | 1 + 33 files changed, 1623 insertions(+), 407 deletions(-) create mode 100644 extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java create mode 100644 extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java create mode 100644 extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java create mode 100644 extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java create mode 100644 extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java create mode 100644 integration-tests/ai-tool-langchain4j/README.adoc create mode 100644 integration-tests/ai-tool-langchain4j/pom.xml create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java create mode 100644 integration-tests/ai-tool-langchain4j/src/main/resources/application.properties create mode 100644 integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java create mode 100644 integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java create mode 100644 integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java create mode 100644 integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java diff --git a/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc b/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc index 16920abb9a4e..10120444692e 100644 --- a/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc @@ -92,3 +92,59 @@ from("ai-tool:greet?description=Greet a user" .setBody(simple("Hello, ${header.name}!")); ---- +[id="extensions-ai-tool-usage-langchain4j-integration"] +=== LangChain4j integration + +When both `camel-quarkus-ai-tool` and a Quarkus LangChain4j extension (`quarkus-langchain4j-ollama`, `quarkus-langchain4j-openai`, etc.) are on the classpath, a `ToolProvider` CDI bean is registered automatically at build time. +All routes registered via `ai-tool:` endpoints become available to `@RegisterAiService` AI services without explicit wiring. + +You must also add `camel-langchain4j-agent` to your project dependencies. This artifact provides the `AiToolSpec` to `ToolSpecification` conversion and is not brought in transitively. + +[source,xml] +---- + + org.apache.camel + camel-langchain4j-agent + +---- + +With both dependencies in place, a `@RegisterAiService` interface can call Camel tools directly: + +[source,java] +---- +@RegisterAiService +public interface WeatherAiService { + String chat(@UserMessage String question); +} +---- + +Any `ai-tool:` route is now callable by the LLM behind `WeatherAiService`. No `toolProvider` attribute or manual bean reference is needed. + +[id="extensions-ai-tool-usage-tag-filtering-with-camelaitools"] +==== Tag filtering with `@CamelAiTools` + +By default, all registered `ai-tool:` routes are visible to every AI service. When you need different AI services to see different subsets of tools, annotate the service interface with `@CamelAiTools` and specify a tag value that matches the `tags` parameter on your `ai-tool:` routes. + +[source,java] +---- +@RegisterAiService +@CamelAiTools("weather") +public interface WeatherAgent { + String chat(@UserMessage String question); +} + +@RegisterAiService +@CamelAiTools("support") +public interface SupportAgent { + String chat(@UserMessage String question); +} +---- + +With this setup: + +* `WeatherAgent` sees only tools tagged with `weather` (plus any untagged tools from the default pool). +* `SupportAgent` sees only tools tagged with `support` (plus any untagged tools from the default pool). +* A `@RegisterAiService` without `@CamelAiTools` sees all registered tools regardless of tags. + +NOTE: The `@CamelAiTools` value must match a tag declared in the `ai-tool:` endpoint URI (e.g. `from("ai-tool:getWeather?tags=weather&...")`). If the annotation value is blank or does not match any route tag, the AI service will only see untagged tools. + diff --git a/docs/modules/ROOT/pages/reference/extensions/core.adoc b/docs/modules/ROOT/pages/reference/extensions/core.adoc index 9a9aa274f97f..db73e5ab498f 100644 --- a/docs/modules/ROOT/pages/reference/extensions/core.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/core.adoc @@ -124,409 +124,3 @@ camel.beans.customBeanWithSetterInjection.counter = 123 As such, the class `PropertiesCustomBeanWithSetterInjection` needs to be link:https://quarkus.io/guides/writing-native-applications-tips#registering-for-reflection[registered for reflection], note that field access could be omitted in this case. - -[width="100%",cols="80,5,15",options="header"] -|=== -| Configuration property | Type | Default - - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-discovery-exclude-patterns]]`link:#quarkus-camel-service-discovery-exclude-patterns[quarkus.camel.service.discovery.exclude-patterns]` - -A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The -services defined in the matching files will *not* be discoverable via the **`org.apache.camel.spi.FactoryFinder` -mechanism. - -The excludes have higher precedence than includes. The excludes defined here can also be used to veto the -discoverability of services included by Camel Quarkus extensions. - -Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar` -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-discovery-include-patterns]]`link:#quarkus-camel-service-discovery-include-patterns[quarkus.camel.service.discovery.include-patterns]` - -A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The -services defined in the matching files will be discoverable via the `org.apache.camel.spi.FactoryFinder` mechanism -unless the given file is excluded via `exclude-patterns`. - -Note that Camel Quarkus extensions may include some services by default. The services selected here added to those -services and the exclusions defined in `exclude-patterns` are applied to the union set. - -Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar` -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-registry-exclude-patterns]]`link:#quarkus-camel-service-registry-exclude-patterns[quarkus.camel.service.registry.exclude-patterns]` - -A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The -services defined in the matching files will *not* be added to Camel registry during application's static -initialization. - -The excludes have higher precedence than includes. The excludes defined here can also be used to veto the -registration of services included by Camel Quarkus extensions. - -Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar`** -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-registry-include-patterns]]`link:#quarkus-camel-service-registry-include-patterns[quarkus.camel.service.registry.include-patterns]` - -A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The -services defined in the matching files will be added to Camel registry during application's static initialization -unless the given file is excluded via `exclude-patterns`. - -Note that Camel Quarkus extensions may include some services by default. The services selected here added to those -services and the exclusions defined in `exclude-patterns` are applied to the union set. - -Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar` -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-components]]`link:#quarkus-camel-runtime-catalog-components[quarkus.camel.runtime-catalog.components]` - -If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel components -available in the application; otherwise component JSON schemas will not be available in the Runtime Camel Catalog and -any attempt to access those will result in a RuntimeException. - -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `false` except for making the behavior consistent with native mode. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-languages]]`link:#quarkus-camel-runtime-catalog-languages[quarkus.camel.runtime-catalog.languages]` - -If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel languages -available in the application; otherwise language JSON schemas will not be available in the Runtime Camel Catalog and -any attempt to access those will result in a RuntimeException. - -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `false` except for making the behavior consistent with native mode. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-dataformats]]`link:#quarkus-camel-runtime-catalog-dataformats[quarkus.camel.runtime-catalog.dataformats]` - -If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel data formats -available in the application; otherwise data format JSON schemas will not be available in the Runtime Camel Catalog -and any attempt to access those will result in a RuntimeException. - -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `false` except for making the behavior consistent with native mode. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-devconsoles]]`link:#quarkus-camel-runtime-catalog-devconsoles[quarkus.camel.runtime-catalog.devconsoles]` - -If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel dev consoles -available in the application; otherwise dev console JSON schemas will not be available in the Runtime Camel Catalog -and any attempt to access those will result in a RuntimeException. - -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `false` except for making the behavior consistent with native mode. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-models]]`link:#quarkus-camel-runtime-catalog-models[quarkus.camel.runtime-catalog.models]` - -If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel EIP models -available in the application; otherwise EIP model JSON schemas will not be available in the Runtime Camel Catalog and -any attempt to access those will result in a RuntimeException. - -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `false` except for making the behavior consistent with native mode. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-transformers]]`link:#quarkus-camel-runtime-catalog-transformers[quarkus.camel.runtime-catalog.transformers]` - -If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel transformers -available in the application; otherwise transformer JSON schemas will not be available in the Runtime Camel Catalog -and any attempt to access those will result in a RuntimeException. - -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `false` except for making the behavior consistent with native mode. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-routes-discovery-exclude-patterns]]`link:#quarkus-camel-routes-discovery-exclude-patterns[quarkus.camel.routes-discovery.exclude-patterns]` - -Used for exclusive filtering scanning of RouteBuilder classes. The exclusive filtering takes precedence over -inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by -comma. For example to exclude all classes starting with Bar use: ++**++/Bar++*++ To exclude all routes from a -specific package use: com/mycompany/bar/++*++ To exclude all routes from a specific package and its sub-packages use -double wildcards: com/mycompany/bar/++**++ And to exclude all routes from two specific packages use: -com/mycompany/bar/++*++,com/mycompany/stuff/++*++ -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-routes-discovery-include-patterns]]`link:#quarkus-camel-routes-discovery-include-patterns[quarkus.camel.routes-discovery.include-patterns]` - -Used for inclusive filtering scanning of RouteBuilder classes. The exclusive filtering takes precedence over -inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by -comma. For example to include all classes starting with Foo use: ++**++/Foo++*++ To include all routes from a -specific package use: com/mycompany/foo/++*++ To include all routes from a specific package and its sub-packages use -double wildcards: com/mycompany/foo/++**++ And to include all routes from two specific packages use: -com/mycompany/foo/++*++,com/mycompany/stuff/++*++ -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-native-reflection-exclude-patterns]]`link:#quarkus-camel-native-reflection-exclude-patterns[quarkus.camel.native.reflection.exclude-patterns]` - -A comma separated list of Ant-path style patterns to match class names that should be *excluded* from registering for -reflection. Use the class name format as returned by the `java.lang.Class.getName()` method: package segments -delimited by period `.` and inner classes by dollar sign `$`. - -This option narrows down the set selected by `include-patterns`. By default, no classes are excluded. - -This option cannot be used to unregister classes which have been registered internally by Quarkus extensions. -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-native-reflection-include-patterns]]`link:#quarkus-camel-native-reflection-include-patterns[quarkus.camel.native.reflection.include-patterns]` - -A comma separated list of Ant-path style patterns to match class names that should be registered for reflection. Use -the class name format as returned by the `java.lang.Class.getName()` method: package segments delimited by period `.` -and inner classes by dollar sign `$`. - -By default, no classes are included. The set selected by this option can be narrowed down by `exclude-patterns`. - -Note that Quarkus extensions typically register the required classes for reflection by themselves. This option is -useful in situations when the built in functionality is not sufficient. - -Note that this option enables the full reflective access for constructors, fields and methods. If you need a finer -grained control, consider using `io.quarkus.runtime.annotations.RegisterForReflection` annotation in your Java code. - -For this option to work properly, at least one of the following conditions must be satisfied: - -- There are no wildcards (`++*++` or `/`) in the patterns -- The artifacts containing the selected classes contain a Jandex index (`META-INF/jandex.idx`) -- The artifacts containing the selected classes are registered for indexing using the -`quarkus.index-dependency.++*++` family of options in `application.properties` - e.g. - -[source,properties] ----- -quarkus.index-dependency.my-dep.group-id = org.my-group -quarkus.index-dependency.my-dep.artifact-id = my-artifact ----- - -where `my-dep` is a label of your choice to tell Quarkus that `org.my-group` and with `my-artifact` belong together. -| List of `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-native-reflection-serialization-enabled]]`link:#quarkus-camel-native-reflection-serialization-enabled[quarkus.camel.native.reflection.serialization-enabled]` - -If `true`, basic classes are registered for serialization; otherwise basic classes won't be registered automatically -for serialization in native mode. The list of classes automatically registered for serialization can be found in -link:https://github.com/apache/camel-quarkus/blob/main/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelSerializationProcessor.java[CamelSerializationProcessor.BASE_SERIALIZATION_CLASSES]. -Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of -setting this flag to `true` except for making the behavior consistent with native mode. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-expression-on-build-time-analysis-failure]]`link:#quarkus-camel-expression-on-build-time-analysis-failure[quarkus.camel.expression.on-build-time-analysis-failure]` - -What to do if it is not possible to extract expressions from a route definition at build time. -| `fail`, `warn`, `ignore` -| `warn` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-expression-extraction-enabled]]`link:#quarkus-camel-expression-extraction-enabled[quarkus.camel.expression.extraction-enabled]` - -Indicates whether the expression extraction from the route definitions at build time must be done. If disabled, the -expressions are compiled at runtime. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-event-bridge-enabled]]`link:#quarkus-camel-event-bridge-enabled[quarkus.camel.event-bridge.enabled]` - -Whether to enable the bridging of Camel events to CDI events. - -This allows CDI observers to be configured for Camel events. E.g. those belonging to the -`org.apache.camel.quarkus.core.events`, `org.apache.camel.quarkus.main.events` & `org.apache.camel.impl.event` -packages. - -Note that this configuration item only has any effect when observers configured for Camel events are present in the -application. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-source-location-enabled]]`link:#quarkus-camel-source-location-enabled[quarkus.camel.source-location-enabled]` - -Build time configuration options for enable/disable camel source location. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-enabled]]`link:#quarkus-camel-trace-enabled[quarkus.camel.trace.enabled]` - -Enables tracer in your Camel application. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-standby]]`link:#quarkus-camel-trace-standby[quarkus.camel.trace.standby]` - -To set the tracer in standby mode, where the tracer will be installed, but not automatically enabled. The tracer can -then be enabled explicitly later from Java, JMX or tooling. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-backlog-size]]`link:#quarkus-camel-trace-backlog-size[quarkus.camel.trace.backlog-size]` - -Defines how many of the last messages to keep in the tracer. -| `int` -| `1000` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-remove-on-dump]]`link:#quarkus-camel-trace-remove-on-dump[quarkus.camel.trace.remove-on-dump]` - -Whether all traced messages should be removed when the tracer is dumping. By default, the messages are removed, which -means that dumping will not contain previous dumped messages. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-body-max-chars]]`link:#quarkus-camel-trace-body-max-chars[quarkus.camel.trace.body-max-chars]` - -To limit the message body to a maximum size in the traced message. Use 0 or negative value to use unlimited size. -| `int` -| `131072` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-body-include-streams]]`link:#quarkus-camel-trace-body-include-streams[quarkus.camel.trace.body-include-streams]` - -Whether to include the message body of stream based messages. If enabled then beware the stream may not be -re-readable later. See more about Stream Caching. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-body-include-files]]`link:#quarkus-camel-trace-body-include-files[quarkus.camel.trace.body-include-files]` - -Whether to include the message body of file based messages. The overhead is that the file content has to be read from -the file. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-include-exchange-properties]]`link:#quarkus-camel-trace-include-exchange-properties[quarkus.camel.trace.include-exchange-properties]` - -Whether to include the exchange properties in the traced message. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-include-exchange-variables]]`link:#quarkus-camel-trace-include-exchange-variables[quarkus.camel.trace.include-exchange-variables]` - -Whether to include the exchange variables in the traced message. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-include-exception]]`link:#quarkus-camel-trace-include-exception[quarkus.camel.trace.include-exception]` - -Whether to include the exception in the traced message in case of failed exchange. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-rests]]`link:#quarkus-camel-trace-trace-rests[quarkus.camel.trace.trace-rests]` - -Whether to trace routes that is created from Rest DSL. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-templates]]`link:#quarkus-camel-trace-trace-templates[quarkus.camel.trace.trace-templates]` - -Whether to trace routes that is created from route templates or kamelets. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-pattern]]`link:#quarkus-camel-trace-trace-pattern[quarkus.camel.trace.trace-pattern]` - -Filter for tracing by route or node id. -| `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-filter]]`link:#quarkus-camel-trace-trace-filter[quarkus.camel.trace.trace-filter]` - -Filter for tracing messages. -| `string` -| - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-type-converter-statistics-enabled]]`link:#quarkus-camel-type-converter-statistics-enabled[quarkus.camel.type-converter.statistics-enabled]` - -Whether type converter statistics are enabled. By default, type converter utilization statistics are disabled. Note -that enabling statistics incurs a minor performance impact under very heavy load. -| `boolean` -| `false` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-dev-ui-update-interval]]`link:#quarkus-camel-dev-ui-update-interval[quarkus.camel.dev-ui.update-interval]` - -The interval at which data is updated in Camel Quarkus Dev UI pages. -| link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[`Duration`] link:#duration-note-anchor-core[icon:question-circle[title=More information about the Duration format]] -| `5S` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-main-shutdown-timeout]]`link:#quarkus-camel-main-shutdown-timeout[quarkus.camel.main.shutdown.timeout]` - -A timeout (with millisecond precision) to wait for `CamelMain++#++stop()` to finish -| link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[`Duration`] link:#duration-note-anchor-core[icon:question-circle[title=More information about the Duration format]] -| `PT3S` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-main-arguments-on-unknown]]`link:#quarkus-camel-main-arguments-on-unknown[quarkus.camel.main.arguments.on-unknown]` - -The action to take when `CamelMain` encounters an unknown argument. fail - Prints the `CamelMain` usage statement and -throws a `RuntimeException` ignore - Suppresses any warnings and the application startup proceeds as normal warn - -Prints the `CamelMain` usage statement but allows the application startup to proceed as normal -| `fail`, `warn`, `ignore` -| `warn` - -a| [[camel-dataformat-data-format-configs]]`link:#camel-dataformat-data-format-configs[camel.dataformat."data-format-configs"]` - -Camel data format configuration. - -The format of the configuration is as follows. - -[source,properties] ----- -camel.dataformat.. = value ----- - -For example. -[source,properties] ----- -camel.dataformat.beanio.stream-name = test-stream -camel.dataformat.beanio.mapping = test-mapping.xml ----- -| `Map>` -| - -a| [[quarkus-camel-bootstrap-enabled]]`link:#quarkus-camel-bootstrap-enabled[quarkus.camel.bootstrap.enabled]` - -When set to true, the {@link CamelRuntime} will be started automatically. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-routes-discovery-enabled]]`link:#quarkus-camel-routes-discovery-enabled[quarkus.camel.routes-discovery.enabled]` - -Enable automatic discovery of routes during static initialization. -| `boolean` -| `true` - -a|icon:lock[title=Fixed at build time] [[quarkus-camel-csimple-on-build-time-analysis-failure]]`link:#quarkus-camel-csimple-on-build-time-analysis-failure[quarkus.camel.csimple.on-build-time-analysis-failure]` - -What to do if it is not possible to extract CSimple expressions from a route definition at build time. -| `fail`, `warn`, `ignore` -| `warn` -|=== - -[.configuration-legend] -{doc-link-icon-lock}[title=Fixed at build time] Configuration property fixed at build time. All other configuration properties are overridable at runtime. - -[NOTE] -[id=duration-note-anchor-core] -.About the Duration format -==== -To write duration values, use the standard `java.time.Duration` format. -See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information. - -You can also use a simplified format, starting with a number: - -* If the value is only a number, it represents time in seconds. -* If the value is a number followed by `ms`, it represents time in milliseconds. - -In other cases, the simplified format is translated to the `java.time.Duration` format for parsing: - -* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`. -* If the value is a number followed by `d`, it is prefixed with `P`. -==== - diff --git a/extensions-support/langchain4j/deployment/pom.xml b/extensions-support/langchain4j/deployment/pom.xml index 34dc90609648..d0e48ad94c6f 100644 --- a/extensions-support/langchain4j/deployment/pom.xml +++ b/extensions-support/langchain4j/deployment/pom.xml @@ -42,6 +42,11 @@ org.apache.camel.quarkus camel-quarkus-support-langchain4j + + org.apache.camel + camel-ai-tool + true + diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java new file mode 100644 index 000000000000..7168ec2d43da --- /dev/null +++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java @@ -0,0 +1,36 @@ +/* + * 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.support.langchain4j.deployment; + +import java.util.function.BooleanSupplier; + +public class AiToolPresent implements BooleanSupplier { + private static final String AI_TOOL_REGISTRY_CLASS = "org.apache.camel.component.ai.tool.AiToolRegistry"; + private static final String AI_TOOL_SPEC_CONVERTER_CLASS = "org.apache.camel.component.langchain4j.agent.AiToolSpecToLangChain4j"; + + @Override + public boolean getAsBoolean() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + try { + cl.loadClass(AI_TOOL_REGISTRY_CLASS); + cl.loadClass(AI_TOOL_SPEC_CONVERTER_CLASS); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java index 79283e86939e..8dcabb9aaa80 100644 --- a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java +++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java @@ -16,13 +16,19 @@ */ package org.apache.camel.quarkus.component.support.langchain4j.deployment; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import dev.langchain4j.guardrail.Guardrail; import dev.langchain4j.guardrail.InputGuardrail; import dev.langchain4j.guardrail.OutputGuardrail; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -34,7 +40,14 @@ import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; import jakarta.inject.Singleton; +import org.apache.camel.quarkus.component.support.langchain4j.AiToolSpecConverter; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolProvider; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolsInterceptor; import org.apache.camel.quarkus.component.support.langchain4j.QuarkusLangchain4jRecorder; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -53,6 +66,8 @@ class SupportQuarkusLangchain4jProcessor { public static final DotName REGISTER_AI_SERVICES_DOTNAME = DotName .createSimple("io.quarkiverse.langchain4j.RegisterAiService"); + private static final DotName CAMEL_AI_TOOLS_DOTNAME = DotName + .createSimple("org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools"); private static final Logger LOG = Logger.getLogger(SupportQuarkusLangchain4jProcessor.class); @@ -127,10 +142,104 @@ void registerQuarkusLangchain4jNativeSupport( .build()); } + @BuildStep(onlyIf = AiToolPresent.class) + AdditionalBeanBuildItem registerCamelAiToolProvider() { + LOG.info("Camel AI Tool detected - registering CamelAiToolProvider as CDI bean for ToolProvider auto-discovery"); + return AdditionalBeanBuildItem.unremovableOf(CamelAiToolProvider.class); + } + + @BuildStep(onlyIf = AiToolPresent.class) + void generateAiToolSpecConverter(BuildProducer generatedBeans, + BuildProducer unremovableBeans) { + + String generatedClassName = "org.apache.camel.quarkus.component.support.langchain4j.AiToolSpecConverterImpl"; + + try (ClassCreator cc = ClassCreator.builder() + .classOutput(new GeneratedBeanGizmoAdaptor(generatedBeans)) + .className(generatedClassName) + .interfaces(AiToolSpecConverter.class) + .build()) { + + cc.addAnnotation(Singleton.class); + + String toolSpecClass = "dev.langchain4j.agent.tool.ToolSpecification"; + String aiToolSpecClass = "org.apache.camel.component.ai.tool.AiToolSpec"; + + try (MethodCreator mc = cc.getMethodCreator("toToolSpecification", + toolSpecClass, aiToolSpecClass)) { + + ResultHandle spec = mc.getMethodParam(0); + ResultHandle result = mc.invokeStaticMethod( + MethodDescriptor.ofMethod( + "org.apache.camel.component.langchain4j.agent.AiToolSpecToLangChain4j", + "toToolSpecification", + toolSpecClass, + aiToolSpecClass), + spec); + mc.returnValue(result); + } + } + + unremovableBeans.produce(beanClassNames(generatedClassName)); + } + + @BuildStep(onlyIf = AiToolPresent.class) + @Record(ExecutionTime.STATIC_INIT) + void configureCamelAiToolTags( + CombinedIndexBuildItem combinedIndex, + BuildProducer additionalBeans, + QuarkusLangchain4jRecorder recorder) { + + IndexView index = combinedIndex.getIndex(); + Map tagMap = new HashMap<>(); + for (AnnotationInstance annotation : index.getAnnotations(CAMEL_AI_TOOLS_DOTNAME)) { + if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) { + String className = annotation.target().asClass().name().toString(); + if (annotation.value() == null) { + LOG.warnf("@CamelAiTools on %s has no value — skipping", className); + continue; + } + String tagValue = annotation.value().asString(); + if (tagValue.isBlank()) { + LOG.warnf("@CamelAiTools on %s has blank value — skipping", className); + continue; + } + tagMap.put(className, tagValue); + LOG.infof("Discovered @CamelAiTools(\"%s\") on %s", tagValue, className); + } + } + + if (tagMap.isEmpty()) { + return; + } + + recorder.setCamelAiToolTagMap(tagMap); + + additionalBeans.produce(AdditionalBeanBuildItem.builder() + .addBeanClasses(CamelAiToolsInterceptor.class) + .setUnremovable() + .build()); + } + @BuildStep - void markAiServicesAsUnremovable( + void validateAndRegisterAiServices( CombinedIndexBuildItem indexBuildItem, BuildProducer unremovableBeans) { + + if (!new AiToolPresent().getAsBoolean()) { + Collection aiToolsAnnotations = indexBuildItem.getIndex() + .getAnnotations(CAMEL_AI_TOOLS_DOTNAME); + if (!aiToolsAnnotations.isEmpty()) { + LOG.warnf("@CamelAiTools annotations found but camel-langchain4j-agent is not on the classpath. " + + "Add camel-langchain4j-agent dependency to enable the Camel AI tool bridge. " + + "Affected classes: %s", + aiToolsAnnotations.stream() + .filter(a -> a.target().kind() == AnnotationTarget.Kind.CLASS) + .map(a -> a.target().asClass().name().toString()) + .collect(Collectors.joining(", "))); + } + } + LOG.debug("Discovering classes annotated with @RegisterAiService to mark implementation beans as unremovable"); for (AnnotationInstance instance : indexBuildItem.getIndex().getAnnotations(REGISTER_AI_SERVICES_DOTNAME)) { diff --git a/extensions-support/langchain4j/runtime/pom.xml b/extensions-support/langchain4j/runtime/pom.xml index 85b61f8a6ede..9214158d857e 100644 --- a/extensions-support/langchain4j/runtime/pom.xml +++ b/extensions-support/langchain4j/runtime/pom.xml @@ -50,6 +50,16 @@ dev.langchain4j langchain4j-embeddings + + org.apache.camel + camel-ai-tool + true + + + org.apache.camel + camel-langchain4j-agent + true + diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java new file mode 100644 index 000000000000..adf443d9fc1b --- /dev/null +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java @@ -0,0 +1,29 @@ +/* + * 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.support.langchain4j; + +import dev.langchain4j.agent.tool.ToolSpecification; +import org.apache.camel.component.ai.tool.AiToolSpec; + +/** + * Converts a Camel {@link AiToolSpec} to a langchain4j {@link ToolSpecification}. + * The implementation is generated at build time via Gizmo to avoid a compile-time + * dependency on {@code camel-langchain4j-agent}. + */ +public interface AiToolSpecConverter { + ToolSpecification toToolSpecification(AiToolSpec spec); +} diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java new file mode 100644 index 000000000000..dbe66ddf67bd --- /dev/null +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java @@ -0,0 +1,129 @@ +/* + * 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.support.langchain4j; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.service.tool.ToolExecutor; +import dev.langchain4j.service.tool.ToolProvider; +import dev.langchain4j.service.tool.ToolProviderRequest; +import dev.langchain4j.service.tool.ToolProviderResult; +import jakarta.inject.Inject; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.component.ai.tool.AiToolExecutor; +import org.apache.camel.component.ai.tool.AiToolRegistry; +import org.apache.camel.component.ai.tool.AiToolResult; +import org.apache.camel.component.ai.tool.AiToolSpec; +import org.apache.camel.support.DefaultExchange; +import org.jboss.logging.Logger; + +/** + * Bridges Camel's {@link AiToolRegistry} to langchain4j's {@link ToolProvider} SPI. When registered as a CDI bean (done + * automatically by the deployment processor when both {@code camel-ai-tool} and quarkus-langchain4j are on the + * classpath), all Camel routes registered via {@code ai-tool:} endpoints become available to + * {@code @RegisterAiService} AI services without explicit configuration. + */ +public class CamelAiToolProvider implements ToolProvider { + + private static final Logger LOG = Logger.getLogger(CamelAiToolProvider.class); + static final Map TAG_MAP = new ConcurrentHashMap<>(); + // Set by CamelAiToolsInterceptor before each AI service call so provideTools() can filter by the calling service's tag + private static final ThreadLocal CURRENT_TAG = new ThreadLocal<>(); + + @Inject + CamelContext camelContext; + + @Inject + ObjectMapper objectMapper; + + @Inject + AiToolSpecConverter converter; + + static String getCurrentTag() { + return CURRENT_TAG.get(); + } + + static void setCurrentTag(String tag) { + CURRENT_TAG.set(tag); + } + + static void clearCurrentTag() { + CURRENT_TAG.remove(); + } + + @Override + public ToolProviderResult provideTools(ToolProviderRequest request) { + AiToolRegistry registry = AiToolRegistry.getOrCreate(camelContext); + String effectiveTag = CURRENT_TAG.get(); + Set tools = effectiveTag != null ? registry.getToolsByTag(effectiveTag) : registry.getAllTools(); + + ToolProviderResult.Builder resultBuilder = ToolProviderResult.builder(); + for (AiToolSpec spec : tools) { + ToolSpecification toolSpec = converter.toToolSpecification(spec); + ToolExecutor executor = createExecutor(spec); + resultBuilder.add(toolSpec, executor); + } + + return resultBuilder.build(); + } + + private ToolExecutor createExecutor(AiToolSpec spec) { + return (ToolExecutionRequest request, Object memoryId) -> { + Map arguments = parseArguments(request); + if (arguments == null) { + return "Invalid arguments: could not parse the provided JSON arguments: " + request.arguments(); + } + Exchange exchange = new DefaultExchange(camelContext); + AiToolResult result = AiToolExecutor.execute(spec, arguments, exchange); + return toToolResponse(spec.getName(), result); + }; + } + + private Map parseArguments(ToolExecutionRequest request) { + String jsonArguments = request.arguments(); + if (jsonArguments == null || jsonArguments.trim().isEmpty()) { + return Map.of(); + } + try { + return objectMapper.readValue(jsonArguments, new TypeReference<>() { + }); + } catch (Exception e) { + LOG.debugf(e, "Failed to parse tool arguments: %s", jsonArguments); + return null; + } + } + + private String toToolResponse(String toolName, AiToolResult result) { + if (result instanceof AiToolResult.Success success) { + return success.value(); + } else if (result instanceof AiToolResult.ArgumentError error) { + return "Invalid arguments: " + error.message(); + } else if (result instanceof AiToolResult.ExecutionError error) { + LOG.warnf("Tool '%s' execution failed: %s", toolName, error.message()); + return "Tool execution failed"; + } + return "Tool execution failed"; + } + +} diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java new file mode 100644 index 000000000000..8be0eeacbff8 --- /dev/null +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java @@ -0,0 +1,48 @@ +/* + * 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.support.langchain4j; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.Nonbinding; +import jakarta.interceptor.InterceptorBinding; + +/** + * Filters the Camel AI tools exposed to a {@code @RegisterAiService} by tag. When placed on a + * {@code @RegisterAiService} interface, only {@code ai-tool:} routes whose {@code tags} parameter includes the + * specified value (plus any routes in the default pool) are provided to the AI service. + * + *
+ * @RegisterAiService
+ * @CamelAiTools("support")
+ * public interface SupportAgent {
+ *     String chat(@UserMessage String message);
+ * }
+ * 
+ */ +@InterceptorBinding +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface CamelAiTools { + @Nonbinding + String value() default ""; +} diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java new file mode 100644 index 000000000000..239ab269762a --- /dev/null +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java @@ -0,0 +1,73 @@ +/* + * 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.support.langchain4j; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +/** + * CDI interceptor that sets the current Camel AI tool tag on a ThreadLocal before an AI service method executes. + * This allows {@link CamelAiToolProvider#provideTools} to filter tools by the tag associated with the calling AI + * service, + * enabling multiple {@code @RegisterAiService} interfaces with different {@code @CamelAiTools} tags in the same + * application. + */ +@Interceptor +@CamelAiTools +@Priority(Interceptor.Priority.LIBRARY_BEFORE + 100) +public class CamelAiToolsInterceptor { + + @AroundInvoke + Object aroundInvoke(InvocationContext ctx) throws Exception { + Class targetClass = ctx.getTarget().getClass(); + String tag = resolveTag(targetClass); + String previous = CamelAiToolProvider.getCurrentTag(); + if (tag != null) { + CamelAiToolProvider.setCurrentTag(tag); + } + try { + return ctx.proceed(); + } finally { + if (previous != null) { + CamelAiToolProvider.setCurrentTag(previous); + } else { + CamelAiToolProvider.clearCurrentTag(); + } + } + } + + private String resolveTag(Class targetClass) { + // Walk the full class hierarchy: ArC creates $$QuarkusImpl_Subclass extending $$QuarkusImpl, + // and the @CamelAiTools interface is declared on $$QuarkusImpl (not the subclass), so we + // must check superclasses and all their interfaces. + for (Class clazz = targetClass; clazz != null && clazz != Object.class; clazz = clazz.getSuperclass()) { + String tag = CamelAiToolProvider.TAG_MAP.get(clazz.getName()); + if (tag != null) { + return tag; + } + for (Class iface : clazz.getInterfaces()) { + tag = CamelAiToolProvider.TAG_MAP.get(iface.getName()); + if (tag != null) { + return tag; + } + } + } + return null; + } +} diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java index af6301a6c939..ac23912b1bdf 100644 --- a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java +++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java @@ -17,6 +17,7 @@ package org.apache.camel.quarkus.component.support.langchain4j; import java.lang.reflect.InvocationTargetException; +import java.util.Map; import dev.langchain4j.guardrail.Guardrail; import io.quarkus.runtime.RuntimeValue; @@ -26,6 +27,10 @@ @Recorder public class QuarkusLangchain4jRecorder { + public void setCamelAiToolTagMap(Map tagMap) { + CamelAiToolProvider.TAG_MAP.putAll(tagMap); + } + public RuntimeValue> instantiateGuardrails(Class> guardrailClass) { try { return new RuntimeValue<>(guardrailClass.getConstructor().newInstance()); diff --git a/extensions/ai-tool/runtime/src/main/doc/usage.adoc b/extensions/ai-tool/runtime/src/main/doc/usage.adoc index 677f114f7cb6..36359bf4106f 100644 --- a/extensions/ai-tool/runtime/src/main/doc/usage.adoc +++ b/extensions/ai-tool/runtime/src/main/doc/usage.adoc @@ -41,3 +41,57 @@ from("ai-tool:greet?description=Greet a user" + "¶meter.name=string¶meter.name.required=true") .setBody(simple("Hello, ${header.name}!")); ---- + +=== LangChain4j integration + +When both `camel-quarkus-ai-tool` and a Quarkus LangChain4j extension (`quarkus-langchain4j-ollama`, `quarkus-langchain4j-openai`, etc.) are on the classpath, a `ToolProvider` CDI bean is registered automatically at build time. +All routes registered via `ai-tool:` endpoints become available to `@RegisterAiService` AI services without explicit wiring. + +You must also add `camel-langchain4j-agent` to your project dependencies. This artifact provides the `AiToolSpec` to `ToolSpecification` conversion and is not brought in transitively. + +[source,xml] +---- + + org.apache.camel + camel-langchain4j-agent + +---- + +With both dependencies in place, a `@RegisterAiService` interface can call Camel tools directly: + +[source,java] +---- +@RegisterAiService +public interface WeatherAiService { + String chat(@UserMessage String question); +} +---- + +Any `ai-tool:` route is now callable by the LLM behind `WeatherAiService`. No `toolProvider` attribute or manual bean reference is needed. + +==== Tag filtering with `@CamelAiTools` + +By default, all registered `ai-tool:` routes are visible to every AI service. When you need different AI services to see different subsets of tools, annotate the service interface with `@CamelAiTools` and specify a tag value that matches the `tags` parameter on your `ai-tool:` routes. + +[source,java] +---- +@RegisterAiService +@CamelAiTools("weather") +public interface WeatherAgent { + String chat(@UserMessage String question); +} + +@RegisterAiService +@CamelAiTools("support") +public interface SupportAgent { + String chat(@UserMessage String question); +} +---- + +With this setup: + +* `WeatherAgent` sees only tools tagged with `weather` (plus any untagged tools from the default pool). +* `SupportAgent` sees only tools tagged with `support` (plus any untagged tools from the default pool). +* A `@RegisterAiService` without `@CamelAiTools` sees all registered tools regardless of tags. + +NOTE: The `@CamelAiTools` value must match a tag declared in the `ai-tool:` endpoint URI (e.g. `from("ai-tool:getWeather?tags=weather&...")`). If the annotation value is blank or does not match any route tag, the AI service will only see untagged tools. diff --git a/integration-tests/ai-tool-langchain4j/README.adoc b/integration-tests/ai-tool-langchain4j/README.adoc new file mode 100644 index 000000000000..40a56d5e2416 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/README.adoc @@ -0,0 +1,15 @@ +== Camel Quarkus AI Tool LangChain4j Integration Tests + +By default, the tests use a mock `ChatModel` that simulates tool calling without a real LLM. + +=== Running with a real Ollama LLM + +To run additional tests against a real Ollama model (qwen3:1.7b) via dev services, activate the `ollama` Maven profile. +This requires Docker to be running. + +[source,shell] +---- +./mvnw verify -f integration-tests/ai-tool-langchain4j -Pollama +---- + +The first run may take longer as the Ollama container image and model are downloaded. diff --git a/integration-tests/ai-tool-langchain4j/pom.xml b/integration-tests/ai-tool-langchain4j/pom.xml new file mode 100644 index 000000000000..433e1df1898c --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/pom.xml @@ -0,0 +1,222 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + camel-quarkus-build-parent-it + 3.39.0-SNAPSHOT + ../../poms/build-parent-it/pom.xml + + + camel-quarkus-integration-test-ai-tool-langchain4j + Camel Quarkus :: Integration Tests :: AI Tool LangChain4j + Integration tests for Camel AI Tool bridge with Quarkus LangChain4j + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-bom + ${quarkiverse-langchain4j.version} + pom + import + + + + + + + org.apache.camel.quarkus + camel-quarkus-ai-tool + + + org.apache.camel.quarkus + camel-quarkus-bean + + + org.apache.camel.quarkus + camel-quarkus-direct + + + io.quarkus + quarkus-rest + + + + + org.apache.camel.quarkus + camel-quarkus-support-langchain4j + + + + org.apache.camel + camel-langchain4j-agent + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-ollama + + + + + io.quarkus + quarkus-junit + test + + + io.rest-assured + rest-assured + test + + + + + + native + + + native + + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + + + ollama + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + true + + + + + + + + virtualDependencies + + + !noVirtualDependencies + + + + + + org.apache.camel.quarkus + camel-quarkus-ai-tool-deployment + ${project.version} + pom + test + + + * + * + + + + + org.apache.camel.quarkus + camel-quarkus-bean-deployment + ${project.version} + pom + test + + + * + * + + + + + org.apache.camel.quarkus + camel-quarkus-direct-deployment + ${project.version} + pom + test + + + * + * + + + + + org.apache.camel.quarkus + camel-quarkus-support-langchain4j-deployment + ${project.version} + pom + test + + + * + * + + + + + + + skip-testcontainers-tests + + + skip-testcontainers-tests + + + + true + + + + diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java new file mode 100644 index 000000000000..60fdd44967e6 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java @@ -0,0 +1,54 @@ +/* + * 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.ai.tool.langchain4j.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiServiceOllama; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiServiceOllamaWrongTag; + +@Path("/ai-tool-langchain4j-ollama") +@ApplicationScoped +public class AiToolLangchain4jOllamaResource { + + @Inject + WeatherAiServiceOllama weatherAiService; + + @Inject + WeatherAiServiceOllamaWrongTag weatherAiServiceWrongTag; + + @Path("/chat") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String chat(String message) { + return weatherAiService.chat(message); + } + + @Path("/chat-wrong-tag") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String chatWrongTag(String message) { + return weatherAiServiceWrongTag.chat(message); + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java new file mode 100644 index 000000000000..82770665ac2a --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java @@ -0,0 +1,90 @@ +/* + * 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.ai.tool.langchain4j.it; + +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.apache.camel.CamelContext; +import org.apache.camel.component.ai.tool.AiToolRegistry; +import org.apache.camel.component.ai.tool.AiToolSpec; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.AdminAiService; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiService; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiServiceWrongTag; + +@Path("/ai-tool-langchain4j") +@ApplicationScoped +public class AiToolLangchain4jResource { + + @Inject + CamelContext camelContext; + + @Inject + WeatherAiService weatherAiService; + + @Inject + AdminAiService adminAiService; + + @Inject + WeatherAiServiceWrongTag weatherAiServiceWrongTag; + + @Path("/weather/chat") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String weatherChat(String message) { + return weatherAiService.chat(message); + } + + @Path("/admin/chat") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String adminChat(String message) { + return adminAiService.chat(message); + } + + @Path("/weather-wrong-tag/chat") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String weatherChatWrongTag(String message) { + return weatherAiServiceWrongTag.chat(message); + } + + @Path("/tools/{tag}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String listToolsByTag(@PathParam("tag") String tag) { + AiToolRegistry registry = AiToolRegistry.getOrCreate(camelContext); + Set tools = registry.getToolsByTag(tag); + return tools.stream() + .map(AiToolSpec::getName) + .sorted() + .collect(Collectors.joining(",")); + } + +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java new file mode 100644 index 000000000000..8f8b45acf66b --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java @@ -0,0 +1,40 @@ +/* + * 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.ai.tool.langchain4j.it; + +import org.apache.camel.builder.RouteBuilder; + +public class AiToolLangchain4jRoutes extends RouteBuilder { + @Override + public void configure() { + from("ai-tool:getWeather?" + + "tags=weatherTag" + + "&description=Get the current weather for a city" + + "¶meter.city=string" + + "¶meter.city.required=true" + + "¶meter.city.description=The city name") + .setBody(simple("Sunny in ${header.city}, 1111 celsius")); + + from("ai-tool:getNews?" + + "tags=adminTag" + + "&description=Get the latest news about a topic" + + "¶meter.topic=string" + + "¶meter.topic.required=true" + + "¶meter.topic.description=The news topic") + .setBody(simple("Latest news about ${header.topic}")); + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java new file mode 100644 index 000000000000..a4be0986b90c --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java @@ -0,0 +1,93 @@ +/* + * 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.ai.tool.langchain4j.it.model; + +import java.util.List; +import java.util.function.Supplier; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.ToolExecutionResultMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; + +/** + * Simulates the two-turn tool-calling protocol used by langchain4j: + * + *
    + *
  1. Turn 1 — langchain4j sends the user message and available tool specifications to the model. + * The model responds with a {@link ToolExecutionRequest} ("I want to call tool X with args Y"). + * No tool is executed yet.
  2. + *
  3. Tool execution — langchain4j sees the request, invokes the matching {@code ToolExecutor} + * (in our case the Camel {@code ai-tool:} route), and captures the result.
  4. + *
  5. Turn 2 — langchain4j calls the model again, appending a {@link ToolExecutionResultMessage} + * with the tool's output. The model returns a final text answer.
  6. + *
+ * + * Subclasses only define which tool to call and how to format the final response. + */ +public abstract class AbstractToolCallingChatModel implements Supplier { + + // Correlation ID for matching ToolExecutionResultMessage back to the request — required by the + // builder but the actual value is irrelevant in tests with a single tool call per turn. + protected abstract String toolCallId(); + + protected abstract String toolName(); + + protected abstract String toolArguments(); + + protected abstract String formatResponse(String toolResult); + + protected abstract String getNameOfService(); + + @Override + public ChatModel get() { + return new ChatModel() { + @Override + public ChatResponse doChat(ChatRequest chatRequest) { + List messages = chatRequest.messages(); + + boolean hasToolResult = messages.stream() + .anyMatch(m -> m instanceof ToolExecutionResultMessage); + + if (!hasToolResult) { + ToolExecutionRequest toolRequest = ToolExecutionRequest.builder() + .id(toolCallId()) + .name(toolName()) + .arguments(toolArguments()) + .build(); + return ChatResponse.builder() + .aiMessage(AiMessage.from(toolRequest)) + .build(); + } + + // #2 tool's response already present + String toolResult = messages.stream() + .filter(m -> m instanceof ToolExecutionResultMessage) + .map(m -> ((ToolExecutionResultMessage) m).text()) + .findFirst() + .orElse("unknown"); + + return ChatResponse.builder() + .aiMessage(new AiMessage(formatResponse(toolResult))) + .build(); + } + }; + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java new file mode 100644 index 000000000000..2bff6c1e1260 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java @@ -0,0 +1,45 @@ +/* + * 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.ai.tool.langchain4j.it.model; + +public class AdminToolCallingChatModel extends AbstractToolCallingChatModel { + + @Override + protected String getNameOfService() { + return "adminTag"; + } + + @Override + protected String toolCallId() { + return "call_admin_1"; + } + + @Override + protected String toolName() { + return "getNews"; + } + + @Override + protected String toolArguments() { + return "{\"topic\":\"camel\"}"; + } + + @Override + protected String formatResponse(String toolResult) { + return "News result: " + toolResult; + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java new file mode 100644 index 000000000000..4a52902d80cd --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java @@ -0,0 +1,45 @@ +/* + * 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.ai.tool.langchain4j.it.model; + +public class WeatherToolCallingChatModel extends AbstractToolCallingChatModel { + + @Override + protected String getNameOfService() { + return "weatherTag"; + } + + @Override + protected String toolCallId() { + return "call_1"; + } + + @Override + protected String toolName() { + return "getWeather"; + } + + @Override + protected String toolArguments() { + return "{\"city\":\"Prague\"}"; + } + + @Override + protected String formatResponse(String toolResult) { + return toolResult; + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java new file mode 100644 index 000000000000..a4c1803a46e9 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java @@ -0,0 +1,78 @@ +/* + * 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.ai.tool.langchain4j.it.model; + +import java.util.List; +import java.util.function.Supplier; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.ToolExecutionResultMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; + +/** + * Mock model for testing tag isolation. Calls {@code getWeather} only if it appears in the + * available tool specifications — when the interceptor works correctly and this service is + * tagged with {@code adminTag}, the weather tool should NOT be visible. + */ +public class WrongTagToolCallingChatModel implements Supplier { + + @Override + public ChatModel get() { + return new ChatModel() { + @Override + public ChatResponse doChat(ChatRequest chatRequest) { + List messages = chatRequest.messages(); + boolean hasToolResult = messages.stream() + .anyMatch(m -> m instanceof ToolExecutionResultMessage); + + if (!hasToolResult) { + boolean weatherToolAvailable = chatRequest.toolSpecifications() != null + && chatRequest.toolSpecifications().stream() + .anyMatch(ts -> "getWeather".equals(ts.name())); + + if (weatherToolAvailable) { + ToolExecutionRequest request = ToolExecutionRequest.builder() + .id("call_wrong_tag") + .name("getWeather") + .arguments("{\"city\":\"Prague\"}") + .build(); + return ChatResponse.builder() + .aiMessage(AiMessage.from(request)) + .build(); + } + + return ChatResponse.builder() + .aiMessage(new AiMessage("No weather tool available")) + .build(); + } + + String toolResult = messages.stream() + .filter(m -> m instanceof ToolExecutionResultMessage) + .map(m -> ((ToolExecutionResultMessage) m).text()) + .findFirst() + .orElse("unknown"); + return ChatResponse.builder() + .aiMessage(new AiMessage(toolResult)) + .build(); + } + }; + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java new file mode 100644 index 000000000000..b54e0d4d1286 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java @@ -0,0 +1,31 @@ +/* + * 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.ai.tool.langchain4j.it.service; + +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model.AdminToolCallingChatModel; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools; + +@ApplicationScoped +@RegisterAiService(chatLanguageModelSupplier = AdminToolCallingChatModel.class) +@CamelAiTools("adminTag") +public interface AdminAiService { + + String chat(@UserMessage String message); +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java new file mode 100644 index 000000000000..9a1e8501a1ee --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java @@ -0,0 +1,31 @@ +/* + * 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.ai.tool.langchain4j.it.service; + +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model.WeatherToolCallingChatModel; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools; + +@ApplicationScoped +@RegisterAiService(chatLanguageModelSupplier = WeatherToolCallingChatModel.class) +@CamelAiTools("weatherTag") +public interface WeatherAiService { + + String chat(@UserMessage String message); +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java new file mode 100644 index 000000000000..76829813cb69 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java @@ -0,0 +1,30 @@ +/* + * 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.ai.tool.langchain4j.it.service; + +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools; + +@ApplicationScoped +@RegisterAiService +@CamelAiTools("weatherTag") +public interface WeatherAiServiceOllama { + + String chat(@UserMessage String message); +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java new file mode 100644 index 000000000000..1545589a3375 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java @@ -0,0 +1,30 @@ +/* + * 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.ai.tool.langchain4j.it.service; + +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools; + +@ApplicationScoped +@RegisterAiService +@CamelAiTools("adminTag") +public interface WeatherAiServiceOllamaWrongTag { + + String chat(@UserMessage String message); +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java new file mode 100644 index 000000000000..7e4a78d7b6a8 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java @@ -0,0 +1,31 @@ +/* + * 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.ai.tool.langchain4j.it.service; + +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model.WrongTagToolCallingChatModel; +import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools; + +@ApplicationScoped +@RegisterAiService(chatLanguageModelSupplier = WrongTagToolCallingChatModel.class) +@CamelAiTools("adminTag") +public interface WeatherAiServiceWrongTag { + + String chat(@UserMessage String message); +} diff --git a/integration-tests/ai-tool-langchain4j/src/main/resources/application.properties b/integration-tests/ai-tool-langchain4j/src/main/resources/application.properties new file mode 100644 index 000000000000..a58bbb874a93 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/main/resources/application.properties @@ -0,0 +1,18 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- +quarkus.devservices.enabled=false +quarkus.langchain4j.ollama.chat-model.model-id=qwen3:1.7b diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java new file mode 100644 index 000000000000..40dd473753c9 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java @@ -0,0 +1,23 @@ +/* + * 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.ai.tool.langchain4j.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class AiToolLangchain4jIT extends AiToolLangchain4jTest { +} diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java new file mode 100644 index 000000000000..b263b5abc2d3 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java @@ -0,0 +1,25 @@ +/* + * 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.ai.tool.langchain4j.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +@QuarkusIntegrationTest +@EnabledIfSystemProperty(named = "ollama.test", matches = "true") +class AiToolLangchain4jOllamaIT extends AiToolLangchain4jOllamaTest { +} diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java new file mode 100644 index 000000000000..d4b5fef08d2a --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java @@ -0,0 +1,68 @@ +/* + * 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.ai.tool.langchain4j.it; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +@QuarkusTest +@TestProfile(AiToolLangchain4jOllamaTest.OllamaProfile.class) +@EnabledIfSystemProperty(named = "ollama.test", matches = "true") +class AiToolLangchain4jOllamaTest { + + @Test + void weatherFromToolTest() { + RestAssured.given() + .contentType("text/plain") + .body("What is the weather in Prague?") + .post("/ai-tool-langchain4j-ollama/chat") + .then() + .statusCode(200) + .body(containsString("1111")); + } + + // Verifies that the @CamelAiTools interceptor filters tools by tag: a service tagged "adminTag" + // should not see the getWeather tool (tagged "weatherTag"), so the real LLM won't call it + // and "1111" won't appear in the response. Fails if the interceptor is broken and all tools are visible. + @Test + void weatherWithWrongTag() { + RestAssured.given() + .contentType("text/plain") + .body("What is the weather in Prague?") + .post("/ai-tool-langchain4j-ollama/chat-wrong-tag") + .then() + .statusCode(200) + .body(not(containsString("1111"))); + } + + public static class OllamaProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.devservices.enabled", "true"); + } + } +} diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java new file mode 100644 index 000000000000..2b7cf401a679 --- /dev/null +++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java @@ -0,0 +1,97 @@ +/* + * 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.ai.tool.langchain4j.it; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +@QuarkusTest +class AiToolLangchain4jTest { + + @Test + void weatherServiceTest() { + RestAssured.given() + .contentType("text/plain") + .body("What is the weather in Prague?") + .post("/ai-tool-langchain4j/weather/chat") + .then() + .statusCode(200) + .body(containsString("1111")); + } + + @Test + void weatherServiceTestWithWrongTag() { + RestAssured.given() + .contentType("text/plain") + .body("What is the weather in Prague?") + .post("/ai-tool-langchain4j/admin/chat") + .then() + .statusCode(200) + .body(containsString("Latest news about camel")); + } + + @Test + void aiToolRegistryToolsByWeatherTagTest() { + RestAssured.given() + .get("/ai-tool-langchain4j/tools/weatherTag") + .then() + .statusCode(200) + .body(containsString("getWeather")) + .body(not(containsString("getNews"))); + } + + @Test + void aiToolRegistryToolsByAdminTagTest() { + RestAssured.given() + .get("/ai-tool-langchain4j/tools/adminTag") + .then() + .statusCode(200) + .body(containsString("getNews")) + .body(not(containsString("getWeather"))); + } + + @Test + void adminServiceTest() { + RestAssured.given() + .contentType("text/plain") + .body("What are the latest news about camel?") + .post("/ai-tool-langchain4j/admin/chat") + .then() + .statusCode(200) + .body(containsString("camel")) + .body(containsString("news")); + } + + // Verifies that the @CamelAiTools interceptor filters tools by tag: a service tagged "adminTag" + // should not see the getWeather tool (tagged "weatherTag"), so the mock model won't call it + // and "1111" won't appear in the response. Fails if the interceptor is broken and all tools are visible. + @Test + void weatherWithWrongTagTest() { + RestAssured.given() + .contentType("text/plain") + .body("What is the weather in Prague?") + .post("/ai-tool-langchain4j/weather-wrong-tag/chat") + .then() + .statusCode(200) + .body(not(containsString("1111"))); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 8ffee5a4dd62..ce58500c8370 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -52,6 +52,7 @@ activemq activemq6 ai-tool + ai-tool-langchain4j amqp arangodb as2 diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index a4df39026536..97b679b994ca 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -232,6 +232,7 @@ group-11: - xslt-saxon group-12: - ai-tool + - ai-tool-langchain4j - aws2-grouped - csimple - github2 From 59a416ab075a620bdcf45bc1cf66a696d6e8182e Mon Sep 17 00:00:00 2001 From: Jiri Ondrusek Date: Thu, 30 Jul 2026 15:35:00 +0200 Subject: [PATCH 2/2] Generated core.adoc --- .../ROOT/pages/reference/extensions/core.adoc | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) diff --git a/docs/modules/ROOT/pages/reference/extensions/core.adoc b/docs/modules/ROOT/pages/reference/extensions/core.adoc index db73e5ab498f..9a9aa274f97f 100644 --- a/docs/modules/ROOT/pages/reference/extensions/core.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/core.adoc @@ -124,3 +124,409 @@ camel.beans.customBeanWithSetterInjection.counter = 123 As such, the class `PropertiesCustomBeanWithSetterInjection` needs to be link:https://quarkus.io/guides/writing-native-applications-tips#registering-for-reflection[registered for reflection], note that field access could be omitted in this case. + +[width="100%",cols="80,5,15",options="header"] +|=== +| Configuration property | Type | Default + + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-discovery-exclude-patterns]]`link:#quarkus-camel-service-discovery-exclude-patterns[quarkus.camel.service.discovery.exclude-patterns]` + +A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The +services defined in the matching files will *not* be discoverable via the **`org.apache.camel.spi.FactoryFinder` +mechanism. + +The excludes have higher precedence than includes. The excludes defined here can also be used to veto the +discoverability of services included by Camel Quarkus extensions. + +Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar` +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-discovery-include-patterns]]`link:#quarkus-camel-service-discovery-include-patterns[quarkus.camel.service.discovery.include-patterns]` + +A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The +services defined in the matching files will be discoverable via the `org.apache.camel.spi.FactoryFinder` mechanism +unless the given file is excluded via `exclude-patterns`. + +Note that Camel Quarkus extensions may include some services by default. The services selected here added to those +services and the exclusions defined in `exclude-patterns` are applied to the union set. + +Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar` +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-registry-exclude-patterns]]`link:#quarkus-camel-service-registry-exclude-patterns[quarkus.camel.service.registry.exclude-patterns]` + +A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The +services defined in the matching files will *not* be added to Camel registry during application's static +initialization. + +The excludes have higher precedence than includes. The excludes defined here can also be used to veto the +registration of services included by Camel Quarkus extensions. + +Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar`** +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-service-registry-include-patterns]]`link:#quarkus-camel-service-registry-include-patterns[quarkus.camel.service.registry.include-patterns]` + +A comma-separated list of Ant-path style patterns to match Camel service definition files in the classpath. The +services defined in the matching files will be added to Camel registry during application's static initialization +unless the given file is excluded via `exclude-patterns`. + +Note that Camel Quarkus extensions may include some services by default. The services selected here added to those +services and the exclusions defined in `exclude-patterns` are applied to the union set. + +Example values: `META-INF/services/org/apache/camel/foo/++*++,META-INF/services/org/apache/camel/foo/++**++/bar` +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-components]]`link:#quarkus-camel-runtime-catalog-components[quarkus.camel.runtime-catalog.components]` + +If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel components +available in the application; otherwise component JSON schemas will not be available in the Runtime Camel Catalog and +any attempt to access those will result in a RuntimeException. + +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `false` except for making the behavior consistent with native mode. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-languages]]`link:#quarkus-camel-runtime-catalog-languages[quarkus.camel.runtime-catalog.languages]` + +If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel languages +available in the application; otherwise language JSON schemas will not be available in the Runtime Camel Catalog and +any attempt to access those will result in a RuntimeException. + +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `false` except for making the behavior consistent with native mode. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-dataformats]]`link:#quarkus-camel-runtime-catalog-dataformats[quarkus.camel.runtime-catalog.dataformats]` + +If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel data formats +available in the application; otherwise data format JSON schemas will not be available in the Runtime Camel Catalog +and any attempt to access those will result in a RuntimeException. + +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `false` except for making the behavior consistent with native mode. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-devconsoles]]`link:#quarkus-camel-runtime-catalog-devconsoles[quarkus.camel.runtime-catalog.devconsoles]` + +If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel dev consoles +available in the application; otherwise dev console JSON schemas will not be available in the Runtime Camel Catalog +and any attempt to access those will result in a RuntimeException. + +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `false` except for making the behavior consistent with native mode. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-models]]`link:#quarkus-camel-runtime-catalog-models[quarkus.camel.runtime-catalog.models]` + +If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel EIP models +available in the application; otherwise EIP model JSON schemas will not be available in the Runtime Camel Catalog and +any attempt to access those will result in a RuntimeException. + +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `false` except for making the behavior consistent with native mode. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-runtime-catalog-transformers]]`link:#quarkus-camel-runtime-catalog-transformers[quarkus.camel.runtime-catalog.transformers]` + +If `true` the Runtime Camel Catalog embedded in the application will contain JSON schemas of Camel transformers +available in the application; otherwise transformer JSON schemas will not be available in the Runtime Camel Catalog +and any attempt to access those will result in a RuntimeException. + +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `false` except for making the behavior consistent with native mode. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-routes-discovery-exclude-patterns]]`link:#quarkus-camel-routes-discovery-exclude-patterns[quarkus.camel.routes-discovery.exclude-patterns]` + +Used for exclusive filtering scanning of RouteBuilder classes. The exclusive filtering takes precedence over +inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by +comma. For example to exclude all classes starting with Bar use: ++**++/Bar++*++ To exclude all routes from a +specific package use: com/mycompany/bar/++*++ To exclude all routes from a specific package and its sub-packages use +double wildcards: com/mycompany/bar/++**++ And to exclude all routes from two specific packages use: +com/mycompany/bar/++*++,com/mycompany/stuff/++*++ +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-routes-discovery-include-patterns]]`link:#quarkus-camel-routes-discovery-include-patterns[quarkus.camel.routes-discovery.include-patterns]` + +Used for inclusive filtering scanning of RouteBuilder classes. The exclusive filtering takes precedence over +inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by +comma. For example to include all classes starting with Foo use: ++**++/Foo++*++ To include all routes from a +specific package use: com/mycompany/foo/++*++ To include all routes from a specific package and its sub-packages use +double wildcards: com/mycompany/foo/++**++ And to include all routes from two specific packages use: +com/mycompany/foo/++*++,com/mycompany/stuff/++*++ +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-native-reflection-exclude-patterns]]`link:#quarkus-camel-native-reflection-exclude-patterns[quarkus.camel.native.reflection.exclude-patterns]` + +A comma separated list of Ant-path style patterns to match class names that should be *excluded* from registering for +reflection. Use the class name format as returned by the `java.lang.Class.getName()` method: package segments +delimited by period `.` and inner classes by dollar sign `$`. + +This option narrows down the set selected by `include-patterns`. By default, no classes are excluded. + +This option cannot be used to unregister classes which have been registered internally by Quarkus extensions. +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-native-reflection-include-patterns]]`link:#quarkus-camel-native-reflection-include-patterns[quarkus.camel.native.reflection.include-patterns]` + +A comma separated list of Ant-path style patterns to match class names that should be registered for reflection. Use +the class name format as returned by the `java.lang.Class.getName()` method: package segments delimited by period `.` +and inner classes by dollar sign `$`. + +By default, no classes are included. The set selected by this option can be narrowed down by `exclude-patterns`. + +Note that Quarkus extensions typically register the required classes for reflection by themselves. This option is +useful in situations when the built in functionality is not sufficient. + +Note that this option enables the full reflective access for constructors, fields and methods. If you need a finer +grained control, consider using `io.quarkus.runtime.annotations.RegisterForReflection` annotation in your Java code. + +For this option to work properly, at least one of the following conditions must be satisfied: + +- There are no wildcards (`++*++` or `/`) in the patterns +- The artifacts containing the selected classes contain a Jandex index (`META-INF/jandex.idx`) +- The artifacts containing the selected classes are registered for indexing using the +`quarkus.index-dependency.++*++` family of options in `application.properties` - e.g. + +[source,properties] +---- +quarkus.index-dependency.my-dep.group-id = org.my-group +quarkus.index-dependency.my-dep.artifact-id = my-artifact +---- + +where `my-dep` is a label of your choice to tell Quarkus that `org.my-group` and with `my-artifact` belong together. +| List of `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-native-reflection-serialization-enabled]]`link:#quarkus-camel-native-reflection-serialization-enabled[quarkus.camel.native.reflection.serialization-enabled]` + +If `true`, basic classes are registered for serialization; otherwise basic classes won't be registered automatically +for serialization in native mode. The list of classes automatically registered for serialization can be found in +link:https://github.com/apache/camel-quarkus/blob/main/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelSerializationProcessor.java[CamelSerializationProcessor.BASE_SERIALIZATION_CLASSES]. +Setting this to `false` helps to reduce the size of the native image. In JVM mode, there is no real benefit of +setting this flag to `true` except for making the behavior consistent with native mode. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-expression-on-build-time-analysis-failure]]`link:#quarkus-camel-expression-on-build-time-analysis-failure[quarkus.camel.expression.on-build-time-analysis-failure]` + +What to do if it is not possible to extract expressions from a route definition at build time. +| `fail`, `warn`, `ignore` +| `warn` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-expression-extraction-enabled]]`link:#quarkus-camel-expression-extraction-enabled[quarkus.camel.expression.extraction-enabled]` + +Indicates whether the expression extraction from the route definitions at build time must be done. If disabled, the +expressions are compiled at runtime. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-event-bridge-enabled]]`link:#quarkus-camel-event-bridge-enabled[quarkus.camel.event-bridge.enabled]` + +Whether to enable the bridging of Camel events to CDI events. + +This allows CDI observers to be configured for Camel events. E.g. those belonging to the +`org.apache.camel.quarkus.core.events`, `org.apache.camel.quarkus.main.events` & `org.apache.camel.impl.event` +packages. + +Note that this configuration item only has any effect when observers configured for Camel events are present in the +application. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-source-location-enabled]]`link:#quarkus-camel-source-location-enabled[quarkus.camel.source-location-enabled]` + +Build time configuration options for enable/disable camel source location. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-enabled]]`link:#quarkus-camel-trace-enabled[quarkus.camel.trace.enabled]` + +Enables tracer in your Camel application. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-standby]]`link:#quarkus-camel-trace-standby[quarkus.camel.trace.standby]` + +To set the tracer in standby mode, where the tracer will be installed, but not automatically enabled. The tracer can +then be enabled explicitly later from Java, JMX or tooling. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-backlog-size]]`link:#quarkus-camel-trace-backlog-size[quarkus.camel.trace.backlog-size]` + +Defines how many of the last messages to keep in the tracer. +| `int` +| `1000` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-remove-on-dump]]`link:#quarkus-camel-trace-remove-on-dump[quarkus.camel.trace.remove-on-dump]` + +Whether all traced messages should be removed when the tracer is dumping. By default, the messages are removed, which +means that dumping will not contain previous dumped messages. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-body-max-chars]]`link:#quarkus-camel-trace-body-max-chars[quarkus.camel.trace.body-max-chars]` + +To limit the message body to a maximum size in the traced message. Use 0 or negative value to use unlimited size. +| `int` +| `131072` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-body-include-streams]]`link:#quarkus-camel-trace-body-include-streams[quarkus.camel.trace.body-include-streams]` + +Whether to include the message body of stream based messages. If enabled then beware the stream may not be +re-readable later. See more about Stream Caching. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-body-include-files]]`link:#quarkus-camel-trace-body-include-files[quarkus.camel.trace.body-include-files]` + +Whether to include the message body of file based messages. The overhead is that the file content has to be read from +the file. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-include-exchange-properties]]`link:#quarkus-camel-trace-include-exchange-properties[quarkus.camel.trace.include-exchange-properties]` + +Whether to include the exchange properties in the traced message. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-include-exchange-variables]]`link:#quarkus-camel-trace-include-exchange-variables[quarkus.camel.trace.include-exchange-variables]` + +Whether to include the exchange variables in the traced message. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-include-exception]]`link:#quarkus-camel-trace-include-exception[quarkus.camel.trace.include-exception]` + +Whether to include the exception in the traced message in case of failed exchange. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-rests]]`link:#quarkus-camel-trace-trace-rests[quarkus.camel.trace.trace-rests]` + +Whether to trace routes that is created from Rest DSL. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-templates]]`link:#quarkus-camel-trace-trace-templates[quarkus.camel.trace.trace-templates]` + +Whether to trace routes that is created from route templates or kamelets. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-pattern]]`link:#quarkus-camel-trace-trace-pattern[quarkus.camel.trace.trace-pattern]` + +Filter for tracing by route or node id. +| `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-trace-trace-filter]]`link:#quarkus-camel-trace-trace-filter[quarkus.camel.trace.trace-filter]` + +Filter for tracing messages. +| `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-type-converter-statistics-enabled]]`link:#quarkus-camel-type-converter-statistics-enabled[quarkus.camel.type-converter.statistics-enabled]` + +Whether type converter statistics are enabled. By default, type converter utilization statistics are disabled. Note +that enabling statistics incurs a minor performance impact under very heavy load. +| `boolean` +| `false` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-dev-ui-update-interval]]`link:#quarkus-camel-dev-ui-update-interval[quarkus.camel.dev-ui.update-interval]` + +The interval at which data is updated in Camel Quarkus Dev UI pages. +| link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[`Duration`] link:#duration-note-anchor-core[icon:question-circle[title=More information about the Duration format]] +| `5S` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-main-shutdown-timeout]]`link:#quarkus-camel-main-shutdown-timeout[quarkus.camel.main.shutdown.timeout]` + +A timeout (with millisecond precision) to wait for `CamelMain++#++stop()` to finish +| link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[`Duration`] link:#duration-note-anchor-core[icon:question-circle[title=More information about the Duration format]] +| `PT3S` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-main-arguments-on-unknown]]`link:#quarkus-camel-main-arguments-on-unknown[quarkus.camel.main.arguments.on-unknown]` + +The action to take when `CamelMain` encounters an unknown argument. fail - Prints the `CamelMain` usage statement and +throws a `RuntimeException` ignore - Suppresses any warnings and the application startup proceeds as normal warn - +Prints the `CamelMain` usage statement but allows the application startup to proceed as normal +| `fail`, `warn`, `ignore` +| `warn` + +a| [[camel-dataformat-data-format-configs]]`link:#camel-dataformat-data-format-configs[camel.dataformat."data-format-configs"]` + +Camel data format configuration. + +The format of the configuration is as follows. + +[source,properties] +---- +camel.dataformat.. = value +---- + +For example. +[source,properties] +---- +camel.dataformat.beanio.stream-name = test-stream +camel.dataformat.beanio.mapping = test-mapping.xml +---- +| `Map>` +| + +a| [[quarkus-camel-bootstrap-enabled]]`link:#quarkus-camel-bootstrap-enabled[quarkus.camel.bootstrap.enabled]` + +When set to true, the {@link CamelRuntime} will be started automatically. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-routes-discovery-enabled]]`link:#quarkus-camel-routes-discovery-enabled[quarkus.camel.routes-discovery.enabled]` + +Enable automatic discovery of routes during static initialization. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-csimple-on-build-time-analysis-failure]]`link:#quarkus-camel-csimple-on-build-time-analysis-failure[quarkus.camel.csimple.on-build-time-analysis-failure]` + +What to do if it is not possible to extract CSimple expressions from a route definition at build time. +| `fail`, `warn`, `ignore` +| `warn` +|=== + +[.configuration-legend] +{doc-link-icon-lock}[title=Fixed at build time] Configuration property fixed at build time. All other configuration properties are overridable at runtime. + +[NOTE] +[id=duration-note-anchor-core] +.About the Duration format +==== +To write duration values, use the standard `java.time.Duration` format. +See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information. + +You can also use a simplified format, starting with a number: + +* If the value is only a number, it represents time in seconds. +* If the value is a number followed by `ms`, it represents time in milliseconds. + +In other cases, the simplified format is translated to the `java.time.Duration` format for parsing: + +* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`. +* If the value is a number followed by `d`, it is prefixed with `P`. +==== +