Skip to content

Commit

Permalink
Add support for Invites
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanBratanov committed Aug 25, 2024
1 parent cf1aca9 commit 5eaf39b
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 23 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple

## Supported APIs

> **_NOTE:_** Legacy APIs are not supported
#### 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) | ✔️ |

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

| API | Status |
|--------------------------------------------------------------------------------------------------------|:------:|
Expand All @@ -71,6 +71,20 @@ 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

| API | Status |
|----------------------------------------------------------------------------------|:------:|
| [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) | |
| [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) | |
| [Audit Logs](https://platform.openai.com/docs/api-reference/audit-logs) | |

> **_NOTE:_** Legacy APIs are not supported
## More examples

- Configure an organization and project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public Batch cancelBatch(String batchId) {
* @param limit A limit on the number of objects to be returned.
* @throws OpenAIException in case of API errors
*/
public PaginatedBatches listBatches(Optional<String> after, Optional<String> limit) {
public PaginatedBatches listBatches(Optional<String> after, Optional<Integer> limit) {
String queryParameters =
createQueryParameters(
Map.of(Constants.LIMIT_QUERY_PARAMETER, limit, Constants.AFTER_QUERY_PARAMETER, after));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ enum Endpoint {
// Beta
THREADS("threads"),
ASSISTANTS("assistants"),
VECTOR_STORES("vector_stores");
VECTOR_STORES("vector_stores"),
// Administration
INVITES("organization/invites");

private final String path;

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/Invite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.stefanbratanov.jvm.openai;

/** Represents an individual `invite` to the organization. */
public record Invite(
String id,
String email,
String role,
String status,
long invitedAt,
long expiresAt,
Long acceptedAt) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.stefanbratanov.jvm.openai;

public record InviteRequest(String email, String role) {

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

public static class Builder {

private String email;
private String role;

/**
* @param email Send an email to this address
*/
public Builder email(String email) {
this.email = email;
return this;
}

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

public InviteRequest build() {
return new InviteRequest(email, role);
}
}
}
102 changes: 102 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/InvitesClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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;

/**
* Invite and manage invitations for an organization. Invited users are automatically added to the
* Default project.
*
* <p>Based on <a href="https://platform.openai.com/docs/api-reference/invite">Invites</a>
*/
public class InvitesClient extends OpenAIClient {

private final URI baseUrl;

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

/**
* Returns a list of invites 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 PaginatedInvites listInvites(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.INVITES.getPath() + queryParameters))
.GET()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), PaginatedInvites.class);
}

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

/**
* Create an invite for a user to the organization. The invite must be accepted by the user before
* they have access to the organization.
*
* @throws OpenAIException in case of API errors
*/
public Invite createInvite(InviteRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.INVITES.getPath()))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Invite.class);
}

/**
* Retrieves an invite.
*
* @param inviteId The ID of the invite to retrieve.
* @throws OpenAIException in case of API errors
*/
public Invite retrieveInvite(String inviteId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.INVITES.getPath() + "/" + inviteId))
.GET()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Invite.class);
}

