-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
174 lines (160 loc) · 5.9 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import express from 'express'
import { CopilotError, CopilotErrorCodes, CopilotErrorType, CopilotRequest } from './types'
import crypto from 'crypto'
import { generateAgentResponse } from './actions'
const app = express()
const port = process.env.PORT || 9121
const GITHUB_KEYS_URI = 'https://api.github.com/meta/public_keys/copilot_api'
app.use(express.json())
function generateConfirmationMsg({ title, message, data }: { title: string; message: string; data: Record<string, string | number> }) {
// this format is required by the client
// see: https://docs.github.com/en/copilot/building-copilot-extensions/building-a-copilot-agent-for-your-copilot-extension/configuring-your-copilot-agent-to-communicate-with-the-copilot-platform#copilot_confirmation
return `event: copilot_confirmation\ndata: ${JSON.stringify({
type: 'action',
title,
// Confirmation message shown to the user.
message,
// Optional field for the agent to include any data needed to uniquely identify this confirmation and take action once the decision is received from the client.
confirmation: data,
})}\n\n`
}
async function verifyPayload({ payload, signature, keyID }: { payload: string; signature: string; keyID: string }) {
const keysRes = await fetch(GITHUB_KEYS_URI)
const keys = (await keysRes.json()) as { public_keys: { key_identifier: string; key: string }[] }
const publicKey = keys.public_keys.find((k) => k.key_identifier === keyID)
if (!publicKey) {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.githubError,
message: 'No public key found matching key identifier',
})
}
const verify = crypto.createVerify('SHA256').update(payload)
if (!verify.verify(publicKey.key, signature, 'base64')) {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.githubError,
message: 'Signature does not match payload',
})
}
}
app.post('/agent', async (req, res) => {
res.type('text/event-stream')
res.setHeader('Transfer-Encoding', 'chunked')
try {
const token = req.headers['x-github-token']
if (!token || typeof token !== 'string') {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.readmeError,
message: 'Not authorized with github',
identifier: 'agent',
})
}
const body: CopilotRequest = req.body
const signature = req.headers['github-public-key-signature'] as string
const keyID = req.headers['github-public-key-identifier'] as string
if (!signature || !keyID) {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.readmeError,
message: 'Not authorized with github',
identifier: 'agent',
})
}
try {
await verifyPayload({
payload: JSON.stringify(body),
signature,
keyID,
})
} catch (error) {
// Ignore this error for now. We can assume if this is not from github, then copilot would not work either
console.error('Error verifying payload', error)
}
const history = body.messages
const stream = await generateAgentResponse({
token,
history,
})
for await (const chunk of stream) {
if (chunk?.choices?.[0]?.delta?.tool_calls) continue
const resMsg = `data: ${JSON.stringify(chunk)}\n\n`
res.write(resMsg)
}
const chatCompletion = await stream.finalChatCompletion()
// only support one tool call for now
const toolCall = chatCompletion?.choices?.[0]?.message?.tool_calls?.[0]
if (toolCall) {
const functionName = toolCall.function.name
if (functionName !== 'fetch') {
throw new CopilotError({
type: CopilotErrorType.function,
code: CopilotErrorCodes.readmeError,
identifier: `invalid function: ${functionName}`,
message: 'Issue processing request, try stating the request again',
})
}
const rawArgs = toolCall.function.arguments
let args
try {
args = JSON.parse(rawArgs)
} catch (error) {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.readmeError,
message: 'Issue processing request, try stating the request again',
originalError: error as Error,
})
}
if (!args.url && !args.method) {
throw new CopilotError({
type: CopilotErrorType.function,
code: CopilotErrorCodes.readmeError,
identifier: `function missing args: ${rawArgs}`,
message: 'Issue processing request, try stating the request again',
})
}
const confirmationMsg = generateConfirmationMsg({
title: 'Confirmation',
message: `Do you want to make this request?\nmethod: ${args.method}\nurl: ${args.url}${args.body ? `\nbody: ${JSON.stringify(args.body, null, 2)}` : ''}${
args.headers ? `\nheaders: ${JSON.stringify(args.headers, null, 2)}` : ''
}`,
data: {
args,
functionName,
id: toolCall.id,
},
})
res.write(confirmationMsg)
return
}
res.write(`data: [DONE]\n\n`)
} catch (e) {
let finalError = e as CopilotError
if (finalError.name !== 'CopilotError') {
finalError = new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.readmeError,
message: 'Issue processing request',
originalError: e as Error,
})
}
console.error(
JSON.stringify({
stack: finalError.stack?.replace(/\n /g, ' |'),
message: finalError.message,
code: finalError.code,
type: finalError.type,
identifier: finalError.identifier,
originalError: finalError.originalError,
})
)
res.write(finalError.generateCopilotError())
} finally {
res.end()
}
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})