@orangekame3/pi-qcaleval 0.0.2-beta.4 → 0.0.2-beta.6

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.
@@ -20,6 +20,10 @@ import { Type, type Static } from "typebox";
20
20
  /** Figures per evaluation; a 16k-token deployment holds a few plots comfortably. */
21
21
  export const MAX_IMAGES = 4;
22
22
 
23
+ /** Labeled example plots shown before the query, and the figures they may hold in total. */
24
+ export const MAX_EXAMPLES = 4;
25
+ export const MAX_EXAMPLE_IMAGES = 4;
26
+
23
27
  /** Model ids of models fine-tuned for calibration plots; preferred when configured. */
24
28
  export const CALIBRATION_MODEL_PATTERN = /ising-calibration/i;
25
29
 
@@ -113,36 +117,111 @@ export const SYSTEM_PROMPT = [
113
117
  "Base every statement on the plot, the reference, and the given context only.",
114
118
  ].join("\n");
115
119
 
120
+ /** One labeled example: its figure(s) followed by the expert's diagnosis. */
121
+ export interface LabeledExample {
122
+ label: string;
123
+ images: ReadonlyArray<{ data: string; mimeType: string }>;
124
+ }
125
+
126
+ export type UserContentBlock =
127
+ | { type: "text"; text: string }
128
+ | { type: "image"; data: string; mimeType: string };
129
+
116
130
  /**
117
- * Build the single user turn sent to the evaluation model. The host's
118
- * reference for the task, when given, comes first so the context and the plot
119
- * are read against it. With several figures, the model is told how many follow
120
- * so it reads them in the order the context describes (expected reference
121
- * first, measured result last).
131
+ * The single user turn sent to the evaluation model, as the QCalEval papers
132
+ * lay it out: the reference for the experiment, then each labeled example as
133
+ * its figure followed by its diagnosis, then the context and question, then
134
+ * the figure to judge. Everything is one ordered content array; images are
135
+ * native blocks, never placeholders in the text.
136
+ *
137
+ * `prompt` is the same turn as text, with `[figure]` where an image was, for
138
+ * the record of what the model saw.
122
139
  */
