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.
Files changed (69) hide show
  1. package/README.md +58 -19
  2. package/dist/evaluator.d.ts.map +1 -1
  3. package/dist/evaluator.js +89 -45
  4. package/dist/evaluator.js.map +1 -1
  5. package/dist/main.js +51 -22
  6. package/dist/main.js.map +1 -1
  7. package/dist/providers/localai.d.ts +17 -0
  8. package/dist/providers/localai.d.ts.map +1 -0
  9. package/dist/providers/localai.js +94 -0
  10. package/dist/providers/localai.js.map +1 -0
  11. package/dist/providers/openai.d.ts +26 -0
  12. package/dist/providers/openai.d.ts.map +1 -0
  13. package/dist/providers/openai.js +223 -0
  14. package/dist/providers/openai.js.map +1 -0
  15. package/dist/providers/shared.d.ts +2 -0
  16. package/dist/providers/shared.d.ts.map +1 -0
  17. package/dist/providers/shared.js +4 -0
  18. package/dist/providers/shared.js.map +1 -0
  19. package/dist/providers.d.ts +1 -20
  20. package/dist/providers.d.ts.map +1 -1
  21. package/dist/providers.js +16 -153
  22. package/dist/providers.js.map +1 -1
  23. package/dist/types.d.ts +21 -2
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/util.d.ts +4 -0
  26. package/dist/util.d.ts.map +1 -1
  27. package/dist/util.js +33 -1
  28. package/dist/util.js.map +1 -1
  29. package/dist/web/client/assets/index-710f1308.css +1 -0
  30. package/dist/web/client/assets/index-900b20c0.js +172 -0
  31. package/dist/web/client/favicon.ico +0 -0
  32. package/dist/web/client/index.html +15 -0
  33. package/dist/web/client/logo.svg +30 -0
  34. package/dist/web/server.d.ts +2 -0
  35. package/dist/web/server.d.ts.map +1 -0
  36. package/dist/web/server.js +74 -0
  37. package/dist/web/server.js.map +1 -0
  38. package/package.json +15 -3
  39. package/src/evaluator.ts +106 -50
  40. package/src/main.ts +43 -12
  41. package/src/providers/localai.ts +112 -0
  42. package/src/providers/openai.ts +256 -0
  43. package/src/providers/shared.ts +3 -0
  44. package/src/providers.ts +17 -177
  45. package/src/types.ts +24 -2
  46. package/src/util.ts +36 -1
  47. package/src/web/client/.eslintrc.cjs +14 -0
  48. package/src/web/client/index.html +13 -0
  49. package/src/web/client/package.json +37 -0
  50. package/src/web/client/public/favicon.ico +0 -0
  51. package/src/web/client/public/logo.svg +30 -0
  52. package/src/web/client/src/App.css +0 -0
  53. package/src/web/client/src/App.tsx +43 -0
  54. package/src/web/client/src/Logo.css +13 -0
  55. package/src/web/client/src/Logo.tsx +11 -0
  56. package/src/web/client/src/NavBar.css +3 -0
  57. package/src/web/client/src/NavBar.tsx +11 -0
  58. package/src/web/client/src/ResultsTable.css +133 -0
  59. package/src/web/client/src/ResultsTable.tsx +261 -0
  60. package/src/web/client/src/ResultsView.tsx +110 -0
  61. package/src/web/client/src/index.css +35 -0
  62. package/src/web/client/src/main.tsx +10 -0
  63. package/src/web/client/src/store.ts +13 -0
  64. package/src/web/client/src/types.ts +14 -0
  65. package/src/web/client/src/vite-env.d.ts +1 -0
  66. package/src/web/client/tsconfig.json +24 -0
  67. package/src/web/client/tsconfig.node.json +10 -0
  68. package/src/web/client/vite.config.ts +7 -0
  69. 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
+ }
@@ -0,0 +1,3 @@
1
+ export const REQUEST_TIMEOUT_MS = process.env.REQUEST_TIMEOUT_MS
2
+ ? parseInt(process.env.REQUEST_TIMEOUT_MS, 10)
3
+ : 60_000;
package/src/providers.ts CHANGED
@@ -1,183 +1,9 @@
1
1
  import path from 'node:path';
2
2
 
3
- import { ApiProvider, ProviderResponse } from './types.js';
4
- import { fetchWithTimeout } from './util.js';
5
- import logger from './logger.js';
3
+ import { ApiProvider } from './types.js';
6
4
 
7
- const DEFAULT_OPENAI_HOST = 'api.openai.com';
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?: number;
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: string[][];
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(summary.table);
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
+ }
@@ -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