Skip to content

Commit

Permalink
feat: createAckEvent(), createTextEvent(), `createConfirmationEve…
Browse files Browse the repository at this point in the history
…nt()`, `createReferencesEvent()`, `createErrorsEvent()`, `createDoneEvent()` (#32)
  • Loading branch information
gr2m authored Aug 31, 2024
1 parent 06310d7 commit 2c0e04f
Show file tree
Hide file tree
Showing 13 changed files with 812 additions and 83 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ jobs:
strategy:
matrix:
node_version:
- 20
# v20 does not support snapshot testing which we use for the alpha version
# - 20
- 22

steps:
Expand Down
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,38 @@ Thank you for considering to contribute to `github-project` 💖

Please note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md).
By participating you agree to abide by its terms.

## Ways to contribute

- **Reporting bugs** - if you find a bug, please [report it](https://github.com/copilot-extensions/preview-sdk.js/issues/new)!
- **Suggesting features** - if you have an idea for a new feature, please [suggest it](https://github.com/copilot-extensions/preview-sdk.js/issues/new)!
- **Contribute dreamcode** - like dreaming big? Same! Send a pull request with your beautiful API design that is so good, we just _have_ to make it real: [dreamcode.md](https://github.com/copilot-extensions/preview-sdk.js/blob/main/dreamcode.md)!
- **Contribute code** - Yes! Please! We might even have [issues that are ready to be worked on](https://github.com/copilot-extensions/preview-sdk.js/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22pull%20request%20welcome%22)!

## Development

### Prerequisites

- [Node.js](https://nodejs.org/) (v22.x)

We currently depend on Node 22+ for local development as we use new test APIs such as [snapshot testing](https://nodejs.org/api/test.html#snapshot-testing)! At some point we might move to a different test runner, but this works great to move fast in early aplha days.

### Setup

Use a codespace and you are all set: https://github.com/copilot-extensions/preview-sdk.js/codespaces.

Or, if you prefer to develop locally:

```
gh repo clone copilot-extensions/preview-sdk.js
cd preview-sdk.js
npm install
```

### Running tests

```
npm test
```

As part of the tests, we test types for our public APIs (`npm run test:types`) and our code (`npm run test:tsc`). Run `npm run` to see all available scripts.
16 changes: 16 additions & 0 deletions MAINTAINING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Maintaining

## Merging Pull Request & releasing a new version

Releases are automated using [semantic-release](https://github.com/semantic-release/semantic-release).
The following commit message conventions determine which version is released:

1. `fix: ...` or `fix(scope name): ...` prefix in subject: bumps fix version, e.g. `1.2.3``1.2.4`
2. `feat: ...` or `feat(scope name): ...` prefix in subject: bumps feature version, e.g. `1.2.3``1.3.0`
3. `BREAKING CHANGE:` in body: bumps breaking version, e.g. `1.2.3``2.0.0`

Only one version number is bumped at a time, the highest version change trumps the others.
Besides, publishing a new version to npm, semantic-release also creates a git tag and release
on GitHub, generates changelogs from the commit messages and puts them into the release notes.

If the pull request looks good but does not follow the commit conventions, update the pull request title and use the <kbd>Squash & merge</kbd> button, at which point you can set a custom commit message.
129 changes: 126 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,27 @@ const payloadIsVerified = await verifyRequestByKeyId(
// true or false
```

### Build a response

```js
import { createAckEvent, createDoneEvent, createTextEvent } from "@copilot-extensions/preview-sdk";

export default handler(request, response) {
const ackEvent = createAckEvent();
const textEvent = createTextEvent("Hello, world!");
const doneEvent = createDoneEvent();

response.write(ackEvent.toString());
response.write(textEvent.toString());
response.end(doneEvent.toString());
}
```

## API

### `async verifyRequestByKeyId(rawBody, signature, keyId, options)`
### Verification

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

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 All @@ -46,7 +64,7 @@ await verifyRequestByKeyId(request.body, signature, key, { token: "ghp_1234" });
await verifyRequestByKeyId(request.body, signature, key, { request });
```

### `async fetchVerificationKeys(options)`
#### `async fetchVerificationKeys(options)`

Fetches public keys for verifying copilot extension requests [from GitHub's API](https://api.github.com/meta/public_keys/copilot_api)
and returns them as an array. The request can be made without authentication, with a token, or with a custom [octokit request](https://github.com/octokit/request.js) instance.
Expand All @@ -64,7 +82,7 @@ const [current] = await fetchVerificationKeys({ token: "ghp_1234" });
const [current] = await fetchVerificationKeys({ request });)
```

### `async verifyRequestPayload(rawBody, signature, keyId)`
#### `async verifyRequestPayload(rawBody, signature, keyId)`

Verify the request payload using the provided signature and key. Note that the raw body as received by GitHub must be passed, before any parsing.

Expand All @@ -79,6 +97,111 @@ const payloadIsVerified = await verifyRequestPayload(
// true or false
```

### Response

All `create*Event()` methods return an object with a `.toString()` method, which is called automatically when a string is expected. Unfortunately that's not the case for `response.write()`, you need to call `.toString()` explicitly.

#### `createAckEvent()`

Acknowledge the request so that the chat UI can tell the user that the agent started generating a response.
The `ack` event should only be sent once.

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

response.write(createAckEvent().toString());
```

#### `createTextEvent(message)`

Send a text message to the chat UI. Multiple messages can be sent. The `message` argument must be a string and may include markdown.

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

response.write(createTextEvent("Hello, world!").toString());
response.write(createTextEvent("Hello, again!").toString());
```

#### `createConfirmationEvent({ id, title, message, metadata })`

Ask the user to confirm an action. The `confirmation` event should only be sent once.

The `meta` data object will be sent along the user's response.

See additional documentation about Copilot confirmations at https://github.com/github/copilot-partners/blob/main/docs/confirmations.md.

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

response.write(
createConfirmationEvent({
id: "123",
title: "Are you sure?",
message: "This will do something.",
}).toString()
);
```

#### `createReferencesEvent(references)`

Send a list of references to the chat UI. The `references` argument must be an array of objects with the following properties:

- `id`
- `type`

The following properties are optional

- `data`: object with any properties.
- `is_implicit`: a boolean
- `metadata`: an object with a required `display_name` and the optional properties: `display_icon` and `display_url`

Multiple `references` events can be sent.

See additional documentation about Copilot references at https://github.com/github/copilot-partners/blob/main/docs/copilot-references.md.

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

response.write(
createReferencesEvent([
{
id: "123",
type: "issue",
data: {
number: 123,
},
is_implicit: false,
metadata: {
display_name: "My issue",
display_icon: "issue-opened",
display_url: "https://github.com/monalisa/hello-world/issues/123",
},
]).toString()
);
```
#### `createErrorsEvent(errors)`
An array of objects with the following properties:
- `type`: must be one of: `"reference"`, `"function"`, `"agent"`
- `code`
- `message`
- `identifier`
See additional documentation about Copilot errors at https://github.com/github/copilot-partners/blob/main/docs/copilot-errors.md.
#### `createDoneEvent()`
The `done` event should only be sent once, at the end of the response. No further events can be sent after the `done` event.
```js
import { createDoneEvent } from "@copilot-extensions/preview-sdk";

response.write(createDoneEvent().toString());
```
## 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
99 changes: 99 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,105 @@ interface VerifyRequestByKeyIdInterface {
): Promise<boolean>;
}

export interface CreateAckEventInterface {
(): ResponseEvent<"ack">
}

export interface CreateTextEventInterface {
(message: string): ResponseEvent<"text">
}

export type CreateConfirmationEventOptions = { id: string, title: string, message: string, metadata?: Record<string, unknown> }

export interface CreateConfirmationEventInterface {
(options: CreateConfirmationEventOptions): ResponseEvent<"copilot_confirmation">
}
export interface CreateReferencesEventInterface {
(references: CopilotReference[]): ResponseEvent<"copilot_references">
}
export interface CreateErrorsEventInterface {
(errors: CopilotError[]): ResponseEvent<"copilot_errors">
}
export interface CreateDoneEventInterface {
(): ResponseEvent<"done">
}

type ResponseEventType = "ack" | "done" | "text" | "copilot_references" | "copilot_confirmation" | "copilot_errors"
type EventsWithoutEventKey = "ack" | "done" | "text"
type ResponseEvent<T extends ResponseEventType = "text"> =
T extends EventsWithoutEventKey ? {
data: T extends "ack" ? CopilotAckResponseEventData : T extends "done" ? CopilotDoneResponseEventData : T extends "text" ? CopilotTextResponseEventData : never
toString: () => string
} : {
event: T
data: T extends "copilot_references" ? CopilotReferenceResponseEventData : T extends "copilot_confirmation" ? CopilotConfirmationResponseEventData : T extends "copilot_errors" ? CopilotErrorsResponseEventData : never
toString: () => string
}

type CopilotAckResponseEventData = {
choices: [{
delta: {
content: "", role: "assistant"
}
}]
}

type CopilotDoneResponseEventData = {
choices: [{
finish_reason: "stop"
delta: {
content: null
}
}]
}

type CopilotTextResponseEventData = {
choices: [{
delta: {
content: string, role: "assistant"
}
}]
}
type CopilotConfirmationResponseEventData = {
type: 'action';
title: string;
message: string;
confirmation?: {
id: string;
[key: string]: any;
};
}
type CopilotErrorsResponseEventData = CopilotError[]
type CopilotReferenceResponseEventData = CopilotReference[]

type CopilotError = {
type: "reference" | "function" | "agent";
code: string;
message: string;
identifier: string;
}

interface CopilotReference {
type: string;
id: string;
data?: {
[key: string]: unknown;
};
is_implicit?: boolean;
metadata?: {
display_name: string;
display_icon?: string;
display_url?: string;
};
}

export declare const verifyRequest: VerifyRequestInterface;
export declare const fetchVerificationKeys: FetchVerificationKeysInterface;
export declare const verifyRequestByKeyId: VerifyRequestByKeyIdInterface;

export declare const createAckEvent: CreateAckEventInterface;
export declare const createConfirmationEvent: CreateConfirmationEventInterface;
export declare const createDoneEvent: CreateDoneEventInterface;
export declare const createErrorsEvent: CreateErrorsEventInterface;
export declare const createReferencesEvent: CreateReferencesEventInterface;
export declare const createTextEvent: CreateTextEventInterface;
Loading

0 comments on commit 2c0e04f

Please sign in to comment.