/**
* Delete an invite. If the invite has already been accepted, it cannot be deleted.
*
* @param inviteId The ID of the invite to delete.
* @throws OpenAIException in case of API errors
*/
public DeletionStatus deleteInvite(String inviteId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.INVITES.getPath() + "/" + inviteId))
.DELETE()
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), DeletionStatus.class);
}
}
35 changes: 35 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 @@ -32,10 +32,12 @@ public final class OpenAI {
private final VectorStoresClient vectorStoresClient;
private final VectorStoreFilesClient vectorStoreFilesClient;
private final VectorStoreFileBatchesClient vectorStoreFileBatchesClient;
private final InvitesClient invitesClient;

private OpenAI(
URI baseUrl,
Optional<String> apiKey,
Optional<String> adminKey,
Optional<String> organization,
Optional<String> project,
HttpClient httpClient,
Expand All @@ -54,6 +56,7 @@ private OpenAI(
modelsClient = new ModelsClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
moderationsClient =
new ModerationsClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
// Assistants
assistantsClient =
new AssistantsClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
threadsClient = new ThreadsClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
Expand All @@ -67,6 +70,10 @@ private OpenAI(
vectorStoreFileBatchesClient =
new VectorStoreFileBatchesClient(
baseUrl, authenticationHeaders, httpClient, requestTimeout);
// Administration
String[] adminAuthenticationHeaders = createAdminAuthenticationHeaders(adminKey);
invitesClient =
new InvitesClient(baseUrl, adminAuthenticationHeaders, httpClient, requestTimeout);
}

/**
Expand Down Expand Up @@ -216,6 +223,14 @@ public VectorStoreFileBatchesClient vectorStoreFileBatchesClient() {
return vectorStoreFileBatchesClient;
}

/**
* @return a client based on <a
* href="https://platform.openai.com/docs/api-reference/invite">Invites</a>
*/
public InvitesClient invitesClient() {
return invitesClient;
}

private String[] createAuthenticationHeaders(
Optional<String> apiKey, Optional<String> organization, Optional<String> project) {
List<String> authHeaders = new ArrayList<>();
Expand All @@ -240,6 +255,16 @@ private String[] createAuthenticationHeaders(
return authHeaders.toArray(new String[] {});
}

private String[] createAdminAuthenticationHeaders(Optional<String> adminKey) {
List<String> authHeaders = new ArrayList<>();
adminKey.ifPresent(
key -> {
authHeaders.add(Constants.AUTHORIZATION_HEADER);
authHeaders.add("Bearer " + key);
});
return authHeaders.toArray(new String[] {});
}

public static Builder newBuilder() {
return new Builder();
}
Expand All @@ -256,6 +281,7 @@ public static class Builder {
private static final String DEFAULT_BASE_URL = "https://api.openai.com/v1/";

private Optional<String> apiKey = Optional.empty();
private Optional<String> adminKey = Optional.empty();

private String baseUrl = DEFAULT_BASE_URL;

Expand All @@ -274,6 +300,14 @@ public Builder apiKey(String apiKey) {
return this;
}

/**
* @param adminKey the API key used for administration endpoints.
*/
public Builder adminKey(String adminKey) {
this.adminKey = Optional.of(adminKey);
return this;
}

/**
* @param baseUrl the url which exposes the OpenAI API
*/
Expand Down Expand Up @@ -325,6 +359,7 @@ public OpenAI build() {
return new OpenAI(
URI.create(baseUrl),
apiKey,
adminKey,
organization,
project,
httpClient.orElseGet(HttpClient::newHttpClient),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.stefanbratanov.jvm.openai;

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

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;

@EnabledIfEnvironmentVariable(named = "OPENAI_ADMIN_KEY", matches = ".*\\S.*")
class OpenAIAdminIntegrationTest {

private static OpenAI openAI;

@BeforeAll
public static void setUp() {
String adminKey = System.getenv("OPENAI_ADMIN_KEY");
openAI = OpenAI.newBuilder().adminKey(adminKey).build();
}

@Test
void testInvitesClient() {
InvitesClient invitesClient = openAI.invitesClient();

InviteRequest inviteRequest =
InviteRequest.newBuilder().email("foobar@example.com").role("reader").build();

Invite invite = invitesClient.createInvite(inviteRequest);

assertThat(invite.email()).isEqualTo("foobar@example.com");
assertThat(invite.status()).isEqualTo("pending");
assertThat(invite.expiresAt()).isGreaterThan(Instant.now().getEpochSecond());

List<Invite> invites = invitesClient.listInvites(Optional.empty(), Optional.empty()).data();

assertThat(invites).hasSize(1).containsExactly(invite);

Invite retrievedInvite = invitesClient.retrieveInvite(invite.id());

assertThat(retrievedInvite).isEqualTo(invite);

// cleanup
DeletionStatus deletionStatus = invitesClient.deleteInvite(invite.id());
assertThat(deletionStatus.deleted()).isTrue();
}
}
Loading

0 comments on commit 5eaf39b

Please sign in to comment.