This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new f09459b3c2ea CAMEL-24334: Add argSchema raw JSON Schema support to
camel-ai-tool
f09459b3c2ea is described below
commit f09459b3c2ea605f5d7c3b5ffda0fdaec95a4780
Author: Omar Atie <[email protected]>
AuthorDate: Thu Aug 6 03:26:10 2026 -0700
CAMEL-24334: Add argSchema raw JSON Schema support to camel-ai-tool
Adds optional argSchema endpoint option for declaring complex nested tool
parameters as raw JSON Schema (inline or via classpath/file/resource
references) instead of flat parameter.* options. Includes mutual exclusion
validation, startup schema validation, LangChain4j adapter conversion,
documentation, and tests.
Closes #25368
Co-authored-by: Cursor Agent <[email protected]>
---
.../apache/camel/catalog/components/ai-tool.json | 26 ++--
.../camel/catalog/docs/ai-tool-component.adoc | 52 +++++++
.../ai/tool/AiToolComponentConfigurer.java | 6 +
.../ai/tool/AiToolConfigurationConfigurer.java | 6 +
.../ai/tool/AiToolEndpointConfigurer.java | 6 +
.../ai/tool/AiToolEndpointUriFactory.java | 3 +-
.../apache/camel/component/ai/tool/ai-tool.json | 26 ++--
.../src/main/docs/ai-tool-component.adoc | 52 +++++++
.../component/ai/tool/AiToolConfiguration.java | 17 ++-
.../camel/component/ai/tool/AiToolConsumer.java | 18 ++-
.../camel/component/ai/tool/AiToolExecutor.java | 11 +-
.../component/ai/tool/AiToolParameterHelper.java | 164 +++++++++++++++++++++
.../apache/camel/component/ai/tool/AiToolSpec.java | 38 ++++-
.../ai/tool/AiToolEndpointLifecycleTest.java | 36 +++++
.../component/ai/tool/AiToolExecutorTest.java | 83 +++++++++++
.../ai/tool/AiToolParameterHelperTest.java | 105 +++++++++++++
.../test/resources/arg-schemas/create-order.json | 26 ++++
.../langchain4j/agent/AiToolSpecToLangChain4j.java | 54 +++++++
.../agent/AiToolSpecToLangChain4jTest.java | 37 +++++
.../dsl/AiToolComponentBuilderFactory.java | 26 +++-
.../endpoint/dsl/AiToolEndpointBuilderFactory.java | 28 +++-
21 files changed, 774 insertions(+), 46 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
index f0fd90ba449e..e0ae716192d7 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
@@ -24,20 +24,22 @@
"remote": false
},
"componentProperties": {
- "bridgeErrorHandler": { "index": 0, "kind": "property", "displayName":
"Bridge Error Handler", "group": "consumer", "label": "consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Allows for bridging the consumer to the Camel routing Error Handler, which
mean any exceptions (if possible) occurred while the Camel consumer is trying
to pickup incoming messages, or the like [...]
- "configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "consumer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration" },
- "description": { "index": 2, "kind": "property", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
- "parameters": { "index": 3, "kind": "property", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool in [...]
- "tags": { "index": 4, "kind": "property", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags to [...]
- "autowiredEnabled": { "index": 5, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "argSchema": { "index": 0, "kind": "property", "displayName": "Arg
Schema", "group": "consumer", "label": "consumer", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"supportFileReference": true, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Raw JSON Schema for tool input parameters.
Supports inl [...]
+ "bridgeErrorHandler": { "index": 1, "kind": "property", "displayName":
"Bridge Error Handler", "group": "consumer", "label": "consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Allows for bridging the consumer to the Camel routing Error Handler, which
mean any exceptions (if possible) occurred while the Camel consumer is trying
to pickup incoming messages, or the like [...]
+ "configuration": { "index": 2, "kind": "property", "displayName":
"Configuration", "group": "consumer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration" },
+ "description": { "index": 3, "kind": "property", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
+ "parameters": { "index": 4, "kind": "property", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool in [...]
+ "tags": { "index": 5, "kind": "property", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags to [...]
+ "autowiredEnabled": { "index": 6, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
},
"properties": {
"toolName": { "index": 0, "kind": "path", "displayName": "Tool Name",
"group": "consumer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The tool name. This is the
name the LLM sees and uses to invoke the tool." },
- "description": { "index": 1, "kind": "parameter", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
- "parameters": { "index": 2, "kind": "parameter", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool i [...]
- "tags": { "index": 3, "kind": "parameter", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags t [...]
- "bridgeErrorHandler": { "index": 4, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
- "exceptionHandler": { "index": 5, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By def [...]
- "exchangePattern": { "index": 6, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." }
+ "argSchema": { "index": 1, "kind": "parameter", "displayName": "Arg
Schema", "group": "consumer", "label": "consumer", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"supportFileReference": true, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Raw JSON Schema for tool input parameters.
Supports in [...]
+ "description": { "index": 2, "kind": "parameter", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
+ "parameters": { "index": 3, "kind": "parameter", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool i [...]
+ "tags": { "index": 4, "kind": "parameter", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags t [...]
+ "bridgeErrorHandler": { "index": 5, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
+ "exceptionHandler": { "index": 6, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By def [...]
+ "exchangePattern": { "index": 7, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." }
}
}
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
index e478e135d097..a55a8b898782 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
@@ -145,6 +145,58 @@ YAML::
----
====
+=== Complex Parameters with argSchema
+
+Use `argSchema` when the flat `parameter.*` syntax is not expressive enough
for nested objects, arrays, or other advanced JSON Schema constructs.
`argSchema` is mutually exclusive with `parameter.*`.
+
+[tabs]
+====
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: ai-tool:createOrder
+ parameters:
+ tags: orders
+ description: "Create an order"
+ argSchema: |
+ {
+ "type": "object",
+ "properties": {
+ "customer": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "string" }
+ },
+ "required": ["id"]
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sku": { "type": "string" },
+ "qty": { "type": "integer" }
+ }
+ }
+ }
+ },
+ "required": ["customer", "items"],
+ "additionalProperties": false
+ }
+ steps:
+ - to: direct:createOrder
+----
+====
+
+Top-level schema properties are exposed as exchange headers. Nested values are
passed as Map or List objects.
+
+`argSchema` also supports Camel resource references such as
`classpath:schemas/create-order.json`.
+
+The root schema must be a JSON Schema object with a top-level `properties`
map. Camel always allowlists top-level property names when invoking the tool
route, even if the schema sets `additionalProperties: true`. Nested fields are
not flattened into headers; only top-level properties become exchange headers
(Map/List/primitive values).
+
=== Tag-Based Discovery
Tags group tools so that AI producers can select relevant subsets.
diff --git
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
index 5eb0aeb43c84..029c6b816335 100644
---
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
+++
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
@@ -30,6 +30,8 @@ public class AiToolComponentConfigurer extends
PropertyConfigurerSupport impleme
public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
AiToolComponent target = (AiToolComponent) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema":
getOrCreateConfiguration(target).setArgSchema(property(camelContext,
java.lang.String.class, value)); return true;
case "autowiredenabled":
case "autowiredEnabled":
target.setAutowiredEnabled(property(camelContext, boolean.class, value));
return true;
case "bridgeerrorhandler":
@@ -45,6 +47,8 @@ public class AiToolComponentConfigurer extends
PropertyConfigurerSupport impleme
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": return java.lang.String.class;
case "autowiredenabled":
case "autowiredEnabled": return boolean.class;
case "bridgeerrorhandler":
@@ -61,6 +65,8 @@ public class AiToolComponentConfigurer extends
PropertyConfigurerSupport impleme
public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
AiToolComponent target = (AiToolComponent) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": return
getOrCreateConfiguration(target).getArgSchema();
case "autowiredenabled":
case "autowiredEnabled": return target.isAutowiredEnabled();
case "bridgeerrorhandler":
diff --git
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
index 5639cfc7474c..f79d66df604b 100644
---
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
+++
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
@@ -23,6 +23,8 @@ public class AiToolConfigurationConfigurer extends
org.apache.camel.support.comp
public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
org.apache.camel.component.ai.tool.AiToolConfiguration target =
(org.apache.camel.component.ai.tool.AiToolConfiguration) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": target.setArgSchema(property(camelContext,
java.lang.String.class, value)); return true;
case "description": target.setDescription(property(camelContext,
java.lang.String.class, value)); return true;
case "parameters": target.setParameters(property(camelContext,
java.util.Map.class, value)); return true;
case "tags": target.setTags(property(camelContext,
java.lang.String.class, value)); return true;
@@ -33,6 +35,8 @@ public class AiToolConfigurationConfigurer extends
org.apache.camel.support.comp
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": return java.lang.String.class;
case "description": return java.lang.String.class;
case "parameters": return java.util.Map.class;
case "tags": return java.lang.String.class;
@@ -44,6 +48,8 @@ public class AiToolConfigurationConfigurer extends
org.apache.camel.support.comp
public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
org.apache.camel.component.ai.tool.AiToolConfiguration target =
(org.apache.camel.component.ai.tool.AiToolConfiguration) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": return target.getArgSchema();
case "description": return target.getDescription();
case "parameters": return target.getParameters();
case "tags": return target.getTags();
diff --git
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
index e799e69354da..d658045e6821 100644
---
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
+++
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
@@ -23,6 +23,8 @@ public class AiToolEndpointConfigurer extends
PropertyConfigurerSupport implemen
public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
AiToolEndpoint target = (AiToolEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema":
target.getConfiguration().setArgSchema(property(camelContext,
java.lang.String.class, value)); return true;
case "bridgeerrorhandler":
case "bridgeErrorHandler":
target.setBridgeErrorHandler(property(camelContext, boolean.class, value));
return true;
case "description":
target.getConfiguration().setDescription(property(camelContext,
java.lang.String.class, value)); return true;
@@ -39,6 +41,8 @@ public class AiToolEndpointConfigurer extends
PropertyConfigurerSupport implemen
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": return java.lang.String.class;
case "bridgeerrorhandler":
case "bridgeErrorHandler": return boolean.class;
case "description": return java.lang.String.class;
@@ -56,6 +60,8 @@ public class AiToolEndpointConfigurer extends
PropertyConfigurerSupport implemen
public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
AiToolEndpoint target = (AiToolEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "argschema":
+ case "argSchema": return target.getConfiguration().getArgSchema();
case "bridgeerrorhandler":
case "bridgeErrorHandler": return target.isBridgeErrorHandler();
case "description": return target.getConfiguration().getDescription();
diff --git
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
index 3bdaa5000b8c..85d40f65427c 100644
---
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
+++
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
@@ -24,7 +24,8 @@ public class AiToolEndpointUriFactory extends
org.apache.camel.support.component
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(7);
+ Set<String> props = new HashSet<>(8);
+ props.add("argSchema");
props.add("bridgeErrorHandler");
props.add("description");
props.add("exceptionHandler");
diff --git
a/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
b/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
index f0fd90ba449e..e0ae716192d7 100644
---
a/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
+++
b/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
@@ -24,20 +24,22 @@
"remote": false
},
"componentProperties": {
- "bridgeErrorHandler": { "index": 0, "kind": "property", "displayName":
"Bridge Error Handler", "group": "consumer", "label": "consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Allows for bridging the consumer to the Camel routing Error Handler, which
mean any exceptions (if possible) occurred while the Camel consumer is trying
to pickup incoming messages, or the like [...]
- "configuration": { "index": 1, "kind": "property", "displayName":
"Configuration", "group": "consumer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration" },
- "description": { "index": 2, "kind": "property", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
- "parameters": { "index": 3, "kind": "property", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool in [...]
- "tags": { "index": 4, "kind": "property", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags to [...]
- "autowiredEnabled": { "index": 5, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
+ "argSchema": { "index": 0, "kind": "property", "displayName": "Arg
Schema", "group": "consumer", "label": "consumer", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"supportFileReference": true, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Raw JSON Schema for tool input parameters.
Supports inl [...]
+ "bridgeErrorHandler": { "index": 1, "kind": "property", "displayName":
"Bridge Error Handler", "group": "consumer", "label": "consumer", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": false, "description":
"Allows for bridging the consumer to the Camel routing Error Handler, which
mean any exceptions (if possible) occurred while the Camel consumer is trying
to pickup incoming messages, or the like [...]
+ "configuration": { "index": 2, "kind": "property", "displayName":
"Configuration", "group": "consumer", "label": "", "required": false, "type":
"object", "javaType": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"deprecated": false, "autowired": false, "secret": false, "description": "The
component configuration" },
+ "description": { "index": 3, "kind": "property", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
+ "parameters": { "index": 4, "kind": "property", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool in [...]
+ "tags": { "index": 5, "kind": "property", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags to [...]
+ "autowiredEnabled": { "index": 6, "kind": "property", "displayName":
"Autowired Enabled", "group": "advanced", "label": "advanced", "required":
false, "type": "boolean", "javaType": "boolean", "deprecated": false,
"autowired": false, "secret": false, "defaultValue": true, "description":
"Whether autowiring is enabled. This is used for automatic autowiring options
(the option must be marked as autowired) by looking up in the registry to find
if there is a single instance of matching t [...]
},
"properties": {
"toolName": { "index": 0, "kind": "path", "displayName": "Tool Name",
"group": "consumer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The tool name. This is the
name the LLM sees and uses to invoke the tool." },
- "description": { "index": 1, "kind": "parameter", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
- "parameters": { "index": 2, "kind": "parameter", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool i [...]
- "tags": { "index": 3, "kind": "parameter", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags t [...]
- "bridgeErrorHandler": { "index": 4, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
- "exceptionHandler": { "index": 5, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By def [...]
- "exchangePattern": { "index": 6, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." }
+ "argSchema": { "index": 1, "kind": "parameter", "displayName": "Arg
Schema", "group": "consumer", "label": "consumer", "required": false, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"supportFileReference": true, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Raw JSON Schema for tool input parameters.
Supports in [...]
+ "description": { "index": 2, "kind": "parameter", "displayName":
"Description", "group": "consumer", "label": "consumer", "required": false,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Human-readable
description of what this tool does. Passed verbatim to the LLM; be [...]
+ "parameters": { "index": 3, "kind": "parameter", "displayName":
"Parameters", "group": "consumer", "label": "consumer", "required": false,
"type": "object", "javaType": "java.util.Map<java.lang.String,
java.lang.String>", "prefix": "parameter.", "multiValue": true, "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration",
"configurationField": "configuration", "description": "Tool i [...]
+ "tags": { "index": 4, "kind": "parameter", "displayName": "Tags", "group":
"consumer", "label": "consumer", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "configurationClass":
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField":
"configuration", "description": "Comma-separated list of tags used to group
tools. Producers filter the registry by these tags t [...]
+ "bridgeErrorHandler": { "index": 5, "kind": "parameter", "displayName":
"Bridge Error Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Allows for bridging the consumer to the
Camel routing Error Handler, which mean any exceptions (if possible) occurred
while the Camel consumer is trying to pickup incoming [...]
+ "exceptionHandler": { "index": 6, "kind": "parameter", "displayName":
"Exception Handler", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "object", "javaType":
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.",
"deprecated": false, "autowired": false, "secret": false, "description": "To
let the consumer use a custom ExceptionHandler. Notice if the option
bridgeErrorHandler is enabled then this option is not in use. By def [...]
+ "exchangePattern": { "index": 7, "kind": "parameter", "displayName":
"Exchange Pattern", "group": "consumer (advanced)", "label":
"consumer,advanced", "required": false, "type": "enum", "javaType":
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ],
"deprecated": false, "autowired": false, "secret": false, "description": "Sets
the exchange pattern when the consumer creates an exchange." }
}
}
diff --git
a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
index e478e135d097..a55a8b898782 100644
--- a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
+++ b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
@@ -145,6 +145,58 @@ YAML::
----
====
+=== Complex Parameters with argSchema
+
+Use `argSchema` when the flat `parameter.*` syntax is not expressive enough
for nested objects, arrays, or other advanced JSON Schema constructs.
`argSchema` is mutually exclusive with `parameter.*`.
+
+[tabs]
+====
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: ai-tool:createOrder
+ parameters:
+ tags: orders
+ description: "Create an order"
+ argSchema: |
+ {
+ "type": "object",
+ "properties": {
+ "customer": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "string" }
+ },
+ "required": ["id"]
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sku": { "type": "string" },
+ "qty": { "type": "integer" }
+ }
+ }
+ }
+ },
+ "required": ["customer", "items"],
+ "additionalProperties": false
+ }
+ steps:
+ - to: direct:createOrder
+----
+====
+
+Top-level schema properties are exposed as exchange headers. Nested values are
passed as Map or List objects.
+
+`argSchema` also supports Camel resource references such as
`classpath:schemas/create-order.json`.
+
+The root schema must be a JSON Schema object with a top-level `properties`
map. Camel always allowlists top-level property names when invoking the tool
route, even if the schema sets `additionalProperties: true`. Nested fields are
not flattened into headers; only top-level properties become exchange headers
(Map/List/primitive values).
+
=== Tag-Based Discovery
Tags group tools so that AI producers can select relevant subsets.
diff --git
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
index c74738951808..78259c98687e 100644
---
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
+++
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
@@ -50,10 +50,17 @@ public class AiToolConfiguration implements Cloneable {
@UriParam(description = "Tool input parameters. "
+ "Format: parameter.NAME=TYPE,
parameter.NAME.description=TEXT, "
+ "parameter.NAME.required=true or false,
parameter.NAME.enum=val1,val2. "
- + "Supported types: string, integer, number,
boolean.",
+ + "Supported types: string, integer, number,
boolean. "
+ + "Mutually exclusive with argSchema.",
prefix = "parameter.", multiValue = true)
private Map<String, String> parameters;
+ @UriParam(description = "Raw JSON Schema for tool input parameters.
Supports inline JSON and resource "
+ + "references (classpath:, file:, resource:).
Mutually exclusive with the parameter "
+ + "multi-value options. Use for nested objects,
arrays, oneOf, and other complex schemas.")
+ @Metadata(label = "consumer", supportFileReference = true, largeInput =
true, inputLanguage = "json")
+ private String argSchema;
+
public AiToolConfiguration() {
}
@@ -81,6 +88,14 @@ public class AiToolConfiguration implements Cloneable {
this.parameters = parameters;
}
+ public String getArgSchema() {
+ return argSchema;
+ }
+
+ public void setArgSchema(String argSchema) {
+ this.argSchema = argSchema;
+ }
+
public AiToolConfiguration copy() {
try {
AiToolConfiguration copy = (AiToolConfiguration) super.clone();
diff --git
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
index 0b07f7c25821..a2ca9db90a38 100644
---
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
+++
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
@@ -49,14 +49,18 @@ public class AiToolConsumer extends DefaultConsumer {
super.doStart();
Map<String, String> params = configuration.getParameters();
- Map<String, AiToolParameterHelper.ParameterDef> parameterDefs
- = (params != null && !params.isEmpty())
- ? AiToolParameterHelper.parseParameterMetadata(params)
- : Map.of();
+ String argSchema = configuration.getArgSchema();
+ AiToolParameterHelper.validateParameterSourceExclusive(params,
argSchema);
- String jsonSchema = !parameterDefs.isEmpty()
- ? AiToolParameterHelper.buildJsonSchemaFromDefs(parameterDefs)
- : null;
+ Map<String, AiToolParameterHelper.ParameterDef> parameterDefs =
Map.of();
+ String jsonSchema = null;
+
+ if (params != null && !params.isEmpty()) {
+ parameterDefs =
AiToolParameterHelper.parseParameterMetadata(params);
+ jsonSchema =
AiToolParameterHelper.buildJsonSchemaFromDefs(parameterDefs);
+ } else if (argSchema != null && !argSchema.isBlank()) {
+ jsonSchema =
AiToolParameterHelper.resolveArgSchema(getEndpoint().getCamelContext(),
argSchema);
+ }
String desc = configuration.getDescription();
if (desc == null || desc.isBlank()) {
diff --git
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
index bacd29dbc37b..8f918232d77a 100644
---
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
+++
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
@@ -100,7 +100,7 @@ public final class AiToolExecutor {
// A tool that declares no parameters accepts none, so an empty
declaration must filter
// everything rather than let every argument through.
if (!argsCopy.isEmpty()) {
- Set<String> declaredParams = spec.getParameterDefs().keySet();
+ Set<String> declaredParams = spec.getDeclaredArgumentNames();
argsCopy.keySet().removeIf(name -> {
if (!declaredParams.contains(name)) {
@@ -113,14 +113,13 @@ public final class AiToolExecutor {
});
}
- for (Map.Entry<String, AiToolParameterHelper.ParameterDef> entry :
spec.getParameterDefs().entrySet()) {
- if (entry.getValue().isRequired()
- && (arguments == null ||
!arguments.containsKey(entry.getKey()))) {
+ for (String requiredName : spec.getRequiredArgumentNames()) {
+ if (!argsCopy.containsKey(requiredName)) {
LOG.warn("Missing required argument '{}' for tool '{}' -- the
LLM did not send "
+ "a parameter that is declared as required in the
tool specification",
- entry.getKey(), toolName);
+ requiredName, toolName);
IllegalArgumentException cause = new IllegalArgumentException(
- String.format("Missing required argument '%s' for tool
'%s'", entry.getKey(), toolName));
+ String.format("Missing required argument '%s' for tool
'%s'", requiredName, toolName));
return new AiToolResult.ArgumentError(cause.getMessage(),
cause);
}
}
diff --git
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolParameterHelper.java
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolParameterHelper.java
index 95732b14d1de..2be215949b48 100644
---
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolParameterHelper.java
+++
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolParameterHelper.java
@@ -16,16 +16,25 @@
*/
package org.apache.camel.component.ai.tool;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
+import org.apache.camel.CamelContext;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.util.json.DeserializationException;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -150,6 +159,161 @@ public final class AiToolParameterHelper {
return schema.toJson();
}
+ /**
+ * Validates that flat {@code parameter.*} metadata and {@code argSchema}
are not both configured.
+ */
+ public static void validateParameterSourceExclusive(Map<String, String>
parameters, String argSchema) {
+ boolean hasParameters = parameters != null && !parameters.isEmpty();
+ boolean hasArgSchema = argSchema != null && !argSchema.isBlank();
+ if (hasParameters && hasArgSchema) {
+ throw new IllegalArgumentException(
+ "argSchema and parameter.* are mutually exclusive on
ai-tool endpoints");
+ }
+ }
+
+ /**
+ * Resolves, validates, and normalizes a raw JSON Schema for tool input.
+ */
+ public static String resolveArgSchema(CamelContext camelContext, String
argSchema) {
+ if (argSchema == null || argSchema.isBlank()) {
+ throw new IllegalArgumentException("argSchema must not be blank");
+ }
+
+ String resolved = camelContext.resolvePropertyPlaceholders(argSchema);
+ String content = resolveResourceContent(camelContext, resolved);
+ if (content != null) {
+ resolved = content;
+ }
+
+ JsonObject root = parseJsonObject(resolved, argSchema);
+ validateRootSchemaObject(root);
+ return root.toJson();
+ }
+
+ /**
+ * Returns top-level property names declared in a JSON Schema object.
+ */
+ public static Set<String> extractTopLevelPropertyNames(String jsonSchema) {
+ if (jsonSchema == null || jsonSchema.isBlank()) {
+ return Set.of();
+ }
+ JsonObject root = parseJsonObject(jsonSchema, jsonSchema);
+ Map<String, Object> properties = requirePropertiesMap(root,
jsonSchema);
+ if (properties.isEmpty()) {
+ return Set.of();
+ }
+ return Set.copyOf(properties.keySet());
+ }
+
+ /**
+ * Returns top-level required property names declared in a JSON Schema
object.
+ */
+ public static Set<String> extractRequiredPropertyNames(String jsonSchema) {
+ if (jsonSchema == null || jsonSchema.isBlank()) {
+ return Set.of();
+ }
+ JsonObject root = parseJsonObject(jsonSchema, jsonSchema);
+ Collection<?> required = readRequiredArray(root, jsonSchema);
+ if (required.isEmpty()) {
+ return Set.of();
+ }
+ Set<String> names = new LinkedHashSet<>();
+ for (Object value : required) {
+ if (value != null) {
+ names.add(value.toString());
+ }
+ }
+ return Set.copyOf(names);
+ }
+
+ private static JsonObject parseJsonObject(String json, String
originalValue) {
+ try {
+ Object parsed = Jsoner.deserialize(json);
+ if (parsed == null) {
+ throw new IllegalArgumentException("argSchema must be a JSON
object, but was: null");
+ }
+ if (!(parsed instanceof JsonObject root)) {
+ throw new IllegalArgumentException(
+ "argSchema must be a JSON object, but was: " +
parsed.getClass().getSimpleName());
+ }
+ return root;
+ } catch (DeserializationException e) {
+ throw new IllegalArgumentException(
+ "argSchema does not contain valid JSON. Provided value: "
+ originalValue, e);
+ }
+ }
+
+ private static void validateRootSchemaObject(JsonObject root) {
+ Object type = root.get("type");
+ if (type != null && !"object".equals(type)) {
+ throw new IllegalArgumentException(
+ "argSchema root type must be 'object' when specified, but
was: " + type);
+ }
+ Map<String, Object> properties = requirePropertiesMap(root,
"argSchema");
+ if (properties.isEmpty()) {
+ throw new IllegalArgumentException("argSchema must declare at
least one top-level property");
+ }
+ Collection<?> required = readRequiredArray(root, "argSchema");
+ for (Object requiredName : required) {
+ if (requiredName == null ||
!properties.containsKey(requiredName.toString())) {
+ throw new IllegalArgumentException(
+ "argSchema required entry '" + requiredName + "' is
not declared in properties");
+ }
+ }
+ }
+
+ private static Map<String, Object> requirePropertiesMap(JsonObject root,
String context) {
+ Object properties = root.get("properties");
+ if (properties == null) {
+ throw new IllegalArgumentException(context + " must be a JSON
Schema object with a properties map");
+ }
+ if (!(properties instanceof Map<?, ?> map)) {
+ throw new IllegalArgumentException(
+ context + " properties must be a JSON object, but was: "
+ +
properties.getClass().getSimpleName());
+ }
+ Map<String, Object> typed = new HashMap<>();
+ for (Map.Entry<?, ?> entry : map.entrySet()) {
+ if (entry.getKey() != null) {
+ typed.put(entry.getKey().toString(), entry.getValue());
+ }
+ }
+ return typed;
+ }
+
+ private static Collection<?> readRequiredArray(JsonObject root, String
context) {
+ Object required = root.get("required");
+ if (required == null) {
+ return List.of();
+ }
+ if (!(required instanceof Collection<?> collection)) {
+ throw new IllegalArgumentException(
+ context + " required must be a JSON array, but was: "
+ +
required.getClass().getSimpleName());
+ }
+ return collection;
+ }
+
+ private static String resolveResourceContent(CamelContext camelContext,
String property) {
+ try {
+ if (ResourceHelper.hasScheme(property)) {
+ try (InputStream is =
ResourceHelper.resolveMandatoryResourceAsInputStream(camelContext, property)) {
+ return
camelContext.getTypeConverter().convertTo(String.class, is);
+ }
+ }
+ try (InputStream is =
ResourceHelper.resolveResourceAsInputStream(camelContext, property)) {
+ if (is != null) {
+ return
camelContext.getTypeConverter().convertTo(String.class, is);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to load argSchema
resource: " + property, e);
+ } catch (Exception e) {
+ // not a resolvable resource URI — fall through and treat as
inline JSON content
+ }
+ return null;
+ }
+
private static String mapType(String type) {
if (type == null) {
return "string";
diff --git
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolSpec.java
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolSpec.java
index dbecd4f7d41c..2206b06fc6d0 100644
---
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolSpec.java
+++
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolSpec.java
@@ -19,6 +19,8 @@ package org.apache.camel.component.ai.tool;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.apache.camel.support.DefaultConsumer;
@@ -36,6 +38,8 @@ public final class AiToolSpec {
private final String description;
private final Map<String, AiToolParameterHelper.ParameterDef>
parameterDefs;
private final String parametersJsonSchema;
+ private final Set<String> declaredArgumentNames;
+ private final Set<String> requiredArgumentNames;
private final DefaultConsumer consumer;
public AiToolSpec(
@@ -48,6 +52,19 @@ public final class AiToolSpec {
this.description = description;
this.parameterDefs = parameterDefs != null ?
Collections.unmodifiableMap(parameterDefs) : Map.of();
this.parametersJsonSchema = parametersJsonSchema;
+ if (!this.parameterDefs.isEmpty()) {
+ this.declaredArgumentNames =
Set.copyOf(this.parameterDefs.keySet());
+ this.requiredArgumentNames = this.parameterDefs.entrySet().stream()
+ .filter(entry -> entry.getValue().isRequired())
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toUnmodifiableSet());
+ } else if (parametersJsonSchema != null &&
!parametersJsonSchema.isBlank()) {
+ this.declaredArgumentNames =
AiToolParameterHelper.extractTopLevelPropertyNames(parametersJsonSchema);
+ this.requiredArgumentNames =
AiToolParameterHelper.extractRequiredPropertyNames(parametersJsonSchema);
+ } else {
+ this.declaredArgumentNames = Set.of();
+ this.requiredArgumentNames = Set.of();
+ }
this.consumer = consumer;
}
@@ -75,6 +92,22 @@ public final class AiToolSpec {
return parametersJsonSchema;
}
+ /**
+ * Top-level argument names accepted by this tool. Derived from flat
parameter definitions or from a raw
+ * {@code argSchema}'s top-level {@code properties} map.
+ */
+ public Set<String> getDeclaredArgumentNames() {
+ return declaredArgumentNames;
+ }
+
+ /**
+ * Top-level required argument names. Derived from flat parameter
definitions or from a raw schema's top-level
+ * {@code required} array.
+ */
+ public Set<String> getRequiredArgumentNames() {
+ return requiredArgumentNames;
+ }
+
/**
* The Camel consumer that executes this tool's route when the LLM invokes
it.
*/
@@ -97,12 +130,15 @@ public final class AiToolSpec {
&& Objects.equals(description, that.description)
&& Objects.equals(parameterDefs, that.parameterDefs)
&& Objects.equals(parametersJsonSchema,
that.parametersJsonSchema)
+ && Objects.equals(declaredArgumentNames,
that.declaredArgumentNames)
+ && Objects.equals(requiredArgumentNames,
that.requiredArgumentNames)
&& Objects.equals(consumer, that.consumer);
}
@Override
public int hashCode() {
- return Objects.hash(name, description, parameterDefs,
parametersJsonSchema, consumer);
+ return Objects.hash(name, description, parameterDefs,
parametersJsonSchema, declaredArgumentNames,
+ requiredArgumentNames, consumer);
}
@Override
diff --git
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
index 762a0349f856..d5367b4b8222 100644
---
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
+++
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolEndpointLifecycleTest.java
@@ -320,6 +320,42 @@ public class AiToolEndpointLifecycleTest extends
CamelTestSupport {
.contains("suspendableDefault");
}
+ @Test
+ void testArgSchemaToolRegisteredOnStart() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ public void configure() {
+ from("ai-tool:createOrder"
+ + "?tags=orders"
+ + "&description=Create an order"
+ + "&argSchema=classpath:arg-schemas/create-order.json")
+ .setBody(constant("ok"));
+ }
+ });
+
+ AiToolRegistry registry = AiToolRegistry.getOrCreate(context);
+ AiToolSpec spec = registry.getToolsByTag("orders").iterator().next();
+
+ assertThat(spec.getName()).isEqualTo("createOrder");
+ assertThat(spec.getParameterDefs()).isEmpty();
+
assertThat(spec.getParametersJsonSchema()).isNotNull().contains("\"customer\"");
+
assertThat(spec.getDeclaredArgumentNames()).containsExactlyInAnyOrder("customer",
"items");
+
assertThat(spec.getRequiredArgumentNames()).containsExactlyInAnyOrder("customer",
"items");
+ }
+
+ @Test
+ void testArgSchemaAndParametersAreMutuallyExclusive() {
+ assertThatThrownBy(() -> context.addRoutes(new RouteBuilder() {
+ public void configure() {
+ from("ai-tool:invalid"
+ + "?tags=test"
+ + "¶meter.city=string"
+ + "&argSchema={\"type\":\"object\"}")
+ .setBody(constant("invalid"));
+ }
+ })).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("mutually exclusive");
+ }
+
@Test
public void testGetAllToolsReturnsAllPools() throws Exception {
context.addRoutes(new RouteBuilder() {
diff --git
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolExecutorTest.java
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolExecutorTest.java
index 5b04de63623b..605486c8d798 100644
---
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolExecutorTest.java
+++
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolExecutorTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.ai.tool;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.apache.camel.Exchange;
@@ -74,6 +75,16 @@ public class AiToolExecutorTest extends CamelTestSupport {
+ "&description=A tool that sets exception on exchange")
.process(exchange -> exchange.setException(
new RuntimeException("Exchange-level
failure")));
+
+ from("ai-tool:createOrder"
+ + "?tags=test"
+ + "&description=Create an order"
+ + "&argSchema=classpath:arg-schemas/create-order.json")
+ .process(exchange -> {
+ Map<?, ?> customer =
exchange.getMessage().getHeader("customer", Map.class);
+ List<?> items =
exchange.getMessage().getHeader("items", List.class);
+ exchange.getMessage().setBody("order:" +
customer.get("id") + ":" + items.size());
+ });
}
};
}
@@ -298,6 +309,78 @@ public class AiToolExecutorTest extends CamelTestSupport {
.isEqualTo("ok");
}
+ @Test
+ void testExecuteWithRawArgSchemaNestedArguments() {
+ AiToolSpec spec = findSpec("createOrder");
+ Exchange exchange = new DefaultExchange(context);
+
+ Map<String, Object> customer = Map.of("id", "cust-1", "email",
"[email protected]");
+ List<Map<String, Object>> items = List.of(Map.of("sku", "ABC", "qty",
2));
+
+ Map<String, Object> arguments = new HashMap<>();
+ arguments.put("customer", customer);
+ arguments.put("items", items);
+
+ AiToolResult result = AiToolExecutor.execute(spec, arguments,
exchange);
+
+ assertThat(result).isInstanceOf(AiToolResult.Success.class);
+ assertThat(((AiToolResult.Success)
result).value()).isEqualTo("order:cust-1:1");
+ assertThat(exchange.getMessage().getHeader("customer",
Map.class)).isEqualTo(customer);
+ assertThat(exchange.getMessage().getHeader("items",
List.class)).isEqualTo(items);
+ }
+
+ @Test
+ void testExecuteIgnoresUndeclaredArgumentsForRawArgSchema() {
+ AiToolSpec spec = findSpec("createOrder");
+ Exchange exchange = new DefaultExchange(context);
+
+ Map<String, Object> arguments = new HashMap<>();
+ arguments.put("customer", Map.of("id", "cust-1"));
+ arguments.put("items", List.of(Map.of("sku", "ABC", "qty", 1)));
+ arguments.put("promoCode", "SAVE10");
+
+ AiToolResult result = AiToolExecutor.execute(spec, arguments,
exchange);
+
+ assertThat(result).isInstanceOf(AiToolResult.Success.class);
+ assertThat(exchange.getMessage().getHeader("promoCode")).isNull();
+ }
+
+ @Test
+ void testExecuteReturnsArgumentErrorForMissingRequiredRawSchemaArgument() {
+ AiToolSpec spec = findSpec("createOrder");
+ Exchange exchange = new DefaultExchange(context);
+
+ Map<String, Object> arguments = new HashMap<>();
+ arguments.put("customer", Map.of("id", "cust-1"));
+
+ AiToolResult result = AiToolExecutor.execute(spec, arguments,
exchange);
+
+ assertThat(result).isInstanceOf(AiToolResult.ArgumentError.class);
+ assertThat(((AiToolResult.ArgumentError)
result).message()).contains("Missing required argument 'items'");
+ }
+
+ @Test
+ void testExecuteReturnsArgumentErrorWhenRequiredNameIsUndeclared() {
+ String schema = """
+ {
+ "type": "object",
+ "properties": {
+ "city": { "type": "string" }
+ },
+ "required": ["country"]
+ }
+ """;
+ AiToolSpec spec = new AiToolSpec("badRequired", "test", Map.of(),
schema, findSpec("greetUser").getConsumer());
+ Exchange exchange = new DefaultExchange(context);
+
+ Map<String, Object> arguments = Map.of("country", "Paris");
+
+ AiToolResult result = AiToolExecutor.execute(spec, arguments,
exchange);
+
+ assertThat(result).isInstanceOf(AiToolResult.ArgumentError.class);
+ assertThat(((AiToolResult.ArgumentError)
result).message()).contains("Missing required argument 'country'");
+ }
+
private AiToolSpec findSpec(String toolName) {
return
AiToolRegistry.getOrCreate(context).getToolsByTag("test").stream()
.filter(s -> toolName.equals(s.getName()))
diff --git
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolParameterHelperTest.java
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolParameterHelperTest.java
index 09b1c9144b00..81068d72ba90 100644
---
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolParameterHelperTest.java
+++
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolParameterHelperTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.ai.tool;
import java.util.LinkedHashMap;
import java.util.Map;
+import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.util.json.DeserializationException;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@@ -26,9 +27,12 @@ import org.apache.camel.util.json.Jsoner;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class AiToolParameterHelperTest {
+ private final DefaultCamelContext camelContext = new DefaultCamelContext();
+
@Test
public void testSplitTags() {
assertThat(AiToolParameterHelper.splitTags("a,b,c"))
@@ -266,4 +270,105 @@ public class AiToolParameterHelperTest {
.as("Default type should be string when only description is
provided")
.isEqualTo("string");
}
+
+ @Test
+ void testValidateParameterSourceExclusive() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.validateParameterSourceExclusive(
+ Map.of("city", "string"), "{\"type\":\"object\"}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("mutually exclusive");
+ }
+
+ @Test
+ void testExtractTopLevelPropertyNamesFromRawSchema() throws
DeserializationException {
+ String schema = """
+ {
+ "type": "object",
+ "properties": {
+ "customer": { "type": "object" },
+ "items": { "type": "array" }
+ }
+ }
+ """;
+
+ assertThat(AiToolParameterHelper.extractTopLevelPropertyNames(schema))
+ .containsExactlyInAnyOrder("customer", "items");
+ }
+
+ @Test
+ void testExtractRequiredPropertyNamesFromRawSchema() throws
DeserializationException {
+ String schema = """
+ {
+ "type": "object",
+ "properties": {
+ "customer": { "type": "object" },
+ "items": { "type": "array" }
+ },
+ "required": ["customer"]
+ }
+ """;
+
+ assertThat(AiToolParameterHelper.extractRequiredPropertyNames(schema))
+ .containsExactly("customer");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsJsonNull() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext, "null"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("null");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsInvalidJson() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext, "{not-json"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("valid JSON");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsNonObjectRootType() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext, "{\"type\":\"string\"}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("root type must be 'object'");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsMissingProperties() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext, "{\"type\":\"object\"}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("properties map");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsEmptyProperties() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext,
+ "{\"type\":\"object\",\"properties\":{}}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("at least one top-level property");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsInvalidRequiredArray() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext,
+
"{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":\"city\"}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("required must be a JSON array");
+ }
+
+ @Test
+ void testResolveArgSchemaRejectsUnknownRequiredName() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.resolveArgSchema(camelContext,
+
"{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"country\"]}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("not declared in properties");
+ }
+
+ @Test
+ void testExtractTopLevelPropertyNamesRejectsInvalidProperties() {
+ assertThatThrownBy(() ->
AiToolParameterHelper.extractTopLevelPropertyNames(
+ "{\"type\":\"object\",\"properties\":[]}"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("properties must be a JSON object");
+ }
}
diff --git
a/components/camel-ai/camel-ai-tool/src/test/resources/arg-schemas/create-order.json
b/components/camel-ai/camel-ai-tool/src/test/resources/arg-schemas/create-order.json
new file mode 100644
index 000000000000..04bb17a74a94
--- /dev/null
+++
b/components/camel-ai/camel-ai-tool/src/test/resources/arg-schemas/create-order.json
@@ -0,0 +1,26 @@
+{
+ "type": "object",
+ "properties": {
+ "customer": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "string" },
+ "email": { "type": "string" }
+ },
+ "required": ["id"]
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sku": { "type": "string" },
+ "qty": { "type": "integer" }
+ },
+ "required": ["sku", "qty"]
+ }
+ }
+ },
+ "required": ["customer", "items"],
+ "additionalProperties": false
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4j.java
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4j.java
index ab398899c72e..31e34dc3300a 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4j.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4j.java
@@ -22,15 +22,20 @@ import java.util.Locale;
import java.util.Map;
import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.internal.JsonSchemaElementJsonUtils;
import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
import dev.langchain4j.model.chat.request.json.JsonEnumSchema;
import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
+import dev.langchain4j.model.chat.request.json.JsonRawSchema;
import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
import dev.langchain4j.model.chat.request.json.JsonStringSchema;
import org.apache.camel.component.ai.tool.AiToolParameterHelper;
import org.apache.camel.component.ai.tool.AiToolSpec;
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
/**
* Converts {@link AiToolSpec} instances to langchain4j {@link
ToolSpecification} objects, mapping Camel parameter
@@ -48,11 +53,60 @@ public final class AiToolSpecToLangChain4j {
if (spec.getParameterDefs() != null &&
!spec.getParameterDefs().isEmpty()) {
builder.parameters(buildSchema(spec.getParameterDefs()));
+ } else if (spec.getParametersJsonSchema() != null &&
!spec.getParametersJsonSchema().isBlank()) {
+
builder.parameters(buildSchemaFromJson(spec.getParametersJsonSchema()));
}
return builder.build();
}
+ private static JsonObjectSchema buildSchemaFromJson(String jsonSchema) {
+ try {
+ Object parsed = Jsoner.deserialize(jsonSchema);
+ if (parsed == null) {
+ throw new IllegalArgumentException("Tool JSON Schema must be a
JSON object, but was: null");
+ }
+ if (!(parsed instanceof JsonObject root)) {
+ throw new IllegalArgumentException(
+ "Tool JSON Schema must be a JSON object, but was: " +
parsed.getClass().getSimpleName());
+ }
+ JsonSchemaElement element =
JsonSchemaElementJsonUtils.fromMap(root);
+ if (element instanceof JsonObjectSchema objectSchema) {
+ return objectSchema;
+ }
+ JsonObject stripped = stripSchemaMetadata(root);
+ if (!stripped.equals(root)) {
+ element = JsonSchemaElementJsonUtils.fromMap(stripped);
+ if (element instanceof JsonObjectSchema objectSchema) {
+ return objectSchema;
+ }
+ }
+ if (element instanceof JsonRawSchema rawSchema) {
+ throw new IllegalArgumentException(
+ "Tool JSON Schema root could not be converted to
JsonObjectSchema. "
+ + "Remove unsupported root
keywords such as $schema or schema-valued "
+ + "additionalProperties, or
simplify the root schema.");
+ }
+ throw new IllegalArgumentException(
+ "Tool JSON Schema root must deserialize to
JsonObjectSchema, but was: "
+ +
element.getClass().getSimpleName());
+ } catch (DeserializationException e) {
+ throw new IllegalArgumentException("Tool JSON Schema is not valid
JSON", e);
+ }
+ }
+
+ private static JsonObject stripSchemaMetadata(JsonObject root) {
+ JsonObject copy = new JsonObject();
+ for (Map.Entry<String, Object> entry : root.entrySet()) {
+ String key = entry.getKey();
+ if ("$schema".equals(key) || "$id".equals(key) ||
"$defs".equals(key) || "definitions".equals(key)) {
+ continue;
+ }
+ copy.put(key, entry.getValue());
+ }
+ return copy;
+ }
+
private static JsonObjectSchema buildSchema(Map<String,
AiToolParameterHelper.ParameterDef> defs) {
JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder();
List<String> required = new ArrayList<>();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4jTest.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4jTest.java
index c6bb8e509677..ae8d51b65465 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4jTest.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AiToolSpecToLangChain4jTest.java
@@ -30,6 +30,7 @@ import
org.apache.camel.component.ai.tool.AiToolParameterHelper;
import org.apache.camel.component.ai.tool.AiToolSpec;
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -160,6 +161,42 @@ class AiToolSpecToLangChain4jTest {
assertEquals(1, result.parameters().required().size());
}
+ @Test
+ void testRawArgSchemaConversion() {
+ String schema = """
+ {
+ "type": "object",
+ "properties": {
+ "customer": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "string" }
+ },
+ "required": ["id"]
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sku": { "type": "string" },
+ "qty": { "type": "integer" }
+ }
+ }
+ }
+ },
+ "required": ["customer", "items"]
+ }
+ """;
+
+ AiToolSpec spec = new AiToolSpec("createOrder", "Create order",
Map.of(), schema, null);
+ ToolSpecification result =
AiToolSpecToLangChain4j.toToolSpecification(spec);
+
+ assertThat(result.parameters()).isNotNull();
+
assertThat(result.parameters().properties()).hasSize(2).containsKeys("customer",
"items");
+ assertThat(result.parameters().required()).contains("customer",
"items");
+ }
+
@Test
void testEmptyParameters() {
AiToolSpec spec = new AiToolSpec("noParams", "Tool with no params",
Map.of(), null, null);
diff --git
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
index c21d5d719391..b91bac833c9d 100644
---
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
+++
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
@@ -52,6 +52,27 @@ public interface AiToolComponentBuilderFactory {
*/
interface AiToolComponentBuilder extends ComponentBuilder<AiToolComponent>
{
+ /**
+ * Raw JSON Schema for tool input parameters. Supports inline JSON and
+ * resource references (classpath:, file:, resource:). Mutually
+ * exclusive with the parameter multi-value options. Use for nested
+ * objects, arrays, oneOf, and other complex schemas.
+ *
+ * This option can also be loaded from an existing file, by prefixing
+ * with file: or classpath: followed by the location of the file.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: consumer
+ *
+ * @param argSchema the value to set
+ * @return the dsl builder
+ */
+ default AiToolComponentBuilder argSchema(java.lang.String argSchema) {
+ doSetProperty("argSchema", argSchema);
+ return this;
+ }
+
/**
* Allows for bridging the consumer to the Camel routing Error Handler,
@@ -117,8 +138,8 @@ public interface AiToolComponentBuilderFactory {
* Tool input parameters. Format: parameter.NAME=TYPE,
* parameter.NAME.description=TEXT, parameter.NAME.required=true or
* false, parameter.NAME.enum=val1,val2. Supported types: string,
- * integer, number, boolean. This is a multi-value option with prefix:
- * parameter.
+ * integer, number, boolean. Mutually exclusive with argSchema. This is
+ * a multi-value option with prefix: parameter.
*
* The option is a: <code>java.util.Map&lt;java.lang.String,
* java.lang.String&gt;</code> type.
@@ -193,6 +214,7 @@ public interface AiToolComponentBuilderFactory {
String name,
Object value) {
switch (name) {
+ case "argSchema": getOrCreateConfiguration((AiToolComponent)
component).setArgSchema((java.lang.String) value); return true;
case "bridgeErrorHandler": ((AiToolComponent)
component).setBridgeErrorHandler((boolean) value); return true;
case "configuration": ((AiToolComponent)
component).setConfiguration((org.apache.camel.component.ai.tool.AiToolConfiguration)
value); return true;
case "description": getOrCreateConfiguration((AiToolComponent)
component).setDescription((java.lang.String) value); return true;
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
index e0828c82a002..abf21d5ff39e 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
@@ -45,6 +45,26 @@ public interface AiToolEndpointBuilderFactory {
return (AdvancedAiToolEndpointBuilder) this;
}
+ /**
+ * Raw JSON Schema for tool input parameters. Supports inline JSON and
+ * resource references (classpath:, file:, resource:). Mutually
+ * exclusive with the parameter multi-value options. Use for nested
+ * objects, arrays, oneOf, and other complex schemas.
+ *
+ * This option can also be loaded from an existing file, by prefixing
+ * with file: or classpath: followed by the location of the file.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Group: consumer
+ *
+ * @param argSchema the value to set
+ * @return the dsl builder
+ */
+ default AiToolEndpointBuilder argSchema(String argSchema) {
+ doSetProperty("argSchema", argSchema);
+ return this;
+ }
/**
* Human-readable description of what this tool does. Passed verbatim
to
* the LLM; be precise and action-oriented. When omitted, defaults to
@@ -65,8 +85,8 @@ public interface AiToolEndpointBuilderFactory {
* Tool input parameters. Format: parameter.NAME=TYPE,
* parameter.NAME.description=TEXT, parameter.NAME.required=true or
* false, parameter.NAME.enum=val1,val2. Supported types: string,
- * integer, number, boolean. This is a multi-value option with prefix:
- * parameter.
+ * integer, number, boolean. Mutually exclusive with argSchema. This is
+ * a multi-value option with prefix: parameter.
*
* The option is a: <code>java.util.Map<java.lang.String,
* java.lang.String></code> type.
@@ -88,8 +108,8 @@ public interface AiToolEndpointBuilderFactory {
* Tool input parameters. Format: parameter.NAME=TYPE,
* parameter.NAME.description=TEXT, parameter.NAME.required=true or
* false, parameter.NAME.enum=val1,val2. Supported types: string,
- * integer, number, boolean. This is a multi-value option with prefix:
- * parameter.
+ * integer, number, boolean. Mutually exclusive with argSchema. This is
+ * a multi-value option with prefix: parameter.
*
* The option is a: <code>java.util.Map<java.lang.String,
* java.lang.String></code> type.