123
- export function buildUserPrompt(
140
+ export function buildUserContent(
124
141
  context: string,
125
142
  question: string | undefined,
126
- imageCount = 1,
143
+ images: ReadonlyArray<{ data: string; mimeType: string }>,
127
144
  knowledge?: string,
128
- ): string {
129
- const lines: string[] = [];
145
+ examples: ReadonlyArray<LabeledExample> = [],
146
+ ): { content: UserContentBlock[]; prompt: string; examplesUsed: number } {
147
+ const content: UserContentBlock[] = [];
148
+ const shown = fitExamples(examples);
149
+ const text = (lines: string[]) => {
150
+ content.push({ type: "text", text: lines.join("\n") });
151
+ };
152
+ const image = (figure: { data: string; mimeType: string }) => {
153
+ content.push({ type: "image", data: figure.data, mimeType: figure.mimeType });
154
+ };
155
+
156
+ const opening: string[] = [];
130
157
  if (knowledge?.trim()) {
131
- lines.push(
132
- "Reference for this experiment (expected result, review questions, failure patterns):",
133
- knowledge.trim(),
134
- "",
158
+ opening.push("Reference for this experiment (expected plot, outcomes, parameters):", knowledge.trim(), "");
159
+ }
160
+ if (shown.length > 0) {
161
+ opening.push(
162
+ `${shown.length} labeled example${shown.length === 1 ? "" : "s"} of this experiment follow, each figure followed by the diagnosis an expert validated. Use them as the standard for the figure to judge, which comes last.`,
135
163
  );
136
164
  }
137
- lines.push("Experiment context:", context.trim() || "(none)");
138
- if (imageCount > 1) {
139
- lines.push(
165
+ if (opening.length > 0) text(opening);
166
+
167
+ shown.forEach((example, index) => {
168
+ text(["", `Example ${index + 1} of ${shown.length}:`]);
169
+ for (const figure of example.images) image(figure);
170
+ text([`Diagnosis of example ${index + 1}:`, example.label.trim()]);
171
+ });
172
+
173
+ const closing: string[] = [];
174
+ if (shown.length > 0) closing.push("", "Now the figure to judge, evaluated independently of the examples.");
175
+ closing.push("Experiment context:", context.trim() || "(none)");
176
+ if (images.length > 1) {
177
+ closing.push(
140
178
  "",
141
- `${imageCount} figures follow, in the order the context describes; the last one is the measured result to judge.`,
179
+ `${images.length} figures follow, in the order the context describes; the last one is the measured result to judge.`,
142
180
  );
143
181
  }
144
- if (question?.trim()) lines.push("", "Specific question:", question.trim());
145
- return lines.join("\n");
182
+ if (question?.trim()) closing.push("", "Specific question:", question.trim());
183
+ // Repeated here because a long reference ahead of the plot pulls the model
184
+ // towards prose; the last instruction it reads is the output format.
185
+ closing.push("", "Answer with the JSON object described in the system prompt, and nothing else.");
186
+ text(closing);
187
+ for (const figure of images) image(figure);
188
+
189
+ const prompt = content
190
+ .map((block) => (block.type === "text" ? block.text : "[figure]"))
191
+ .join("\n")
192
+ .replace(/\n{3,}/g, "\n\n")
193
+ .trim();
194
+ return { content, prompt, examplesUsed: shown.length };
195
+ }
196
+
197
+ /** Keep examples in order within the example and figure budgets; later ones are dropped. */
198
+ export function fitExamples(examples: ReadonlyArray<LabeledExample>): LabeledExample[] {
199
+ const kept: LabeledExample[] = [];
200
+ let figures = 0;
201
+ for (const example of examples) {
202
+ if (kept.length >= MAX_EXAMPLES) break;
203
+ if (!example.label?.trim() || !example.images?.length) continue;
204
+ if (figures + example.images.length > MAX_EXAMPLE_IMAGES) break;
205
+ figures += example.images.length;
206
+ kept.push(example);
207
+ }
208
+ return kept;
209
+ }
210
+
211
+ /** The user turn as text only (no examples), kept for callers that render the prompt. */
212
+ export function buildUserPrompt(
213
+ context: string,
214
+ question: string | undefined,
215
+ imageCount = 1,
216
+ knowledge?: string,
217
+ ): string {
218
+ const placeholder = { data: "", mimeType: "image/png" };
219
+ return buildUserContent(context, question, Array(imageCount).fill(placeholder), knowledge)
220
+ .content.filter((block) => block.type === "text")
221
+ .map((block) => (block as { text: string }).text)
222
+ .join("\n")
223
+ .replace(/\n{3,}/g, "\n\n")
224
+ .trim();
146
225
  }
147
226
 
148
227
  /** Pull the text blocks of the assistant answer, dropping thinking. */
@@ -203,6 +282,11 @@ const imageSchema = Type.Object({
203
282
  mimeType: Type.String({ description: "image/png or image/jpeg" }),
204
283
  });
205
284
 
285
+ const exampleSchema = Type.Object({
286
+ label: Type.String({ description: "The expert's diagnosis of the example figure(s)" }),
287
+ images: Type.Array(imageSchema, { minItems: 1 }),
288
+ });
289
+
206
290
  const parameters = Type.Object({
207
291
  context: Type.String({
208
292
  description:
@@ -237,6 +321,19 @@ const parameters = Type.Object({
237
321
  "How many of the newest figures the host should supply when `images` is omitted (default 1). Use 2 when an expected reference figure precedes the measured one, and describe their order in `context`.",
238
322
  }),
239
323
  ),
324
+ use_examples: Type.Optional(
325
+ Type.Boolean({
326
+ description:
327
+ "Set true only when the user asked to judge against labeled reference examples: the host then supplies its expert-labeled example plots for `task_name` (in-context examples). Default false.",
328
+ }),
329
+ ),
330
+ examples: Type.Optional(
331
+ Type.Array(exampleSchema, {
332
+ maxItems: MAX_EXAMPLES,
333
+ description:
334
+ "Normally omitted: the host supplies labeled examples when `use_examples` is true. Each is one or more real figures followed by the expert's diagnosis.",
335
+ }),
336
+ ),
240
337
  });
241
338
 
242
339
  /** Stored on the tool result; the host reads `diagnosis` without reparsing text. */
@@ -248,8 +345,10 @@ export interface EvaluationDetails {
248
345
  diagnosis?: Record<string, unknown> | null;
249
346
  /** The model's answer when it was not the requested JSON. */
250
347
  raw?: string;
251
- /** The user turn the model was sent (reference, context, question), so the host can show what it saw. */
348
+ /** The user turn the model was sent (reference, examples, context, question), so the host can show what it saw. */
252
349
  prompt?: string;
350
+ /** How many labeled examples were shown before the figure. */
351
+ examples_used?: number;
253
352
  }
254
353
 
255
354
  function failure(spec: ModelSpec | undefined, text: string): AgentToolResult<EvaluationDetails> {
@@ -271,6 +370,7 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
271
370
  "Use qcal_evaluate for every calibration figure you need to judge, instead of judging it yourself.",
272
371
  "Pass the task name, target, and expected result as context so the diagnosis is specific.",
273
372
  "Pass task_name when you know it: the host then supplies its reference knowledge for that task.",
373
+ "Set use_examples true only when the user asked for a judgement against labeled reference examples.",
274
374
  "Treat the returned diagnosis as the evaluation of record; add your own reasoning only on top of it.",
275
375
  ],
276
376
  parameters,
@@ -294,7 +394,13 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
294
394
  return failure(spec, "no plot was given. Pass the figure in `images` or fetch it first.");
295
395
  }
296
396
 
297
- const prompt = buildUserPrompt(params.context, params.question, images.length, params.knowledge);
397
+ const { content, prompt, examplesUsed } = buildUserContent(
398
+ params.context,
399
+ params.question,
400
+ images.map((image) => ({ data: image.data, mimeType: image.mimeType })),
401
+ params.knowledge,
402
+ params.examples ?? [],
403
+ );
298
404
  const message = await ctx.modelRegistry.complete(
299
405
  model,
300
406
  {
@@ -302,10 +408,11 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
302
408
  messages: [
303
409
  {
304
410
  role: "user",
305
- content: [
306
- { type: "text", text: prompt },
307
- ...images,
308
- ],
411
+ content: content.map((block) =>
412
+ block.type === "text"
413
+ ? { type: "text" as const, text: block.text }
414
+ : { type: "image" as const, data: block.data, mimeType: block.mimeType },
415
+ ),
309
416
  timestamp: Date.now(),
310
417
  },
311
418
  ],
@@ -325,6 +432,7 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
325
432
  diagnosis: diagnosis ?? null,
326
433
  ...(diagnosis ? {} : { raw: text }),
327
434
  prompt,
435
+ ...(examplesUsed ? { examples_used: examplesUsed } : {}),
328
436
  },
329
437
  ...(message.usage ? { usage: message.usage } : {}),
330
438
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangekame3/pi-qcaleval",
3
- "version": "0.0.2-beta.4",
3
+ "version": "0.0.2-beta.6",
4
4
  "description": "pi extension that sends calibration plots to a calibration-evaluation VLM such as NVIDIA Ising-Calibration and returns a structured diagnosis",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -27,6 +27,14 @@ Ising-Calibration by default) and returns a JSON diagnosis with
27
27
  in `context` that the first figure is the reference and the last the
28
28
  measurement. The host then supplies both; the default is the newest figure only.
29
29
 
30
+ ## Labeled examples (in-context)
31
+
32
+ - When the user asks to judge a plot against the labeled reference examples
33
+ (in-context examples, few-shot), pass `use_examples: true` with `task_name`.
34
+ The host supplies its expert-labeled example plots for that task, and the
35
+ model sees each example figure followed by its diagnosis before the figure
36
+ to judge, as in the QCalEval papers. Leave it off otherwise.
37
+
30
38
  ## How to use the result
31
39
 
32
40
  - Treat the diagnosis as the evaluation of record; reason on top of it instead