promptfoo 0.18.2 → 0.18.3

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 (45) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/assertions.d.ts.map +1 -1
  3. package/dist/src/assertions.js +4 -4
  4. package/dist/src/assertions.js.map +1 -1
  5. package/dist/src/cache.d.ts +1 -1
  6. package/dist/src/cache.d.ts.map +1 -1
  7. package/dist/src/cache.js +4 -4
  8. package/dist/src/cache.js.map +1 -1
  9. package/dist/src/evaluator.d.ts.map +1 -1
  10. package/dist/src/evaluator.js +3 -2
  11. package/dist/src/evaluator.js.map +1 -1
  12. package/dist/src/providers/azureopenai.d.ts.map +1 -1
  13. package/dist/src/providers/azureopenai.js +3 -3
  14. package/dist/src/providers/azureopenai.js.map +1 -1
  15. package/dist/src/providers/llama.js +1 -1
  16. package/dist/src/providers/llama.js.map +1 -1
  17. package/dist/src/providers/localai.js +2 -2
  18. package/dist/src/providers/localai.js.map +1 -1
  19. package/dist/src/providers/ollama.d.ts +9 -0
  20. package/dist/src/providers/ollama.d.ts.map +1 -0
  21. package/dist/src/providers/ollama.js +66 -0
  22. package/dist/src/providers/ollama.js.map +1 -0
  23. package/dist/src/providers/openai.js +3 -3
  24. package/dist/src/providers/openai.js.map +1 -1
  25. package/dist/src/providers.d.ts.map +1 -1
  26. package/dist/src/providers.js +5 -0
  27. package/dist/src/providers.js.map +1 -1
  28. package/dist/src/util.d.ts +2 -0
  29. package/dist/src/util.d.ts.map +1 -1
  30. package/dist/src/util.js +13 -5
  31. package/dist/src/util.js.map +1 -1
  32. package/dist/src/web/client/assets/{index-af22e73c.js → index-6d2a3573.js} +1 -1
  33. package/dist/src/web/client/index.html +1 -1
  34. package/package.json +1 -1
  35. package/src/assertions.ts +3 -2
  36. package/src/cache.ts +3 -2
  37. package/src/evaluator.ts +3 -1
  38. package/src/providers/azureopenai.ts +16 -6
  39. package/src/providers/llama.ts +2 -2
  40. package/src/providers/localai.ts +3 -3
  41. package/src/providers/ollama.ts +88 -0
  42. package/src/providers/openai.ts +4 -4
  43. package/src/providers.ts +16 -2
  44. package/src/util.ts +13 -4
  45. package/src/web/client/src/ResultsView.tsx +1 -1
@@ -0,0 +1,88 @@
1
+ import logger from '../logger';
2
+ import { fetchWithCache } from '../cache';
3
+
4
+ import type { ApiProvider, ProviderResponse } from '../types.js';
5
+ import { REQUEST_TIMEOUT_MS } from './shared';
6
+
7
+ interface OllamaJsonL {
8
+ model: string;
9
+ created_at: string;
10
+ response?: string;
11
+ done: boolean;
12
+ context?: number[];
13
+ total_duration?: number;
14
+ load_duration?: number;
15
+ sample_count?: number;
16
+ sample_duration?: number;
17
+ prompt_eval_count?: number;
18
+ prompt_eval_duration?: number;
19
+ eval_count?: number;
20
+ eval_duration?: number;
21
+ }
22
+
23
+ export class OllamaProvider implements ApiProvider {
24
+ modelName: string;
25
+
26
+ constructor(modelName: string) {
27
+ this.modelName = modelName;
28
+ }
29
+
30
+ id(): string {
31
+ return `ollama:${this.modelName}`;
32
+ }
33
+
34
+ toString(): string {
35
+ return `[Ollama Provider ${this.modelName}]`;
36
+ }
37
+
38
+ async callApi(prompt: string): Promise<ProviderResponse> {
39
+ const params = {
40
+ model: this.modelName,
41
+ prompt,
42
+ };
43
+
44
+ logger.debug(`Calling Ollama API: ${JSON.stringify(params)}`);
45
+ let response;
46
+ try {
47
+ response = await fetchWithCache(
48
+ `${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`,
49
+ {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ },
54
+ body: JSON.stringify(params),
55
+ },
56
+ REQUEST_TIMEOUT_MS,
57
+ 'text',
58
+ );
59
+ } catch (err) {
60
+ return {
61
+ error: `API call error: ${String(err)}`,
62
+ };
63
+ }
64
+ logger.debug(`\tOllama API response: ${response.data}`);
65
+
66
+ try {
67
+ const output = response.data
68
+ .split('\n')
69
+ .map((line: string) => {
70
+ const parsed = JSON.parse(line) as OllamaJsonL;
71
+ if (parsed.response) {
72
+ return parsed.response;
73
+ }
74
+ return null;
75
+ })
76
+ .filter((s: string | null) => s !== null)
77
+ .join('');
78
+
79
+ return {
80
+ output,
81
+ };
82
+ } catch (err) {
83
+ return {
84
+ error: `API response error: ${String(err)}: ${JSON.stringify(response.data)}`,
85
+ };
86
+ }
87
+ }
88
+ }
@@ -1,5 +1,5 @@
1
1
  import logger from '../logger';
