promptfoo 0.17.4 → 0.17.5
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 +1 -0
- package/dist/package.json +2 -2
- package/dist/src/assertions.d.ts.map +1 -1
- package/dist/src/assertions.js +14 -2
- package/dist/src/assertions.js.map +1 -1
- package/dist/src/main.js +60 -34
- package/dist/src/main.js.map +1 -1
- package/dist/src/providers/anthropic.js.map +1 -1
- package/dist/src/providers/azureopenai.d.ts +34 -0
- package/dist/src/providers/azureopenai.d.ts.map +1 -0
- package/dist/src/providers/azureopenai.js +234 -0
- package/dist/src/providers/azureopenai.js.map +1 -0
- package/dist/src/providers/openai.d.ts.map +1 -1
- package/dist/src/providers/openai.js +23 -9
- package/dist/src/providers/openai.js.map +1 -1
- package/dist/src/providers.d.ts.map +1 -1
- package/dist/src/providers.js +16 -0
- package/dist/src/providers.js.map +1 -1
- package/dist/src/types.d.ts +2 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/updates.d.ts.map +1 -1
- package/dist/src/updates.js +3 -0
- package/dist/src/updates.js.map +1 -1
- package/dist/src/web/client/assets/{index-58a0e3e3.js → index-c2756e5d.js} +1 -1
- package/dist/src/web/client/index.html +1 -1
- package/package.json +2 -2
- package/src/assertions.ts +18 -2
- package/src/main.ts +77 -36
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/azureopenai.ts +280 -0
- package/src/providers/openai.ts +27 -11
- package/src/providers.ts +19 -0
- package/src/types.ts +4 -0
- package/src/updates.ts +4 -0
- package/src/web/client/src/ResultsView.tsx +12 -10
package/src/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
3
|
import { join as pathJoin, dirname } from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
4
5
|
|
|
5
6
|
import chalk from 'chalk';
|
|
6
7
|
import { Command } from 'commander';
|
|
@@ -56,7 +57,11 @@ function createDummyFiles(directory: string | null) {
|
|
|
56
57
|
writeFileSync(pathJoin(process.cwd(), directory, 'README.md'), DEFAULT_README);
|
|
57
58
|
|
|
58
59
|
if (directory === '.') {
|
|
59
|
-
logger.info(
|
|
60
|
+
logger.info(
|
|
61
|
+
chalk.green.bold(
|
|
62
|
+
'Wrote prompts.txt and promptfooconfig.yaml. Open README.md to get started!',
|
|
63
|
+
),
|
|
64
|
+
);
|
|
60
65
|
} else {
|
|
61
66
|
logger.info(chalk.green.bold(`Wrote prompts.txt and promptfooconfig.yaml to ./${directory}`));
|
|
62
67
|
logger.info(chalk.green(`\`cd ${directory}\` and open README.md to get started!`));
|
|
@@ -72,20 +77,20 @@ async function main() {
|
|
|
72
77
|
pathJoin(pwd, 'promptfooconfig.json'),
|
|
73
78
|
pathJoin(pwd, 'promptfooconfig.yaml'),
|
|
74
79
|
];
|
|
75
|
-
let
|
|
80
|
+
let defaultConfig: Partial<UnifiedConfig> = {};
|
|
76
81
|
for (const path of potentialPaths) {
|
|
77
82
|
const maybeConfig = await maybeReadConfig(path);
|
|
78
83
|
if (maybeConfig) {
|
|
79
|
-
|
|
84
|
+
defaultConfig = maybeConfig;
|
|
80
85
|
break;
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
let evaluateOptions: EvaluateOptions = {};
|
|
85
|
-
if (
|
|
86
|
-
evaluateOptions.generateSuggestions =
|
|
87
|
-
evaluateOptions.maxConcurrency =
|
|
88
|
-
evaluateOptions.showProgressBar =
|
|
90
|
+
if (defaultConfig.evaluateOptions) {
|
|
91
|
+
evaluateOptions.generateSuggestions = defaultConfig.evaluateOptions.generateSuggestions;
|
|
92
|
+
evaluateOptions.maxConcurrency = defaultConfig.evaluateOptions.maxConcurrency;
|
|
93
|
+
evaluateOptions.showProgressBar = defaultConfig.evaluateOptions.showProgressBar;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
const program = new Command();
|
|
@@ -126,20 +131,46 @@ async function main() {
|
|
|
126
131
|
program
|
|
127
132
|
.command('share')
|
|
128
133
|
.description('Share your most recent result')
|
|
129
|
-
.
|
|
134
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
135
|
+
.action(async (cmdObj: { yes: boolean } & Command) => {
|
|
130
136
|
telemetry.maybeShowNotice();
|
|
131
137
|
telemetry.record('command_used', {
|
|
132
138
|
name: 'share',
|
|
133
139
|
});
|
|
134
140
|
await telemetry.send();
|
|
135
141
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
const createPublicUrl = async () => {
|
|
143
|
+
const latestResults = readLatestResults();
|
|
144
|
+
if (!latestResults) {
|
|
145
|
+
logger.error('Could not load results. Do you need to run `promptfoo eval` first?');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const url = await createShareableUrl(latestResults.results, latestResults.config);
|
|
149
|
+
logger.info(`View results: ${chalk.greenBright.bold(url)}`);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (cmdObj.yes || process.env.PROMPTFOO_DISABLE_SHARE_WARNING) {
|
|
153
|
+
createPublicUrl();
|
|
154
|
+
} else {
|
|
155
|
+
const reader = readline.createInterface({
|
|
156
|
+
input: process.stdin,
|
|
157
|
+
output: process.stdout,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
reader.question(
|
|
161
|
+
'Are you sure you want to create a public URL? [y/N] ',
|
|
162
|
+
async function (answer: string) {
|
|
163
|
+
if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') {
|
|
164
|
+
logger.info('Did not create a public URL.');
|
|
165
|
+
reader.close();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
reader.close();
|
|
169
|
+
|
|
170
|
+
createPublicUrl();
|
|
171
|
+
},
|
|
172
|
+
);
|
|
140
173
|
}
|
|
141
|
-
const url = await createShareableUrl(latestResults.results, latestResults.config);
|
|
142
|
-
logger.info(`View results: ${chalk.greenBright.bold(url)}`);
|
|
143
174
|
});
|
|
144
175
|
|
|
145
176
|
program
|
|
@@ -159,28 +190,32 @@ async function main() {
|
|
|
159
190
|
program
|
|
160
191
|
.command('eval')
|
|
161
192
|
.description('Evaluate prompts')
|
|
162
|
-
.option('-p, --prompts <paths...>', 'Paths to prompt files (.txt)'
|
|
193
|
+
.option('-p, --prompts <paths...>', 'Paths to prompt files (.txt)')
|
|
163
194
|
.option(
|
|
164
195
|
'-r, --providers <name or path...>',
|
|
165
196
|
'One of: openai:chat, openai:completion, openai:<model name>, or path to custom API caller module',
|
|
166
197
|
)
|
|
167
198
|
.option(
|
|
168
199
|
'-c, --config <path>',
|
|
169
|
-
'Path to configuration file. Automatically loads
|
|
200
|
+
'Path to configuration file. Automatically loads promptfoodefaultConfig.js/json/yaml',
|
|
170
201
|
)
|
|
171
202
|
.option(
|
|
172
203
|
// TODO(ian): Remove `vars` for v1
|
|
173
204
|
'-v, --vars, -t, --tests <path>',
|
|
174
205
|
'Path to CSV with test cases',
|
|
175
|
-
|
|
206
|
+
defaultConfig?.commandLineOptions?.vars,
|
|
207
|
+
)
|
|
208
|
+
.option('-t, --tests <path>', 'Path to CSV with test cases')
|
|
209
|
+
.option(
|
|
210
|
+
'-o, --output <path>',
|
|
211
|
+
'Path to output file (csv, json, yaml, html)',
|
|
212
|
+
defaultConfig.outputPath,
|
|
176
213
|
)
|
|
177
|
-
.option('-t, --tests <path>', 'Path to CSV with test cases', config?.commandLineOptions?.tests)
|
|
178
|
-
.option('-o, --output <path>', 'Path to output file (csv, json, yaml, html)', config.outputPath)
|
|
179
214
|
.option(
|
|
180
215
|
'-j, --max-concurrency <number>',
|
|
181
216
|
'Maximum number of concurrent API calls',
|
|
182
|
-
|
|
183
|
-
? String(
|
|
217
|
+
defaultConfig.evaluateOptions?.maxConcurrency
|
|
218
|
+
? String(defaultConfig.evaluateOptions.maxConcurrency)
|
|
184
219
|
: undefined,
|
|
185
220
|
)
|
|
186
221
|
.option(
|
|
@@ -195,28 +230,28 @@ async function main() {
|
|
|
195
230
|
.option(
|
|
196
231
|
'--prompt-prefix <path>',
|
|
197
232
|
'This prefix is prepended to every prompt',
|
|
198
|
-
|
|
233
|
+
defaultConfig.defaultTest?.options?.prefix,
|
|
199
234
|
)
|
|
200
235
|
.option(
|
|
201
236
|
'--prompt-suffix <path>',
|
|
202
237
|
'This suffix is append to every prompt',
|
|
203
|
-
|
|
238
|
+
defaultConfig.defaultTest?.options?.suffix,
|
|
204
239
|
)
|
|
205
240
|
.option(
|
|
206
241
|
'--no-write',
|
|
207
242
|
'Do not write results to promptfoo directory',
|
|
208
|
-
|
|
243
|
+
defaultConfig?.commandLineOptions?.write,
|
|
209
244
|
)
|
|
210
245
|
.option(
|
|
211
246
|
'--no-cache',
|
|
212
247
|
'Do not read or write results to disk cache',
|
|
213
|
-
|
|
248
|
+
defaultConfig?.commandLineOptions?.cache,
|
|
214
249
|
)
|
|
215
250
|
.option('--no-progress-bar', 'Do not show progress bar')
|
|
216
|
-
.option('--no-table', 'Do not output table in CLI',
|
|
217
|
-
.option('--share', 'Create a shareable URL',
|
|
218
|
-
.option('--grader', 'Model that will grade outputs',
|
|
219
|
-
.option('--verbose', 'Show debug logs',
|
|
251
|
+
.option('--no-table', 'Do not output table in CLI', defaultConfig?.commandLineOptions?.table)
|
|
252
|
+
.option('--share', 'Create a shareable URL', defaultConfig?.commandLineOptions?.share)
|
|
253
|
+
.option('--grader', 'Model that will grade outputs', defaultConfig?.commandLineOptions?.grader)
|
|
254
|
+
.option('--verbose', 'Show debug logs', defaultConfig?.commandLineOptions?.verbose)
|
|
220
255
|
.option('--view [port]', 'View in browser ui')
|
|
221
256
|
.action(async (cmdObj: CommandLineOptions & Command) => {
|
|
222
257
|
// Misc settings
|
|
@@ -229,15 +264,20 @@ async function main() {
|
|
|
229
264
|
|
|
230
265
|
// Config parsing
|
|
231
266
|
const maxConcurrency = parseInt(cmdObj.maxConcurrency || '', 10);
|
|
267
|
+
let fileConfig: Partial<UnifiedConfig> = {};
|
|
232
268
|
const configPath = cmdObj.config;
|
|
233
269
|
if (configPath) {
|
|
234
|
-
|
|
270
|
+
fileConfig = await readConfig(configPath);
|
|
235
271
|
}
|
|
236
|
-
config = {
|
|
237
|
-
prompts: cmdObj.prompts ||
|
|
238
|
-
providers: cmdObj.providers ||
|
|
239
|
-
tests: cmdObj.tests || cmdObj.vars ||
|
|
240
|
-
|
|
272
|
+
const config: Partial<UnifiedConfig> = {
|
|
273
|
+
prompts: cmdObj.prompts || fileConfig.prompts || defaultConfig.prompts,
|
|
274
|
+
providers: cmdObj.providers || fileConfig.providers || defaultConfig.providers,
|
|
275
|
+
tests: cmdObj.tests || cmdObj.vars || fileConfig.tests || defaultConfig.tests,
|
|
276
|
+
sharing:
|
|
277
|
+
process.env.PROMPTFOO_DISABLE_SHARING === '1'
|
|
278
|
+
? false
|
|
279
|
+
: fileConfig.sharing ?? defaultConfig.sharing ?? true,
|
|
280
|
+
defaultTest: fileConfig.defaultTest,
|
|
241
281
|
};
|
|
242
282
|
|
|
243
283
|
// Validation
|
|
@@ -305,7 +345,8 @@ async function main() {
|
|
|
305
345
|
|
|
306
346
|
const summary = await evaluate(testSuite, options);
|
|
307
347
|
|
|
308
|
-
const shareableUrl =
|
|
348
|
+
const shareableUrl =
|
|
349
|
+
cmdObj.share && config.sharing ? await createShareableUrl(summary, config) : null;
|
|
309
350
|
|
|
310
351
|
if (cmdObj.output) {
|
|
311
352
|
logger.info(chalk.yellow(`Writing output to ${cmdObj.output}`));
|
|
@@ -25,7 +25,7 @@ export class AnthropicCompletionProvider implements ApiProvider {
|
|
|
25
25
|
constructor(modelName: string, apiKey?: string, context?: AnthropicCompletionOptions) {
|
|
26
26
|
this.modelName = modelName;
|
|
27
27
|
this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
28
|
-
this.anthropic = new Anthropic({apiKey: this.apiKey});
|
|
28
|
+
this.anthropic = new Anthropic({ apiKey: this.apiKey });
|
|
29
29
|
this.options = context || {};
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import logger from '../logger';
|
|
2
|
+
import { fetchJsonWithCache } from '../cache';
|
|
3
|
+
import { REQUEST_TIMEOUT_MS } from './shared';
|
|
4
|
+
|
|
5
|
+
import type { ApiProvider, ProviderEmbeddingResponse, ProviderResponse } from '../types.js';
|
|
6
|
+
|
|
7
|
+
interface AzureOpenAiCompletionOptions {
|
|
8
|
+
temperature?: number;
|
|
9
|
+
functions?: {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
parameters: any;
|
|
13
|
+
}[];
|
|
14
|
+
function_call?: 'none' | 'auto';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class AzureOpenAiGenericProvider implements ApiProvider {
|
|
18
|
+
deploymentName: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
apiHost?: string;
|
|
21
|
+
|
|
22
|
+
constructor(deploymentName: string, apiKey?: string) {
|
|
23
|
+
this.deploymentName = deploymentName;
|
|
24
|
+
|
|
25
|
+
this.apiKey = apiKey || process.env.AZURE_OPENAI_API_KEY;
|
|
26
|
+
|
|
27
|
+
this.apiHost = process.env.AZURE_OPENAI_API_HOST;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
id(): string {
|
|
31
|
+
return `azureopenai:${this.deploymentName}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toString(): string {
|
|
35
|
+
return `[Azure OpenAI Provider ${this.deploymentName}]`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// @ts-ignore: Prompt is not used in this implementation
|
|
39
|
+
async callApi(prompt: string, options?: AzureOpenAiCompletionOptions): Promise<ProviderResponse> {
|
|
40
|
+
throw new Error('Not implemented');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class AzureOpenAiEmbeddingProvider extends AzureOpenAiGenericProvider {
|
|
45
|
+
async callEmbeddingApi(text: string): Promise<ProviderEmbeddingResponse> {
|
|
46
|
+
if (!this.apiKey) {
|
|
47
|
+
throw new Error('Azure OpenAI API key must be set for similarity comparison');
|
|
48
|
+
}
|
|
49
|
+
if (!this.apiHost) {
|
|
50
|
+
throw new Error('Azure OpenAI API host must be set');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const body = {
|
|
54
|
+
input: text,
|
|
55
|
+
model: this.deploymentName,
|
|
56
|
+
};
|
|
57
|
+
let data,
|
|
58
|
+
cached = false;
|
|
59
|
+
try {
|
|
60
|
+
({ data, cached } = (await fetchJsonWithCache(
|
|
61
|
+
`https://${this.apiHost}/openai/deployments/${this.deploymentName}/embeddings?api-version=2023-07-01-preview`,
|
|
62
|
+
{
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
'api-key': this.apiKey,
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(body),
|
|
69
|
+
},
|
|
70
|
+
REQUEST_TIMEOUT_MS,
|
|
71
|
+
)) as unknown as any);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
error: `API call error: ${String(err)}`,
|
|
75
|
+
tokenUsage: {
|
|
76
|
+
total: 0,
|
|
77
|
+
prompt: 0,
|
|
78
|
+
completion: 0,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
logger.debug(`\tAzure OpenAI API response (embeddings): ${JSON.stringify(data)}`);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const embedding = data?.data?.[0]?.embedding;
|
|
86
|
+
if (!embedding) {
|
|
87
|
+
throw new Error('No embedding returned');
|
|
88
|
+
}
|
|
89
|
+
const ret = {
|
|
90
|
+
embedding,
|
|
91
|
+
tokenUsage: cached
|
|
92
|
+
? { cached: data.usage.total_tokens }
|
|
93
|
+
: {
|
|
94
|
+
total: data.usage.total_tokens,
|
|
95
|
+
prompt: data.usage.prompt_tokens,
|
|
96
|
+
completion: data.usage.completion_tokens,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return ret;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
103
|
+
tokenUsage: {
|
|
104
|
+
total: data?.usage?.total_tokens,
|
|
105
|
+
prompt: data?.usage?.prompt_tokens,
|
|
106
|
+
completion: data?.usage?.completion_tokens,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class AzureOpenAiCompletionProvider extends AzureOpenAiGenericProvider {
|
|
114
|
+
options: AzureOpenAiCompletionOptions;
|
|
115
|
+
|
|
116
|
+
constructor(deploymentName: string, apiKey?: string, context?: AzureOpenAiCompletionOptions) {
|
|
117
|
+
super(deploymentName, apiKey);
|
|
118
|
+
this.options = context || {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async callApi(prompt: string, options?: AzureOpenAiCompletionOptions): Promise<ProviderResponse> {
|
|
122
|
+
if (!this.apiKey) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'Azure OpenAI API key is not set. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!this.apiHost) {
|
|
128
|
+
throw new Error('Azure OpenAI API host must be set');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let stop: string;
|
|
132
|
+
try {
|
|
133
|
+
stop = process.env.OPENAI_STOP
|
|
134
|
+
? JSON.parse(process.env.OPENAI_STOP)
|
|
135
|
+
: ['<|im_end|>', '<|endoftext|>'];
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new Error(`OPENAI_STOP is not a valid JSON string: ${err}`);
|
|
138
|
+
}
|
|
139
|
+
const body = {
|
|
140
|
+
model: this.deploymentName,
|
|
141
|
+
prompt,
|
|
142
|
+
max_tokens: parseInt(process.env.OPENAI_MAX_TOKENS || '1024'),
|
|
143
|
+
temperature:
|
|
144
|
+
options?.temperature ??
|
|
145
|
+
this.options.temperature ??
|
|
146
|
+
parseFloat(process.env.OPENAI_TEMPERATURE || '0'),
|
|
147
|
+
stop,
|
|
148
|
+
};
|
|
149
|
+
logger.debug(`Calling Azure OpenAI API: ${JSON.stringify(body)}`);
|
|
150
|
+
let data,
|
|
151
|
+
cached = false;
|
|
152
|
+
try {
|
|
153
|
+
({ data, cached } = (await fetchJsonWithCache(
|
|
154
|
+
`https://${this.apiHost}/openai/deployments/${this.deploymentName}/completions?api-version=2023-07-01-preview`,
|
|
155
|
+
{
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
'api-key': this.apiKey,
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(body),
|
|
162
|
+
},
|
|
163
|
+
REQUEST_TIMEOUT_MS,
|
|
164
|
+
)) as unknown as any);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
error: `API call error: ${String(err)}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
logger.debug(`\tAzure OpenAI API response: ${JSON.stringify(data)}`);
|
|
171
|
+
try {
|
|
172
|
+
return {
|
|
173
|
+
output: data.choices[0].text,
|
|
174
|
+
tokenUsage: cached
|
|
175
|
+
? { cached: data.usage.total_tokens }
|
|
176
|
+
: {
|
|
177
|
+
total: data.usage.total_tokens,
|
|
178
|
+
prompt: data.usage.prompt_tokens,
|
|
179
|
+
completion: data.usage.completion_tokens,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return {
|
|
184
|
+
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class AzureOpenAiChatCompletionProvider extends AzureOpenAiGenericProvider {
|
|
191
|
+
options: AzureOpenAiCompletionOptions;
|
|
192
|
+
|
|
193
|
+
constructor(deploymentName: string, apiKey?: string, context?: AzureOpenAiCompletionOptions) {
|
|
194
|
+
super(deploymentName, apiKey);
|
|
195
|
+
this.options = context || {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async callApi(prompt: string, options?: AzureOpenAiCompletionOptions): Promise<ProviderResponse> {
|
|
199
|
+
if (!this.apiKey) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'Azure OpenAI API key is not set. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (!this.apiHost) {
|
|
205
|
+
throw new Error('Azure OpenAI API host must be set');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let messages: { role: string; content: string; name?: string }[];
|
|
209
|
+
try {
|
|
210
|
+
messages = JSON.parse(prompt) as { role: string; content: string }[];
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const trimmedPrompt = prompt.trim();
|
|
213
|
+
if (
|
|
214
|
+
process.env.PROMPTFOO_REQUIRE_JSON_PROMPTS ||
|
|
215
|
+
trimmedPrompt.startsWith('{') ||
|
|
216
|
+
trimmedPrompt.startsWith('[')
|
|
217
|
+
) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Azure OpenAI Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
messages = [{ role: 'user', content: prompt }];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const body = {
|
|
226
|
+
model: this.deploymentName,
|
|
227
|
+
messages: messages,
|
|
228
|
+
max_tokens: parseInt(process.env.OPENAI_MAX_TOKENS || '1024'),
|
|
229
|
+
temperature:
|
|
230
|
+
options?.temperature ??
|
|
231
|
+
this.options.temperature ??
|
|
232
|
+
parseFloat(process.env.OPENAI_TEMPERATURE || '0'),
|
|
233
|
+
functions: options?.functions || this.options.functions || undefined,
|
|
234
|
+
function_call: options?.function_call || this.options.function_call || undefined,
|
|
235
|
+
};
|
|
236
|
+
logger.debug(`Calling Azure OpenAI API: ${JSON.stringify(body)}`);
|
|
237
|
+
|
|
238
|
+
let data,
|
|
239
|
+
cached = false;
|
|
240
|
+
try {
|
|
241
|
+
({ data, cached } = (await fetchJsonWithCache(
|
|
242
|
+
`https://${this.apiHost}/openai/deployments/${this.deploymentName}/chat/completions?api-version=2023-07-01-preview`,
|
|
243
|
+
{
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
'api-key': this.apiKey,
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify(body),
|
|
250
|
+
},
|
|
251
|
+
REQUEST_TIMEOUT_MS,
|
|
252
|
+
)) as unknown as any);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
error: `API call error: ${String(err)}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logger.debug(`\tAzure OpenAI API response: ${JSON.stringify(data)}`);
|
|
260
|
+
try {
|
|
261
|
+
const message = data.choices[0].message;
|
|
262
|
+
const output =
|
|
263
|
+
message.content === null ? JSON.stringify(message.function_call) : message.content;
|
|
264
|
+
return {
|
|
265
|
+
output,
|
|
266
|
+
tokenUsage: cached
|
|
267
|
+
? { cached: data.usage.total_tokens }
|
|
268
|
+
: {
|
|
269
|
+
total: data.usage.total_tokens,
|
|
270
|
+
prompt: data.usage.prompt_tokens,
|
|
271
|
+
completion: data.usage.completion_tokens,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return {
|
|
276
|
+
error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
package/src/providers/openai.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
1
3
|
import logger from '../logger';
|
|
2
4
|
import { fetchJsonWithCache } from '../cache';
|
|
3
5
|
import { REQUEST_TIMEOUT_MS } from './shared';
|
|
@@ -226,20 +228,34 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
let messages: { role: string; content: string; name?: string }[];
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
trimmedPrompt.startsWith('{') ||
|
|
236
|
-
trimmedPrompt.startsWith('[')
|
|
237
|
-
) {
|
|
231
|
+
const trimmedPrompt = prompt.trim();
|
|
232
|
+
if (trimmedPrompt.startsWith('- role:')) {
|
|
233
|
+
try {
|
|
234
|
+
// Try YAML
|
|
235
|
+
messages = yaml.load(prompt) as { role: string; content: string }[];
|
|
236
|
+
} catch (err) {
|
|
238
237
|
throw new Error(
|
|
239
|
-
`OpenAI Chat Completion prompt is not a valid
|
|
238
|
+
`OpenAI Chat Completion prompt is not a valid YAML string: ${err}\n\n${prompt}`,
|
|
240
239
|
);
|
|
241
240
|
}
|
|
242
|
-
|
|
241
|
+
} else {
|
|
242
|
+
try {
|
|
243
|
+
// Try JSON
|
|
244
|
+
messages = JSON.parse(prompt) as { role: string; content: string }[];
|
|
245
|
+
} catch (err) {
|
|
246
|
+
if (
|
|
247
|
+
process.env.PROMPTFOO_REQUIRE_JSON_PROMPTS ||
|
|
248
|
+
trimmedPrompt.startsWith('{') ||
|
|
249
|
+
trimmedPrompt.startsWith('[')
|
|
250
|
+
) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`OpenAI Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Fall back to wrapping the prompt in a user message
|
|
257
|
+
messages = [{ role: 'user', content: prompt }];
|
|
258
|
+
}
|
|
243
259
|
}
|
|
244
260
|
|
|
245
261
|
const body = {
|
package/src/providers.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { OpenAiCompletionProvider, OpenAiChatCompletionProvider } from './provid
|
|
|
6
6
|
import { AnthropicCompletionProvider } from './providers/anthropic';
|
|
7
7
|
import { LocalAiCompletionProvider, LocalAiChatProvider } from './providers/localai';
|
|
8
8
|
import { ScriptCompletionProvider } from './providers/scriptCompletion';
|
|
9
|
+
import {
|
|
10
|
+
AzureOpenAiChatCompletionProvider,
|
|
11
|
+
AzureOpenAiCompletionProvider,
|
|
12
|
+
} from './providers/azureopenai';
|
|
9
13
|
|
|
10
14
|
export async function loadApiProviders(
|
|
11
15
|
providerPaths: ProviderId | ProviderId[] | RawProviderConfig[],
|
|
@@ -68,6 +72,21 @@ export async function loadApiProvider(
|
|
|
68
72
|
`Unknown OpenAI model type: ${modelType}. Use one of the following providers: openai:chat:<model name>, openai:completion:<model name>`,
|
|
69
73
|
);
|
|
70
74
|
}
|
|
75
|
+
} else if (providerPath?.startsWith('azureopenai:')) {
|
|
76
|
+
// Load Azure OpenAI module
|
|
77
|
+
const options = providerPath.split(':');
|
|
78
|
+
const modelType = options[1];
|
|
79
|
+
const deploymentName = options[2];
|
|
80
|
+
|
|
81
|
+
if (modelType === 'chat') {
|
|
82
|
+
return new AzureOpenAiChatCompletionProvider(deploymentName, undefined, context?.config);
|
|
83
|
+
} else if (modelType === 'completion') {
|
|
84
|
+
return new AzureOpenAiCompletionProvider(deploymentName, undefined, context?.config);
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Unknown Azure OpenAI model type: ${modelType}. Use one of the following providers: openai:chat:<model name>, openai:completion:<model name>`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
71
90
|
} else if (providerPath?.startsWith('anthropic:')) {
|
|
72
91
|
// Load Anthropic module
|
|
73
92
|
const options = providerPath.split(':');
|
package/src/types.ts
CHANGED
|
@@ -136,6 +136,7 @@ type BaseAssertionTypes =
|
|
|
136
136
|
| 'icontains'
|
|
137
137
|
| 'contains-all'
|
|
138
138
|
| 'contains-any'
|
|
139
|
+
| 'starts-with'
|
|
139
140
|
| 'regex'
|
|
140
141
|
| 'is-json'
|
|
141
142
|
| 'contains-json'
|
|
@@ -230,6 +231,9 @@ export interface TestSuiteConfig {
|
|
|
230
231
|
|
|
231
232
|
// Path to write output. Writes to console/web viewer if not set.
|
|
232
233
|
outputPath?: string;
|
|
234
|
+
|
|
235
|
+
// Determines whether or not sharing is enabled.
|
|
236
|
+
sharing?: boolean;
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
export type UnifiedConfig = TestSuiteConfig & {
|
package/src/updates.ts
CHANGED
|
@@ -17,6 +17,10 @@ export async function getLatestVersion(packageName: string) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export async function checkForUpdates(): Promise<boolean> {
|
|
20
|
+
if (process.env.PROMPTFOO_DISABLE_UPDATE) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
let latestVersion: string;
|
|
21
25
|
try {
|
|
22
26
|
latestVersion = await getLatestVersion('promptfoo');
|
|
@@ -218,16 +218,18 @@ export default function ResultsView() {
|
|
|
218
218
|
</Button>
|
|
219
219
|
</Tooltip>
|
|
220
220
|
)}
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
{config?.sharing && (
|
|
222
|
+
<Tooltip title="Generate a unique URL that others can access">
|
|
223
|
+
<Button
|
|
224
|
+
color="primary"
|
|
225
|
+
onClick={handleShareButtonClick}
|
|
226
|
+
disabled={shareLoading}
|
|
227
|
+
startIcon={shareLoading ? <CircularProgress size={16} /> : <ShareIcon />}
|
|
228
|
+
>
|
|
229
|
+
Share
|
|
230
|
+
</Button>
|
|
231
|
+
</Tooltip>
|
|
232
|
+
)}
|
|
231
233
|
</ResponsiveStack>
|
|
232
234
|
</Box>
|
|
233
235
|
</ResponsiveStack>
|