oh-my-promptfoo 1.26.0 → 1.28.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 CHANGED
@@ -94,7 +94,8 @@ pinned Git and OCI sources, and larger runs.
94
94
  assertion works with all four agents, with no tracing setup.
95
95
  - **Grade many criteria in one judge call.** `llmAssert` works like
96
96
  `llm-rubric`, but scores a whole list of named criteria at once, and each
97
- one shows up as its own score.
97
+ one shows up as its own score. Both rubric assertions default to binary
98
+ scoring; set `config.scoring: graded` for partial credit or thresholds below 1.
98
99
  - **Grade what the agent did, not what it says it did.** `agentAssert` sends a
99
100
  read-only agent into the workspace to check the changes. Turn on
100
101
  `fileChanges` to read them in your own JavaScript assertions.
@@ -120,6 +121,39 @@ providers here and dropped their `Provider` suffix; the ids of earlier versions
120
121
  no longer resolve (see the
121
122
  [changelog](https://github.com/allagentsdev/oh-my-promptfoo/blob/main/CHANGELOG.md)).
122
123
 
124
+ ## Copilot conversations
125
+
126
+ Use [Promptfoo's ordinary chat templates](https://www.promptfoo.dev/docs/configuration/chat/)
127
+ with either `CopilotSdk` or `Agent` delegating to `copilot-sdk`. JSON and YAML
128
+ prompt files, simplified chat markup expanded by Nunjucks, shared fixed history,
129
+ `_conversation` replay, `metadata.conversationId`, `storeOutputAs` and output
130
+ transforms belong to Promptfoo, not a JavaScript conversation adapter.
131
+ Normal assertions grade each new reply; the provider reads only the rendered prompt.
132
+
133
+ Copilot's optional `inputFormat` is `auto` (default), `chat` or `text`. Set it in
134
+ direct provider config or the Agent's `delegate.config`; per-test
135
+ `options.delegate.config.inputFormat` overrides either. `auto` recognizes only
136
+ complete chat-shaped JSON arrays; business records with other roles or incomplete
137
+ records remain literal. Chat-shaped business data is inherently ambiguous: use
138
+ `text` to bypass provider parsing and preserve the supplied/rendered string exactly.
139
+ It does not bypass Promptfoo's earlier Nunjucks rendering or variable expansion.
140
+ YAML **file** loading is Promptfoo's job; raw YAML text is not provider chat input.
141
+
142
+ All system messages, even later ones, are consolidated in original system order
143
+ and appended to Copilot's core instructions through the public SDK. Other
144
+ messages preserve string names, tool IDs, order and content in a delimited
145
+ **user-channel JSON transcript**, not native assistant/tool roles. Delimiters
146
+ are not a security boundary. Each row uses a fresh session and, with `Agent`, a
147
+ fresh workspace. History is supplied input, not tool evidence or filesystem state.
148
+ Returned errors, including settled aborts, retain published workspaces until
149
+ cleanup; thrown failures release unpublished workspaces.
150
+
151
+ The [two-provider examples](examples/copilot-conversations/promptfooconfig.yaml)
152
+ include three generated turns, interleaved IDs, JSON/YAML and simplified fixed
153
+ history, embedded/literal JSON, full-prompt replay and transformed-output storage.
154
+ See the [feature/provenance and compatibility matrices](docs/provider-guide.md#feature-and-verification-provenance)
155
+ for engine-owned behavior, SDK limits and verification status.
156
+
123
157
  ## Local MCP fixtures
124
158
 
125
159
  Copilot and Claude candidates can call suite-authored Node MCP servers staged in
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_fs = require("./fs-D9YrrmPG.cjs");
2
+ const require_fs = require("./fs-CNr5ZxiH.cjs");
3
3
  let node_path = require("node:path");
4
4
  let node_url = require("node:url");
5
5
  let node_fs = require("node:fs");
@@ -45,11 +45,11 @@ function render(source, vars, label) {
45
45
  function prepareRubric(output, context, workspace = false) {
46
46
  const config = context?.config;
47
47
  const components = config?.components;
48
- const scoring = config?.scoring ?? "graded";
48
+ const scoring = config?.scoring ?? "binary";
49
49
  if (config && Object.hasOwn(config, "rubric")) throw new Error("LLM assertion config.rubric was renamed to config.rubricPrompt");
50
50
  if (config && Object.hasOwn(config, "candidate")) throw new Error("LLM assertion config.candidate was removed; state the grading rule in config.rubricPrompt");
51
51
  if (config?.rubricPrompt !== void 0 && typeof config.rubricPrompt !== "string") throw new Error("LLM assertion rubricPrompt must be a string, not an object or array (including parsed JSON/YAML file contents): use inline text or a text-file string. llmAssert keeps its own system prompt and reply format; chat templates cannot replace them");
52
- if (!config || !Array.isArray(components) || components.length === 0 || !components.every((component) => component && typeof component.metric === "string" && component.metric.trim() && typeof component.value === "string" && component.value.trim() && (component.weight === void 0 || typeof component.weight === "number" && Number.isFinite(component.weight) && component.weight > 0)) || new Set(components.map(({ metric }) => metric)).size !== components.length || ![config.threshold, ...components.map(({ threshold }) => threshold)].every((threshold) => threshold === void 0 || typeof threshold === "number" && Number.isFinite(threshold) && threshold >= 0 && threshold <= 1 && (scoring !== "binary" || threshold === 1)) || config.rubricPrompt !== void 0 && !config.rubricPrompt.trim() || config.scoring !== void 0 && config.scoring !== "graded" && config.scoring !== "binary") throw new Error("LLM assertion requires unique named components with nonempty criteria, positive weights, a config.threshold and component thresholds between 0 and 1, a nonempty string rubricPrompt, and scoring of 'graded' or 'binary' without a conflicting config.threshold or component threshold");
52
+ if (!config || !Array.isArray(components) || components.length === 0 || !components.every((component) => component && typeof component.metric === "string" && component.metric.trim() && typeof component.value === "string" && component.value.trim() && (component.weight === void 0 || typeof component.weight === "number" && Number.isFinite(component.weight) && component.weight > 0)) || new Set(components.map(({ metric }) => metric)).size !== components.length || ![config.threshold, ...components.map(({ threshold }) => threshold)].every((threshold) => threshold === void 0 || typeof threshold === "number" && Number.isFinite(threshold) && threshold >= 0 && threshold <= 1 && (scoring !== "binary" || threshold === 1)) || config.rubricPrompt !== void 0 && !config.rubricPrompt.trim() || config.scoring !== void 0 && config.scoring !== "graded" && config.scoring !== "binary") throw new Error("LLM assertion requires unique named components with nonempty criteria, positive weights, a config.threshold and component thresholds between 0 and 1, a nonempty string rubricPrompt, and scoring of 'graded' or 'binary' without a conflicting config.threshold or component threshold; scoring defaults to 'binary', so set scoring: 'graded' for thresholds other than 1");
53
53
  if (components.some(({ metric }) => Object.hasOwn(Object.prototype, metric))) throw new Error("LLM assertion requires no reserved component metric names");
54
54
  const totalWeight = components.reduce((sum, component) => sum + (component.weight ?? 1), 0);
55
55
  if (!Number.isFinite(totalWeight)) throw new Error("LLM assertion component weights exceed the supported range");