2
- import { fetchJsonWithCache } from '../cache';
2
+ import { fetchWithCache } from '../cache';
3
3
  import { REQUEST_TIMEOUT_MS, parseChatPrompt } from './shared';
4
4
 
5
5
  import type { ApiProvider, ProviderEmbeddingResponse, ProviderResponse } from '../types.js';
@@ -61,7 +61,7 @@ export class OpenAiEmbeddingProvider extends OpenAiGenericProvider {
61
61
  let data,
62
62
  cached = false;
63
63
  try {
64
- ({ data, cached } = (await fetchJsonWithCache(
64
+ ({ data, cached } = (await fetchWithCache(
65
65
  `https://${this.apiHost}/v1/embeddings`,
66
66
  {
67
67
  method: 'POST',
@@ -177,7 +177,7 @@ export class OpenAiCompletionProvider extends OpenAiGenericProvider {
177
177
  let data,
178
178
  cached = false;
179
179
  try {
180
- ({ data, cached } = (await fetchJsonWithCache(
180
+ ({ data, cached } = (await fetchWithCache(
181
181
  `https://${this.apiHost}/v1/completions`,
182
182
  {
183
183
  method: 'POST',
@@ -275,7 +275,7 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
275
275
  let data,
276
276
  cached = false;
277
277
  try {
278
- ({ data, cached } = (await fetchJsonWithCache(
278
+ ({ data, cached } = (await fetchWithCache(
279
279
  `https://${this.apiHost}/v1/chat/completions`,
280
280
  {
281
281
  method: 'POST',
package/src/providers.ts CHANGED
@@ -5,6 +5,7 @@ import { AnthropicCompletionProvider } from './providers/anthropic';
5
5
  import { ReplicateProvider } from './providers/replicate';
6
6
  import { LocalAiCompletionProvider, LocalAiChatProvider } from './providers/localai';
7
7
  import { LlamaProvider } from './providers/llama';
8
+ import { OllamaProvider } from './providers/ollama';
8
9
  import { ScriptCompletionProvider } from './providers/scriptCompletion';
9
10
  import {
10
11
  AzureOpenAiChatCompletionProvider,
@@ -100,9 +101,19 @@ export async function loadApiProvider(
100
101
  const deploymentName = options[2];
101
102
 
102
103
  if (modelType === 'chat') {
103
- return new AzureOpenAiChatCompletionProvider(deploymentName, undefined, context?.config, context?.id);
104
+ return new AzureOpenAiChatCompletionProvider(
105
+ deploymentName,
106
+ undefined,
107
+ context?.config,
108
+ context?.id,
109
+ );
104
110
  } else if (modelType === 'completion') {
105
- return new AzureOpenAiCompletionProvider(deploymentName, undefined, context?.config, context?.id);
111
+ return new AzureOpenAiCompletionProvider(
112
+ deploymentName,
113
+ undefined,
114
+ context?.config,
115
+ context?.id,
116
+ );
106
117
  } else {
107
118
  throw new Error(
108
119
  `Unknown Azure OpenAI model type: ${modelType}. Use one of the following providers: openai:chat:<model name>, openai:completion:<model name>`,
@@ -138,6 +149,9 @@ export async function loadApiProvider(
138
149
  if (providerPath === 'llama' || providerPath.startsWith('llama:')) {
139
150
  const modelName = providerPath.split(':')[1];
140
151
  return new LlamaProvider(modelName, context?.config);
152
+ } else if (providerPath.startsWith('ollama:')) {
153
+ const modelName = providerPath.split(':')[1];
154
+ return new OllamaProvider(modelName);
141
155
  } else if (providerPath?.startsWith('localai:')) {
142
156
  const options = providerPath.split(':');
143
157
  const modelType = options[1];
package/src/util.ts CHANGED
@@ -12,7 +12,6 @@ import { parse as parseCsv } from 'csv-parse/sync';
12
12
  import { stringify } from 'csv-stringify/sync';
13
13
 
14
14
  import logger from './logger';
15
- import { assertionFromString } from './assertions';
16
15
  import { getDirectory } from './esm';
17
16
 
18
17
  import type { RequestInfo, RequestInit, Response } from 'node-fetch';
@@ -368,7 +367,7 @@ export function writeOutput(
368
367
  [...results.table.head.prompts, ...results.table.head.vars],
369
368
  ...results.table.body.map((row) => [...row.outputs.map(outputToSimpleString), ...row.vars]),
370
369
  ];
371
- const htmlOutput = nunjucks.renderString(template, {
370
+ const htmlOutput = getNunjucksEngine().renderString(template, {
372
371
  table,
373
372
  results: results.results,
374
373
  });
@@ -456,10 +455,12 @@ export function writeLatestResults(results: EvaluateSummary, config: Partial<Uni
456
455
  2,
457
456
  ),
458
457
  );
459
- if (fs.existsSync(latestResultsPath) && fs.lstatSync(latestResultsPath).isSymbolicLink()) {
458
+
459
+ try {
460
460
  fs.unlinkSync(latestResultsPath);
461
- }
461
+ } catch {}
462
462
  fs.symlinkSync(newResultsPath, latestResultsPath);
463
+
463
464
  cleanupOldResults();
464
465
  } catch (err) {
465
466
  logger.error(`Failed to write latest results to ${newResultsPath}:\n${err}`);
@@ -520,6 +521,7 @@ export function testCaseFromCsvRow(row: CsvRow): TestCase {
520
521
  for (const [key, value] of Object.entries(row)) {
521
522
  if (key === '__expected') {
522
523
  if (value.trim() !== '') {
524
+ const { assertionFromString } = require('./assertions');
523
525
  asserts.push(assertionFromString(value));
524
526
  }
525
527
  } else {
@@ -532,3 +534,10 @@ export function testCaseFromCsvRow(row: CsvRow): TestCase {
532
534
  assert: asserts,
533
535
  };
534
536
  }
537
+
538
+ export function getNunjucksEngine() {
539
+ nunjucks.configure({
540
+ autoescape: false,
541
+ });
542
+ return nunjucks;
543
+ }
@@ -86,7 +86,7 @@ export default function ResultsView({ recentFiles, onRecentFileSelected }: Resul
86
86
  setFailureFilter(newFailureFilter);
87
87
  };
88
88
 
89
- const [wordBreak, setWordBreak] = React.useState<'break-word' | 'break-all'>('break-all');
89
+ const [wordBreak, setWordBreak] = React.useState<'break-word' | 'break-all'>('break-word');
90
90
  const handleWordBreakChange = (event: React.ChangeEvent<HTMLInputElement>) => {
91
91
  setWordBreak(event.target.checked ? 'break-all' : 'break-word');
92
92
  };