Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,21 @@
</exclusions>
</dependency>

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId>
<version>${langchain4j.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.jenkins.plugins.explain_error.provider;

import dev.langchain4j.model.bedrock.BedrockChatModel;
import dev.langchain4j.model.bedrock.BedrockChatRequestParameters;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
import hudson.model.TaskListener;
import hudson.util.FormValidation;
import io.jenkins.plugins.explain_error.ExplanationException;
import java.time.Duration;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;
import software.amazon.awssdk.regions.Region;

public class BedrockProvider extends BaseAIProvider {

private static final Logger LOGGER = Logger.getLogger(BedrockProvider.class.getName());

private String region;

@DataBoundConstructor
public BedrockProvider(String url, String model, String region) {
super(url, model);
this.region = Util.fixEmptyAndTrim(region);
}

public String getRegion() {
return region;
}

@Override
public Assistant createAssistant() {
var builder = BedrockChatModel.builder()
.modelId(getModel())
.defaultRequestParameters(
BedrockChatRequestParameters.builder()
.temperature(0.3)
.build())
.timeout(Duration.ofSeconds(180))
.logRequests(LOGGER.isLoggable(Level.FINE))
.logResponses(LOGGER.isLoggable(Level.FINE));

if (region != null) {
builder.region(Region.of(region));
}

ChatModel model = builder.build();
return AiServices.create(Assistant.class, model);
}

@Override
public boolean isNotValid(@CheckForNull TaskListener listener) {
if (listener != null) {
if (Util.fixEmptyAndTrim(getModel()) == null) {
listener.getLogger().println("No Model configured for AWS Bedrock.");
}
}
return Util.fixEmptyAndTrim(getModel()) == null;
}

@Extension
@Symbol("bedrock")
public static class DescriptorImpl extends BaseProviderDescriptor {

@NonNull
@Override
public String getDisplayName() {
return "AWS Bedrock";
}

public String getDefaultModel() {
return "eu.anthropic.claude-3-5-sonnet-20240620-v1:0";
}

public String getDefaultRegion() {
return "eu-west-1";
}

/**
* Method to test the AI API configuration.
* This is called when the "Test Configuration" button is clicked.
*/
@POST
public FormValidation doTestConfiguration(@QueryParameter("model") String model,
@QueryParameter("region") String region) throws ExplanationException {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

BedrockProvider provider = new BedrockProvider(null, model, region);
try {
provider.explainError("Send 'Configuration test successful' to me.", null);
return FormValidation.ok("Configuration test successful! AWS Bedrock connection is working properly.");
} catch (ExplanationException e) {
return FormValidation.error("Configuration test failed: " + e.getMessage(), e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry title="Region" field="region">
<f:textbox default="${descriptor.defaultRegion}"/>
</f:entry>

<f:entry title="Model" field="model">
<f:textbox clazz="required" default="${descriptor.defaultModel}"/>
</f:entry>

<f:validateButton title="Test Configuration" progress="Testing..."
method="testConfiguration" with="model,region" />
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div>
The Bedrock model ID or inference profile to use.
<br/>
Examples:
<ul>
<li><code>anthropic.claude-3-5-sonnet-20240620-v1:0</code> - Claude 3.5 Sonnet</li>
<li><code>eu.anthropic.claude-3-5-sonnet-20240620-v1:0</code> - Claude 3.5 Sonnet (EU cross-region)</li>
<li><code>meta.llama3-8b-instruct-v1:0</code> - Llama 3 8B Instruct</li>
<li><code>us.amazon.nova-lite-v1:0</code> - Amazon Nova Lite</li>
</ul>
See <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html" target="_blank" rel="noopener noreferrer">
AWS Bedrock supported models</a> for the full list.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div>
The AWS region where Bedrock is available (e.g., <code>us-east-1</code>, <code>eu-west-1</code>).
<br/>
If not specified, the default AWS SDK region resolution will be used
(environment variables, instance profile, etc.).
<br/>
For cross-region inference profiles (e.g., <code>eu.anthropic.*</code>), use the region
where you want to send requests.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.jenkins.plugins.explain_error.provider;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class BedrockProviderTest {

@Test
void testCreateAssistantDoesNotThrowOnBuild() {
// This test verifies that the assistant creation doesn't fail on the builder configuration itself
// It will fail when trying to actually call the API, but that's expected without real credentials
BedrockProvider provider = new BedrockProvider(null, "anthropic.claude-3-5-sonnet-20240620-v1:0", "eu-west-1");

// This should not throw any IllegalArgumentException or similar from invalid configuration
// The responseFormat parameter was causing this issue before
assertDoesNotThrow(() -> {
try {
BaseAIProvider.Assistant assistant = provider.createAssistant();
assertNotNull(assistant, "Assistant should be created");
} catch (Exception e) {
// We expect failures related to credentials/network, not configuration
// If it's a configuration error, it will typically be IllegalArgumentException
assertFalse(
e.getClass().getSimpleName().contains("IllegalArgument") ||
e.getMessage() != null && e.getMessage().contains("Unknown field"),
"Should not fail due to configuration errors: " + e.getMessage()
);
}
});
}

@Test
void testValidationWithNullModel() {
BedrockProvider provider = new BedrockProvider(null, null, "eu-west-1");
assertTrue(provider.isNotValid(null), "Should be invalid with null model");
}

@Test
void testValidationWithEmptyModel() {
BedrockProvider provider = new BedrockProvider(null, "", "eu-west-1");
assertTrue(provider.isNotValid(null), "Should be invalid with empty model");
}

@Test
void testValidationWithValidModel() {
BedrockProvider provider = new BedrockProvider(null, "anthropic.claude-3-5-sonnet-20240620-v1:0", "eu-west-1");
assertFalse(provider.isNotValid(null), "Should be valid with model");
}

@Test
void testRegionConfiguration() {
BedrockProvider provider = new BedrockProvider(null, "test-model", "us-east-1");
assertEquals("us-east-1", provider.getRegion());
}

@Test
void testNullRegion() {
BedrockProvider provider = new BedrockProvider(null, "test-model", null);
assertNull(provider.getRegion());
}

@Test
void testEmptyRegionIsTrimmedToNull() {
BedrockProvider provider = new BedrockProvider(null, "test-model", " ");
assertNull(provider.getRegion(), "Empty/whitespace region should be trimmed to null");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,20 @@ void testOllamaNullUrl() {

assertEquals("The provider is not properly configured.", result.getMessage());
}

@Test
void testBedrockNullModel() {
BaseAIProvider provider = new BedrockProvider(null, null, "eu-west-1");
ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));

assertEquals("The provider is not properly configured.", result.getMessage());
}

@Test
void testBedrockEmptyModel() {
BaseAIProvider provider = new BedrockProvider(null, "", "eu-west-1");
ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));

assertEquals("The provider is not properly configured.", result.getMessage());
}
}
Loading