promptfoo 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -19
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/evaluator.js +89 -45
- package/dist/evaluator.js.map +1 -1
- package/dist/main.js +51 -22
- package/dist/main.js.map +1 -1
- package/dist/providers/localai.d.ts +17 -0
- package/dist/providers/localai.d.ts.map +1 -0
- package/dist/providers/localai.js +94 -0
- package/dist/providers/localai.js.map +1 -0
- package/dist/providers/openai.d.ts +26 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +223 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/shared.d.ts +2 -0
- package/dist/providers/shared.d.ts.map +1 -0
- package/dist/providers/shared.js +4 -0
- package/dist/providers/shared.js.map +1 -0
- package/dist/providers.d.ts +1 -20
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +16 -153
- package/dist/providers.js.map +1 -1
- package/dist/types.d.ts +21 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +33 -1
- package/dist/util.js.map +1 -1
- package/dist/web/client/assets/index-710f1308.css +1 -0
- package/dist/web/client/assets/index-900b20c0.js +172 -0
- package/dist/web/client/favicon.ico +0 -0
- package/dist/web/client/index.html +15 -0
- package/dist/web/client/logo.svg +30 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +74 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +15 -3
- package/src/evaluator.ts +106 -50
- package/src/main.ts +43 -12
- package/src/providers/localai.ts +112 -0
- package/src/providers/openai.ts +256 -0
- package/src/providers/shared.ts +3 -0
- package/src/providers.ts +17 -177
- package/src/types.ts +24 -2
- package/src/util.ts +36 -1
- package/src/web/client/.eslintrc.cjs +14 -0
- package/src/web/client/index.html +13 -0
- package/src/web/client/package.json +37 -0
- package/src/web/client/public/favicon.ico +0 -0
- package/src/web/client/public/logo.svg +30 -0
- package/src/web/client/src/App.css +0 -0
- package/src/web/client/src/App.tsx +43 -0
- package/src/web/client/src/Logo.css +13 -0
- package/src/web/client/src/Logo.tsx +11 -0
- package/src/web/client/src/NavBar.css +3 -0
- package/src/web/client/src/NavBar.tsx +11 -0
- package/src/web/client/src/ResultsTable.css +133 -0
- package/src/web/client/src/ResultsTable.tsx +261 -0
- package/src/web/client/src/ResultsView.tsx +110 -0
- package/src/web/client/src/index.css +35 -0
- package/src/web/client/src/main.tsx +10 -0
- package/src/web/client/src/store.ts +13 -0
- package/src/web/client/src/types.ts +14 -0
- package/src/web/client/src/vite-env.d.ts +1 -0
- package/src/web/client/tsconfig.json +24 -0
- package/src/web/client/tsconfig.node.json +10 -0
- package/src/web/client/vite.config.ts +7 -0
- package/src/web/server.ts +96 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
|
|
3
|
+
import logger from '../logger.js';
|
|
4
|
+
import { fetchWithTimeout } from '../util.js';
|
|
5
|
+
import { REQUEST_TIMEOUT_MS } from './shared.js';
|
|
6
|
+
|
|
7
|
+
import type { ApiProvider, ProviderEmbeddingResponse, ProviderResponse } from '../types.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_OPENAI_HOST = 'api.openai.com';
|
|
10
|
+
|
|
11
|
+
const embeddingsCache = new LRUCache<string, ProviderEmbeddingResponse>({
|
|
12
|
+
max: 1000,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
class OpenAiGenericProvider implements ApiProvider {
|
|
16
|
+
modelName: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
apiHost: string;
|
|
19
|
+
|
|
20
|
+
constructor(modelName: string, apiKey?: string) {
|
|
21
|
+
this.modelName = modelName;
|
|
22
|
+
|
|
23
|
+
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
24
|
+
if (!key) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'OpenAI API key is not set. Set OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
this.apiKey = key;
|
|
30
|
+
|
|
31
|
+
this.apiHost = process.env.OPENAI_API_HOST || DEFAULT_OPENAI_HOST;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
id(): string {
|
|
35
|
+
return `openai:${this.modelName}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toString(): string {
|
|
39
|
+
return `[OpenAI Provider ${this.modelName}]`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// @ts-ignore: Prompt is not used in this implementation
|
|
43
|
+
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
44
|
+
throw new Error('Not implemented');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class OpenAiEmbeddingProvider extends OpenAiGenericProvider {
|
|
49
|
+
async callEmbeddingApi(text: string): Promise<ProviderEmbeddingResponse> {
|
|
50
|
+
if (!this.apiKey) {
|
|
51
|
+
throw new Error('OpenAI API key must be set for similarity comparison');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// TODO(ian): Improve cache
|
|
55
|
+
const cached = embeddingsCache.get(text);
|
|
56
|
+
if (cached) {
|
|
57
|
+
return cached;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const body = {
|
|
61
|
+
input: text,
|
|
62
|
+
model: this.modelName,
|
|
63
|
+
};
|
|
64
|
+
let response, data;
|
|
65
|
+
try {
|
|
66
|
+
response = await fetchWithTimeout(
|
|
67
|
+
`https://${this.apiHost}/v1/embeddings`,
|
|
68
|
+
{
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
},
|
|
76
|
+
REQUEST_TIMEOUT_MS,
|
|
77
|
+
);
|
|
78
|
+
data = (await response.json()) as unknown as any;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
error: `API call error: ${String(err)}`,
|
|
82
|
+
tokenUsage: {
|
|
83
|
+
total: 0,
|
|
84
|
+
prompt: 0,
|
|
85
|
+
completion: 0,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
logger.debug(`\tOpenAI API response (embeddings): ${JSON.stringify(data)}`);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const embedding = data?.data?.[0]?.embedding;
|
|
93
|
+
if (!embedding) {
|
|
94
|
+
throw new Error('No embedding returned');
|
|
95
|
+
}
|
|
96
|
+
const ret = {
|
|
97
|
+
embedding,
|
|
98
|
+
tokenUsage: {
|
|
99
|
+
total: data.usage.total_tokens,
|
|
100
|
+
prompt: data.usage.prompt_tokens,
|
|
101
|
+
completion: data.usage.completion_tokens,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
embeddingsCache.set(text, ret);
|
|
105
|
+
return ret;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
109
|
+
tokenUsage: {
|
|
110
|
+
total: data?.usage?.total_tokens,
|
|
111
|
+
prompt: data?.usage?.prompt_tokens,
|
|
112
|
+
completion: data?.usage?.completion_tokens,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const DefaultEmbeddingProvider = new OpenAiEmbeddingProvider('text-embedding-ada-002');
|
|
120
|
+
|
|
121
|
+
export class OpenAiCompletionProvider extends OpenAiGenericProvider {
|
|
122
|
+
static OPENAI_COMPLETION_MODELS = [
|
|
123
|
+
'text-davinci-003',
|
|
124
|
+
'text-davinci-002',
|
|
125
|
+
'text-curie-001',
|
|
126
|
+
'text-babbage-001',
|
|
127
|
+
'text-ada-001',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
constructor(modelName: string, apiKey?: string) {
|
|
131
|
+
if (!OpenAiCompletionProvider.OPENAI_COMPLETION_MODELS.includes(modelName)) {
|
|
132
|
+
logger.warn(`Using unknown OpenAI completion model: ${modelName}`);
|
|
133
|
+
}
|
|
134
|
+
super(modelName, apiKey);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
138
|
+
const body = {
|
|
139
|
+
model: this.modelName,
|
|
140
|
+
prompt,
|
|
141
|
+
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
142
|
+
temperature: process.env.OPENAI_TEMPERATURE || 0,
|
|
143
|
+
stop: process.env.OPENAI_STOP ? JSON.parse(process.env.OPENAI_STOP) : undefined,
|
|
144
|
+
};
|
|
145
|
+
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
146
|
+
let response, data;
|
|
147
|
+
try {
|
|
148
|
+
response = await fetchWithTimeout(
|
|
149
|
+
`https://${this.apiHost}/v1/completions`,
|
|
150
|
+
{
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(body),
|
|
157
|
+
},
|
|
158
|
+
REQUEST_TIMEOUT_MS,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
data = (await response.json()) as unknown as any;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return {
|
|
164
|
+
error: `API call error: ${String(err)}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
logger.debug(`\tOpenAI API response: ${JSON.stringify(data)}`);
|
|
168
|
+
try {
|
|
169
|
+
return {
|
|
170
|
+
output: data.choices[0].text,
|
|
171
|
+
tokenUsage: {
|
|
172
|
+
total: data.usage.total_tokens,
|
|
173
|
+
prompt: data.usage.prompt_tokens,
|
|
174
|
+
completion: data.usage.completion_tokens,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return {
|
|
179
|
+
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
186
|
+
static OPENAI_CHAT_MODELS = [
|
|
187
|
+
'gpt-4',
|
|
188
|
+
'gpt-4-0314',
|
|
189
|
+
'gpt-4-32k',
|
|
190
|
+
'gpt-4-32k-0314',
|
|
191
|
+
'gpt-3.5-turbo',
|
|
192
|
+
'gpt-3.5-turbo-0301',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
constructor(modelName: string, apiKey?: string) {
|
|
196
|
+
if (!OpenAiChatCompletionProvider.OPENAI_CHAT_MODELS.includes(modelName)) {
|
|
197
|
+
logger.warn(`Using unknown OpenAI chat model: ${modelName}`);
|
|
198
|
+
}
|
|
199
|
+
super(modelName, apiKey);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
203
|
+
let messages: { role: string; content: string }[];
|
|
204
|
+
try {
|
|
205
|
+
// User can specify `messages` payload as JSON, or we'll just put the
|
|
206
|
+
// string prompt into a `messages` array.
|
|
207
|
+
messages = JSON.parse(prompt);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
messages = [{ role: 'user', content: prompt }];
|
|
210
|
+
}
|
|
211
|
+
const body = {
|
|
212
|
+
model: this.modelName,
|
|
213
|
+
messages: messages,
|
|
214
|
+
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
215
|
+
temperature: process.env.OPENAI_MAX_TEMPERATURE || 0,
|
|
216
|
+
};
|
|
217
|
+
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
218
|
+
|
|
219
|
+
let response, data;
|
|
220
|
+
try {
|
|
221
|
+
response = await fetchWithTimeout(
|
|
222
|
+
`https://${this.apiHost}/v1/chat/completions`,
|
|
223
|
+
{
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'application/json',
|
|
227
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify(body),
|
|
230
|
+
},
|
|
231
|
+
REQUEST_TIMEOUT_MS,
|
|
232
|
+
);
|
|
233
|
+
data = (await response.json()) as unknown as any;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return {
|
|
236
|
+
error: `API call error: ${String(err)}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.debug(`\tOpenAI API response: ${JSON.stringify(data)}`);
|
|
241
|
+
try {
|
|
242
|
+
return {
|
|
243
|
+
output: data.choices[0].message.content,
|
|
244
|
+
tokenUsage: {
|
|
245
|
+
total: data.usage.total_tokens,
|
|
246
|
+
prompt: data.usage.prompt_tokens,
|
|
247
|
+
completion: data.usage.completion_tokens,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return {
|
|
252
|
+
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/providers.ts
CHANGED
|
@@ -1,183 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import { ApiProvider
|
|
4
|
-
import { fetchWithTimeout } from './util.js';
|
|
5
|
-
import logger from './logger.js';
|
|
3
|
+
import { ApiProvider } from './types.js';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const REQUEST_TIMEOUT_MS = process.env.REQUEST_TIMEOUT_MS
|
|
10
|
-
? parseInt(process.env.REQUEST_TIMEOUT_MS, 10)
|
|
11
|
-
: 10_000;
|
|
12
|
-
|
|
13
|
-
export class OpenAiGenericProvider implements ApiProvider {
|
|
14
|
-
modelName: string;
|
|
15
|
-
apiKey: string;
|
|
16
|
-
apiHost: string;
|
|
17
|
-
|
|
18
|
-
constructor(modelName: string, apiKey?: string) {
|
|
19
|
-
this.modelName = modelName;
|
|
20
|
-
|
|
21
|
-
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
22
|
-
if (!key) {
|
|
23
|
-
throw new Error(
|
|
24
|
-
'OpenAI API key is not set. Set OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
this.apiKey = key;
|
|
28
|
-
|
|
29
|
-
this.apiHost = process.env.OPENAI_API_HOST || DEFAULT_OPENAI_HOST;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
id(): string {
|
|
33
|
-
return `openai:${this.modelName}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
toString(): string {
|
|
37
|
-
return `[OpenAI Provider ${this.modelName}]`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// @ts-ignore: Prompt is not used in this implementation
|
|
41
|
-
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
42
|
-
throw new Error('Not implemented');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class OpenAiCompletionProvider extends OpenAiGenericProvider {
|
|
47
|
-
static OPENAI_COMPLETION_MODELS = [
|
|
48
|
-
'text-davinci-003',
|
|
49
|
-
'text-davinci-002',
|
|
50
|
-
'text-curie-001',
|
|
51
|
-
'text-babbage-001',
|
|
52
|
-
'text-ada-001',
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
constructor(modelName: string, apiKey?: string) {
|
|
56
|
-
if (!OpenAiCompletionProvider.OPENAI_COMPLETION_MODELS.includes(modelName)) {
|
|
57
|
-
logger.warn(`Using unknown OpenAI completion model: ${modelName}`);
|
|
58
|
-
}
|
|
59
|
-
super(modelName, apiKey);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
63
|
-
const body = {
|
|
64
|
-
model: this.modelName,
|
|
65
|
-
prompt,
|
|
66
|
-
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
67
|
-
temperature: process.env.OPENAI_TEMPERATURE || 0,
|
|
68
|
-
};
|
|
69
|
-
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
70
|
-
let response, data;
|
|
71
|
-
try {
|
|
72
|
-
response = await fetchWithTimeout(
|
|
73
|
-
`https://${this.apiHost}/v1/completions`,
|
|
74
|
-
{
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: {
|
|
77
|
-
'Content-Type': 'application/json',
|
|
78
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
79
|
-
},
|
|
80
|
-
body: JSON.stringify(body),
|
|
81
|
-
},
|
|
82
|
-
REQUEST_TIMEOUT_MS,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
data = (await response.json()) as unknown as any;
|
|
86
|
-
} catch (err) {
|
|
87
|
-
return {
|
|
88
|
-
error: `API call error: ${String(err)}`,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
logger.debug(`\tOpenAI API response: ${JSON.stringify(data)}`);
|
|
92
|
-
try {
|
|
93
|
-
return {
|
|
94
|
-
output: data.choices[0].text,
|
|
95
|
-
tokenUsage: {
|
|
96
|
-
total: data.usage.total_tokens,
|
|
97
|
-
prompt: data.usage.prompt_tokens,
|
|
98
|
-
completion: data.usage.completion_tokens,
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
} catch (err) {
|
|
102
|
-
return {
|
|
103
|
-
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
110
|
-
static OPENAI_CHAT_MODELS = [
|
|
111
|
-
'gpt-4',
|
|
112
|
-
'gpt-4-0314',
|
|
113
|
-
'gpt-4-32k',
|
|
114
|
-
'gpt-4-32k-0314',
|
|
115
|
-
'gpt-3.5-turbo',
|
|
116
|
-
'gpt-3.5-turbo-0301',
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
constructor(modelName: string, apiKey?: string) {
|
|
120
|
-
if (!OpenAiChatCompletionProvider.OPENAI_CHAT_MODELS.includes(modelName)) {
|
|
121
|
-
logger.warn(`Using unknown OpenAI chat model: ${modelName}`);
|
|
122
|
-
}
|
|
123
|
-
super(modelName, apiKey);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async callApi(prompt: string): Promise<ProviderResponse> {
|
|
127
|
-
let messages: { role: string; content: string }[];
|
|
128
|
-
try {
|
|
129
|
-
// User can specify `messages` payload as JSON, or we'll just put the
|
|
130
|
-
// string prompt into a `messages` array.
|
|
131
|
-
messages = JSON.parse(prompt);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
messages = [{ role: 'user', content: prompt }];
|
|
134
|
-
}
|
|
135
|
-
const body = {
|
|
136
|
-
model: this.modelName,
|
|
137
|
-
messages: messages,
|
|
138
|
-
max_tokens: process.env.OPENAI_MAX_TOKENS || 1024,
|
|
139
|
-
temperature: process.env.OPENAI_MAX_TEMPERATURE || 0,
|
|
140
|
-
};
|
|
141
|
-
logger.debug(`Calling OpenAI API: ${JSON.stringify(body)}`);
|
|
142
|
-
|
|
143
|
-
let response, data;
|
|
144
|
-
try {
|
|
145
|
-
response = await fetchWithTimeout(
|
|
146
|
-
`https://${this.apiHost}/v1/chat/completions`,
|
|
147
|
-
{
|
|
148
|
-
method: 'POST',
|
|
149
|
-
headers: {
|
|
150
|
-
'Content-Type': 'application/json',
|
|
151
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
152
|
-
},
|
|
153
|
-
body: JSON.stringify(body),
|
|
154
|
-
},
|
|
155
|
-
REQUEST_TIMEOUT_MS,
|
|
156
|
-
);
|
|
157
|
-
data = (await response.json()) as unknown as any;
|
|
158
|
-
} catch (err) {
|
|
159
|
-
return {
|
|
160
|
-
error: `API call error: ${String(err)}`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
logger.debug(`\tOpenAI API response: ${JSON.stringify(data)}`);
|
|
165
|
-
try {
|
|
166
|
-
return {
|
|
167
|
-
output: data.choices[0].message.content,
|
|
168
|
-
tokenUsage: {
|
|
169
|
-
total: data.usage.total_tokens,
|
|
170
|
-
prompt: data.usage.prompt_tokens,
|
|
171
|
-
completion: data.usage.completion_tokens,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
} catch (err) {
|
|
175
|
-
return {
|
|
176
|
-
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
5
|
+
import { OpenAiCompletionProvider, OpenAiChatCompletionProvider } from './providers/openai.js';
|
|
6
|
+
import { LocalAiCompletionProvider, LocalAiChatProvider } from './providers/localai.js';
|
|
181
7
|
|
|
182
8
|
export async function loadApiProvider(providerPath: string): Promise<ApiProvider> {
|
|
183
9
|
if (providerPath?.startsWith('openai:')) {
|
|
@@ -201,6 +27,20 @@ export async function loadApiProvider(providerPath: string): Promise<ApiProvider
|
|
|
201
27
|
}
|
|
202
28
|
}
|
|
203
29
|
|
|
30
|
+
if (providerPath?.startsWith('localai:')) {
|
|
31
|
+
const options = providerPath.split(':');
|
|
32
|
+
const modelType = options[1];
|
|
33
|
+
const modelName = options[2];
|
|
34
|
+
|
|
35
|
+
if (modelType === 'chat') {
|
|
36
|
+
return new LocalAiChatProvider(modelName);
|
|
37
|
+
} else if (modelType === 'completion') {
|
|
38
|
+
return new LocalAiCompletionProvider(modelName);
|
|
39
|
+
} else {
|
|
40
|
+
return new LocalAiChatProvider(modelType);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
204
44
|
// Load custom module
|
|
205
45
|
const CustomApiProvider = (await import(path.join(process.cwd(), providerPath))).default;
|
|
206
46
|
return new CustomApiProvider();
|
package/src/types.ts
CHANGED
|
@@ -5,8 +5,11 @@ export interface CommandLineOptions {
|
|
|
5
5
|
vars?: string;
|
|
6
6
|
config?: string;
|
|
7
7
|
verbose?: boolean;
|
|
8
|
-
maxConcurrency?:
|
|
8
|
+
maxConcurrency?: string;
|
|
9
9
|
grader?: string;
|
|
10
|
+
view?: boolean;
|
|
11
|
+
noWrite?: boolean;
|
|
12
|
+
tableCellMaxLength?: string;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export interface ApiProvider {
|
|
@@ -26,6 +29,12 @@ export interface ProviderResponse {
|
|
|
26
29
|
tokenUsage?: TokenUsage;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
export interface ProviderEmbeddingResponse {
|
|
33
|
+
error?: string;
|
|
34
|
+
embedding?: number[];
|
|
35
|
+
tokenUsage?: TokenUsage;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export interface CsvRow {
|
|
30
39
|
[key: string]: string;
|
|
31
40
|
}
|
|
@@ -61,6 +70,18 @@ export interface EvaluateResult {
|
|
|
61
70
|
success: boolean;
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
export interface EvaluateTable {
|
|
74
|
+
head: {
|
|
75
|
+
prompts: string[];
|
|
76
|
+
vars: string[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
body: {
|
|
80
|
+
outputs: string[];
|
|
81
|
+
vars: string[];
|
|
82
|
+
}[];
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
export interface EvaluateStats {
|
|
65
86
|
successes: number;
|
|
66
87
|
failures: number;
|
|
@@ -68,7 +89,8 @@ export interface EvaluateStats {
|
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
export interface EvaluateSummary {
|
|
92
|
+
version: number;
|
|
71
93
|
results: EvaluateResult[];
|
|
72
|
-
table:
|
|
94
|
+
table: EvaluateTable;
|
|
73
95
|
stats: EvaluateStats;
|
|
74
96
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
2
4
|
|
|
3
5
|
import fetch from 'node-fetch';
|
|
4
6
|
import yaml from 'js-yaml';
|
|
@@ -8,6 +10,7 @@ import { CsvRow } from './types.js';
|
|
|
8
10
|
import { parse as parseCsv } from 'csv-parse/sync';
|
|
9
11
|
import { stringify } from 'csv-stringify/sync';
|
|
10
12
|
|
|
13
|
+
import logger from './logger.js';
|
|
11
14
|
import { getDirectory } from './esm.js';
|
|
12
15
|
|
|
13
16
|
import type { RequestInfo, RequestInit, Response } from 'node-fetch';
|
|
@@ -51,7 +54,10 @@ export function writeOutput(outputPath: string, summary: EvaluateSummary): void
|
|
|
51
54
|
const outputExtension = outputPath.split('.').pop()?.toLowerCase();
|
|
52
55
|
|
|
53
56
|
if (outputExtension === 'csv' || outputExtension === 'txt') {
|
|
54
|
-
const csvOutput = stringify(
|
|
57
|
+
const csvOutput = stringify([
|
|
58
|
+
[...summary.table.head.prompts, ...summary.table.head.vars],
|
|
59
|
+
...summary.table.body.map((row) => [...row.outputs, ...row.vars]),
|
|
60
|
+
]);
|
|
55
61
|
fs.writeFileSync(outputPath, csvOutput);
|
|
56
62
|
} else if (outputExtension === 'json') {
|
|
57
63
|
fs.writeFileSync(outputPath, JSON.stringify(summary, null, 2));
|
|
@@ -98,3 +104,32 @@ export function fetchWithTimeout(
|
|
|
98
104
|
}
|
|
99
105
|
});
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
export function getConfigDirectoryPath(): string {
|
|
109
|
+
return path.join(os.homedir(), '.promptfoo');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getLatestResultsPath(): string {
|
|
113
|
+
return path.join(getConfigDirectoryPath(), 'output', 'latest.json');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function writeLatestResults(results: EvaluateSummary) {
|
|
117
|
+
const latestResultsPath = getLatestResultsPath();
|
|
118
|
+
try {
|
|
119
|
+
fs.mkdirSync(path.dirname(latestResultsPath), { recursive: true });
|
|
120
|
+
fs.writeFileSync(latestResultsPath, JSON.stringify(results, null, 2));
|
|
121
|
+
logger.info(`Wrote latest results to ${latestResultsPath}.`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.error(`Failed to write latest results to ${latestResultsPath}:\n${err}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function cosineSimilarity(vecA: number[], vecB: number[]) {
|
|
128
|
+
if (vecA.length !== vecB.length) {
|
|
129
|
+
throw new Error('Vectors must be of equal length');
|
|
130
|
+
}
|
|
131
|
+
const dotProduct = vecA.reduce((acc, val, idx) => acc + val * vecB[idx], 0);
|
|
132
|
+
const vecAMagnitude = Math.sqrt(vecA.reduce((acc, val) => acc + val * val, 0));
|
|
133
|
+
const vecBMagnitude = Math.sqrt(vecB.reduce((acc, val) => acc + val * val, 0));
|
|
134
|
+
return dotProduct / (vecAMagnitude * vecBMagnitude);
|
|
135
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: { browser: true, es2020: true },
|
|
3
|
+
extends: [
|
|
4
|
+
'eslint:recommended',
|
|
5
|
+
'plugin:@typescript-eslint/recommended',
|
|
6
|
+
'plugin:react-hooks/recommended',
|
|
7
|
+
],
|
|
8
|
+
parser: '@typescript-eslint/parser',
|
|
9
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
10
|
+
plugins: ['react-refresh'],
|
|
11
|
+
rules: {
|
|
12
|
+
'react-refresh/only-export-components': 'warn',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>promptfoo web viewer</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "promptfoo-client",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@emotion/react": "^11.11.0",
|
|
14
|
+
"@emotion/styled": "^11.11.0",
|
|
15
|
+
"@mui/material": "^5.13.0",
|
|
16
|
+
"@tanstack/react-table": "^8.9.1",
|
|
17
|
+
"react": "^18.2.0",
|
|
18
|
+
"react-dnd": "^16.0.1",
|
|
19
|
+
"react-dnd-html5-backend": "^16.0.1",
|
|
20
|
+
"react-dom": "^18.2.0",
|
|
21
|
+
"socket.io-client": "^4.6.1",
|
|
22
|
+
"tiny-invariant": "^1.3.1",
|
|
23
|
+
"zustand": "^4.3.8"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^18.0.28",
|
|
27
|
+
"@types/react-dom": "^18.0.11",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
|
29
|
+
"@typescript-eslint/parser": "^5.57.1",
|
|
30
|
+
"@vitejs/plugin-react-swc": "^3.0.0",
|
|
31
|
+
"eslint": "^8.38.0",
|
|
32
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
33
|
+
"eslint-plugin-react-refresh": "^0.3.4",
|
|
34
|
+
"typescript": "^5.0.2",
|
|
35
|
+
"vite": "^4.3.2"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
3
|
+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
4
|
+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
|
6
|
+
preserveAspectRatio="xMidYMid meet">
|
|
7
|
+
<metadata>
|
|
8
|
+
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
|
9
|
+
</metadata>
|
|
10
|
+
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
|
11
|
+
fill="#000000" stroke="none">
|
|
12
|
+
<path d="M597 6384 c-77 -13 -152 -51 -204 -103 -44 -43 -88 -101 -77 -101 3
|
|
13
|
+
0 1 -6 -5 -14 -6 -7 -18 -35 -26 -62 -13 -43 -15 -320 -12 -2194 l2 -2145 33
|
|
14
|
+
-67 c53 -109 147 -187 262 -218 33 -9 301 -12 1046 -12 922 0 1004 -1 1017
|
|
15
|
+
-16 8 -10 37 -49 64 -88 274 -400 435 -626 470 -660 124 -118 298 -141 458
|
|
16
|
+
-61 43 21 135 110 187 181 18 24 49 63 69 87 19 24 127 159 238 301 l203 256
|
|
17
|
+
1033 0 c906 0 1040 2 1086 16 130 37 230 136 269 264 20 63 20 95 20 2187 0
|
|
18
|
+
1909 -2 2127 -16 2176 -9 30 -22 61 -30 70 -8 8 -14 19 -14 23 0 17 -55 78
|
|
19
|
+
-99 110 -25 19 -75 44 -111 57 l-65 22 -2868 1 c-1966 1 -2888 -2 -2930 -10z
|
|
20
|
+
m3951 -954 c57 -26 379 -345 411 -408 30 -59 31 -133 3 -196 -26 -58 -220
|
|
21
|
+
-256 -251 -256 -25 0 -599 573 -607 607 -5 19 0 32 23 56 17 18 57 61 89 96
|
|
22
|
+
74 79 106 102 163 117 58 14 111 9 169 -16z m-351 -712 c156 -156 290 -294
|
|
23
|
+
297 -308 8 -15 11 -33 6 -45 -12 -33 -1398 -1412 -1425 -1419 -22 -6 -58 27
|
|
24
|
+
-315 285 -160 160 -295 299 -300 309 -13 24 -14 23 270 310 866 876 1142 1150
|
|
25
|
+
1161 1150 14 0 116 -94 306 -282z m-1617 -1658 c322 -321 325 -326 270 -381
|
|
26
|
+
-16 -16 -37 -29 -47 -30 -22 -1 -12 1 -198 -44 -410 -99 -479 -115 -504 -115
|
|
27
|
+
-16 0 -40 11 -55 25 -25 23 -27 31 -23 73 2 26 8 61 11 77 19 82 79 327 91
|
|
28
|
+
375 8 30 26 101 39 158 27 115 51 152 97 152 24 0 77 -48 319 -290z"/>
|
|
29
|
+
</g>
|
|
30
|
+
</svg>
|
|
File without changes
|