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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.1] - 2026-03-20

### Added
- Implemented File API capabilities (`FileUpload`, multipart uploads) matching the Python SDK.
- Support for attaching files to `Test` and `TestSet` entities.
- Added overloaded convenience methods to directly accept `Path`, `java.io.File`, and base64 strings for file upload.
- Added `Lombok` dependency (`@Builder`) to eliminate boilerplate in SDK entities.

### Changed
- Refactored all entity classes (`Test`, `TestSet`, `Endpoint`, `Project`, etc.) to use the Builder pattern instead of verbose constructors.
- Updated `CreateEndpointExample` and `FileSupportExample` to use the new Builder syntax.
- Updated unit tests (`EntityTest`, `ClientWiremockTest`) and integration tests to use the new entity builders.
- Fixed a Jackson deserialization issue using `NameStringDeserializer` to handle polymorphic JSON fields from the backend API.

## [0.1.0] - 2026-03-18

### Added
- Initial release of the Rhesis Java SDK.
- Core entities and API clients (`Test`, `TestSet`, `Endpoint`, `Project`, `TestRun`, `TestResult`, `Prompt`, `Status`).
- Local synthesizers for test generation using Jinja templates.
- Comprehensive unit testing suite using WireMock, JUnit 5, and AssertJ.
- Integration tests setup using Maven Failsafe plugin.
- Support for loading environment variables via `dotenv-java`.
- CI/CD workflows for linting (Spotless, PMD, ErrorProne) and testing on pull requests.
- Makefile for common build tasks.
- GitHub Packages publishing setup.
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ lint:

check: lint

# Run all tests (unit and integration)
# Run all tests (unit and integration) (disables PMD checking since it's handled by lint)
test:
mvn verify
mvn verify -Dpmd.skip=true

# Run only unit tests
test-unit:
mvn test
mvn test -Dpmd.skip=true

# Run only integration tests
# Run only integration tests (disables PMD checking since it's handled by lint)
test-integration:
mvn verify -Dsurefire.skip=true
mvn verify -Dsurefire.skip=true -Dpmd.skip=true

# Build the project
build:
Expand Down
15 changes: 14 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>ai.rhesis</groupId>
<artifactId>rhesis-java</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
Expand Down Expand Up @@ -106,6 +106,14 @@
<version>3.0.0</version>
</dependency>

<!-- Lombok for reducing boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>

<!-- Templating -->
<dependency>
<groupId>com.hubspot.jinjava</groupId>
Expand Down Expand Up @@ -141,6 +149,11 @@
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
Expand Down
1 change: 1 addition & 0 deletions sdk
17 changes: 17 additions & 0 deletions src/main/java/ai/rhesis/sdk/clients/FileClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import ai.rhesis.sdk.entities.File;
import ai.rhesis.sdk.http.InternalHttpClient;
import ai.rhesis.sdk.http.MultipartBuilder;
import ai.rhesis.sdk.models.FileUpload;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;

Expand All @@ -23,4 +25,19 @@ public File get(String id) {
public byte[] download(String id) {
return httpClient.get("/files/" + id + "/content", byte[].class);
}

public List<File> upload(List<FileUpload> files, String entityId, String entityType) {
MultipartBuilder multipartBuilder = new MultipartBuilder();
for (FileUpload file : files) {
multipartBuilder.addPart(
"files", file.getFilename(), file.getContentType(), file.getContent());
}

String path = "/files/?entity_id=" + entityId + "&entity_type=" + entityType;
return httpClient.postMultipart(path, multipartBuilder, new TypeReference<List<File>>() {});
}

public void delete(String id) {
httpClient.delete("/files/" + id);
}
}
70 changes: 69 additions & 1 deletion src/main/java/ai/rhesis/sdk/clients/TestClient.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package ai.rhesis.sdk.clients;

