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 227e918a105ac..0ce16172cd96e 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 @@ -25,39 +25,45 @@ }, "componentProperties": { "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 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." }, - "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 likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, - "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 precise and action-oriented. When omitted, defaults to the tool name." }, - "destructiveHint": { "index": 4, "kind": "property", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, - "idempotentHint": { "index": 5, "kind": "property", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, - "openWorldHint": { "index": 6, "kind": "property", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, - "outputParameters": { "index": 7, "kind": "property", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, - "outputSchema": { "index": 8, "kind": "property", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, - "parameters": { "index": 9, "kind": "property", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, - "readOnlyHint": { "index": 10, "kind": "property", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, - "returnDirect": { "index": 11, "kind": "property", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, - "tags": { "index": 12, "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 select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, - "title": { "index": 13, "kind": "property", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, - "autowiredEnabled": { "index": 14, "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 type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, - "authorizationPolicy": { "index": 15, "kind": "property", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } + "argumentsAsBody": { "index": 1, "kind": "property", "displayName": "Arguments As Body", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Whether to populate the message body with the tool arguments as a JSON string, excluding undeclared arguments and reserved names. Declared argument headers remain available. When disabled, the message body is left unchanged." }, + "bridgeErrorHandler": { "index": 2, "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 likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, + "configuration": { "index": 3, "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": 4, "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 precise and action-oriented. When omitted, defaults to the tool name." }, + "destructiveHint": { "index": 5, "kind": "property", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, + "idempotentHint": { "index": 6, "kind": "property", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, + "openWorldHint": { "index": 7, "kind": "property", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, + "outputParameters": { "index": 8, "kind": "property", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, + "outputSchema": { "index": 9, "kind": "property", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, + "parameters": { "index": 10, "kind": "property", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, + "readOnlyHint": { "index": 11, "kind": "property", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, + "returnDirect": { "index": 12, "kind": "property", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, + "tags": { "index": 13, "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 select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, + "title": { "index": 14, "kind": "property", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, + "autowiredEnabled": { "index": 15, "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 type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, + "authorizationPolicy": { "index": 16, "kind": "property", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } + }, + "headers": { + "CamelAiToolCallId": { "index": 0, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The tool invocation identifier supplied by the calling framework. Absent when the framework does not supply one.", "constantName": "org.apache.camel.component.ai.tool.AiToolConstants#TOOL_CALL_ID" }, + "CamelAiToolConversationId": { "index": 1, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The conversation identifier supplied by the calling framework. Absent when the framework does not supply one.", "constantName": "org.apache.camel.component.ai.tool.AiToolConstants#CONVERSATION_ID" } }, "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." }, "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 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." }, - "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 precise and action-oriented. When omitted, defaults to the tool name." }, - "destructiveHint": { "index": 3, "kind": "parameter", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, - "idempotentHint": { "index": 4, "kind": "parameter", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, - "openWorldHint": { "index": 5, "kind": "parameter", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, - "outputParameters": { "index": 6, "kind": "parameter", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, - "outputSchema": { "index": 7, "kind": "parameter", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, - "parameters": { "index": 8, "kind": "parameter", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, - "readOnlyHint": { "index": 9, "kind": "parameter", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, - "returnDirect": { "index": 10, "kind": "parameter", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, - "tags": { "index": 11, "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 to select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, - "title": { "index": 12, "kind": "parameter", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, - "bridgeErrorHandler": { "index": 13, "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 messages, or the likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, - "exceptionHandler": { "index": 14, "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 default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, - "exchangePattern": { "index": 15, "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." }, - "authorizationPolicy": { "index": 16, "kind": "parameter", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } + "argumentsAsBody": { "index": 2, "kind": "parameter", "displayName": "Arguments As Body", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Whether to populate the message body with the tool arguments as a JSON string, excluding undeclared arguments and reserved names. Declared argument headers remain available. When disabled, the message body is left unchanged." }, + "description": { "index": 3, "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 precise and action-oriented. When omitted, defaults to the tool name." }, + "destructiveHint": { "index": 4, "kind": "parameter", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, + "idempotentHint": { "index": 5, "kind": "parameter", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, + "openWorldHint": { "index": 6, "kind": "parameter", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, + "outputParameters": { "index": 7, "kind": "parameter", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, + "outputSchema": { "index": 8, "kind": "parameter", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, + "parameters": { "index": 9, "kind": "parameter", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, + "readOnlyHint": { "index": 10, "kind": "parameter", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, + "returnDirect": { "index": 11, "kind": "parameter", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, + "tags": { "index": 12, "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 to select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, + "title": { "index": 13, "kind": "parameter", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, + "bridgeErrorHandler": { "index": 14, "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 messages, or the likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, + "exceptionHandler": { "index": 15, "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 default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, + "exchangePattern": { "index": 16, "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." }, + "authorizationPolicy": { "index": 17, "kind": "parameter", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } } } 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 a57c415026cdf..d45cec7224ba5 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 @@ -289,6 +289,62 @@ Supported options: `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, The `title` option maps to the MCP tool's top-level `title` field (not `ToolAnnotations.title`). Boolean hint options use nullable configuration: when unset, Camel omits them from the published MCP tool even though the component catalog may list `defaultValue: false` for documentation tooling. +=== Tool Arguments in the Body + +By default, tool arguments are exposed as individual headers and the message body is left unchanged. +Set `argumentsAsBody=true` to also populate the body with a JSON string containing the tool-call arguments. +This is useful when mapping a complete JSON document, for example with Kaoto Data Mapper. + +[source,yaml] +---- +- route: + from: + uri: ai-tool:place_order + parameters: + description: Place a merchandise order + argSchema: classpath:place-order.args.schema.json + argumentsAsBody: true + steps: + - to: + uri: direct:mapOrder +---- + +For a tool call with a customer name and two items, `direct:mapOrder` receives this JSON-string body: + +[source,json] +---- +{ + "customerName": "Alice", + "items": [ + { "sku": "TSHIRT", "quantity": 2 }, + { "sku": "MUG", "quantity": 1 } + ] +} +---- + +Nested objects, arrays and JSON value types are preserved. A call with no arguments produces `{}`. +The body and individual argument headers include only declared arguments, after the same allowlist filtering. +Camel logs a warning for each undeclared argument. Reserved names starting with `camel` or `org.apache.camel.` +(case-insensitive) are excluded from both the body and argument headers. Missing required arguments still reject +an invocation before the tool route runs. Required arguments are checked after reserved names are removed, +so a required argument with a reserved name rejects the call even if the model supplied it. +Rename such arguments to use names outside the reserved namespace. This option does not add full JSON Schema validation. + +=== Invocation Identifiers + +Tool routes receive `CamelAiToolCallId` and `CamelAiToolConversationId` when the calling adapter can supply them. +These headers are populated independently of `argumentsAsBody`. An unavailable identifier is omitted rather than generated. + +* LangChain4j Agent supplies the tool execution request ID and the memory ID (converted to a string) as the conversation ID when the agent uses memory. + Agents without memory omit the conversation header, including the framework's default memory ID. +* OpenAI supplies the tool-call ID. The Responses API also supplies its resolved conversation ID + (`CamelOpenAIConversationId` or the `conversationId` option); chat-completion callbacks omit it. +* Spring AI Chat supplies the `CamelSpringAiChatConversationId` from the calling exchange. Its tool callback does not supply a call ID. +* MCP server tool callbacks currently do not propagate these identifiers. + +For example, use `${header.CamelAiToolCallId}` to correlate route processing with a particular tool invocation. +Several calls in the same conversation can share `CamelAiToolConversationId` while having different call IDs. + === 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 c78b3ef676e00..ef009fbb1ffc0 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 @@ -32,6 +32,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": getOrCreateConfiguration(target).setArgSchema(property(camelContext, java.lang.String.class, value)); return true; + case "argumentsasbody": + case "argumentsAsBody": getOrCreateConfiguration(target).setArgumentsAsBody(property(camelContext, boolean.class, value)); return true; case "authorizationpolicy": case "authorizationPolicy": getOrCreateConfiguration(target).setAuthorizationPolicy(property(camelContext, org.apache.camel.spi.AuthorizationPolicy.class, value)); return true; case "autowiredenabled": @@ -66,6 +68,8 @@ public Class getOptionType(String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": return java.lang.String.class; + case "argumentsasbody": + case "argumentsAsBody": return boolean.class; case "authorizationpolicy": case "authorizationPolicy": return org.apache.camel.spi.AuthorizationPolicy.class; case "autowiredenabled": @@ -101,6 +105,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": return getOrCreateConfiguration(target).getArgSchema(); + case "argumentsasbody": + case "argumentsAsBody": return getOrCreateConfiguration(target).isArgumentsAsBody(); case "authorizationpolicy": case "authorizationPolicy": return getOrCreateConfiguration(target).getAuthorizationPolicy(); case "autowiredenabled": 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 0b56cc32a0713..cf9a748e5fb49 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 @@ -25,6 +25,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": target.setArgSchema(property(camelContext, java.lang.String.class, value)); return true; + case "argumentsasbody": + case "argumentsAsBody": target.setArgumentsAsBody(property(camelContext, boolean.class, value)); return true; case "authorizationpolicy": case "authorizationPolicy": target.setAuthorizationPolicy(property(camelContext, org.apache.camel.spi.AuthorizationPolicy.class, value)); return true; case "description": target.setDescription(property(camelContext, java.lang.String.class, value)); return true; @@ -54,6 +56,8 @@ public Class getOptionType(String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": return java.lang.String.class; + case "argumentsasbody": + case "argumentsAsBody": return boolean.class; case "authorizationpolicy": case "authorizationPolicy": return org.apache.camel.spi.AuthorizationPolicy.class; case "description": return java.lang.String.class; @@ -84,6 +88,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": return target.getArgSchema(); + case "argumentsasbody": + case "argumentsAsBody": return target.isArgumentsAsBody(); case "authorizationpolicy": case "authorizationPolicy": return target.getAuthorizationPolicy(); case "description": return target.getDescription(); 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 cd65ce6ad3761..b2df2543a518d 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 @@ -25,6 +25,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": target.getConfiguration().setArgSchema(property(camelContext, java.lang.String.class, value)); return true; + case "argumentsasbody": + case "argumentsAsBody": target.getConfiguration().setArgumentsAsBody(property(camelContext, boolean.class, value)); return true; case "authorizationpolicy": case "authorizationPolicy": target.getConfiguration().setAuthorizationPolicy(property(camelContext, org.apache.camel.spi.AuthorizationPolicy.class, value)); return true; case "bridgeerrorhandler": @@ -60,6 +62,8 @@ public Class getOptionType(String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": return java.lang.String.class; + case "argumentsasbody": + case "argumentsAsBody": return boolean.class; case "authorizationpolicy": case "authorizationPolicy": return org.apache.camel.spi.AuthorizationPolicy.class; case "bridgeerrorhandler": @@ -96,6 +100,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { case "argschema": case "argSchema": return target.getConfiguration().getArgSchema(); + case "argumentsasbody": + case "argumentsAsBody": return target.getConfiguration().isArgumentsAsBody(); case "authorizationpolicy": case "authorizationPolicy": return target.getConfiguration().getAuthorizationPolicy(); case "bridgeerrorhandler": 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 1a3532fbd0c81..f2cce825a2426 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,8 +24,9 @@ public class AiToolEndpointUriFactory extends org.apache.camel.support.component private static final Set ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(17); + Set props = new HashSet<>(18); props.add("argSchema"); + props.add("argumentsAsBody"); props.add("authorizationPolicy"); props.add("bridgeErrorHandler"); props.add("description"); 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 227e918a105ac..0ce16172cd96e 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 @@ -25,39 +25,45 @@ }, "componentProperties": { "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 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." }, - "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 likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, - "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 precise and action-oriented. When omitted, defaults to the tool name." }, - "destructiveHint": { "index": 4, "kind": "property", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, - "idempotentHint": { "index": 5, "kind": "property", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, - "openWorldHint": { "index": 6, "kind": "property", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, - "outputParameters": { "index": 7, "kind": "property", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, - "outputSchema": { "index": 8, "kind": "property", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, - "parameters": { "index": 9, "kind": "property", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, - "readOnlyHint": { "index": 10, "kind": "property", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, - "returnDirect": { "index": 11, "kind": "property", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, - "tags": { "index": 12, "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 select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, - "title": { "index": 13, "kind": "property", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, - "autowiredEnabled": { "index": 14, "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 type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, - "authorizationPolicy": { "index": 15, "kind": "property", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } + "argumentsAsBody": { "index": 1, "kind": "property", "displayName": "Arguments As Body", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Whether to populate the message body with the tool arguments as a JSON string, excluding undeclared arguments and reserved names. Declared argument headers remain available. When disabled, the message body is left unchanged." }, + "bridgeErrorHandler": { "index": 2, "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 likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, + "configuration": { "index": 3, "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": 4, "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 precise and action-oriented. When omitted, defaults to the tool name." }, + "destructiveHint": { "index": 5, "kind": "property", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, + "idempotentHint": { "index": 6, "kind": "property", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, + "openWorldHint": { "index": 7, "kind": "property", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, + "outputParameters": { "index": 8, "kind": "property", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, + "outputSchema": { "index": 9, "kind": "property", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, + "parameters": { "index": 10, "kind": "property", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, + "readOnlyHint": { "index": 11, "kind": "property", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, + "returnDirect": { "index": 12, "kind": "property", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, + "tags": { "index": 13, "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 select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, + "title": { "index": 14, "kind": "property", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, + "autowiredEnabled": { "index": 15, "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 type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, + "authorizationPolicy": { "index": 16, "kind": "property", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } + }, + "headers": { + "CamelAiToolCallId": { "index": 0, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The tool invocation identifier supplied by the calling framework. Absent when the framework does not supply one.", "constantName": "org.apache.camel.component.ai.tool.AiToolConstants#TOOL_CALL_ID" }, + "CamelAiToolConversationId": { "index": 1, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The conversation identifier supplied by the calling framework. Absent when the framework does not supply one.", "constantName": "org.apache.camel.component.ai.tool.AiToolConstants#CONVERSATION_ID" } }, "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." }, "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 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." }, - "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 precise and action-oriented. When omitted, defaults to the tool name." }, - "destructiveHint": { "index": 3, "kind": "parameter", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, - "idempotentHint": { "index": 4, "kind": "parameter", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, - "openWorldHint": { "index": 5, "kind": "parameter", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, - "outputParameters": { "index": 6, "kind": "parameter", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, - "outputSchema": { "index": 7, "kind": "parameter", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, - "parameters": { "index": 8, "kind": "parameter", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, - "readOnlyHint": { "index": 9, "kind": "parameter", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, - "returnDirect": { "index": 10, "kind": "parameter", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, - "tags": { "index": 11, "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 to select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, - "title": { "index": 12, "kind": "parameter", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, - "bridgeErrorHandler": { "index": 13, "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 messages, or the likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, - "exceptionHandler": { "index": 14, "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 default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, - "exchangePattern": { "index": 15, "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." }, - "authorizationPolicy": { "index": 16, "kind": "parameter", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } + "argumentsAsBody": { "index": 2, "kind": "parameter", "displayName": "Arguments As Body", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Whether to populate the message body with the tool arguments as a JSON string, excluding undeclared arguments and reserved names. Declared argument headers remain available. When disabled, the message body is left unchanged." }, + "description": { "index": 3, "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 precise and action-oriented. When omitted, defaults to the tool name." }, + "destructiveHint": { "index": 4, "kind": "parameter", "displayName": "Destructive Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool may perform destructive or irreversible updates. Advisory for MCP clients; not enforced by Camel." }, + "idempotentHint": { "index": 5, "kind": "parameter", "displayName": "Idempotent Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that repeating the tool call with the same arguments has no additional effect. Advisory for MCP clients; not enforced by Camel." }, + "openWorldHint": { "index": 6, "kind": "parameter", "displayName": "Open World Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool interacts with external systems outside the application's control. Advisory for MCP clients; not enforced by Camel." }, + "outputParameters": { "index": 7, "kind": "parameter", "displayName": "Output Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "outputParameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Tool output schema fields. Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. Supported types: string, integer, number, boolean. Mutually exclusive with outputSchema. This is a multi-value option with prefix: outputParameter." }, + "outputSchema": { "index": 8, "kind": "parameter", "displayName": "Output 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 describing the tool's structured output. Supports inline JSON and resource references (classpath:, file:, resource:). Mutually exclusive with the outputParameter multi-value options. When declared, the route body is parsed as JSON and exposed as structured content to MCP clients." }, + "parameters": { "index": 9, "kind": "parameter", "displayName": "Parameters", "group": "consumer", "label": "consumer", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "parameter.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "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. Mutually exclusive with argSchema. This is a multi-value option with prefix: parameter." }, + "readOnlyHint": { "index": 10, "kind": "parameter", "displayName": "Read Only Hint", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "MCP hint that the tool only reads data and does not modify state. Advisory for MCP clients; not enforced by Camel." }, + "returnDirect": { "index": 11, "kind": "parameter", "displayName": "Return Direct", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "When true, AI producers that support agentic tool loops (such as camel-openai) return this tool's result directly to the caller without sending it back to the model. Also published as an MCP tool annotation when the tool is exposed via camel-mcp-server." }, + "tags": { "index": 12, "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 to select which tools to expose to the LLM. When omitted, the tool goes into a default pool available to all producers." }, + "title": { "index": 13, "kind": "parameter", "displayName": "Title", "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": "Optional display title for MCP tool listings. Advisory hint for MCP clients only." }, + "bridgeErrorHandler": { "index": 14, "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 messages, or the likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, + "exceptionHandler": { "index": 15, "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 default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." }, + "exchangePattern": { "index": 16, "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." }, + "authorizationPolicy": { "index": 17, "kind": "parameter", "displayName": "Authorization Policy", "group": "security", "label": "consumer,security", "required": false, "type": "object", "javaType": "org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": "configuration", "description": "Reference to an org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls before the route runs. Set it on the component to guard every tool route by construction, or per endpoint to override. The policy authorizes on trustworthy input only: the tool name comes from the route (never from model output), and the caller identity is carried as an exchange property set before the agent ran (for example by camel-spiffe or camel-keycloak), which the model cannot set. Authorize on exchange properties or validated tokens only, never on message headers (on a tool route the headers carry the model-controlled tool arguments). A denied call surfaces to the model as a short refusal rather than a stack trace." } } } 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 a57c415026cdf..d45cec7224ba5 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 @@ -289,6 +289,62 @@ Supported options: `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, The `title` option maps to the MCP tool's top-level `title` field (not `ToolAnnotations.title`). Boolean hint options use nullable configuration: when unset, Camel omits them from the published MCP tool even though the component catalog may list `defaultValue: false` for documentation tooling. +=== Tool Arguments in the Body + +By default, tool arguments are exposed as individual headers and the message body is left unchanged. +Set `argumentsAsBody=true` to also populate the body with a JSON string containing the tool-call arguments. +This is useful when mapping a complete JSON document, for example with Kaoto Data Mapper. + +[source,yaml] +---- +- route: + from: + uri: ai-tool:place_order + parameters: + description: Place a merchandise order + argSchema: classpath:place-order.args.schema.json + argumentsAsBody: true + steps: + - to: + uri: direct:mapOrder +---- + +For a tool call with a customer name and two items, `direct:mapOrder` receives this JSON-string body: + +[source,json] +---- +{ + "customerName": "Alice", + "items": [ + { "sku": "TSHIRT", "quantity": 2 }, + { "sku": "MUG", "quantity": 1 } + ] +} +---- + +Nested objects, arrays and JSON value types are preserved. A call with no arguments produces `{}`. +The body and individual argument headers include only declared arguments, after the same allowlist filtering. +Camel logs a warning for each undeclared argument. Reserved names starting with `camel` or `org.apache.camel.` +(case-insensitive) are excluded from both the body and argument headers. Missing required arguments still reject +an invocation before the tool route runs. Required arguments are checked after reserved names are removed, +so a required argument with a reserved name rejects the call even if the model supplied it. +Rename such arguments to use names outside the reserved namespace. This option does not add full JSON Schema validation. + +=== Invocation Identifiers + +Tool routes receive `CamelAiToolCallId` and `CamelAiToolConversationId` when the calling adapter can supply them. +These headers are populated independently of `argumentsAsBody`. An unavailable identifier is omitted rather than generated. + +* LangChain4j Agent supplies the tool execution request ID and the memory ID (converted to a string) as the conversation ID when the agent uses memory. + Agents without memory omit the conversation header, including the framework's default memory ID. +* OpenAI supplies the tool-call ID. The Responses API also supplies its resolved conversation ID + (`CamelOpenAIConversationId` or the `conversationId` option); chat-completion callbacks omit it. +* Spring AI Chat supplies the `CamelSpringAiChatConversationId` from the calling exchange. Its tool callback does not supply a call ID. +* MCP server tool callbacks currently do not propagate these identifiers. + +For example, use `${header.CamelAiToolCallId}` to correlate route processing with a particular tool invocation. +Several calls in the same conversation can share `CamelAiToolConversationId` while having different call IDs. + === 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 a0f8d2a443ddd..086ee433ac847 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 @@ -62,6 +62,13 @@ public class AiToolConfiguration implements Cloneable { @Metadata(label = "consumer", supportFileReference = true, largeInput = true, inputLanguage = "json") private String argSchema; + @Metadata(label = "consumer") + @UriParam(defaultValue = "false", description = "Whether to populate the message body with the tool arguments " + + "as a JSON string, excluding undeclared arguments and reserved names. " + + "Declared argument headers remain available. " + + "When disabled, the message body is left unchanged.") + private boolean argumentsAsBody; + @Metadata(label = "consumer") @UriParam(description = "Tool output schema fields. " + "Format: outputParameter.NAME=TYPE, outputParameter.NAME.description=TEXT. " @@ -154,6 +161,14 @@ public void setArgSchema(String argSchema) { this.argSchema = argSchema; } + public boolean isArgumentsAsBody() { + return argumentsAsBody; + } + + public void setArgumentsAsBody(boolean argumentsAsBody) { + this.argumentsAsBody = argumentsAsBody; + } + public Map getOutputParameters() { return outputParameters; } diff --git a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConstants.java b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConstants.java new file mode 100644 index 0000000000000..3137494782b0b --- /dev/null +++ b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConstants.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.ai.tool; + +import org.apache.camel.spi.Metadata; + +/** + * Headers populated when an AI framework invokes a tool route. + */ +public final class AiToolConstants { + + @Metadata(label = "consumer", description = "The tool invocation identifier supplied by the calling framework. " + + "Absent when the framework does not supply one.", + javaType = "String") + public static final String TOOL_CALL_ID = "CamelAiToolCallId"; + + @Metadata(label = "consumer", description = "The conversation identifier supplied by the calling framework. " + + "Absent when the framework does not supply one.", + javaType = "String") + public static final String CONVERSATION_ID = "CamelAiToolConversationId"; + + private AiToolConstants() { + } +} diff --git a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolEndpoint.java b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolEndpoint.java index 13959fa36d63c..c6edab87148ff 100644 --- a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolEndpoint.java +++ b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolEndpoint.java @@ -41,6 +41,7 @@ syntax = "ai-tool:toolName", consumerOnly = true, remote = false, + headersClass = AiToolConstants.class, category = { Category.AI }) public class AiToolEndpoint extends DefaultEndpoint { 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 c86858e66283f..5b7c643ceb770 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 @@ -26,6 +26,7 @@ import org.apache.camel.Processor; import org.apache.camel.support.DefaultConsumer; import org.apache.camel.support.EventHelper; +import org.apache.camel.util.json.Jsoner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,11 +57,11 @@ private AiToolExecutor() { * Executes a Camel route tool by resolving the route processor from the spec's consumer, populating the exchange * with the provided arguments, and invoking the route. *

- * Arguments are validated against the tool's declared parameters (undeclared arguments are filtered out with a - * warning, required arguments are checked). Each argument is set as an individual exchange header so that route - * expressions like {@code ${header.city}} and SQL bindings work naturally. Argument names that start with - * {@code camel} or {@code org.apache.camel.} (case-insensitive) are rejected to prevent collision with internal - * Camel headers (following the same pattern as the A2A component). + * Arguments are checked against the tool's declared parameters (undeclared arguments are excluded from argument + * headers with a warning, required arguments are checked). Each declared argument is set as an individual exchange + * header so that route expressions like {@code ${header.city}} and SQL bindings work naturally. Argument names that + * start with {@code camel} or {@code org.apache.camel.} (case-insensitive) are rejected to prevent collision with + * internal Camel headers (following the same pattern as the A2A component). *

* The calling adapter obtains the exchange from {@link #createToolExchange(Exchange)} and passes it in. That copy * is not a pooled consumer exchange, so the adapter does not release it afterwards (there is no @@ -77,6 +78,22 @@ private AiToolExecutor() { * @return an {@link AiToolResult} classifying the outcome; never null */ public static AiToolResult execute(AiToolSpec spec, Map arguments, Exchange exchange) { + return execute(spec, arguments, exchange, null, null); + } + + /** + * Executes a tool route with the invocation identifiers supplied by the calling framework. Identifiers are exposed + * as headers independently of whether the tool populates arguments into the body. + * + * @param spec the tool specification + * @param arguments the tool arguments + * @param exchange the exchange to populate and execute + * @param toolCallId the tool invocation identifier, or {@code null} when unavailable + * @param conversationId the conversation identifier, or {@code null} when unavailable + * @return the outcome of the invocation + */ + public static AiToolResult execute( + AiToolSpec spec, Map arguments, Exchange exchange, String toolCallId, String conversationId) { String toolName = spec.getName(); DefaultConsumer consumer = spec.getConsumer(); @@ -118,33 +135,38 @@ public static AiToolResult execute(AiToolSpec spec, Map argument }); } + // Filter out Camel-internal names before required-argument validation so a discarded + // argument cannot satisfy a required parameter. + argsCopy.keySet().removeIf(name -> { + String lower = name.toLowerCase(Locale.ROOT); + if (lower.startsWith("camel") || lower.startsWith("org.apache.camel.")) { + LOG.warn("Rejecting tool argument '{}' for tool '{}' -- argument names starting with " + + "'Camel' or 'org.apache.camel.' are reserved for internal use", + name, toolName); + return true; + } + return false; + }); + 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", + LOG.warn("Missing required argument '{}' for tool '{}' after argument filtering", requiredName, toolName); IllegalArgumentException cause = new IllegalArgumentException( String.format("Missing required argument '%s' for tool '%s'", requiredName, toolName)); return new AiToolResult.ArgumentError(cause.getMessage(), cause); } } - // Set each argument as an exchange header so route expressions (${header.city}) - // and SQL bindings work naturally. Filter out Camel-internal names to prevent - // header-namespace injection (CVE-2025-27636 family). - for (Map.Entry entry : argsCopy.entrySet()) { - String name = entry.getKey(); - String lower = name.toLowerCase(Locale.ROOT); - if (lower.startsWith("camel") || lower.startsWith("org.apache.camel.")) { - LOG.warn("Rejecting tool argument '{}' for tool '{}' -- argument names starting with " - + "'Camel' or 'org.apache.camel.' are reserved for internal use", - name, toolName); - continue; - } - exchange.getMessage().setHeader(name, entry.getValue()); - } + // Set declared arguments as headers for route expressions and SQL bindings. + argsCopy.forEach(exchange.getMessage()::setHeader); + setInvocationHeader(exchange, AiToolConstants.TOOL_CALL_ID, toolCallId); + setInvocationHeader(exchange, AiToolConstants.CONVERSATION_ID, conversationId); // Execute the route try { + if (consumer.getEndpoint() instanceof AiToolEndpoint endpoint && endpoint.getConfiguration().isArgumentsAsBody()) { + exchange.getMessage().setBody(Jsoner.serialize(argsCopy)); + } routeProcessor.process(exchange); if (exchange.getException() != null) { @@ -172,6 +194,14 @@ public static AiToolResult execute(AiToolSpec spec, Map argument } } + private static void setInvocationHeader(Exchange exchange, String name, String value) { + if (value != null) { + exchange.getMessage().setHeader(name, value); + } else { + exchange.getMessage().removeHeader(name); + } + } + /** * Classifies an error from route execution as an authorization denial when a {@link CamelAuthorizationException} is * present in its cause chain (the route's {@link org.apache.camel.spi.AuthorizationPolicy} rejected the call). diff --git a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolArgumentsBodyTest.java b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolArgumentsBodyTest.java new file mode 100644 index 0000000000000..260468b675d06 --- /dev/null +++ b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolArgumentsBodyTest.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.ai.tool; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.support.DefaultExchange; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.camel.util.json.Jsoner; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class AiToolArgumentsBodyTest extends CamelTestSupport { + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("ai-tool:order?tags=test&argumentsAsBody=true&argSchema=classpath:arg-schemas/create-order.json") + .setProperty("invoked", constant(true)); + from("ai-tool:defaultBody?tags=test¶meter.name=string") + .setProperty("invoked", constant(true)); + from("ai-tool:disabledBody?tags=test&argumentsAsBody=false¶meter.name=string") + .setProperty("invoked", constant(true)); + from("ai-tool:empty?tags=test&argumentsAsBody=true") + .setProperty("invoked", constant(true)); + from("ai-tool:reserved?tags=test&argumentsAsBody=true¶meter.safe=string" + + "¶meter.CamelAiToolCallId=string¶meter.camelAiToolConversationId=string") + .setProperty("invoked", constant(true)); + for (boolean argumentsAsBody : List.of(false, true)) { + from("ai-tool:requiredReserved" + argumentsAsBody + "?tags=test&argumentsAsBody=" + argumentsAsBody + + "¶meter.camelCase=string¶meter.camelCase.required=true") + .setProperty("invoked", constant(true)); + } + } + }; + } + + @Test + void serializesNestedArgumentsAndKeepsHeaders() throws Exception { + Map customer = new HashMap<>(Map.of("id", "Alice \"A\"", "active", true, "balance", 12.5)); + customer.put("note", null); + List> items = List.of(Map.of("sku", "TSHIRT", "qty", 2), Map.of("sku", "MUG", "qty", 1)); + Map arguments = Map.of("customer", customer, "items", items, "price", 999); + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setBody("previous body"); + + AiToolResult result = AiToolExecutor.execute(spec("order"), arguments, exchange, "call-1", "conversation-1"); + + assertThat(result).isInstanceOf(AiToolResult.Success.class); + assertThat(exchange.getMessage().getBody()).isInstanceOf(String.class); + assertThat(Jsoner.deserialize(exchange.getMessage().getBody(String.class))).isEqualTo(Jsoner.deserialize(""" + {"customer":{"id":"Alice \\"A\\"","active":true,"balance":12.5,"note":null}, + "items":[{"sku":"TSHIRT","qty":2},{"sku":"MUG","qty":1}]} + """)); + assertThat(exchange.getMessage().getHeader("customer")).isSameAs(customer); + assertThat(exchange.getMessage().getHeader("items")).isSameAs(items); + assertThat(exchange.getMessage().getHeaders()).doesNotContainKey("price"); + assertThat(arguments).containsEntry("price", 999); + assertThat(exchange.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID)).isEqualTo("call-1"); + assertThat(exchange.getMessage().getHeader(AiToolConstants.CONVERSATION_ID)).isEqualTo("conversation-1"); + } + + @Test + void leavesBodyUnchangedByDefaultAndStillPropagatesIdentifiers() { + assertBodyUnchanged("defaultBody"); + } + + @Test + void leavesBodyUnchangedWhenExplicitlyDisabled() { + assertBodyUnchanged("disabledBody"); + } + + private void assertBodyUnchanged(String toolName) { + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setBody("original"); + + AiToolResult result = AiToolExecutor.execute(spec(toolName), Map.of("name", "Alice"), exchange, "call-2", "chat-2"); + + assertThat(result).isInstanceOf(AiToolResult.Success.class); + assertThat(exchange.getMessage().getBody()).isEqualTo("original"); + assertThat(exchange.getMessage().getHeader("name")).isEqualTo("Alice"); + assertThat(exchange.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID)).isEqualTo("call-2"); + assertThat(exchange.getMessage().getHeader(AiToolConstants.CONVERSATION_ID)).isEqualTo("chat-2"); + } + + @Test + void serializesAnEmptyObjectForNullArguments() { + Exchange exchange = new DefaultExchange(context); + + AiToolResult result = AiToolExecutor.execute(spec("empty"), null, exchange); + + assertThat(result).isInstanceOf(AiToolResult.Success.class); + assertThat(exchange.getMessage().getBody()).isEqualTo("{}"); + assertThat(exchange.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID)).isNull(); + assertThat(exchange.getMessage().getHeader(AiToolConstants.CONVERSATION_ID)).isNull(); + } + + @Test + void filtersUndeclaredArgumentsFromBodyAndHeaders() throws Exception { + Map arguments = Map.of("extra", List.of("one", "two"), "CamelFileName", "forged"); + Exchange exchange = new DefaultExchange(context); + + AiToolResult result = AiToolExecutor.execute(spec("empty"), arguments, exchange); + + assertThat(result).isInstanceOf(AiToolResult.Success.class); + assertThat(Jsoner.deserialize(exchange.getMessage().getBody(String.class))) + .isEqualTo(Jsoner.deserialize("{}")); + assertThat(exchange.getMessage().getHeaders()).doesNotContainKeys("extra", "CamelFileName"); + assertThat(arguments).hasSize(2); + } + + @Test + void doesNotAllowArgumentsToOverrideInvocationIdentifiers() throws Exception { + Map arguments = Map.of("safe", "ok", "CamelAiToolCallId", "forged-call", + "camelAiToolConversationId", "forged-chat"); + Exchange exchange = new DefaultExchange(context); + + AiToolResult result = AiToolExecutor.execute(spec("reserved"), arguments, exchange, "real-call", "real-chat"); + + assertThat(result).isInstanceOf(AiToolResult.Success.class); + assertThat(Jsoner.deserialize(exchange.getMessage().getBody(String.class))) + .isEqualTo(Jsoner.deserialize("{\"safe\":\"ok\"}")); + assertThat(exchange.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID)).isEqualTo("real-call"); + assertThat(exchange.getMessage().getHeader(AiToolConstants.CONVERSATION_ID)).isEqualTo("real-chat"); + assertThat(arguments).hasSize(3); + } + + @Test + void removesStaleIdentifiersWhenNoIdentifiersAreSupplied() { + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setHeader(AiToolConstants.TOOL_CALL_ID, "stale-call"); + exchange.getMessage().setHeader(AiToolConstants.CONVERSATION_ID, "stale-chat"); + + AiToolExecutor.execute(spec("empty"), Map.of(), exchange); + + assertThat(exchange.getMessage().getHeaders()).doesNotContainKeys( + AiToolConstants.TOOL_CALL_ID, AiToolConstants.CONVERSATION_ID); + } + + @Test + void rejectsMissingRequiredArgumentsBeforeInvokingTheRoute() { + Exchange exchange = new DefaultExchange(context); + + AiToolResult result = AiToolExecutor.execute(spec("order"), Map.of("items", List.of()), exchange); + + assertThat(result).isInstanceOf(AiToolResult.ArgumentError.class); + assertThat(exchange.getProperty("invoked")).isNull(); + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void rejectsRequiredReservedArgumentsBeforeInvokingTheRoute(boolean argumentsAsBody) { + String toolName = "requiredReserved" + argumentsAsBody; + Exchange exchange = new DefaultExchange(context); + exchange.getMessage().setBody("original body"); + + AiToolResult result = AiToolExecutor.execute(spec(toolName), Map.of("camelCase", "value"), exchange); + + assertThat(result).isInstanceOfSatisfying(AiToolResult.ArgumentError.class, + error -> assertThat(error.message()) + .isEqualTo("Missing required argument 'camelCase' for tool '" + toolName + "'")); + assertThat(exchange.getProperty("invoked")).isNull(); + assertThat(exchange.getMessage().getHeader("camelCase")).isNull(); + assertThat(exchange.getMessage().getBody()).isEqualTo("original body"); + } + + private AiToolSpec spec(String name) { + return AiToolRegistry.getOrCreate(context).getToolsByTag("test").stream() + .filter(tool -> tool.getName().equals(name)).findFirst().orElseThrow(); + } +} diff --git a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java index b850b9f3f1951..823d4d287a762 100644 --- a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java +++ b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java @@ -179,7 +179,7 @@ public void process(Exchange exchange) throws Exception { AiAgentBody aiAgentBody = exchange.getMessage().getMandatoryBody(AiAgentBody.class); - ToolProvider toolProvider = createComposedToolProvider(tags, exchange); + ToolProvider toolProvider = createComposedToolProvider(tags, exchange, agent instanceof AgentWithMemory); Object chatModel = resolveChatModel(agent); GenAiObservationContext observationContext = GenAiObservationContext.builder() .operationName(GenAiOperationName.GENERATE_CONTENT) @@ -301,15 +301,15 @@ private static void applyEndpointToolCallingOptions( * Supports dynamic filtering via exchange headers: *

    *
  • {@link Headers#EXCLUDE_TAGS} — exclude Camel tools by tag (handled in - * {@link #createCamelToolProvider(String, Exchange)})
  • + * {@link #createCamelToolProvider(String, Exchange, boolean)}) *
  • {@link Headers#EXCLUDE_MCP_SERVERS} — exclude MCP servers by key/name
  • *
*/ - private ToolProvider createComposedToolProvider(String tags, Exchange exchange) { + private ToolProvider createComposedToolProvider(String tags, Exchange exchange, boolean hasMemory) { List providers = new ArrayList<>(); // 1. Camel route tools from tags (handles EXCLUDE_TAGS header internally) - ToolProvider camelToolProvider = createCamelToolProvider(tags, exchange); + ToolProvider camelToolProvider = createCamelToolProvider(tags, exchange, hasMemory); if (camelToolProvider != null) { providers.add(camelToolProvider); } @@ -379,7 +379,7 @@ private BiPredicate resolveMcpToolProviderFilter() * Creates a tool provider for Camel route tools discovered by tags. If the {@link Headers#EXCLUDE_TAGS} header is * set on the exchange, the specified tags (comma-separated) are excluded from discovery. */ - private ToolProvider createCamelToolProvider(String tags, Exchange exchange) { + private ToolProvider createCamelToolProvider(String tags, Exchange exchange, boolean hasMemory) { if (tags == null || tags.trim().isEmpty()) { return null; } @@ -403,21 +403,21 @@ private ToolProvider createCamelToolProvider(String tags, Exchange exchange) { if (!aiTools.isEmpty()) { LOG.debug("Creating AI Service with {} ai-tool tools for tags: {}", aiTools.size(), tags); - return buildToolProvider(aiTools, exchange); + return buildToolProvider(aiTools, exchange, hasMemory); } else { LOG.debug("No tools found for tags: {}, using simple AI Service", tags); return null; } } - private ToolProvider buildToolProvider(Map aiTools, Exchange exchange) { + private ToolProvider buildToolProvider(Map aiTools, Exchange exchange, boolean hasMemory) { return (ToolProviderRequest toolProviderRequest) -> { ToolProviderResult.Builder resultBuilder = ToolProviderResult.builder(); for (Map.Entry entry : aiTools.entrySet()) { AiToolSpec spec = entry.getValue(); ToolSpecification toolSpec = AiToolSpecToLangChain4j.toToolSpecification(spec); - addToolToResult(resultBuilder, spec, toolSpec, exchange); + addToolToResult(resultBuilder, spec, toolSpec, exchange, hasMemory); } return resultBuilder.build(); @@ -439,7 +439,7 @@ private void addToolToResult( ToolProviderResult.Builder resultBuilder, AiToolSpec spec, ToolSpecification toolSpec, - Exchange exchange) { + Exchange exchange, boolean hasMemory) { ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> { Map arguments = parseArguments(toolExecutionRequest); @@ -450,7 +450,8 @@ private void addToolToResult( // not leak into the calling producer exchange, while the caller's context (properties/variables) reaches // the tool route. Shared across the route-tool runtimes so they cannot drift (CAMEL-24832, CAMEL-23944). Exchange toolExchange = AiToolExecutor.createToolExchange(exchange); - AiToolResult result = AiToolExecutor.execute(spec, arguments, toolExchange); + AiToolResult result = AiToolExecutor.execute(spec, arguments, toolExchange, + toolExecutionRequest.id(), hasMemory && memoryId != null ? memoryId.toString() : null); return toToolResponse(spec.getName(), result); }; diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentExecuteToolsConcurrentlyTest.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentExecuteToolsConcurrentlyTest.java index 90fe96527fabe..db2d1d007890c 100644 --- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentExecuteToolsConcurrentlyTest.java +++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentExecuteToolsConcurrentlyTest.java @@ -17,17 +17,22 @@ package org.apache.camel.component.langchain4j.agent; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.response.ChatResponse; import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.ai.tool.AiToolConstants; import org.apache.camel.component.langchain4j.agent.api.AgentConfiguration; import org.apache.camel.component.langchain4j.agent.api.AiAgentBody; import org.apache.camel.impl.DefaultCamelContext; @@ -35,6 +40,8 @@ import org.apache.camel.test.junit6.CamelTestSupport; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; @@ -50,6 +57,7 @@ class LangChain4jAgentExecuteToolsConcurrentlyTest extends CamelTestSupport { private CountDownLatch bothToolsEntered = new CountDownLatch(2); private final AtomicInteger inFlight = new AtomicInteger(); private final AtomicInteger maxConcurrent = new AtomicInteger(); + private final Map toolConversations = new ConcurrentHashMap<>(); @BeforeEach void resetConcurrencyState() { @@ -57,6 +65,7 @@ void resetConcurrencyState() { bothToolsEntered = new CountDownLatch(2); maxConcurrent.set(0); inFlight.set(0); + toolConversations.clear(); } @Override @@ -67,6 +76,9 @@ public void configure() { from("direct:agent") .to("langchain4j-agent:test?agentConfiguration=#agentConfig&tags=" + TAG); + from("direct:agentWithMemory") + .to("langchain4j-agent:test?agentConfiguration=#memoryConfig&tags=" + TAG); + from("ai-tool:slowToolA?tags=" + TAG + "&description=Slow tool A") .process(exchange -> recordConcurrentEntry("A", exchange)); @@ -82,21 +94,39 @@ protected void bindToRegistry(Registry registry) { .withChatModel(chatModel) .withExecuteToolsConcurrently() .withMaxToolCallingRoundTrips(5)); + registry.bind("memoryConfig", new AgentConfiguration() + .withChatModel(chatModel) + .withChatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20)) + .withExecuteToolsConcurrently() + .withMaxToolCallingRoundTrips(5)); } @Test void concurrentToolExecutionRunsBothToolsInParallel() throws Exception { - String response = template.requestBody("direct:agent", new AiAgentBody<>("run tools"), String.class); + String response = template.requestBody("direct:agent", + new AiAgentBody<>("run tools").withMemoryId("parallel-conversation"), String.class); assertThat(response).isEqualTo("done"); assertThat(bothToolsEntered.await(10, TimeUnit.SECONDS)) .as("both tools should be invoked") .isTrue(); + assertThat(toolConversations).as("memory-less agents must not propagate the default conversation ID").isEmpty(); assertThat(maxConcurrent.get()) .as("tools should overlap when executeToolsConcurrently is enabled") .isGreaterThanOrEqualTo(2); } + @ParameterizedTest + @ValueSource(strings = { "conversation-123", "default" }) + void concurrentCallsHaveDistinctCallIdsAndShareTheConversationId(String conversationId) { + String response = template.requestBody("direct:agentWithMemory", + new AiAgentBody<>("run tools").withMemoryId(conversationId), String.class); + + assertThat(response).isEqualTo("done"); + assertThat(toolConversations).containsExactlyInAnyOrderEntriesOf( + Map.of("a", conversationId, "b", conversationId)); + } + @Test void producerResolvesCamelManagedExecutorWhenNoneConfigured() throws Exception { try (DefaultCamelContext ctx = new DefaultCamelContext()) { @@ -129,10 +159,7 @@ public void configure() { @Test void managedExecutorWorksAfterContextStopAndRestart() throws Exception { context.stop(); - context.getRegistry().bind("agentConfig", new AgentConfiguration() - .withChatModel(chatModel) - .withExecuteToolsConcurrently() - .withMaxToolCallingRoundTrips(5)); + bindToRegistry(context.getRegistry()); context.start(); String response = template.requestBody("direct:agent", new AiAgentBody<>("run tools"), String.class); @@ -141,6 +168,12 @@ void managedExecutorWorksAfterContextStopAndRestart() throws Exception { } private void recordConcurrentEntry(String label, Exchange exchange) throws InterruptedException { + String callId = exchange.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID, String.class); + assertThat(callId).isEqualTo(label.toLowerCase(Locale.ROOT)); + String conversationId = exchange.getMessage().getHeader(AiToolConstants.CONVERSATION_ID, String.class); + if (conversationId != null) { + toolConversations.put(callId, conversationId); + } int concurrent = inFlight.incrementAndGet(); maxConcurrent.updateAndGet(current -> Math.max(current, concurrent)); bothToolsEntered.countDown(); diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java index 2325d1b4b42da..d75ef0391c461 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java @@ -112,6 +112,12 @@ protected void doStop() throws Exception { * @throws Exception when a tool call fails and the configured strategy is to fail the exchange */ List execute(List toolCalls, Exchange callingExchange) throws Exception { + return execute(toolCalls, callingExchange, null); + } + + List execute( + List toolCalls, Exchange callingExchange, String conversationId) + throws Exception { if (toolCalls.isEmpty()) { return List.of(); } @@ -124,16 +130,17 @@ List execute(List toolCalls, Exchange if (executorService == null || toolCalls.size() == 1) { List results = new ArrayList<>(toolCalls.size()); for (ChatCompletionMessageToolCall toolCall : toolCalls) { - results.add(executeOne(toolCall, toolState, callingExchange)); + results.add(executeOne(toolCall, toolState, callingExchange, conversationId)); } return results; } - return executeParallel(toolCalls, toolState, callingExchange); + return executeParallel(toolCalls, toolState, callingExchange, conversationId); } private List executeParallel( - List toolCalls, McpToolState toolState, Exchange callingExchange) + List toolCalls, McpToolState toolState, Exchange callingExchange, + String conversationId) throws Exception { LOG.debug("Executing {} tool call(s) in parallel", toolCalls.size()); @@ -142,7 +149,8 @@ private List executeParallel( List> futures = new ArrayList<>(toolCalls.size()); for (ChatCompletionMessageToolCall toolCall : toolCalls) { - futures.add(executorService.submit(withMdc(mdc, () -> executeOne(toolCall, toolState, callingExchange)))); + futures.add(executorService + .submit(withMdc(mdc, () -> executeOne(toolCall, toolState, callingExchange, conversationId)))); } long timeout = endpoint.getConfiguration().getParallelToolTimeout(); @@ -214,7 +222,8 @@ private static Callable withMdc(Map mdc, Callable input, - String model) + String model, String conversationId) throws Exception { // a stored conversation keeps every item it receives, so after the first request only the new items are sent, // while otherwise each request carries the whole conversation of the exchange @@ -273,7 +274,7 @@ private void processToolLoop( functionCalls.forEach(call -> toolCallsLog.add(call.name())); List toolCalls = OpenAIResponsesSupport.toChatToolCalls(functionCalls); - List results = toolCallExecutor.execute(toolCalls, exchange); + List results = toolCallExecutor.execute(toolCalls, exchange, conversationId); observability.recordIteration( modelCall, iterationStartNanos, iterationPromptTokens, iterationCompletionTokens, toolCalls, results); diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java index f30e54b976585..02835115a3d06 100644 --- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java @@ -31,6 +31,7 @@ import io.modelcontextprotocol.spec.McpSchema; import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.ai.tool.AiToolConstants; import org.apache.camel.component.ai.tool.AiToolRegistry; import org.apache.camel.component.ai.tool.AiToolSpec; import org.apache.camel.impl.DefaultCamelContext; @@ -309,11 +310,16 @@ void routeToolSeesCallerExchangePropertyAndGetsACleanMessage() throws Exception // authenticated subject (kept as an exchange property) and the model cannot forge it; and the tool route gets a // clean message, so a tool that sets no body returns "No result" rather than the caller's prompt. AtomicReference seenSubject = new AtomicReference<>(); + AtomicReference seenCallId = new AtomicReference<>(); context.addRoutes(new RouteBuilder() { @Override public void configure() { from("ai-tool:checkSubject?tags=test&description=Check the caller subject") - .process(e -> seenSubject.set(e.getProperty("CamelAuthenticatedSubject"))); + .process(e -> { + seenSubject.set(e.getProperty("CamelAuthenticatedSubject")); + seenCallId.set(e.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID, String.class)); + assertThat(e.getMessage().getHeader(AiToolConstants.CONVERSATION_ID)).isNull(); + }); } }); @@ -340,11 +346,36 @@ public void configure() { // the tool route saw the caller's authenticated subject: the CALLING exchange was copied in, not a fresh one assertThat(seenSubject).hasValue("alice"); + assertThat(seenCallId).hasValue("id-x"); // and the message was clean: a tool that sets no body returns "No result", not the caller's prompt assertThat(results).singleElement() .satisfies(r -> assertThat(r.content()).isEqualTo("No result")); } + @Test + void parallelRouteToolsShareTheSuppliedConversationId() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("ai-tool:conversation?tags=conversation-test") + .process(e -> { + assertThat(e.getMessage().getHeader(AiToolConstants.CONVERSATION_ID)).isEqualTo("conv_parallel"); + e.getMessage().setBody(e.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID)); + }); + } + }); + AiToolSpec spec = AiToolRegistry.getOrCreate(context).getToolsByTag("conversation-test").iterator().next(); + OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of(), Set.of()); + endpoint.setMcpToolState(new McpToolState(List.of(), Map.of(), Map.of(), Set.of(), Map.of("conversation", spec))); + executor = startExecutor(endpoint); + + List results = executor.execute( + List.of(toolCall("call-a", "conversation"), toolCall("call-b", "conversation")), + new DefaultExchange(context), "conv_parallel"); + + assertThat(results).extracting(McpToolCallExecutor.ToolResult::content).containsExactly("call-a", "call-b"); + } + // ------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------ diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesMockTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesMockTest.java index 389bee95cb2a6..307ad790b7300 100644 --- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesMockTest.java +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesMockTest.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,10 +26,13 @@ import com.openai.models.responses.ResponseCreateParams; import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.ai.tool.AiToolConstants; import org.apache.camel.test.infra.openai.mock.OpenAIMock; import org.apache.camel.test.junit6.CamelTestSupport; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -37,6 +41,9 @@ class OpenAIResponsesMockTest extends CamelTestSupport { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final AtomicReference toolConversationId = new AtomicReference<>(); + private final AtomicReference toolCallId = new AtomicReference<>(); + @RegisterExtension public OpenAIMock openAIMock = new OpenAIMock().builder() .when("hello-responses") @@ -136,6 +143,10 @@ public void configure() { from("ai-tool:get_weather?tags=responses-tools&description=Get the weather for a city" + "¶meter.city=string¶meter.city.required=true") + .process(e -> { + toolConversationId.set(e.getMessage().getHeader(AiToolConstants.CONVERSATION_ID, String.class)); + toolCallId.set(e.getMessage().getHeader(AiToolConstants.TOOL_CALL_ID, String.class)); + }) .setBody(simple("Sunny in ${header.city}")); from("ai-tool:lookup_order?tags=responses-direct&description=Look up an order" @@ -299,6 +310,8 @@ void routeToolsRunInTheToolLoop() { assertThat(result.getException()).isNull(); assertThat(result.getMessage().getBody(String.class)).isEqualTo("It is sunny in Rome"); + assertThat(toolConversationId).hasNullValue(); + assertThat(toolCallId.get()).isNotBlank(); assertThat(result.getMessage().getHeader(OpenAIConstants.TOOL_ITERATIONS, Integer.class)).isEqualTo(1); assertThat(result.getMessage().getHeader(OpenAIConstants.MCP_TOOL_CALLS, List.class)).containsExactly("get_weather"); @@ -399,6 +412,8 @@ void toolLoopInAStoredConversationOnlySendsNewItems() { var requests = openAIMock.getReceivedRequests(); assertThat(requests).hasSize(2); assertThat(requests.get(0).bodyAsJson().path("conversation").asText()).isEqualTo("conv_123"); + assertThat(toolConversationId).hasValue("conv_123"); + assertThat(toolCallId.get()).isNotBlank(); JsonNode followUp = requests.get(1).bodyAsJson(); assertThat(followUp.path("conversation").asText()).isEqualTo("conv_123"); // the server already stored the user input and the function calls, so only the tool result is sent @@ -407,6 +422,21 @@ void toolLoopInAStoredConversationOnlySendsNewItems() { assertThat(followUp.path("input").path(0).path("output").asText()).isEqualTo("Sunny in Rome"); } + @ParameterizedTest + @ValueSource(strings = { "direct:responses-route-tools", "direct:responses-conversation-tools" }) + void propagatesTheConversationIdFromTheHeader(String route) { + Exchange result = template.request(route, e -> { + e.getIn().setBody("weather-tool"); + e.getIn().setHeader(OpenAIConstants.CONVERSATION_ID, "conv_header"); + }); + + assertThat(result.getException()).isNull(); + assertThat(toolConversationId).hasValue("conv_header"); + assertThat(toolCallId.get()).isNotBlank(); + assertThat(openAIMock.getReceivedRequests()) + .allSatisfy(request -> assertThat(request.bodyAsJson().path("conversation").asText()).isEqualTo("conv_header")); + } + @Test void maxToolIterationsLimitsModelCallsAsChatCompletionDoes() { Exchange result = template.request("direct:responses-one-tool-iteration", e -> e.getIn().setBody("weather-tool")); diff --git a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java index 90a221d9ebc47..b014e3c7033ad 100644 --- a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java +++ b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java @@ -46,7 +46,8 @@ static ToolCallback toToolCallback(AiToolSpec spec, Exchange callingExchange) { // exchange property) reaches the tool route; this is not a pooled consumer exchange, so it is not released // here (CAMEL-24832) Exchange toolExchange = AiToolExecutor.createToolExchange(callingExchange); - AiToolResult result = AiToolExecutor.execute(spec, args, toolExchange); + String conversationId = callingExchange.getMessage().getHeader(SpringAiChatConstants.CONVERSATION_ID, String.class); + AiToolResult result = AiToolExecutor.execute(spec, args, toolExchange, null, conversationId); if (result instanceof AiToolResult.Success success) { return success.value(); } else if (result instanceof AiToolResult.ArgumentError argErr) { diff --git a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/apache/camel/component/springai/chat/AiToolInvocationTest.java b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/apache/camel/component/springai/chat/AiToolInvocationTest.java new file mode 100644 index 0000000000000..7f3794e24bae1 --- /dev/null +++ b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/apache/camel/component/springai/chat/AiToolInvocationTest.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.component.springai.chat; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.ai.tool.AiToolConstants; +import org.apache.camel.component.ai.tool.AiToolRegistry; +import org.apache.camel.component.ai.tool.AiToolSpec; +import org.apache.camel.support.DefaultExchange; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.camel.util.json.Jsoner; +import org.junit.jupiter.api.Test; +import org.springframework.ai.tool.ToolCallback; + +import static org.assertj.core.api.Assertions.assertThat; + +class AiToolInvocationTest extends CamelTestSupport { + + private final AtomicReference toolExchange = new AtomicReference<>(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("ai-tool:weather?tags=test¶meter.city=string&argumentsAsBody=true") + .setProperty("inputBody", body()) + .process(toolExchange::set) + .setBody(constant("sunny")); + } + }; + } + + @Test + void propagatesConversationIdAndJsonArguments() throws Exception { + Exchange caller = new DefaultExchange(context); + caller.getMessage().setHeader(SpringAiChatConstants.CONVERSATION_ID, "conversation-123"); + ToolCallback callback = AiToolSpecToSpringAi.toToolCallback(spec(), caller); + + assertThat(callback.call("{\"city\":\"Rome\"}")).isEqualTo("\"sunny\""); + assertThat(toolExchange.get().getMessage().getHeader(AiToolConstants.CONVERSATION_ID)) + .isEqualTo("conversation-123"); + assertThat(toolExchange.get().getMessage().getHeader(AiToolConstants.TOOL_CALL_ID)).isNull(); + assertThat(toolExchange.get().getMessage().getHeader("city")).isEqualTo("Rome"); + assertThat(Jsoner.deserialize(toolExchange.get().getProperty("inputBody", String.class))) + .isEqualTo(Jsoner.deserialize("{\"city\":\"Rome\"}")); + } + + @Test + void omitsUnavailableIdentifiers() { + ToolCallback callback = AiToolSpecToSpringAi.toToolCallback(spec(), new DefaultExchange(context)); + + assertThat(callback.call("{\"city\":\"Rome\"}")).isEqualTo("\"sunny\""); + assertThat(toolExchange.get().getMessage().getHeaders()).doesNotContainKeys( + AiToolConstants.TOOL_CALL_ID, AiToolConstants.CONVERSATION_ID); + } + + private AiToolSpec spec() { + return AiToolRegistry.getOrCreate(context).getToolsByTag("test").iterator().next(); + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index d1dacb6986882..d15c3d8ed894e 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -13,6 +13,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. == Upgrading from 4.22.2 to 4.22.3 +=== camel-ai-tool - required arguments with reserved names reject the call + +Required tool arguments whose names start with `camel` or `org.apache.camel.` (case-insensitive) +now reject the invocation before the tool route runs. Previously, these arguments could pass the +required-argument check and then be discarded by the reserved-name filter, leaving the route without +a required value. Rename these parameters and update their `parameter.*` or `argSchema` declarations. + === camel-azure-eventhubs - an event that completes when the checkpoint timer of its window has run is checkpointed With `checkpointBatchTimeout`, the consumer now starts a new checkpoint window when the timer of the current window diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 46379cf15944b..661f6204023cd 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -13,6 +13,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. == Upgrading Camel 4.23 to 4.24 +=== camel-ai-tool - required arguments with reserved names reject the call + +Required tool arguments whose names start with `camel` or `org.apache.camel.` (case-insensitive) +now reject the invocation before the tool route runs. Previously, these arguments could pass the +required-argument check and then be discarded by the reserved-name filter, leaving the route without +a required value. Rename these parameters and update their `parameter.*` or `argSchema` declarations. + === Java *Breaking change:* Camel 4.24 requires *Java 21* as the minimum runtime version. 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 658e4f8d9102b..39e2958c56b48 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 @@ -74,6 +74,26 @@ default AiToolComponentBuilder argSchema(java.lang.String argSchema) { } + /** + * Whether to populate the message body with the tool arguments as a + * JSON string, excluding undeclared arguments and reserved names. + * Declared argument headers remain available. When disabled, the + * message body is left unchanged. + * + * The option is a: <code>boolean</code> type. + * + * Default: false + * Group: consumer + * + * @param argumentsAsBody the value to set + * @return the dsl builder + */ + default AiToolComponentBuilder argumentsAsBody(boolean argumentsAsBody) { + doSetProperty("argumentsAsBody", argumentsAsBody); + return this; + } + + /** * Allows for bridging the consumer to the Camel routing Error Handler, * which mean any exceptions (if possible) occurred while the Camel @@ -392,6 +412,7 @@ protected boolean setPropertyOnComponent( Object value) { switch (name) { case "argSchema": getOrCreateConfiguration((AiToolComponent) component).setArgSchema((java.lang.String) value); return true; + case "argumentsAsBody": getOrCreateConfiguration((AiToolComponent) component).setArgumentsAsBody((boolean) 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/EndpointHeaderBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java index 1829e977cd1af..507e46a89f832 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/EndpointHeaderBuilders.java @@ -79,6 +79,20 @@ public static ActiveMQEndpointBuilderFactory.ActiveMQHeaderNameBuilder activemq( public static ActiveMQ6EndpointBuilderFactory.ActiveMQ6HeaderNameBuilder activemq6() { return ActiveMQ6EndpointBuilderFactory.ActiveMQ6HeaderNameBuilder.INSTANCE; } + /** + * AI Tool (camel-ai-tool) + * Framework-agnostic consumer endpoint that registers a Camel route as an + * LLM tool in the shared AiToolRegistry. + * + * Category: ai + * Since: 4.22 + * Maven coordinates: org.apache.camel:camel-ai-tool + * + * @return the dsl builder for the headers' name. + */ + public static AiToolEndpointBuilderFactory.AiToolHeaderNameBuilder aiTool() { + return AiToolEndpointBuilderFactory.AiToolHeaderNameBuilder.INSTANCE; + } /** * Alibaba EventBridge (camel-alibaba-eventbridge) * Publish events to Alibaba Cloud EventBridge. 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 9b46adc52f2ee..206d4a0b1ac88 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 @@ -65,6 +65,42 @@ default AiToolEndpointBuilder argSchema(String argSchema) { doSetProperty("argSchema", argSchema); return this; } + /** + * Whether to populate the message body with the tool arguments as a + * JSON string, excluding undeclared arguments and reserved names. + * Declared argument headers remain available. When disabled, the + * message body is left unchanged. + * + * The option is a: boolean type. + * + * Default: false + * Group: consumer + * + * @param argumentsAsBody the value to set + * @return the dsl builder + */ + default AiToolEndpointBuilder argumentsAsBody(boolean argumentsAsBody) { + doSetProperty("argumentsAsBody", argumentsAsBody); + return this; + } + /** + * Whether to populate the message body with the tool arguments as a + * JSON string, excluding undeclared arguments and reserved names. + * Declared argument headers remain available. When disabled, the + * message body is left unchanged. + * + * The option will be converted to a boolean type. + * + * Default: false + * Group: consumer + * + * @param argumentsAsBody the value to set + * @return the dsl builder + */ + default AiToolEndpointBuilder argumentsAsBody(String argumentsAsBody) { + doSetProperty("argumentsAsBody", argumentsAsBody); + return this; + } /** * Human-readable description of what this tool does. Passed verbatim to * the LLM; be precise and action-oriented. When omitted, defaults to @@ -575,6 +611,20 @@ default AdvancedAiToolEndpointBuilder exchangePattern(String exchangePattern) { } public interface AiToolBuilders { + /** + * AI Tool (camel-ai-tool) + * Framework-agnostic consumer endpoint that registers a Camel route as + * an LLM tool in the shared AiToolRegistry. + * + * Category: ai + * Since: 4.22 + * Maven coordinates: org.apache.camel:camel-ai-tool + * + * @return the dsl builder for the headers' name. + */ + default AiToolHeaderNameBuilder aiTool() { + return AiToolHeaderNameBuilder.INSTANCE; + } /** * AI Tool (camel-ai-tool) * Framework-agnostic consumer endpoint that registers a Camel route as @@ -621,6 +671,43 @@ default AiToolEndpointBuilder aiTool(String componentName, String path) { } } + /** + * The builder of headers' name for the AI Tool component. + */ + public static class AiToolHeaderNameBuilder { + /** + * The internal instance of the builder used to access to all the + * methods representing the name of headers. + */ + public static final AiToolHeaderNameBuilder INSTANCE = new AiToolHeaderNameBuilder(); + + /** + * The tool invocation identifier supplied by the calling framework. + * Absent when the framework does not supply one. + * + * The option is a: {@code String} type. + * + * Group: consumer + * + * @return the name of the header {@code AiToolCallId}. + */ + public String aiToolCallId() { + return "CamelAiToolCallId"; + } + /** + * The conversation identifier supplied by the calling framework. Absent + * when the framework does not supply one. + * + * The option is a: {@code String} type. + * + * Group: consumer + * + * @return the name of the header {@code AiToolConversationId}. + */ + public String aiToolConversationId() { + return "CamelAiToolConversationId"; + } + } static AiToolEndpointBuilder endpointBuilder(String componentName, String path) { class AiToolEndpointBuilderImpl extends AbstractEndpointBuilder implements AiToolEndpointBuilder, AdvancedAiToolEndpointBuilder { public AiToolEndpointBuilderImpl(String path) {