Skip to content

Commit 3743211

Browse files
authoredOct 2, 2024
docs(examples): user confirmation (copilot-extensions#111)
<!-- This pull request template provides suggested sections for framing your work. You're welcome to change or remove headers if it doesn't fit your use case. :) --> ### What are you trying to accomplish? Another example to add from the discussion in copilot-extensions#83 ### What approach did you choose and why? Adding a full example how to create the user confirmation and handling the response, for easier exploration. ### What should reviewers focus on? If things fit the right way, happy to adjust when needed 😄
1 parent ab12443 commit 3743211

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { createServer } from "node:http";
2+
import { Octokit } from "octokit";
3+
import {
4+
createAckEvent,
5+
createDoneEvent,
6+
prompt,
7+
verifyAndParseRequest,
8+
createConfirmationEvent,
9+
getUserConfirmation,
10+
createTextEvent,
11+
} from "@copilot-extensions/preview-sdk";
12+
13+
// Define the port to listen on
14+
const PORT = 3000;
15+
16+
// Define the handler function
17+
async function handler(request, response) {
18+
console.log(`Received [${request.method}] to [${request.url}]`);
19+
20+
if (request.method !== "POST") {
21+
// Handle other request methods if necessary
22+
response.writeHead(405, { "Content-Type": "text/plain" });
23+
console.log(`Method ${request.method} not allowed`);
24+
25+
response.end("Method Not Allowed");
26+
return;
27+
}
28+
29+
// get a token to use
30+
const tokenForUser = request.headers["x-github-token"];
31+
32+
// get the user information with the token
33+
const octokit = new Octokit({ auth: tokenForUser });
34+
const user = await octokit.request("GET /user");
35+
36+
// Collect incoming data chunks to use in the `on("end")` event
37+
const body = await getBody(request);
38+
const signature = String(request.headers["github-public-key-signature"]);
39+
const keyID = String(request.headers["github-public-key-identifier"]);
40+
41+
try {
42+
const { isValidRequest, payload } = await verifyAndParseRequest(
43+
body,
44+
signature,
45+
keyID,
46+
{
47+
token: tokenForUser,
48+
},
49+
);
50+
51+
if (!isValidRequest) {
52+
console.error("Request verification failed");
53+
response.writeHead(401, { "Content-Type": "text/plain" });
54+
response.end("Request could not be verified");
55+
return;
56+
}
57+
58+
// write the acknowledge event to let Copilot know we are handling the request
59+
// this will also show the message "Copilot is responding" in the chat
60+
response.write(createAckEvent());
61+
62+
// check if we have user confirmation input in the payload
63+
// this will not be true the first time, only after the user has confirmed
64+
const userConfirmation = getUserConfirmation(payload);
65+
66+
if (userConfirmation) {
67+
console.log("Received a user confirmation", userConfirmation);
68+
// handle the confirmation response here and close the conversation
69+
if (userConfirmation.accepted) {
70+
response.write(createTextEvent("Thank you for confirming you like oranges!"));
71+
} else {
72+
response.write(createTextEvent("Thank you for letting me know you don't like oranges!"));
73+
}
74+
// tell Copilot we are done handling the request
75+
response.end(createDoneEvent());
76+
console.log("Response sent");
77+
// break the code flow here with early exit
78+
return;
79+
} else {
80+
// The user's last response was not a confirmation, so we can continue the flow below
81+
}
82+
83+
// pick up the user prompt and handle it
84+
const payload_message = payload.messages[payload.messages.length - 1];
85+
if (payload_message.content.includes("orange")) {
86+
console.log("The user said orange, so we trigger the confirmation event");
87+
88+
response.write(
89+
createConfirmationEvent({
90+
id: "123",
91+
title: "Please confirm?",
92+
message: "So you like oranges?",
93+
}),
94+
);
95+
}
96+
else {
97+
console.log("Calling the GitHub Copilot API with the user prompt");
98+
// forward the prompt to the Copilot API is the last message in the payload
99+
const { stream } = await prompt.stream(payload_message.content, {
100+
system: `You are a helpful assistant that replies to user messages as if you were the Blackbeard Pirate. Start every response with the user's name, which is @${user.data.login}`, // extra instructions for the prompt
101+
messages: payload.messages, // we are giving the prompt the existing messages in this chat conversation for context
102+
token: tokenForUser,
103+
});
104+
105+
// stream the prompt response back to Copilot
106+
for await (const chunk of stream) {
107+
response.write(new TextDecoder().decode(chunk));
108+
}
109+
}
110+
111+
// write the done event to let Copilot know we are done handling the request
112+
response.end(createDoneEvent());
113+
console.log("Response sent");
114+
} catch (error) {
115+
console.error("Error:", error);
116+
response.writeHead(500, { "Content-Type": "text/plain" });
117+
response.end("Internal Server Error");
118+
}
119+
}
120+
121+
// Create an HTTP server
122+
const server = createServer(handler);
123+
124+
// Start the server
125+
server.listen(PORT);
126+
console.log(`Server started at http://localhost:${PORT}`);
127+
128+
/**
129+
*
130+
* @param {import("node:http").IncomingMessage} request
131+
* @returns
132+
*/
133+
function getBody(request) {
134+
return new Promise((resolve) => {
135+
const bodyParts = [];
136+
let body;
137+
request
138+
.on("data", (chunk) => {
139+
bodyParts.push(chunk);
140+
})
141+
.on("end", () => {
142+
body = Buffer.concat(bodyParts).toString();
143+
resolve(body);
144+
});
145+
});
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"private": true,
3+
"type": "module",
4+
"scripts": {
5+
"start": "node index.js",
6+
"watch": "node --watch index.js"
7+
},
8+
"dependencies": {
9+
"@copilot-extensions/preview-sdk": "../../",
10+
"octokit": "^4.0.2"
11+
}
12+
}

0 commit comments

Comments
 (0)
Failed to load comments.