This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26071-bedrock-agent-runtime in repository https://gitbox.apache.org/repos/asf/camel.git
commit a0bab705ff58d4e21a7b6ccdc5bf13cc6adca790 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 16:19:55 2026 +0100 CAMEL-26071: camel-aws-bedrock - aws-bedrock-agent-runtime: invokeFlow converts the body, and the operation header works on an endpoint of another operation invokeFlow failed for a byte[], stream or file body; it now converts the body to the text of the input node, as invokeAgent does. The async client that invokeFlow, invokeAgent and invokeInlineAgent need was only created for an endpoint of these operations, so picking one with the operation header failed; it is now created on first use. No operation fails with a message instead of a NullPointerException. modelId and knowledgeBaseId are no longer marked required, the citations header type is List<Citation>, and the doc names the right client. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../components/aws-bedrock-agent-runtime.json | 12 +- .../docs/aws-bedrock-agent-runtime-component.adoc | 12 +- .../agentruntime/aws-bedrock-agent-runtime.json | 12 +- .../docs/aws-bedrock-agent-runtime-component.adoc | 12 +- .../BedrockAgentRuntimeConfiguration.java | 6 +- .../agentruntime/BedrockAgentRuntimeConstants.java | 2 +- .../agentruntime/BedrockAgentRuntimeEndpoint.java | 37 +++++- .../agentruntime/BedrockAgentRuntimeProducer.java | 51 +++---- .../BedrockAgentRuntimeEndpointTest.java | 38 ++++++ .../BedrockAgentRuntimeProducerInvokeFlowTest.java | 35 +++++ .../BedrockAgentRuntimeProducerOperationTest.java | 146 +++++++++++++++++++++ ...BedrockAgentRuntimeComponentBuilderFactory.java | 6 +- .../BedrockAgentRuntimeEndpointBuilderFactory.java | 11 +- 13 files changed, 314 insertions(+), 66 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws-bedrock-agent-runtime.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws-bedrock-agent-runtime.json index 3b6d1f8fa0cd..346346285fbe 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws-bedrock-agent-runtime.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws-bedrock-agent-runtime.json @@ -25,9 +25,9 @@ }, "componentProperties": { "configuration": { "index": 0, "kind": "property", "displayName": "Configuration", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "deprecated": false, "autowired": false, "secret": false, "description": "Component configuration" }, - "knowledgeBaseId": { "index": 1, "kind": "property", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use" }, + "knowledgeBaseId": { "index": 1, "kind": "property", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use (used by the retrieve a [...] "lazyStartProducer": { "index": 2, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] - "modelId": { "index": 3, "kind": "property", "displayName": "Model Id", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite-v [...] + "modelId": { "index": 3, "kind": "property", "displayName": "Model Id", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite- [...] "operation": { "index": 4, "kind": "property", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeOperations", "enum": [ "retrieveAndGenerate", "invokeFlow", "invokeAgent", "invokeInlineAgent", "retrieve" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentr [...] "overrideEndpoint": { "index": 5, "kind": "property", "displayName": "Override Endpoint", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Set the need for overriding the endpoint. This option ne [...] "pojoRequest": { "index": 6, "kind": "property", "displayName": "Pojo Request", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "If we want to use a POJO request as body or not" }, @@ -61,7 +61,7 @@ }, "headers": { "CamelAwsBedrockAgentRuntimeOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation we want to perform", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#OPERATION" }, - "CamelAwsBedrockAgentRuntimeCitations": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When retrieving and generating a response, this header will contain the citations", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#CITATIONS" }, + "CamelAwsBedrockAgentRuntimeCitations": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<software.amazon.awssdk.services.bedrockagentruntime.model.Citation>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When retrieving and generating a response, this header will contain the citations", "constantName": "org.apache.camel.component.aws2.bedrock.agentrun [...] "CamelAwsBedrockAgentRuntimeSessionId": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When retrieving and generating a response, this header will contain he unique identifier of the session. Reuse the same value to continue the same session with the knowledge base.", "constantName": "org.apache.camel.component [...] "CamelAwsBedrockAgentRuntimeAgentId": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The unique identifier of the agent to invoke. Overrides the agentId configured on the endpoint.", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#AGENT_ID" }, "CamelAwsBedrockAgentRuntimeAgentAliasId": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The unique identifier of the agent alias to invoke. Overrides the agentAliasId configured on the endpoint.", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#AGENT_ALIAS_ID" }, @@ -88,13 +88,13 @@ "CamelAwsBedrockAgentRuntimeRetrieveGuardrailAction": { "index": 25, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When performing a retrieve operation, this header will contain the guardrail action (if any) applied by the knowledge base.", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRu [...] }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "byOption": [ { "option": "operation", "value": "invokeFlow", "carriers": [ "text", "list", "pojo" ] } ], "description": "The question, the query or the input of the agent as text for retrieveAndGenerate, retrieve, invokeAgent and invokeInlineAgent: bytes, a stream or a file are read as text, a Map or a POJO is sent as its toString(), so take the text out of it first, and no body fails. invokeFlow takes a String, whic [...] + "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "byOption": [ { "option": "operation", "value": "invokeFlow", "carriers": [ "text", "bytes", "stream", "file", "list", "pojo" ] } ], "description": "The question, the query or the input of the agent as text for retrieveAndGenerate, retrieve, invokeAgent and invokeInlineAgent: bytes, a stream or a file are read as text, a Map or a POJO is sent as its toString(), so take the text out of it first, and no body fails. invo [...] "producerOut": { "carriers": [ "text", "list" ], "byOption": [ { "option": "operation", "value": "retrieveAndGenerate", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "retrieve", "carriers": [ "list" ], "javaType": "java.util.List<software.amazon.awssdk.services.bedrockagentruntime.model.KnowledgeBaseRetrievalResult>" }, { "option": "operation", "value": "invokeFlow", "carriers": [ "text", "pojo", "unchanged" ] } ], "description": "The ans [...] }, "properties": { "label": { "index": 0, "kind": "path", "displayName": "Label", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Logical name" }, - "knowledgeBaseId": { "index": 1, "kind": "parameter", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use" }, - "modelId": { "index": 2, "kind": "parameter", "displayName": "Model Id", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite- [...] + "knowledgeBaseId": { "index": 1, "kind": "parameter", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use (used by the retrieve [...] + "modelId": { "index": 2, "kind": "parameter", "displayName": "Model Id", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite [...] "operation": { "index": 3, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeOperations", "enum": [ "retrieveAndGenerate", "invokeFlow", "invokeAgent", "invokeInlineAgent", "retrieve" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agent [...] "overrideEndpoint": { "index": 4, "kind": "parameter", "displayName": "Override Endpoint", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Set the need for overriding the endpoint. This option n [...] "pojoRequest": { "index": 5, "kind": "parameter", "displayName": "Pojo Request", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "If we want to use a POJO request as body or not" }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws-bedrock-agent-runtime-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws-bedrock-agent-runtime-component.adoc index 2bc52892e234..5fbb5407c4e5 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws-bedrock-agent-runtime-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws-bedrock-agent-runtime-component.adoc @@ -44,7 +44,7 @@ include::partial$component-endpoint-headers.adoc[] Required Bedrock component options -You have to provide the bedrockRuntimeClient in the +You have to provide the bedrockAgentRuntimeClient in the Registry or your accessKey and secretKey to access the https://aws.amazon.com/bedrock/[Amazon Bedrock] service. @@ -60,6 +60,9 @@ The component supports the following operations on the `operation` URI option: * `invokeAgent` — Invoke a deployed Bedrock Agent by id/alias and stream the response back. * `invokeInlineAgent` — Invoke an agent defined inline at invocation time (foundation model plus instruction). +The `CamelAwsBedrockAgentRuntimeOperation` header overrides the `operation` option per exchange. With neither set, +the exchange fails with an `IllegalArgumentException`. + === Retrieving from a Bedrock Knowledge Base The `retrieve` operation performs a semantic search against a Bedrock Knowledge Base and returns the matching chunks @@ -89,7 +92,9 @@ following headers: === Invoking a Bedrock Flow The `invokeFlow` operation streams flow events using the AWS SDK async client; the producer transparently allocates a -`BedrockAgentRuntimeAsyncClient` when the endpoint is configured with `operation=invokeFlow`. +`BedrockAgentRuntimeAsyncClient` when the endpoint is configured with `operation=invokeFlow`. When the +`CamelAwsBedrockAgentRuntimeOperation` header picks `invokeFlow`, `invokeAgent` or `invokeInlineAgent` on an endpoint +of another operation, the async client is created when the first exchange needs it. Mandatory options: @@ -107,7 +112,8 @@ The body of the in-message can be: * a `String` — wrapped in a single `FlowInput` targeting node `FlowInputNode`, output `document`; * a `FlowInput` instance — used as the sole input to the flow; -* a `List<FlowInput>` — passed verbatim to the request (use this form to chain multiple flow inputs). +* a `List<FlowInput>` — passed verbatim to the request (use this form to chain multiple flow inputs); +* another body, such as a `byte[]`, an `InputStream` or a file, is converted to a `String` and used as above. When `pojoRequest=true`, the body must be an `InvokeFlowRequest` and is sent unchanged. diff --git a/components/camel-aws/camel-aws-bedrock/src/generated/resources/META-INF/org/apache/camel/component/aws2/bedrock/agentruntime/aws-bedrock-agent-runtime.json b/components/camel-aws/camel-aws-bedrock/src/generated/resources/META-INF/org/apache/camel/component/aws2/bedrock/agentruntime/aws-bedrock-agent-runtime.json index 3b6d1f8fa0cd..346346285fbe 100644 --- a/components/camel-aws/camel-aws-bedrock/src/generated/resources/META-INF/org/apache/camel/component/aws2/bedrock/agentruntime/aws-bedrock-agent-runtime.json +++ b/components/camel-aws/camel-aws-bedrock/src/generated/resources/META-INF/org/apache/camel/component/aws2/bedrock/agentruntime/aws-bedrock-agent-runtime.json @@ -25,9 +25,9 @@ }, "componentProperties": { "configuration": { "index": 0, "kind": "property", "displayName": "Configuration", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "deprecated": false, "autowired": false, "secret": false, "description": "Component configuration" }, - "knowledgeBaseId": { "index": 1, "kind": "property", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use" }, + "knowledgeBaseId": { "index": 1, "kind": "property", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use (used by the retrieve a [...] "lazyStartProducer": { "index": 2, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail [...] - "modelId": { "index": 3, "kind": "property", "displayName": "Model Id", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite-v [...] + "modelId": { "index": 3, "kind": "property", "displayName": "Model Id", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite- [...] "operation": { "index": 4, "kind": "property", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeOperations", "enum": [ "retrieveAndGenerate", "invokeFlow", "invokeAgent", "invokeInlineAgent", "retrieve" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentr [...] "overrideEndpoint": { "index": 5, "kind": "property", "displayName": "Override Endpoint", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Set the need for overriding the endpoint. This option ne [...] "pojoRequest": { "index": 6, "kind": "property", "displayName": "Pojo Request", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "If we want to use a POJO request as body or not" }, @@ -61,7 +61,7 @@ }, "headers": { "CamelAwsBedrockAgentRuntimeOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation we want to perform", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#OPERATION" }, - "CamelAwsBedrockAgentRuntimeCitations": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When retrieving and generating a response, this header will contain the citations", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#CITATIONS" }, + "CamelAwsBedrockAgentRuntimeCitations": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<software.amazon.awssdk.services.bedrockagentruntime.model.Citation>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When retrieving and generating a response, this header will contain the citations", "constantName": "org.apache.camel.component.aws2.bedrock.agentrun [...] "CamelAwsBedrockAgentRuntimeSessionId": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When retrieving and generating a response, this header will contain he unique identifier of the session. Reuse the same value to continue the same session with the knowledge base.", "constantName": "org.apache.camel.component [...] "CamelAwsBedrockAgentRuntimeAgentId": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The unique identifier of the agent to invoke. Overrides the agentId configured on the endpoint.", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#AGENT_ID" }, "CamelAwsBedrockAgentRuntimeAgentAliasId": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The unique identifier of the agent alias to invoke. Overrides the agentAliasId configured on the endpoint.", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConstants#AGENT_ALIAS_ID" }, @@ -88,13 +88,13 @@ "CamelAwsBedrockAgentRuntimeRetrieveGuardrailAction": { "index": 25, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When performing a retrieve operation, this header will contain the guardrail action (if any) applied by the knowledge base.", "constantName": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRu [...] }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "byOption": [ { "option": "operation", "value": "invokeFlow", "carriers": [ "text", "list", "pojo" ] } ], "description": "The question, the query or the input of the agent as text for retrieveAndGenerate, retrieve, invokeAgent and invokeInlineAgent: bytes, a stream or a file are read as text, a Map or a POJO is sent as its toString(), so take the text out of it first, and no body fails. invokeFlow takes a String, whic [...] + "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "byOption": [ { "option": "operation", "value": "invokeFlow", "carriers": [ "text", "bytes", "stream", "file", "list", "pojo" ] } ], "description": "The question, the query or the input of the agent as text for retrieveAndGenerate, retrieve, invokeAgent and invokeInlineAgent: bytes, a stream or a file are read as text, a Map or a POJO is sent as its toString(), so take the text out of it first, and no body fails. invo [...] "producerOut": { "carriers": [ "text", "list" ], "byOption": [ { "option": "operation", "value": "retrieveAndGenerate", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "retrieve", "carriers": [ "list" ], "javaType": "java.util.List<software.amazon.awssdk.services.bedrockagentruntime.model.KnowledgeBaseRetrievalResult>" }, { "option": "operation", "value": "invokeFlow", "carriers": [ "text", "pojo", "unchanged" ] } ], "description": "The ans [...] }, "properties": { "label": { "index": 0, "kind": "path", "displayName": "Label", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Logical name" }, - "knowledgeBaseId": { "index": 1, "kind": "parameter", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use" }, - "modelId": { "index": 2, "kind": "parameter", "displayName": "Model Id", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite- [...] + "knowledgeBaseId": { "index": 1, "kind": "parameter", "displayName": "Knowledge Base Id", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Define the Knowledge Base Id we are going to use (used by the retrieve [...] + "modelId": { "index": 2, "kind": "parameter", "displayName": "Model Id", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "amazon.titan-text-express-v1", "amazon.titan-text-lite-v1", "amazon.titan-image-generator-v1", "amazon.titan-embed-text-v1", "amazon.titan-embed-image-v1", "amazon.titan-text-premier-v1:0", "amazon.titan-embed-text-v2:0", "amazon.titan-image-generator-v2:0", "amazon.nova-canvas-v1:0", "amazon.nova-lite [...] "operation": { "index": 3, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeOperations", "enum": [ "retrieveAndGenerate", "invokeFlow", "invokeAgent", "invokeInlineAgent", "retrieve" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agent [...] "overrideEndpoint": { "index": 4, "kind": "parameter", "displayName": "Override Endpoint", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "Set the need for overriding the endpoint. This option n [...] "pojoRequest": { "index": 5, "kind": "parameter", "displayName": "Pojo Request", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.aws2.bedrock.agentruntime.BedrockAgentRuntimeConfiguration", "configurationField": "configuration", "description": "If we want to use a POJO request as body or not" }, diff --git a/components/camel-aws/camel-aws-bedrock/src/main/docs/aws-bedrock-agent-runtime-component.adoc b/components/camel-aws/camel-aws-bedrock/src/main/docs/aws-bedrock-agent-runtime-component.adoc index 2bc52892e234..5fbb5407c4e5 100644 --- a/components/camel-aws/camel-aws-bedrock/src/main/docs/aws-bedrock-agent-runtime-component.adoc +++ b/components/camel-aws/camel-aws-bedrock/src/main/docs/aws-bedrock-agent-runtime-component.adoc @@ -44,7 +44,7 @@ include::partial$component-endpoint-headers.adoc[] Required Bedrock component options -You have to provide the bedrockRuntimeClient in the +You have to provide the bedrockAgentRuntimeClient in the Registry or your accessKey and secretKey to access the https://aws.amazon.com/bedrock/[Amazon Bedrock] service. @@ -60,6 +60,9 @@ The component supports the following operations on the `operation` URI option: * `invokeAgent` — Invoke a deployed Bedrock Agent by id/alias and stream the response back. * `invokeInlineAgent` — Invoke an agent defined inline at invocation time (foundation model plus instruction). +The `CamelAwsBedrockAgentRuntimeOperation` header overrides the `operation` option per exchange. With neither set, +the exchange fails with an `IllegalArgumentException`. + === Retrieving from a Bedrock Knowledge Base The `retrieve` operation performs a semantic search against a Bedrock Knowledge Base and returns the matching chunks @@ -89,7 +92,9 @@ following headers: === Invoking a Bedrock Flow The `invokeFlow` operation streams flow events using the AWS SDK async client; the producer transparently allocates a -`BedrockAgentRuntimeAsyncClient` when the endpoint is configured with `operation=invokeFlow`. +`BedrockAgentRuntimeAsyncClient` when the endpoint is configured with `operation=invokeFlow`. When the +`CamelAwsBedrockAgentRuntimeOperation` header picks `invokeFlow`, `invokeAgent` or `invokeInlineAgent` on an endpoint +of another operation, the async client is created when the first exchange needs it. Mandatory options: @@ -107,7 +112,8 @@ The body of the in-message can be: * a `String` — wrapped in a single `FlowInput` targeting node `FlowInputNode`, output `document`; * a `FlowInput` instance — used as the sole input to the flow; -* a `List<FlowInput>` — passed verbatim to the request (use this form to chain multiple flow inputs). +* a `List<FlowInput>` — passed verbatim to the request (use this form to chain multiple flow inputs); +* another body, such as a `byte[]`, an `InputStream` or a file, is converted to a `String` and used as above. When `pojoRequest=true`, the body must be an `InvokeFlowRequest` and is sent unchanged. diff --git a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConfiguration.java b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConfiguration.java index 425342694ac4..bd33cabf6b5f 100644 --- a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConfiguration.java +++ b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConfiguration.java @@ -45,10 +45,8 @@ public class BedrockAgentRuntimeConfiguration implements Cloneable, AwsCommonCon @UriParam(label = "security", security = "secret") private String sessionToken; @UriParam(enums = "amazon.titan-text-express-v1,amazon.titan-text-lite-v1,amazon.titan-image-generator-v1,amazon.titan-embed-text-v1,amazon.titan-embed-image-v1,amazon.titan-text-premier-v1:0,amazon.titan-embed-text-v2:0,amazon.titan-image-generator-v2:0,amazon.nova-canvas-v1:0,amazon.nova-lite-v1:0,amazon.nova-micro-v1:0,amazon.nova-premier-v1:0,amazon.nova-pro-v1:0,amazon.nova-reel-v1:0,amazon.nova-reel-v1:1,amazon.nova-sonic-v1:0,amazon.rerank-v1:0,ai21.jamba-1-5-large-v1:0,ai21.j [...] - @Metadata(required = true) private String modelId; @UriParam - @Metadata(required = true) private String knowledgeBaseId; @UriParam @Metadata(required = true) @@ -306,7 +304,7 @@ public class BedrockAgentRuntimeConfiguration implements Cloneable, AwsCommonCon } /** - * Define the model Id we are going to use + * Define the model Id we are going to use (used by the retrieveAndGenerate operation) */ public void setModelId(String modelId) { this.modelId = modelId; @@ -317,7 +315,7 @@ public class BedrockAgentRuntimeConfiguration implements Cloneable, AwsCommonCon } /** - * Define the Knowledge Base Id we are going to use + * Define the Knowledge Base Id we are going to use (used by the retrieve and retrieveAndGenerate operations) */ public void setKnowledgeBaseId(String knowledgeBaseId) { this.knowledgeBaseId = knowledgeBaseId; diff --git a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConstants.java b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConstants.java index a3172395527e..beaa1b2ded10 100644 --- a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConstants.java +++ b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeConstants.java @@ -26,7 +26,7 @@ public interface BedrockAgentRuntimeConstants { String OPERATION = "CamelAwsBedrockAgentRuntimeOperation"; @Metadata(description = "When retrieving and generating a response, this header will contain the citations", - javaType = "String") + javaType = "java.util.List<software.amazon.awssdk.services.bedrockagentruntime.model.Citation>") String CITATIONS = "CamelAwsBedrockAgentRuntimeCitations"; @Metadata(description = "When retrieving and generating a response, this header will contain he unique identifier of the session. " diff --git a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpoint.java b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpoint.java index e9c18b44be5c..070b0d16fed5 100644 --- a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpoint.java +++ b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpoint.java @@ -18,6 +18,8 @@ package org.apache.camel.component.aws2.bedrock.agentruntime; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.camel.*; import org.apache.camel.component.aws2.bedrock.agentruntime.client.BedrockAgentRuntimeClientFactory; @@ -40,13 +42,16 @@ import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeCl headersClass = BedrockAgentRuntimeConstants.class) @BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE }, byOption = @BodyType.When(option = "operation", value = "invokeFlow", - carriers = { Carrier.TEXT, Carrier.LIST, Carrier.POJO }), + carriers = { + Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE, + Carrier.LIST, Carrier.POJO }), description = "The question, the query or the input of the agent as text for" + " retrieveAndGenerate, retrieve, invokeAgent and invokeInlineAgent: bytes, a" + " stream or a file are read as text, a Map or a POJO is sent as its" + " toString(), so take the text out of it first, and no body fails. invokeFlow" + " takes a String, which becomes the document of the input node FlowInputNode," - + " or a FlowInput or a List of FlowInput of the AWS SDK: another body fails." + + " or a FlowInput or a List of FlowInput of the AWS SDK: another body is" + + " read as text, as for the other operations." + " With pojoRequest=true the request of the AWS SDK for the operation, such as" + " a RetrieveAndGenerateRequest: another body fails"), producerOut = @BodyType(value = { Carrier.TEXT, Carrier.LIST }, @@ -69,7 +74,8 @@ import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeCl public class BedrockAgentRuntimeEndpoint extends ScheduledPollEndpoint implements EndpointServiceLocation { private BedrockAgentRuntimeClient bedrockAgentRuntimeClient; - private BedrockAgentRuntimeAsyncClient bedrockAgentRuntimeAsyncClient; + private volatile BedrockAgentRuntimeAsyncClient bedrockAgentRuntimeAsyncClient; + private final Lock asyncClientLock = new ReentrantLock(); @UriParam private BedrockAgentRuntimeConfiguration configuration; @@ -104,7 +110,8 @@ public class BedrockAgentRuntimeEndpoint extends ScheduledPollEndpoint implement // Only build the async client when the operation needs event-stream support (invokeFlow, invokeAgent, // invokeInlineAgent), or when the user explicitly supplied one. This avoids creating a Netty-based async HTTP - // client for users that only need synchronous operations like retrieveAndGenerate. + // client for users that only need synchronous operations like retrieveAndGenerate. When the operation header + // picks one of these operations on an endpoint of another operation, the client is created on first use. if (ObjectHelper.isNotEmpty(configuration.getBedrockAgentRuntimeAsyncClient())) { bedrockAgentRuntimeAsyncClient = configuration.getBedrockAgentRuntimeAsyncClient(); } else if (configuration.getOperation() == BedrockAgentRuntimeOperations.invokeFlow @@ -127,6 +134,7 @@ public class BedrockAgentRuntimeEndpoint extends ScheduledPollEndpoint implement bedrockAgentRuntimeAsyncClient.close(); } } + bedrockAgentRuntimeAsyncClient = null; super.doStop(); } @@ -142,6 +150,27 @@ public class BedrockAgentRuntimeEndpoint extends ScheduledPollEndpoint implement return bedrockAgentRuntimeAsyncClient; } + /** + * The async client that invokeFlow, invokeAgent and invokeInlineAgent need. It is created on first use when the + * operation of the endpoint does not need it, but the operation header picks one that does. + */ + public BedrockAgentRuntimeAsyncClient getOrCreateBedrockAgentRuntimeAsyncClient() { + BedrockAgentRuntimeAsyncClient client = bedrockAgentRuntimeAsyncClient; + if (client == null) { + asyncClientLock.lock(); + try { + if (bedrockAgentRuntimeAsyncClient == null) { + bedrockAgentRuntimeAsyncClient + = BedrockAgentRuntimeClientFactory.getBedrockAgentRuntimeAsyncClient(configuration); + } + client = bedrockAgentRuntimeAsyncClient; + } finally { + asyncClientLock.unlock(); + } + } + return client; + } + @Override public String getServiceUrl() { if (!configuration.isOverrideEndpoint()) { diff --git a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducer.java b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducer.java index a68b54bd764b..1d022aaa868d 100644 --- a/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducer.java +++ b/components/camel-aws/camel-aws-bedrock/src/main/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducer.java @@ -81,7 +81,13 @@ public class BedrockAgentRuntimeProducer extends DefaultProducer { @Override public void process(Exchange exchange) throws Exception { - switch (determineOperation(exchange)) { + BedrockAgentRuntimeOperations operation = determineOperation(exchange); + if (operation == null) { + throw new IllegalArgumentException( + "Operation must be specified with the operation option or the " + + BedrockAgentRuntimeConstants.OPERATION + " header"); + } + switch (operation) { case retrieveAndGenerate: retrieveAndGenerate(getEndpoint().getBedrockAgentRuntimeClient(), exchange); break; @@ -273,12 +279,7 @@ public class BedrockAgentRuntimeProducer extends DefaultProducer { } private void invokeFlow(Exchange exchange) throws InvalidPayloadException { - BedrockAgentRuntimeAsyncClient asyncClient = getEndpoint().getBedrockAgentRuntimeAsyncClient(); - if (asyncClient == null) { - throw new IllegalStateException( - "BedrockAgentRuntimeAsyncClient is not available. The invokeFlow operation requires an async client; " - + "set operation=invokeFlow on the endpoint or autowire a BedrockAgentRuntimeAsyncClient."); - } + BedrockAgentRuntimeAsyncClient asyncClient = getEndpoint().getOrCreateBedrockAgentRuntimeAsyncClient(); InvokeFlowRequest request; if (getConfiguration().isPojoRequest()) { @@ -386,18 +387,15 @@ public class BedrockAgentRuntimeProducer extends DefaultProducer { return Collections.singletonList(singleInput); } - if (body instanceof String text) { - FlowInput input = FlowInput.builder() - .nodeName("FlowInputNode") - .nodeOutputName("document") - .content(FlowInputContent.builder().document(Document.fromString(text)).build()) - .build(); - return Collections.singletonList(input); - } - - throw new IllegalArgumentException( - "invokeFlow expects the body to be a List<FlowInput>, a single FlowInput or a String when pojoRequest=false. " - + "Got: " + body.getClass().getName()); + // another body, such as bytes, a stream or a file, is converted to the text of the input node, as invokeAgent + // does with its input text + String text = resolveInputText(exchange, "invokeFlow"); + FlowInput input = FlowInput.builder() + .nodeName("FlowInputNode") + .nodeOutputName("document") + .content(FlowInputContent.builder().document(Document.fromString(text)).build()) + .build(); + return Collections.singletonList(input); } private void prepareFlowResponse( @@ -462,7 +460,7 @@ public class BedrockAgentRuntimeProducer extends DefaultProducer { } private void invokeAgent(Exchange exchange) throws InvalidPayloadException { - BedrockAgentRuntimeAsyncClient asyncClient = requireAsyncClient("invokeAgent"); + BedrockAgentRuntimeAsyncClient asyncClient = getEndpoint().getOrCreateBedrockAgentRuntimeAsyncClient(); InvokeAgentRequest request; if (getConfiguration().isPojoRequest()) { @@ -493,7 +491,7 @@ public class BedrockAgentRuntimeProducer extends DefaultProducer { } private void invokeInlineAgent(Exchange exchange) throws InvalidPayloadException { - BedrockAgentRuntimeAsyncClient asyncClient = requireAsyncClient("invokeInlineAgent"); + BedrockAgentRuntimeAsyncClient asyncClient = getEndpoint().getOrCreateBedrockAgentRuntimeAsyncClient(); InvokeInlineAgentRequest request; if (getConfiguration().isPojoRequest()) { @@ -523,17 +521,6 @@ public class BedrockAgentRuntimeProducer extends DefaultProducer { prepareAgentResponse(result, request.sessionId(), exchange); } - private BedrockAgentRuntimeAsyncClient requireAsyncClient(String operation) { - BedrockAgentRuntimeAsyncClient asyncClient = getEndpoint().getBedrockAgentRuntimeAsyncClient(); - if (asyncClient == null) { - throw new IllegalStateException( - "BedrockAgentRuntimeAsyncClient is not available. The " + operation - + " operation requires an async client; set operation=" + operation - + " on the endpoint or autowire a BedrockAgentRuntimeAsyncClient."); - } - return asyncClient; - } - /** * Awaits an agent invocation, unwrapping the CompletionException that CompletableFuture.join() puts around AWS * service exceptions so callers see the same behavior as synchronous AWS calls. diff --git a/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpointTest.java b/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpointTest.java index 7ad5c2278f9a..ae5eb93277c2 100644 --- a/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpointTest.java +++ b/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeEndpointTest.java @@ -17,12 +17,14 @@ package org.apache.camel.component.aws2.bedrock.agentruntime; import org.apache.camel.CamelContext; +import org.apache.camel.component.aws2.bedrock.agentruntime.client.BedrockAgentRuntimeClientFactory; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.support.SimpleRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeAsyncClient; import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeClient; @@ -32,6 +34,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) public class BedrockAgentRuntimeEndpointTest { @@ -122,4 +128,36 @@ public class BedrockAgentRuntimeEndpointTest { localContext.stop(); } } + + @Test + public void retrieveEndpointCreatesAsyncClientOnFirstUse() throws Exception { + // the operation header can pick invokeFlow, invokeAgent or invokeInlineAgent on an endpoint of retrieve, so the + // async client is created when an exchange first needs it, and closed when the endpoint stops + SimpleRegistry localRegistry = new SimpleRegistry(); + localRegistry.bind("syncClient", syncClient); + CamelContext localContext = new DefaultCamelContext(localRegistry); + + BedrockAgentRuntimeEndpoint endpoint = (BedrockAgentRuntimeEndpoint) localContext.getEndpoint( + "aws-bedrock-agent-runtime://label?bedrockAgentRuntimeClient=#syncClient&operation=retrieve" + + "&knowledgeBaseId=kb-1®ion=us-east-1&accessKey=unused&secretKey=unused"); + + try (MockedStatic<BedrockAgentRuntimeClientFactory> factory = mockStatic(BedrockAgentRuntimeClientFactory.class)) { + factory.when(() -> BedrockAgentRuntimeClientFactory.getBedrockAgentRuntimeAsyncClient(any())) + .thenReturn(asyncClient); + + endpoint.start(); + try { + assertNull(endpoint.getBedrockAgentRuntimeAsyncClient()); + assertSame(asyncClient, endpoint.getOrCreateBedrockAgentRuntimeAsyncClient()); + assertSame(asyncClient, endpoint.getOrCreateBedrockAgentRuntimeAsyncClient()); + assertSame(asyncClient, endpoint.getBedrockAgentRuntimeAsyncClient()); + factory.verify(() -> BedrockAgentRuntimeClientFactory.getBedrockAgentRuntimeAsyncClient(any()), times(1)); + } finally { + endpoint.stop(); + localContext.stop(); + } + } + verify(asyncClient).close(); + assertNull(endpoint.getBedrockAgentRuntimeAsyncClient()); + } } diff --git a/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerInvokeFlowTest.java b/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerInvokeFlowTest.java index 90653dd49d55..4b616b728199 100644 --- a/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerInvokeFlowTest.java +++ b/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerInvokeFlowTest.java @@ -16,6 +16,8 @@ */ package org.apache.camel.component.aws2.bedrock.agentruntime; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -33,6 +35,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.core.document.Document; import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeAsyncClient; import software.amazon.awssdk.services.bedrockagentruntime.model.FlowInput; import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeFlowRequest; @@ -41,6 +44,7 @@ import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeFlowRespo import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -135,6 +139,37 @@ public class BedrockAgentRuntimeProducerInvokeFlowTest { assertTrue(((List<?>) outputs).isEmpty(), "FLOW_OUTPUTS header should be an empty list when no events were emitted"); } + @Test + public void invokeFlowConvertsBytesBodyToText() { + Exchange result = template.send("direct:flow-config-default", + exchange -> exchange.getMessage().setBody("hello bytes".getBytes(StandardCharsets.UTF_8))); + + assertNull(result.getException(), "Exchange should not fail"); + assertEquals("hello bytes", inputDocument().asString()); + } + + @Test + public void invokeFlowConvertsStreamBodyToText() { + Exchange result = template.send("direct:flow-config-default", + exchange -> exchange.getMessage() + .setBody(new ByteArrayInputStream("hello stream".getBytes(StandardCharsets.UTF_8)))); + + assertNull(result.getException(), "Exchange should not fail"); + assertEquals("hello stream", inputDocument().asString()); + } + + private Document inputDocument() { + InvokeFlowRequest request = capturedRequest.get(); + assertNotNull(request, "Producer must have invoked the async client"); + assertEquals(1, request.inputs().size()); + FlowInput input = request.inputs().get(0); + assertEquals("FlowInputNode", input.nodeName()); + assertEquals("document", input.nodeOutputName()); + Document document = input.content().document(); + assertTrue(document.isString(), "Document should be a string"); + return document; + } + @Test public void invokeFlowAllowsHeaderOverridesForFlowIdentifiers() { template.send("direct:flow-config-default", exchange -> { diff --git a/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerOperationTest.java b/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerOperationTest.java new file mode 100644 index 000000000000..acf4d0aa0131 --- /dev/null +++ b/components/camel-aws/camel-aws-bedrock/src/test/java/org/apache/camel/component/aws2/bedrock/agentruntime/BedrockAgentRuntimeProducerOperationTest.java @@ -0,0 +1,146 @@ +/* + * 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.aws2.bedrock.agentruntime; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.camel.CamelContext; +import org.apache.camel.CamelExecutionException; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.aws2.bedrock.agentruntime.client.BedrockAgentRuntimeClientFactory; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.SimpleRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeAsyncClient; +import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeClient; +import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeAgentRequest; +import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeAgentResponseHandler; +import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeFlowRequest; +import software.amazon.awssdk.services.bedrockagentruntime.model.InvokeFlowResponseHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; + +/** + * The operation of an exchange: no operation, and an operation picked by the header that needs the async client on an + * endpoint of an operation that does not. + */ +@ExtendWith(MockitoExtension.class) +class BedrockAgentRuntimeProducerOperationTest { + + @Mock + private BedrockAgentRuntimeClient syncClient; + + @Mock + private BedrockAgentRuntimeAsyncClient asyncClient; + + private CamelContext camelContext; + private ProducerTemplate template; + private AtomicReference<InvokeFlowRequest> capturedFlowRequest; + private AtomicReference<InvokeAgentRequest> capturedAgentRequest; + + @BeforeEach + void setup() throws Exception { + capturedFlowRequest = new AtomicReference<>(); + capturedAgentRequest = new AtomicReference<>(); + lenient().doAnswer(invocation -> { + capturedFlowRequest.set(invocation.getArgument(0, InvokeFlowRequest.class)); + return CompletableFuture.completedFuture(null); + }).when(asyncClient).invokeFlow(any(InvokeFlowRequest.class), any(InvokeFlowResponseHandler.class)); + lenient().doAnswer(invocation -> { + capturedAgentRequest.set(invocation.getArgument(0, InvokeAgentRequest.class)); + return CompletableFuture.completedFuture(null); + }).when(asyncClient).invokeAgent(any(InvokeAgentRequest.class), any(InvokeAgentResponseHandler.class)); + + // only the sync client is in the registry, so the async client is not autowired + SimpleRegistry registry = new SimpleRegistry(); + registry.bind("syncClient", syncClient); + camelContext = new DefaultCamelContext(registry); + camelContext.addRoutes(new RouteBuilder() { + @Override + public void configure() { + String common = "?bedrockAgentRuntimeClient=#syncClient®ion=us-east-1&accessKey=unused&secretKey=unused"; + + from("direct:retrieve") + .to("aws-bedrock-agent-runtime://label" + common + + "&operation=retrieve&knowledgeBaseId=kb-1" + + "&flowIdentifier=flow-1&flowAliasIdentifier=alias-1" + + "&agentId=agent-1&agentAliasId=agent-alias-1"); + + from("direct:no-operation") + .to("aws-bedrock-agent-runtime://label2" + common); + } + }); + camelContext.start(); + template = camelContext.createProducerTemplate(); + } + + @AfterEach + void teardown() { + if (template != null) { + template.stop(); + } + if (camelContext != null) { + camelContext.stop(); + } + } + + @Test + void noOperationFailsWithMessage() { + assertThatThrownBy(() -> template.sendBody("direct:no-operation", "hi")) + .isInstanceOf(CamelExecutionException.class) + .cause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("operation option") + .hasMessageContaining(BedrockAgentRuntimeConstants.OPERATION); + } + + @Test + void operationHeaderPicksInvokeFlowOnRetrieveEndpoint() { + try (MockedStatic<BedrockAgentRuntimeClientFactory> factory = mockStatic(BedrockAgentRuntimeClientFactory.class)) { + factory.when(() -> BedrockAgentRuntimeClientFactory.getBedrockAgentRuntimeAsyncClient(any())) + .thenReturn(asyncClient); + + template.sendBodyAndHeader("direct:retrieve", "hello flow", BedrockAgentRuntimeConstants.OPERATION, + BedrockAgentRuntimeOperations.invokeFlow); + assertThat(capturedFlowRequest.get()).isNotNull(); + assertThat(capturedFlowRequest.get().flowIdentifier()).isEqualTo("flow-1"); + assertThat(capturedFlowRequest.get().inputs().get(0).content().document().asString()).isEqualTo("hello flow"); + + template.sendBodyAndHeader("direct:retrieve", "hello agent", BedrockAgentRuntimeConstants.OPERATION, + "invokeAgent"); + assertThat(capturedAgentRequest.get()).isNotNull(); + assertThat(capturedAgentRequest.get().agentId()).isEqualTo("agent-1"); + assertThat(capturedAgentRequest.get().inputText()).isEqualTo("hello agent"); + + // the async client is created once, on first use, and kept for the next exchanges + factory.verify(() -> BedrockAgentRuntimeClientFactory.getBedrockAgentRuntimeAsyncClient(any()), times(1)); + } + } +} diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AwsBedrockAgentRuntimeComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AwsBedrockAgentRuntimeComponentBuilderFactory.java index 95b76a5b0faa..a4b9d4bd6a8b 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AwsBedrockAgentRuntimeComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AwsBedrockAgentRuntimeComponentBuilderFactory.java @@ -67,7 +67,8 @@ public interface AwsBedrockAgentRuntimeComponentBuilderFactory { } /** - * Define the Knowledge Base Id we are going to use. + * Define the Knowledge Base Id we are going to use (used by the + * retrieve and retrieveAndGenerate operations). * * The option is a: <code>java.lang.String</code> type. * @@ -107,7 +108,8 @@ public interface AwsBedrockAgentRuntimeComponentBuilderFactory { } /** - * Define the model Id we are going to use. + * Define the model Id we are going to use (used by the + * retrieveAndGenerate operation). * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BedrockAgentRuntimeEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BedrockAgentRuntimeEndpointBuilderFactory.java index 5142a159b5d8..f435a9abe3af 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BedrockAgentRuntimeEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BedrockAgentRuntimeEndpointBuilderFactory.java @@ -45,11 +45,11 @@ public interface BedrockAgentRuntimeEndpointBuilderFactory { } /** - * Define the Knowledge Base Id we are going to use. + * Define the Knowledge Base Id we are going to use (used by the + * retrieve and retrieveAndGenerate operations). * * The option is a: <code>java.lang.String</code> type. * - * Required: true * Group: producer * * @param knowledgeBaseId the value to set @@ -60,11 +60,11 @@ public interface BedrockAgentRuntimeEndpointBuilderFactory { return this; } /** - * Define the model Id we are going to use. + * Define the model Id we are going to use (used by the + * retrieveAndGenerate operation). * * The option is a: <code>java.lang.String</code> type. * - * Required: true * Group: producer * * @param modelId the value to set @@ -837,7 +837,8 @@ public interface BedrockAgentRuntimeEndpointBuilderFactory { * When retrieving and generating a response, this header will contain * the citations. * - * The option is a: {@code String} type. + * The option is a: {@code + * java.util.List<software.amazon.awssdk.services.bedrockagentruntime.model.Citation>} type. * * Group: producer *
