@orangekame3/pi-qcaleval 0.0.2-beta.5 → 0.0.2-beta.9
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/extensions/qcaleval.ts +134 -26
- package/package.json +1 -1
- package/skills/qcaleval/SKILL.md +8 -0
package/extensions/qcaleval.ts
CHANGED
|
@@ -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,39 +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
|
-
*
|
|
118
|
-
* reference for the
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
140
|
+
export function buildUserContent(
|
|
124
141
|
context: string,
|
|
125
142
|
question: string | undefined,
|
|
126
|
-
|
|
143
|
+
images: ReadonlyArray<{ data: string; mimeType: string }>,
|
|
127
144
|
knowledge?: string,
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
`${
|
|
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())
|
|
182
|
+
if (question?.trim()) closing.push("", "Specific question:", question.trim());
|
|
145
183
|
// Repeated here because a long reference ahead of the plot pulls the model
|
|
146
184
|
// towards prose; the last instruction it reads is the output format.
|
|
147
|
-
|
|
148
|
-
|
|
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();
|
|
149
225
|
}
|
|
150
226
|
|
|
151
227
|
/** Pull the text blocks of the assistant answer, dropping thinking. */
|
|
@@ -206,6 +282,11 @@ const imageSchema = Type.Object({
|
|
|
206
282
|
mimeType: Type.String({ description: "image/png or image/jpeg" }),
|
|
207
283
|
});
|
|
208
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
|
+
|
|
209
290
|
const parameters = Type.Object({
|
|
210
291
|
context: Type.String({
|
|
211
292
|
description:
|
|
@@ -240,6 +321,19 @@ const parameters = Type.Object({
|
|
|
240
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`.",
|
|
241
322
|
}),
|
|
242
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
|
+
),
|
|
243
337
|
});
|
|
244
338
|
|
|
245
339
|
/** Stored on the tool result; the host reads `diagnosis` without reparsing text. */
|
|
@@ -251,8 +345,12 @@ export interface EvaluationDetails {
|
|
|
251
345
|
diagnosis?: Record<string, unknown> | null;
|
|
252
346
|
/** The model's answer when it was not the requested JSON. */
|
|
253
347
|
raw?: string;
|
|
254
|
-
/** 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. */
|
|
255
349
|
prompt?: string;
|
|
350
|
+
/** How many labeled examples were shown before the figure. */
|
|
351
|
+
examples_used?: number;
|
|
352
|
+
/** The system prompt the model was sent, so the host can show the whole request. */
|
|
353
|
+
system_prompt?: string;
|
|
256
354
|
}
|
|
257
355
|
|
|
258
356
|
function failure(spec: ModelSpec | undefined, text: string): AgentToolResult<EvaluationDetails> {
|
|
@@ -274,6 +372,7 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
274
372
|
"Use qcal_evaluate for every calibration figure you need to judge, instead of judging it yourself.",
|
|
275
373
|
"Pass the task name, target, and expected result as context so the diagnosis is specific.",
|
|
276
374
|
"Pass task_name when you know it: the host then supplies its reference knowledge for that task.",
|
|
375
|
+
"Set use_examples true only when the user asked for a judgement against labeled reference examples.",
|
|
277
376
|
"Treat the returned diagnosis as the evaluation of record; add your own reasoning only on top of it.",
|
|
278
377
|
],
|
|
279
378
|
parameters,
|
|
@@ -297,7 +396,13 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
297
396
|
return failure(spec, "no plot was given. Pass the figure in `images` or fetch it first.");
|
|
298
397
|
}
|
|
299
398
|
|
|
300
|
-
const
|
|
399
|
+
const { content, prompt, examplesUsed } = buildUserContent(
|
|
400
|
+
params.context,
|
|
401
|
+
params.question,
|
|
402
|
+
images.map((image) => ({ data: image.data, mimeType: image.mimeType })),
|
|
403
|
+
params.knowledge,
|
|
404
|
+
params.examples ?? [],
|
|
405
|
+
);
|
|
301
406
|
const message = await ctx.modelRegistry.complete(
|
|
302
407
|
model,
|
|
303
408
|
{
|
|
@@ -305,10 +410,11 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
305
410
|
messages: [
|
|
306
411
|
{
|
|
307
412
|
role: "user",
|
|
308
|
-
content:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
413
|
+
content: content.map((block) =>
|
|
414
|
+
block.type === "text"
|
|
415
|
+
? { type: "text" as const, text: block.text }
|
|
416
|
+
: { type: "image" as const, data: block.data, mimeType: block.mimeType },
|
|
417
|
+
),
|
|
312
418
|
timestamp: Date.now(),
|
|
313
419
|
},
|
|
314
420
|
],
|
|
@@ -328,6 +434,8 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
328
434
|
diagnosis: diagnosis ?? null,
|
|
329
435
|
...(diagnosis ? {} : { raw: text }),
|
|
330
436
|
prompt,
|
|
437
|
+
system_prompt: SYSTEM_PROMPT,
|
|
438
|
+
...(examplesUsed ? { examples_used: examplesUsed } : {}),
|
|
331
439
|
},
|
|
332
440
|
...(message.usage ? { usage: message.usage } : {}),
|
|
333
441
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangekame3/pi-qcaleval",
|
|
3
|
-
"version": "0.0.2-beta.
|
|
3
|
+
"version": "0.0.2-beta.9",
|
|
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": {
|
package/skills/qcaleval/SKILL.md
CHANGED
|
@@ -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
|