Skip to content

Commit

Permalink
Add support for Users
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanBratanov committed Aug 26, 2024
1 parent 5eaf39b commit c902893
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 8 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ Java. The only dependency used is [Jackson](https://github.com/FasterXML/jackson

Java 17+ is a prerequisite

#### Gradle
### Gradle

```groovy
implementation("io.github.stefanbratanov:jvm-openai:${version}")
```

#### Maven
### Maven

```xml
<dependency>
Expand All @@ -43,7 +43,7 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple

## Supported APIs

#### Endpoints
### Endpoints

| API | Status |
|---------------------------------------------------------------------------|:------:|
Expand All @@ -58,7 +58,7 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple
| [Models](https://platform.openai.com/docs/api-reference/models) | ✔️ |
| [Moderations](https://platform.openai.com/docs/api-reference/moderations) | ✔️ |

#### Assistants (Beta)
### Assistants (Beta)

| API | Status |
|--------------------------------------------------------------------------------------------------------|:------:|
Expand All @@ -71,12 +71,12 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple
| [Vector Store Files](https://platform.openai.com/docs/api-reference/vector-stores-files) | ✔️ |
| [Vector Store File Batches](https://platform.openai.com/docs/api-reference/vector-stores-file-batches) | ✔️ |

#### Administration
### Administration

| API | Status |
|----------------------------------------------------------------------------------|:------:|
| [Invites](https://platform.openai.com/docs/api-reference/invite) | ✔️ |
| [Users](https://platform.openai.com/docs/api-reference/users) | |
| [Users](https://platform.openai.com/docs/api-reference/users) | ✔️ |
| [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) | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ enum Endpoint {
ASSISTANTS("assistants"),
VECTOR_STORES("vector_stores"),
// Administration
INVITES("organization/invites");
INVITES("organization/invites"),
USERS("organization/users");

private final String path;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* <p>Based on <a href="https://platform.openai.com/docs/api-reference/invite">Invites</a>
*/
public class InvitesClient extends OpenAIClient {
public final class InvitesClient extends OpenAIClient {

private final URI baseUrl;

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

public record ModifyUserRequest(String role) {

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

public static class Builder {

private String role;

/**
* @param role `owner` or `reader`
*/
public Builder role(String role) {
this.role = role;
return this;
}

public ModifyUserRequest build() {
return new ModifyUserRequest(role);
}
}
}
10 changes: 10 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 @@ -33,6 +33,7 @@ public final class OpenAI {
private final VectorStoreFilesClient vectorStoreFilesClient;
private final VectorStoreFileBatchesClient vectorStoreFileBatchesClient;
private final InvitesClient invitesClient;
private final UsersClient usersClient;

private OpenAI(
URI baseUrl,
Expand Down Expand Up @@ -74,6 +75,7 @@ private OpenAI(
String[] adminAuthenticationHeaders = createAdminAuthenticationHeaders(adminKey);
invitesClient =
new InvitesClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout);
usersClient = new UsersClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout);
}

/**
Expand Down Expand Up @@ -231,6 +233,14 @@ public InvitesClient invitesClient() {
return invitesClient;
}

/**
* @return a client based on <a
* href="https://platform.openai.com/docs/api-reference/users">Users</a>
*/
public UsersClient usersClient() {
return usersClient;
}

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

/** Represents an individual user within an organization. */
public record User(String id, String name, String email, String role, long addedAt) {}
100 changes: 100 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/UsersClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.github.stefanbratanov.jvm.openai;

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

/**
* Manage users and their role in an organization. Users will be automatically added to the Default
* project.
*
* <p>Based on <a href="https://platform.openai.com/docs/api-reference/users">Users</a>
*/
public final class UsersClient extends OpenAIClient {

private final URI baseUrl;

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

/**
* Lists all of the users in the organization.
*
* @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.
* @throws OpenAIException in case of API errors
*/
public PaginatedUsers listUsers(Optional<String> after, Optional<Integer> limit) {
String queryParameters =
createQueryParameters(
Map.of(Constants.LIMIT_QUERY_PARAMETER, limit, Constants.AFTER_QUERY_PARAMETER, after));
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.USERS.getPath() + queryParameters))
.GET()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), PaginatedUsers.class);
}

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

/**
* Modifies a user's role in the organization.
*
* @throws OpenAIException in case of API errors
*/
public User modifyUser(ModifyUserRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.USERS.getPath()))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), User.class);
}

/**
* Retrieves a user by their identifier.
*
* @param userId The ID of the user.
* @throws OpenAIException in case of API errors
*/
public User retrieveUser(String userId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.USERS.getPath() + "/" + userId))
.GET()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), User.class);
}

/**
* Deletes a user from the organization.
*
* @param userId The ID of the user.
* @throws OpenAIException in case of API errors
*/
public DeletionStatus deleteUser(String userId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.USERS.getPath() + "/" + userId))
.DELETE()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), DeletionStatus.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.assertj.core.api.Assertions.assertThat;

import io.github.stefanbratanov.jvm.openai.UsersClient.PaginatedUsers;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -45,4 +46,25 @@ void testInvitesClient() {
DeletionStatus deletionStatus = invitesClient.deleteInvite(invite.id());
assertThat(deletionStatus.deleted()).isTrue();
}

@Test
void testUsersClient() {
UsersClient usersClient = openAI.usersClient();

PaginatedUsers paginatedUsers = usersClient.listUsers(Optional.empty(), Optional.empty());

assertThat(paginatedUsers.hasMore()).isFalse();
assertThat(paginatedUsers.firstId()).isNotBlank();
assertThat(paginatedUsers.lastId()).isNotBlank();

List<User> users = paginatedUsers.data();

assertThat(users).isNotEmpty();

User user = users.get(0);

User retrievedUser = usersClient.retrieveUser(user.id());

assertThat(retrievedUser).isEqualTo(user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,23 @@ void validateInvites() {
validate(request, response);
}

@RepeatedTest(25)
void validateUsers() {
ModifyUserRequest modifyUserRequest = testDataUtil.randomModifyUserRequest();

Request request =
createRequestWithBody(
Method.POST,
"/" + Endpoint.USERS.getPath() + "/{user_id}",
serializeObject(modifyUserRequest));

User user = testDataUtil.randomUser();

Response response = createResponseWithBody(serializeObject(user));

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 @@ -732,6 +732,19 @@ public Invite randomInvite() {
randomLong(10_000, 1_000_000));
}

public ModifyUserRequest randomModifyUserRequest() {
return ModifyUserRequest.newBuilder().role(oneOf("owner", "reader")).build();
}

public User randomUser() {
return new User(
randomString(5),
randomString(7),
"[email protected]",
oneOf("owner", "reader"),
randomLong(10_000, 1_000_000));
}

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

0 comments on commit c902893

Please sign in to comment.