Skip to content

Commit

Permalink
Add support for Projects
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanBratanov committed Aug 26, 2024
1 parent c902893 commit 3c246fc
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 13 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
run: ./gradlew spotlessCheck build
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ADMIN_KEY: ${{ secrets.OPENAI_ADMIN_KEY }}
- name: Run SonarQube analysis
run: |
if [ -n "$SONAR_TOKEN" ]; then
Expand Down
19 changes: 10 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ The following is a set of guidelines for contributing to this repo:
./gradlew spotlessApply
```

* Running some of the tests in classes extending `OpenAIIntegrationTestBase` require to
set `OPENAI_API_KEY` environment variable with your
API key. Refer to
these [instructions](https://platform.openai.com/docs/api-reference/authentication) to create one. No need to run
those tests if you don't have an API key or don't want to spend your balance. Can rely on CI executing those instead.
😉
* Running some of the integration tests require to
set `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variables with your
API keys. Refer to [this](https://platform.openai.com/docs/api-reference/authentication)
and [this](https://platform.openai.com/organization/admin-keys) for more details. No need to run
those tests if you don't have API keys or don't want to spend your balance. Can rely on CI
executing those instead. 😉
* If your PR modifies a request/response object, please add the changes in `TestDataUtil` and
run the `OpenApiSpecificationValidationTest` tests to ensure
the [spec](https://github.com/openai/openai-openapi/raw/master/openapi.yaml) is not violated.
* If your PR adds a new endpoint, please refer to the classes extending `OpenAIClient` for code examples. Also, please
add a test case either in `OpenAIIntegrationTest` or `OpenAIAssistantsApiIntegrationTest` depending on the endpoint
implemented.
* If your PR adds a new endpoint, please refer to the classes extending `OpenAIClient` for code
examples. Also, please add a test case either
in `OpenAIIntegrationTest`, `OpenAIAssistantsApiIntegrationTest`
or `OpenAIAdminIntegrationTest` depending on the endpoint implemented.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple
|----------------------------------------------------------------------------------|:------:|
| [Invites](https://platform.openai.com/docs/api-reference/invite) | ✔️ |
| [Users](https://platform.openai.com/docs/api-reference/users) | ✔️ |
| [Projects](https://platform.openai.com/docs/api-reference/projects) | |
| [Projects](https://platform.openai.com/docs/api-reference/projects) | ✔️ |
| [Project Users](https://platform.openai.com/docs/api-reference/project-users) | |
| [Project Service Accounts](https://platform.openai.com/docs/api-reference/project-service-accounts) | |
| [Project API Keys](https://platform.openai.com/docs/api-reference/project-api-keys) | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.stefanbratanov.jvm.openai;

public record CreateProjectRequest(String name) {

public static Builder newBuilder() {
return new Builder();
}

public static class Builder {

private String name;

/**
* @param name The friendly name of the project, this name appears in reports.
*/
public Builder name(String name) {
this.name = name;
return this;
}

public CreateProjectRequest build() {
return new CreateProjectRequest(name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ enum Endpoint {
VECTOR_STORES("vector_stores"),
// Administration
INVITES("organization/invites"),
USERS("organization/users");
USERS("organization/users"),
PROJECTS("organization/projects");

private final String path;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.stefanbratanov.jvm.openai;

public record ModifyProjectRequest(String name) {

public static Builder newBuilder() {
return new Builder();
}

public static class Builder {

private String name;

/**
* @param name The updated name of the project, this name appears in reports.
*/
public Builder name(String name) {
this.name = name;
return this;
}

public ModifyProjectRequest build() {
return new ModifyProjectRequest(name);
}
}
}
11 changes: 11 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public final class OpenAI {
private final VectorStoreFileBatchesClient vectorStoreFileBatchesClient;
private final InvitesClient invitesClient;
private final UsersClient usersClient;
private final ProjectsClient projectsClient;

private OpenAI(
URI baseUrl,
Expand Down Expand Up @@ -76,6 +77,8 @@ private OpenAI(
invitesClient =
new InvitesClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout);
usersClient = new UsersClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout);
projectsClient =
new ProjectsClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout);
}

/**
Expand Down Expand Up @@ -241,6 +244,14 @@ public UsersClient usersClient() {
return usersClient;
}

/**
* @return a client based on <a
* href="https://platform.openai.com/docs/api-reference/projects">Projects</a>
*/
public ProjectsClient projectsClient() {
return projectsClient;
}

private String[] createAuthenticationHeaders(
Optional<String> apiKey, Optional<String> organization, Optional<String> project) {
List<String> authHeaders = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.github.stefanbratanov.jvm.openai;

/** Represents an individual project. */
public record Project(String id, String name, long createdAt, Long archivedAt, String status) {}
128 changes: 128 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/ProjectsClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.github.stefanbratanov.jvm.openai;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* Manage the projects within an organization includes creation, updating, and archiving or
* projects. The Default project cannot be modified or archived.
*
* <p>Based on <a href="https://platform.openai.com/docs/api-reference/projects">Projects</a>
*/
public final class ProjectsClient extends OpenAIClient {

private final URI baseUrl;

ProjectsClient(
URI baseUrl,
String[] authenticationHeaders,
HttpClient httpClient,
Optional<Duration> requestTimeout) {
super(authenticationHeaders, httpClient, requestTimeout);
this.baseUrl = baseUrl;
}

/**
* Returns a list of projects.
*
* @param after A cursor for use in pagination. after is an object ID that defines your place in
* the list.
* @param limit A limit on the number of objects to be returned.
* @param includeArchived If true returns all projects including those that have been archived.
* Archived projects are not included by default.
* @throws OpenAIException in case of API errors
*/
public PaginatedProjects listProjects(
Optional<String> after, Optional<Integer> limit, Optional<Boolean> includeArchived) {
String queryParameters =
createQueryParameters(
Map.of(
Constants.LIMIT_QUERY_PARAMETER,
limit,
Constants.AFTER_QUERY_PARAMETER,
after,
"include_archived",
includeArchived));
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.PROJECTS.getPath() + queryParameters))
.GET()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), PaginatedProjects.class);
}

public record PaginatedProjects(
List<Project> data, String firstId, String lastId, boolean hasMore) {}

/**
* Create a new project in the organization. Projects can be created and archived, but cannot be
* deleted.
*
* @throws OpenAIException in case of API errors
*/
public Project createProject(CreateProjectRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.PROJECTS.getPath()))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Project.class);
}

/**
* Retrieves a project.
*
* @param projectId The ID of the project.
* @throws OpenAIException in case of API errors
*/
public Project retrieveProject(String projectId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.PROJECTS.getPath() + "/" + projectId))
.GET()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Project.class);
}

/**
* Modifies a project in the organization.
*
* @param projectId The ID of the project.
* @throws OpenAIException in case of API errors
*/
public Project modifyProject(String projectId, ModifyProjectRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.PROJECTS.getPath() + "/" + projectId))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Project.class);
}

/**
* Archives a project in the organization. Archived projects cannot be used or updated.
*
* @param projectId The ID of the project.
* @throws OpenAIException in case of API errors
*/
public Project archiveProject(String projectId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.PROJECTS.getPath() + "/" + projectId + "/archive"))
.POST(BodyPublishers.noBody())
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Project.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ public record PaginatedUsers(List<User> data, String firstId, String lastId, boo
/**
* Modifies a user's role in the organization.
*
* @param userId The ID of the user.
* @throws OpenAIException in case of API errors
*/
public User modifyUser(ModifyUserRequest request) {
public User modifyUser(String userId, ModifyUserRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.USERS.getPath()))
.uri(baseUrl.resolve(Endpoint.USERS.getPath() + "/" + userId))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,22 @@ void testUsersClient() {

assertThat(retrievedUser).isEqualTo(user);
}

@Test
void testProjectsClient() {
ProjectsClient projectsClient = openAI.projectsClient();

List<Project> projects =
projectsClient.listProjects(Optional.empty(), Optional.empty(), Optional.empty()).data();

assertThat(projects).isNotEmpty();

Project project = projects.get(0);

assertThat(project.name()).isNotBlank();

Project retrievedProject = projectsClient.retrieveProject(project.id());

assertThat(retrievedProject).isEqualTo(project);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,21 @@ void validateUsers() {
validate(request, response);
}

@RepeatedTest(25)
void validateProjects() {
CreateProjectRequest createProjectRequest = testDataUtil.randomCreateProjectRequest();

Request request =
createRequestWithBody(
Method.POST, "/" + Endpoint.PROJECTS.getPath(), serializeObject(createProjectRequest));

Project project = testDataUtil.randomProject();

Response response = createResponseWithBody(serializeObject(project));

validate(request, response);
}

private void validate(Request request, Response response, String... reportMessagesToIgnore) {
ValidationReport report = validator.validate(request, response);
validateReport(report, reportMessagesToIgnore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,19 @@ public User randomUser() {
randomLong(10_000, 1_000_000));
}

public CreateProjectRequest randomCreateProjectRequest() {
return CreateProjectRequest.newBuilder().name(randomString(7)).build();
}

public Project randomProject() {
return new Project(
randomString(5),
randomString(7),
randomLong(10_000, 1_000_000),
randomLong(11_111, 1_111_111),
oneOf("active", "archived"));
}

private ChunkingStrategy.StaticChunkingStrategy randomStaticChunkingStrategy() {
int randomMaxChunkSizeTokens = randomInt(100, 4096);
return ChunkingStrategy.staticChunkingStrategy(
Expand Down

0 comments on commit 3c246fc

Please sign in to comment.