Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

100 changes: 100 additions & 0 deletions connectors/aws/aws-bedrock-knowledgebase/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.camunda.connector</groupId>
<artifactId>connector-aws-parent</artifactId>
<version>8.9.0-SNAPSHOT</version>
</parent>

<artifactId>connector-aws-bedrock-knowledgebase</artifactId>
<name>connector-aws-bedrock-knowledgebase</name>
<description>Camunda Connector for AWS Bedrock Knowledge Base retrieval</description>
<packaging>jar</packaging>

<licenses>
<license>
<name>Camunda Self-Managed Free Edition license</name>
<url>
https://camunda.com/legal/terms/cloud-terms-and-conditions/camunda-cloud-self-managed-free-edition-terms/
</url>
</license>
<license>
<name>Camunda Self-Managed Enterprise Edition license</name>
</license>
</licenses>

<dependencies>
<dependency>
<groupId>io.camunda.connector</groupId>
<artifactId>connector-aws-base</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bedrockagentruntime</artifactId>
<version>${version.aws-sdk2}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>auth</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>regions</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sdk-core</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-core</artifactId>
</dependency>

<dependency>
<groupId>io.camunda.connector</groupId>
<artifactId>connector-object-mapper</artifactId>
</dependency>

<dependency>
<groupId>io.camunda.connector</groupId>
<artifactId>element-template-generator-annotations</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>io.camunda.connector</groupId>
<artifactId>element-template-generator-maven-plugin</artifactId>
<version>${project.version}</version>
<configuration>
<connectors>
<connector>
<connectorClass>
io.camunda.connector.aws.bedrock.knowledgebase.BedrockKnowledgeBaseConnectorFunction
</connectorClass>
<files>
<file>
<templateId>io.camunda.connectors.aws.bedrock.knowledgebase.v1</templateId>
<templateFileName>aws-bedrock-knowledgebase-outbound-connector.json</templateFileName>
</file>
</files>
<generateHybridTemplates>true</generateHybridTemplates>
</connector>
</connectors>
<versionHistoryEnabled>true</versionHistoryEnabled>
<includeDependencies>
<includeDependency>io.camunda.connector:connector-aws-base</includeDependency>
</includeDependencies>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. Licensed under a proprietary license.
* See the License.txt file for more information. You may not use this file
* except in compliance with the proprietary license.
*/
package io.camunda.connector.aws.bedrock.knowledgebase;

import io.camunda.connector.api.annotation.OutboundConnector;
import io.camunda.connector.api.outbound.OutboundConnectorContext;
import io.camunda.connector.api.outbound.OutboundConnectorFunction;
import io.camunda.connector.aws.CredentialsProviderSupportV2;
import io.camunda.connector.aws.bedrock.knowledgebase.model.request.BedrockKnowledgeBaseRequest;
import io.camunda.connector.generator.java.annotation.ElementTemplate;
import io.camunda.connector.generator.java.annotation.ElementTemplate.PropertyGroup;
import io.camunda.connector.jackson.ConnectorsObjectMapperSupplier;
import java.net.URI;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeClient;