import ai.rhesis.sdk.entities.File;
import ai.rhesis.sdk.entities.Test;
import ai.rhesis.sdk.http.InternalHttpClient;
import ai.rhesis.sdk.models.FileUpload;
import com.fasterxml.jackson.core.type.TypeReference;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class TestClient {
Expand All @@ -21,10 +25,74 @@ public List<Test> list() {
}

public Test create(Test test) {
return httpClient.post("/tests/", test, Test.class);
// Determine if we have files to upload
boolean hasFiles = test.files() != null && !test.files().isEmpty();

// Create the test without files first
Test testToCreate =
hasFiles
? new Test(
test.id(),
test.testConfiguration(),
test.behavior(),
test.category(),
test.topic(),
test.testType(),
test.prompt(),
test.metadata(),
null)
: test;

Test created = httpClient.post("/tests/", testToCreate, Test.class);

// Upload files if any
if (hasFiles) {
List<FileUpload> fileUploads =
test.files().stream()
.map(
pathStr -> {
Path path = Paths.get(pathStr);
FileUpload upload = FileUpload.fromPath(path);
String contentType = upload.getContentType();
if ("text/plain".equals(contentType)
|| "application/octet-stream".equals(contentType)) {
contentType = "image/png";
}
return new FileUpload(upload.getFilename(), contentType, upload.getContent());
})
.toList();
addFiles(created.id(), fileUploads);
}

return created;
}

public void delete(String id) {
httpClient.delete("/tests/" + id);
}

public List<File> addFiles(String testId, List<FileUpload> files) {
return new FileClient(httpClient).upload(files, testId, "Test");
}

public List<File> addFile(String testId, Path path) {
return addFiles(testId, List.of(FileUpload.fromPath(path)));
}

public List<File> addFile(String testId, java.io.File fileUpload) {
return addFiles(testId, List.of(FileUpload.fromFile(fileUpload)));
}

public List<File> addFileFromBase64(
String testId, String filename, String contentType, String base64Data) {
return addFiles(testId, List.of(FileUpload.fromBase64(filename, contentType, base64Data)));
}

public List<File> getFiles(String testId) {
return httpClient.get("/tests/" + testId + "/files", new TypeReference<List<File>>() {});
}

public void deleteFile(String fileId) {
new FileClient(httpClient).delete(fileId);
}
}
6 changes: 6 additions & 0 deletions src/main/java/ai/rhesis/sdk/clients/TestResultClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ai.rhesis.sdk.clients;

import ai.rhesis.sdk.entities.File;
import ai.rhesis.sdk.entities.TestResult;
import ai.rhesis.sdk.http.InternalHttpClient;
import com.fasterxml.jackson.core.type.TypeReference;
Expand All @@ -19,4 +20,9 @@ public List<TestResult> list() {
public TestResult get(String id) {
return httpClient.get("/test_results/" + id, TestResult.class);
}

public List<File> getFiles(String testResultId) {
return httpClient.get(
"/test_results/" + testResultId + "/files", new TypeReference<List<File>>() {});
}
}
10 changes: 10 additions & 0 deletions src/main/java/ai/rhesis/sdk/clients/TestSetClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ public TestSet create(TestSet testSet) {
return httpClient.post("/test_sets/bulk", testSet, TestSet.class);
}

public List<ai.rhesis.sdk.entities.Test> getTests(String id) {
return getTests(id, 0, 100);
}

public List<ai.rhesis.sdk.entities.Test> getTests(String id, int skip, int limit) {
return httpClient.get(
"/test_sets/" + id + "/tests?skip=" + skip + "&limit=" + limit,
new TypeReference<List<ai.rhesis.sdk.entities.Test>>() {});
}

public void delete(String id) {
httpClient.delete("/test_sets/" + id);
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/Endpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import lombok.Builder;

@Builder
public record Endpoint(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/NameStringDeserializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ai.rhesis.sdk.entities;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;

public class NameStringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
if (node.isObject() && node.has("name")) {
return node.get("name").asText();
} else if (node.isTextual()) {
return node.asText();
} else if (node.isNull()) {
return null;
}
return node.toString();
}
}
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;

@Builder
public record Project(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/Prompt.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import java.util.Map;
import lombok.Builder;

@Builder
public record Prompt(
@JsonProperty("id") String id,
@NotBlank @JsonProperty("content") String content,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/Status.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;

@Builder
public record Status(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
Expand Down
17 changes: 14 additions & 3 deletions src/main/java/ai/rhesis/sdk/entities/Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
import lombok.Builder;

@Builder
public record Test(
@JsonProperty("id") String id,
@JsonProperty("test_configuration") TestConfiguration testConfiguration,
@JsonProperty("behavior") String behavior,
@JsonProperty("category") String category,
@JsonProperty("topic") String topic,
@JsonProperty("behavior")
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(
using = NameStringDeserializer.class)
String behavior,
@JsonProperty("category")
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(
using = NameStringDeserializer.class)
String category,
@JsonProperty("topic")
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(
using = NameStringDeserializer.class)
String topic,
@JsonProperty("test_type") TestType testType,
@JsonProperty("prompt") Prompt prompt,
@JsonProperty("metadata") Map<String, Object> metadata,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/TestConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

@Builder
public record TestConfiguration(
@NotBlank @JsonProperty("goal") String goal,
@JsonProperty("instructions") String instructions,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/TestResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import lombok.Builder;

@Builder
public record TestResult(
@JsonProperty("id") String id,
@JsonProperty("test_configuration_id") String testConfigurationId,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/TestRun.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import lombok.Builder;

@Builder
public record TestRun(
@JsonProperty("id") String id,
@JsonProperty("test_configuration_id") String testConfigurationId,
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/rhesis/sdk/entities/TestSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.IOException;
import java.util.List;
import lombok.Builder;

@Builder
public record TestSet(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
Expand Down
Loading
Loading