Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parseRequestBody(), transformPayloadForOpenAICompatibility(), verifyAndParseRequest(), getUserMessage(), getUserConfirmation() #34

Merged
merged 5 commits into from
Sep 1, 2024
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
85 changes: 84 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export default handler(request, response) {

### Verification

#### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`
<a name=verifyRequestByKeyId></a>

#### `async verifyRequestByKeyId(rawBody, signature, keyId, requestOptions)`

Verify the request payload using the provided signature and key ID. The method will request the public key from GitHub's API for the given keyId and then verify the payload.

Expand Down Expand Up @@ -202,6 +204,87 @@ import { createDoneEvent } from "@copilot-extensions/preview-sdk";
response.write(createDoneEvent().toString());
```

### Parsing

<a name="parseRequestBody"></a>

#### `parseRequestBody(body)`

Parses the raw request body and returns an object with type support.

⚠️ **It's well possible that the type is not 100% correct. Please send pull requests to `index.d.ts` to improve it**

```js
import { parseRequestBody } from "@copilot-extensions/preview-sdk";

const payload = parseRequestBody(rawBody);
// When your IDE supports types, typing "payload." should prompt the available keys and their types.
```

#### `transformPayloadForOpenAICompatibility()`

For cases when you want to pipe a user request directly to OpenAI, use this method to remove Copilot-specific fields from the request payload.

```js
import { transformPayloadForOpenAICompatibility } from "@copilot-extensions/preview-sdk";
import { OpenAI } from "openai";

const openaiPayload = transformPayloadForOpenAICompatibility(payload);

const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
const stream = openai.beta.chat.completions.stream({
...openaiPayload,
model: "gpt-4-1106-preview",
stream: true,
});
```

#### `verifyAndParseRequest()`

Convenience method to verify and parse a request in one go. It calls [`verifyRequestByKeyId()`](#verifyRequestByKeyId) and [`parseRequestBody()`](#parseRequestBody) internally.

```js
import { verifyAndParseRequest } from "@copilot-extensions/preview-sdk";

const { isValidRequest, payload } = await verifyAndParseRequest(
request,
signature,
key
);

if (!isValidRequest) {
throw new Error("Request could not be verified");
}

// `payload` has type support.
```

#### `getUserMessage()`

Convencience method to get the user's message from the request payload.

```js
import { getUserMessage } from "@copilot-extensions/preview-sdk";

const userMessage = getUserMessage(payload);
```

#### `getUserConfirmation()`

Convencience method to get the user's confirmation from the request payload (in case the user's last response was a confirmation).

```js
import { getUserConfirmation } from "@copilot-extensions/preview-sdk";

const userConfirmation = getUserConfirmation(payload);

if (userConfirmation) {
console.log("Received a user confirmation", userConfirmation);
} else {
// The user's last response was not a confirmation
}
```

## Dreamcode

While implementing the lower-level functionality, we also dream big: what would our dream SDK for Coplitot extensions look like? Please have a look and share your thoughts and ideas:
Expand Down
126 changes: 126 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { request } from "@octokit/request";

// verification types

type RequestInterface = typeof request;
type RequestOptions = {
request?: RequestInterface;
Expand Down Expand Up @@ -34,6 +36,8 @@ interface VerifyRequestByKeyIdInterface {
): Promise<boolean>;
}

// response types

export interface CreateAckEventInterface {
(): ResponseEvent<"ack">
}
Expand Down Expand Up @@ -126,6 +130,122 @@ interface CopilotReference {
};
}

// parse types

export interface CopilotRequestPayload {
copilot_thread_id: string
messages: Message[]
stop: any
top_p: number
temperature: number
max_tokens: number
presence_penalty: number
frequency_penalty: number
copilot_skills: any[]
agent: string
}

export interface OpenAICompatibilityPayload {
messages: {
role: string
name?: string
content: string
}[]
}

export interface Message {
role: string
content: string
copilot_references: MessageCopilotReference[]
copilot_confirmations?: MessageCopilotConfirmation[]
name?: string
}

export interface MessageCopilotReference {
type: string
data: CopilotReferenceData
id: string
is_implicit: boolean
metadata: CopilotReferenceMetadata
}

export interface CopilotReferenceData {
type: string
id: number
name?: string
ownerLogin?: string
ownerType?: string
readmePath?: string
description?: string
commitOID?: string
ref?: string
refInfo?: CopilotReferenceDataRefInfo
visibility?: string
languages?: CopilotReferenceDataLanguage[]
login?: string
avatarURL?: string
url?: string
}

export interface CopilotReferenceDataRefInfo {
name: string
type: string
}

export interface CopilotReferenceDataLanguage {
name: string
percent: number
}

export interface CopilotReferenceMetadata {
display_name: string
display_icon: string
display_url: string
}

export interface MessageCopilotConfirmation {
state: "dismissed" | "accepted"
confirmation: {
id: string
[key: string]: unknown
}
}

export interface ParseRequestBodyInterface {
(body: string): CopilotRequestPayload
}

export interface TransformPayloadForOpenAICompatibilityInterface {
(payload: CopilotRequestPayload): OpenAICompatibilityPayload
}


export interface VerifyAndParseRequestInterface {
(
body: string,
signature: string,
keyID: string,
requestOptions?: RequestOptions,
): Promise<{ isValidRequest: boolean; payload: CopilotRequestPayload }>;
}


export interface GetUserMessageInterface {
(payload: CopilotRequestPayload): string;
}

export type UserConfirmation = {
accepted: boolean;
id?: string;
metadata: Record<string, unknown>;
}

export interface GetUserConfirmationInterface {
(payload: CopilotRequestPayload): UserConfirmation | undefined;
}

// exported methods

export declare const verifyRequest: VerifyRequestInterface;
export declare const fetchVerificationKeys: FetchVerificationKeysInterface;
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface;
Expand All @@ -136,3 +256,9 @@ export declare const createDoneEvent: CreateDoneEventInterface;
export declare const createErrorsEvent: CreateErrorsEventInterface;
export declare const createReferencesEvent: CreateReferencesEventInterface;
export declare const createTextEvent: CreateTextEventInterface;

export declare const parseRequestBody: ParseRequestBodyInterface;
export declare const transformPayloadForOpenAICompatibility: TransformPayloadForOpenAICompatibilityInterface;
export declare const verifyAndParseRequest: VerifyAndParseRequestInterface;
export declare const getUserMessage: GetUserMessageInterface;
export declare const getUserConfirmation: GetUserConfirmationInterface;
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @ts-check

export * from "./lib/verification.js";
export * from "./lib/parse.js";
export * from "./lib/response.js";
export * from "./lib/verification.js";
52 changes: 46 additions & 6 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import {
fetchVerificationKeys,
verifyRequest,
verifyRequestByKeyId,
parseRequestBody,
transformPayloadForOpenAICompatibility,
verifyAndParseRequest,
getUserMessage,
getUserConfirmation,
type VerificationPublicKey,
CopilotRequestPayload,
} from "./index.js";

const rawBody = "";
const signature = "";
const keyId = "";
const key = ""
const token = "";

export async function verifyRequestByKeyIdTest() {
export async function verifyRequestByKeyIdTest(rawBody: string, signature: string, keyId: string) {
const result = await verifyRequestByKeyId(rawBody, signature, keyId);
expectType<boolean>(result);

Expand All @@ -43,7 +45,7 @@ export async function verifyRequestByKeyIdTest() {
await verifyRequestByKeyId(rawBody, signature, keyId, { request });
}

export async function verifyRequestTest() {
export async function verifyRequestTest(rawBody: string, signature: string, key: string) {
const result = await verifyRequest(rawBody, signature, key);
expectType<boolean>(result);

Expand Down Expand Up @@ -227,3 +229,41 @@ export function createDoneEventTest() {
// @ts-expect-error - .event is required
event.event
}

export function parseRequestBodyTest(body: string) {
const result = parseRequestBody(body)
expectType<CopilotRequestPayload>(result);
}

export function transformPayloadForOpenAICompatibilityTest(payload: CopilotRequestPayload) {
const result = transformPayloadForOpenAICompatibility(payload)
expectType<{
messages: {
content: string;
role: string;
name?: string
}[]
}
>(result);
}

export async function verifyAndParseRequestTest(rawBody: string, signature: string, keyId: string) {
const result = await verifyAndParseRequest(rawBody, signature, keyId)
expectType<{ isValidRequest: boolean, payload: CopilotRequestPayload }>(result);
}

export function getUserMessageTest(payload: CopilotRequestPayload) {
const result = getUserMessage(payload)
expectType<string>(result)
}

export function getUserConfirmationTest(payload: CopilotRequestPayload) {
const result = getUserConfirmation(payload)

if (result === undefined) {
expectType<undefined>(result)
return
}

expectType<{ accepted: boolean; id?: string; metadata: Record<string, unknown> }>(result)
}
57 changes: 57 additions & 0 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @ts-check

import { verifyRequestByKeyId } from "./verification.js";

/** @type {import('..').ParseRequestBodyInterface} */
export function parseRequestBody(body) {
return JSON.parse(body);
}

/** @type {import('..').TransformPayloadForOpenAICompatibilityInterface} */
export function transformPayloadForOpenAICompatibility(payload) {
return {
messages: payload.messages.map((message) => {
return {
role: message.role,
name: message.name,
content: message.content,
};
}),
};
}

/** @type {import('..').VerifyAndParseRequestInterface} */
export async function verifyAndParseRequest(body, signature, keyID, options) {
const isValidRequest = await verifyRequestByKeyId(
body,
signature,
keyID,
options
);

return {
isValidRequest,
payload: parseRequestBody(body),
};
}

/** @type {import('..').GetUserMessageInterface} */
export function getUserMessage(payload) {
return payload.messages[payload.messages.length - 1].content;
}

/** @type {import('..').GetUserConfirmationInterface} */
export function getUserConfirmation(payload) {
const confirmation =
payload.messages[payload.messages.length - 1].copilot_confirmations?.[0];

if (!confirmation) return;

const { id, ...metadata } = confirmation.confirmation;

return {
accepted: confirmation.state === "accepted",
id,
metadata,
};
}
Loading