-
Notifications
You must be signed in to change notification settings - Fork 0
/
actions.ts
172 lines (163 loc) · 5.66 KB
/
actions.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
import { CopilotError, CopilotErrorCodes, CopilotErrorType, CopilotMessage, GitHubMessage } from './types'
import OpenAI from 'openai'
import { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources'
import { isPrivateIp } from './ipCheck'
type ChatCompletionRequestArgs = {
messages: GitHubMessage[]
token: string
}
const MAX_RESPONSE_SIZE = 3750
const tools: ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'fetch',
description: 'Make an HTTP request',
parameters: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL to make the request to',
},
method: {
type: 'string',
enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT'],
description: 'The HTTP method to use',
},
headers: {
type: 'object',
description: 'Headers to include in the request',
},
body: {
type: 'string',
description: 'The body of the request (for POST, PUT, PATCH)',
},
},
required: ['url', 'method'],
},
},
},
]
async function chatCompletionRequest({ token, messages }: ChatCompletionRequestArgs) {
const client = new OpenAI({
apiKey: token,
baseURL: 'https://api.githubcopilot.com/',
})
const stream = client.beta.chat.completions.stream({
messages: messages as ChatCompletionMessageParam[],
model: 'gpt-4o',
stream: true,
tools,
})
return stream
}
type GenerateAgentResponseArgs = {
history: GitHubMessage[]
token: string
}
async function fetchTool({ url, method, headers, body }: { url: string; method: string; headers: Record<string, string>; body: string }) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
try {
const parsedUrl = new URL(url)
// @ts-ignore
if (isPrivateIp(parsedUrl.hostname) || parsedUrl.hostname === 'localhost' || parsedUrl.hostname === 'broadcasthost') {
throw new Error('Cannot make requests to private IP addresses')
}
} catch (error: any) {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.readmeError,
message: 'Invalid URL',
identifier: 'fetch',
originalError: error,
})
}
try {
const res = await fetch(url, {
method,
headers,
body,
signal: controller.signal,
})
clearTimeout(timeoutId)
return res.text() || 'No response'
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.githubError,
message: 'Request took too long',
identifier: 'fetch',
originalError: error,
})
}
throw error
}
}
export async function generateAgentResponse({ history, token }: GenerateAgentResponseArgs) {
// The last message is the current users message. We remove it here and make a prompt from it.
const currentMessage = history.pop()
if (!currentMessage) {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.githubError,
message: 'No history provided',
})
}
const confirm = (currentMessage as CopilotMessage)?.copilot_confirmations?.[0]
if (confirm) {
if (confirm.state !== 'accepted') {
throw new CopilotError({
type: CopilotErrorType.reference,
code: CopilotErrorCodes.confirmationError,
message: 'Aborted request, try again',
})
}
if (confirm.confirmation.functionName !== 'fetch') {
throw new CopilotError({
type: CopilotErrorType.agent,
code: CopilotErrorCodes.githubError,
identifier: `invalid function: ${confirm.confirmation.functionName}`,
message: 'Invalid function',
})
}
// this is to remove extra empty assistant message
history.pop()
const rawResponse = await fetchTool(confirm.confirmation.args as any)
let response = rawResponse.slice(0, MAX_RESPONSE_SIZE)
if (response.length < rawResponse.length) {
response += '... (truncated)'
}
// add the response to the history
history.push(currentMessage)
history.push({
role: 'assistant',
tool_calls: [{ type: 'function', function: { name: confirm.confirmation.functionName, arguments: JSON.stringify(confirm.confirmation.args) }, id: confirm.confirmation.id }],
})
history.push({
role: 'tool',
name: confirm.confirmation.functionName,
tool_call_id: confirm.confirmation.id,
content: response,
})
} else {
const copilotMessage = currentMessage as CopilotMessage
const context = copilotMessage.copilot_references?.length ? copilotMessage.copilot_references : undefined
const prompt = `user message: ${copilotMessage.content}${context ? `\n\ncontext: ${JSON.stringify(context)}` : ''}`
// add the last message back in with the generated prompt
history.push({
...currentMessage,
content: prompt,
})
}
const messages = [
{
role: 'system',
content: `You are a helpful HTTP request builder and executor assistant. Use the context passed by the user to build the correct request. Ask the user for clarification if you need it. Never make calls to localhost or 127.0.0.1 or 0.0.0.0 or any private IP addresses. Be sure to set content type headers if needed. for example use application/json if you need a json body. Ask the user if you need any required parameters. show the response exactly as it is.`,
},
...history,
]
return chatCompletionRequest({ token, messages })
}