@OutboundConnector(
name = "AWS Bedrock Knowledge Base",
inputVariables = {
"authentication",
"configuration",
"knowledgeBaseId",
"operation",
"operationDiscriminator"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to declare operationDiscriminator here 🤔 ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we do. I followed a similar approach as the actionDiscriminator in the S3 connector.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a difference between how it is done in the S3 connector. Here operationDiscriminator is a nested property of operation whereas in the S3 connector actionDiscriminator is at the root level. You can try removing it as see if the connector works as before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operationDiscriminator does no exist anyways. There is an operation.operationDiscriminator instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct, I've updated it

},
type = "io.camunda:aws-bedrock-knowledgebase:1")
@ElementTemplate(
engineVersion = "^8.9", // TODO: update to ^8.10 when available
id = "io.camunda.connectors.aws.bedrock.knowledgebase.v1",
name = "AWS Bedrock Knowledge Base Outbound Connector",
description = "Retrieve relevant documents from an AWS Bedrock Knowledge Base",
inputDataClass = BedrockKnowledgeBaseRequest.class,
version = 1,
propertyGroups = {
@PropertyGroup(id = "authentication", label = "Authentication"),
@PropertyGroup(id = "configuration", label = "Configuration"),
@PropertyGroup(id = "operation", label = "Operation"),
@PropertyGroup(id = "retrieve", label = "Retrieve from Knowledge Base"),
},
icon = "icon.png")
public class BedrockKnowledgeBaseConnectorFunction implements OutboundConnectorFunction {

@Override
public Object execute(OutboundConnectorContext context) {
var request = context.bindVariables(BedrockKnowledgeBaseRequest.class);
try (var client = buildClient(request)) {
return new BedrockKnowledgeBaseExecutor(client, ConnectorsObjectMapperSupplier.getCopy())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 I guess we could avoid creating a new ObjectMapper copy for each request as it might be rather expensive (e.g. by copying it once and keeping it as a field of the function).

.execute(request, context::create);
}
}

private BedrockAgentRuntimeClient buildClient(BedrockKnowledgeBaseRequest request) {
var builder =
BedrockAgentRuntimeClient.builder()
.credentialsProvider(CredentialsProviderSupportV2.credentialsProvider(request));
var config = request.getConfiguration();
if (config != null && config.region() != null) {
builder.region(Region.of(config.region()));
}
if (config != null && config.endpoint() != null && !config.endpoint().isBlank()) {
builder.endpointOverride(URI.create(config.endpoint()));
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. Licensed under a proprietary license.
* See the License.txt file for more information. You may not use this file
* except in compliance with the proprietary license.
*/
package io.camunda.connector.aws.bedrock.knowledgebase;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.camunda.connector.api.document.Document;
import io.camunda.connector.api.document.DocumentCreationRequest;
import io.camunda.connector.api.error.ConnectorException;
import io.camunda.connector.api.error.ConnectorRetryException;
import io.camunda.connector.aws.bedrock.knowledgebase.model.request.BedrockKnowledgeBaseRequest;
import io.camunda.connector.aws.bedrock.knowledgebase.model.request.RetrieveOperation;
import io.camunda.connector.aws.bedrock.knowledgebase.model.response.BedrockKnowledgeBaseResponse;
import io.camunda.connector.aws.bedrock.knowledgebase.model.response.KnowledgeBaseRetrievalResult;
import io.camunda.connector.aws.bedrock.knowledgebase.model.response.RetrievalResultEntry;
import java.time.Instant;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import software.amazon.awssdk.services.bedrockagentruntime.BedrockAgentRuntimeClient;
import software.amazon.awssdk.services.bedrockagentruntime.model.BedrockAgentRuntimeException;
import software.amazon.awssdk.services.bedrockagentruntime.model.KnowledgeBaseRetrievalConfiguration;
import software.amazon.awssdk.services.bedrockagentruntime.model.KnowledgeBaseVectorSearchConfiguration;
import software.amazon.awssdk.services.bedrockagentruntime.model.RetrieveRequest;
import software.amazon.awssdk.services.bedrockagentruntime.model.RetrieveResponse;
import software.amazon.awssdk.services.bedrockagentruntime.model.ThrottlingException;

public class BedrockKnowledgeBaseExecutor {

private final BedrockAgentRuntimeClient client;
private final ObjectMapper objectMapper;

public BedrockKnowledgeBaseExecutor(BedrockAgentRuntimeClient client, ObjectMapper objectMapper) {
this.client = client;
this.objectMapper = objectMapper;
}

public KnowledgeBaseRetrievalResult execute(
BedrockKnowledgeBaseRequest request,
Function<DocumentCreationRequest, Document> documentFactory) {
return switch (request.getOperation()) {
case RetrieveOperation op -> retrieve(op, request, documentFactory);
};
}

private KnowledgeBaseRetrievalResult retrieve(
RetrieveOperation op,
BedrockKnowledgeBaseRequest request,
Function<DocumentCreationRequest, Document> documentFactory) {
try {
var sdkRequestBuilder =
RetrieveRequest.builder()
.knowledgeBaseId(request.getKnowledgeBaseId())
.retrievalQuery(q -> q.text(op.query()));

if (op.numberOfResults() != null) {
sdkRequestBuilder.retrievalConfiguration(
KnowledgeBaseRetrievalConfiguration.builder()
.vectorSearchConfiguration(
KnowledgeBaseVectorSearchConfiguration.builder()
.numberOfResults(op.numberOfResults())
.build())
.build());
}

RetrieveResponse sdkResponse = client.retrieve(sdkRequestBuilder.build());

var response = mapSdkResponse(sdkResponse);
byte[] json = objectMapper.writeValueAsBytes(response);

Document doc =
documentFactory.apply(
DocumentCreationRequest.from(json)
.contentType("application/json")
.fileName("kb-retrieval-" + Instant.now().toEpochMilli() + ".json")
.build());

return new KnowledgeBaseRetrievalResult(doc, response.results().size(), response.nextToken());
Comment on lines +79 to +86
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 The Vector DB connector (which as I understand it does something very similar) returns a list of individual documents (one per chunk) for the retrieve operation. Do you think it would make sense to align this here?

Regarding the file name, some integrations don't set a file name at all - maybe worth checking if we actually need it. If we keep it, we could maybe use the element instance key from the connector job context as unique key which we can correlate with the job.


} catch (ThrottlingException e) {
throw ConnectorRetryException.builder()
.errorCode("THROTTLED")
.message("Bedrock Knowledge Base request was throttled: " + e.getMessage())
.cause(e)
.build();
} catch (BedrockAgentRuntimeException e) {
var errorMsg =
e.awsErrorDetails() != null ? e.awsErrorDetails().errorMessage() : e.getMessage();
throw new ConnectorException(
"KB_RETRIEVAL_FAILED", "Bedrock Knowledge Base error: " + errorMsg, e);
} catch (JsonProcessingException e) {
throw new ConnectorException(
"SERIALIZATION_ERROR", "Failed to serialize retrieval results to JSON", e);
}
}

private BedrockKnowledgeBaseResponse mapSdkResponse(RetrieveResponse sdkResponse) {
var results =
sdkResponse.retrievalResults().stream()
.map(
r ->
new RetrievalResultEntry(
r.content() != null ? r.content().text() : null,
r.score(),
r.location() != null && r.location().s3Location() != null
? r.location().s3Location().uri()
: null,
r.hasMetadata()
? r.metadata().entrySet().stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue() != null ? e.getValue().toString() : ""))
: Map.of()))
.toList();
return new BedrockKnowledgeBaseResponse(results, sdkResponse.nextToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. Licensed under a proprietary license.
* See the License.txt file for more information. You may not use this file
* except in compliance with the proprietary license.
*/
package io.camunda.connector.aws.bedrock.knowledgebase.model.request;

import io.camunda.connector.api.annotation.FEEL;
import io.camunda.connector.aws.model.impl.AwsBaseRequest;
import io.camunda.connector.generator.java.annotation.TemplateProperty;
import io.camunda.connector.generator.java.annotation.TemplateProperty.PropertyConstraints;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public class BedrockKnowledgeBaseRequest extends AwsBaseRequest {

@FEEL
@NotBlank
@TemplateProperty(
group = "configuration",
label = "Knowledge Base ID",
description = "The ID of the Bedrock Knowledge Base to query.",
constraints = @PropertyConstraints(notEmpty = true))
private String knowledgeBaseId;

@Valid @NotNull private KnowledgeBaseOperation operation;

public String getKnowledgeBaseId() {
return knowledgeBaseId;
}

public void setKnowledgeBaseId(String knowledgeBaseId) {
this.knowledgeBaseId = knowledgeBaseId;
}

public KnowledgeBaseOperation getOperation() {
return operation;
}

public void setOperation(KnowledgeBaseOperation operation) {
this.operation = operation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. Licensed under a proprietary license.
* See the License.txt file for more information. You may not use this file
* except in compliance with the proprietary license.
*/
package io.camunda.connector.aws.bedrock.knowledgebase.model.request;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.camunda.connector.generator.java.annotation.TemplateDiscriminatorProperty;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "operationDiscriminator")
@JsonSubTypes({
@JsonSubTypes.Type(value = RetrieveOperation.class, name = "retrieve"),
})
@TemplateDiscriminatorProperty(
label = "Operation",
group = "operation",
name = "operationDiscriminator",
defaultValue = "retrieve")
public sealed interface KnowledgeBaseOperation permits RetrieveOperation {}
Loading
Loading