@orangekame3/pi-qcaleval 0.0.1 → 0.0.2-beta.10
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 +26 -6
- package/extensions/qcaleval.ts +393 -51
- package/package.json +1 -1
- package/skills/qcaleval/SKILL.md +12 -0
package/README.md
CHANGED
|
@@ -13,23 +13,43 @@ works from the pi CLI and from the QDash Copilot runtime alike.
|
|
|
13
13
|
|
|
14
14
|
## Tool
|
|
15
15
|
|
|
16
|
-
`qcal_evaluate(context, question?, images?, max_images?)` returns a JSON object with
|
|
16
|
+
`qcal_evaluate(context, question?, task_name?, knowledge?, images?, max_images?)` returns a JSON object with
|
|
17
17
|
`technical_description`, `experimental_conclusion`, `experimental_significance`,
|
|
18
18
|
`fit_quality`, `parameter_extraction`, and `success`. The parsed object is also
|
|
19
19
|
stored in the tool result's `details.diagnosis`; when the model did not answer
|
|
20
|
-
with JSON, `details.raw` holds its text
|
|
20
|
+
with JSON, or not with all of it, `details.raw` holds its text and
|
|
21
|
+
`details.missing_keys` the keys it lacks.
|
|
22
|
+
|
|
23
|
+
On a chat-completions API (vLLM, SGLang, llama.cpp) the request carries a
|
|
24
|
+
`response_format` with the diagnosis schema, so decoding is constrained to the
|
|
25
|
+
six keys; a server that refuses it is asked plainly. An answer that still
|
|
26
|
+
lacks keys is asked for once more and the more complete one kept. The details
|
|
27
|
+
record `attempts` (1 or 2), `structured` (whether decoding was constrained),
|
|
28
|
+
and `stop_reason`.
|
|
21
29
|
|
|
22
30
|
`images` are base64 image blocks. A host may fill them in from the most recent
|
|
23
31
|
figures of the conversation, so the planner does not have to pass image bytes;
|
|
24
32
|
`max_images` (default 1, up to 4) says how many of the newest figures to supply,
|
|
25
33
|
for example 2 when an expected reference figure precedes the measured result.
|
|
26
34
|
|
|
35
|
+
`knowledge` is the host's reference for the experiment: what a good result
|
|
36
|
+
looks like, the questions a reviewer asks, and the common failure patterns. A
|
|
37
|
+
host that keeps such knowledge per task fills it in from `task_name`, so the
|
|
38
|
+
planner only names the task; the text is sent before the context. The exact
|
|
39
|
+
user turn the model received (reference, context, question) is stored in
|
|
40
|
+
`details.prompt`, so a host can show what the model saw.
|
|
41
|
+
|
|
27
42
|
## Configuration
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
No setting is required. The evaluation model is resolved in this order:
|
|
45
|
+
|
|
46
|
+
1. `QCALEVAL_MODEL` (`provider/modelId`), an explicit override for pi CLI use;
|
|
47
|
+
2. a configured calibration VLM, recognised by its id (`Ising-Calibration`);
|
|
48
|
+
3. the conversation's own model, when it accepts images. The result is then
|
|
49
|
+
marked `fallback: true` in `details`, so a host without an Ising deployment
|
|
50
|
+
still gets a diagnosis from the vision model it already runs.
|
|
51
|
+
|
|
52
|
+
The output budget follows the model's `maxTokens` (at most 4096).
|
|
33
53
|
|
|
34
54
|
The provider must be declared in pi's `models.json` with its base URL and
|
|
35
55
|
credential, for example:
|
package/extensions/qcaleval.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* pi-qcaleval: evaluate calibration plots with a dedicated calibration VLM.
|
|
3
3
|
*
|
|
4
4
|
* The extension has no QDash dependency. It takes a plot (plus the experiment
|
|
5
|
-
* context the planner already knows
|
|
5
|
+
* context the planner already knows, and the host's reference knowledge for
|
|
6
|
+
* the task when the host supplies one) and asks the evaluation model, which is
|
|
6
7
|
* configured separately from the conversation's model, for a diagnosis. The
|
|
7
8
|
* planner decides what to do with it; whether a calibration loop continues is
|
|
8
9
|
* the host's policy, not this tool's.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import type { Api, AssistantMessage, ImageContent, Model } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { Api, AssistantMessage, ImageContent, Model, Usage } from "@earendil-works/pi-ai";
|
|
12
13
|
import type {
|
|
13
14
|
AgentToolResult,
|
|
14
15
|
ExtensionAPI,
|
|
@@ -16,20 +17,29 @@ import type {
|
|
|
16
17
|
} from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import { Type, type Static } from "typebox";
|
|
18
19
|
|
|
19
|
-
/** `provider/modelId`; the model id itself may contain slashes (nvidia/...). */
|
|
20
|
-
export const DEFAULT_MODEL = "vllm/nvidia/Ising-Calibration-1.5-31B-NVFP4";
|
|
21
|
-
|
|
22
20
|
/** Figures per evaluation; a 16k-token deployment holds a few plots comfortably. */
|
|
23
21
|
export const MAX_IMAGES = 4;
|
|
24
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
|
+
|
|
27
|
+
/** Model ids of models fine-tuned for calibration plots; preferred when configured. */
|
|
28
|
+
export const CALIBRATION_MODEL_PATTERN = /ising-calibration/i;
|
|
29
|
+
|
|
30
|
+
/** Output budget when the model declares none; a diagnosis is a few hundred tokens. */
|
|
31
|
+
const DEFAULT_MAX_TOKENS = 2048;
|
|
32
|
+
const MAX_OUTPUT_TOKENS = 4096;
|
|
33
|
+
|
|
25
34
|
export interface ModelSpec {
|
|
26
35
|
provider: string;
|
|
27
36
|
modelId: string;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
/** Parse `provider/modelId
|
|
31
|
-
export function parseModelSpec(value: string | undefined): ModelSpec {
|
|
32
|
-
const spec = (value ?? "").trim()
|
|
39
|
+
/** Parse an explicit `provider/modelId` override; the model id may contain slashes (nvidia/...). */
|
|
40
|
+
export function parseModelSpec(value: string | undefined): ModelSpec | undefined {
|
|
41
|
+
const spec = (value ?? "").trim();
|
|
42
|
+
if (!spec) return undefined;
|
|
33
43
|
const slash = spec.indexOf("/");
|
|
34
44
|
if (slash <= 0 || slash === spec.length - 1) {
|
|
35
45
|
throw new Error(`QCALEVAL_MODEL must be provider/modelId, got "${spec}"`);
|
|
@@ -37,6 +47,54 @@ export function parseModelSpec(value: string | undefined): ModelSpec {
|
|
|
37
47
|
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
/** The slice of pi's model registry the resolution needs. */
|
|
51
|
+
export interface ModelLookup {
|
|
52
|
+
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
53
|
+
getAvailable(): Model<Api>[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type EvaluationModel =
|
|
57
|
+
| { model: Model<Api>; fallback: false }
|
|
58
|
+
| { model: Model<Api>; fallback: true }
|
|
59
|
+
| { model?: undefined; error: string };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pick the model that judges the plot, without any host-specific setting:
|
|
63
|
+
*
|
|
64
|
+
* 1. `QCALEVAL_MODEL` (`provider/modelId`), an explicit override for the pi CLI;
|
|
65
|
+
* 2. a configured calibration VLM, recognised by its id (Ising-Calibration);
|
|
66
|
+
* 3. the conversation's own model, when it accepts images.
|
|
67
|
+
*
|
|
68
|
+
* A host that declares the calibration model in its models.json gets it
|
|
69
|
+
* everywhere; one that does not (no Ising deployment) still gets a diagnosis
|
|
70
|
+
* from the vision model it already runs, marked as a fallback.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveEvaluationModel(
|
|
73
|
+
registry: ModelLookup,
|
|
74
|
+
current: Model<Api> | undefined,
|
|
75
|
+
override: string | undefined,
|
|
76
|
+
): EvaluationModel {
|
|
77
|
+
const spec = parseModelSpec(override);
|
|
78
|
+
if (spec) {
|
|
79
|
+
const model = registry.find(spec.provider, spec.modelId);
|
|
80
|
+
return model
|
|
81
|
+
? { model, fallback: false }
|
|
82
|
+
: { error: `model ${spec.provider}/${spec.modelId} is not configured (QCALEVAL_MODEL)` };
|
|
83
|
+
}
|
|
84
|
+
const calibration = registry.getAvailable().find((model) => CALIBRATION_MODEL_PATTERN.test(model.id));
|
|
85
|
+
if (calibration) return { model: calibration, fallback: false };
|
|
86
|
+
if (current?.input.includes("image")) return { model: current, fallback: true };
|
|
87
|
+
return {
|
|
88
|
+
error:
|
|
89
|
+
"no evaluation model: declare a calibration VLM (Ising-Calibration) in models.json, or run with a vision model",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Output budget for one evaluation, within what the model allows. */
|
|
94
|
+
export function outputBudget(model: Pick<Model<Api>, "maxTokens">): number {
|
|
95
|
+
return model.maxTokens > 0 ? Math.min(model.maxTokens, MAX_OUTPUT_TOKENS) : DEFAULT_MAX_TOKENS;
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
/** Categories the Ising-Calibration model card describes; asked for in this order. */
|
|
41
99
|
export const DIAGNOSIS_SECTIONS = [
|
|
42
100
|
"technical_description",
|
|
@@ -47,6 +105,50 @@ export const DIAGNOSIS_SECTIONS = [
|
|
|
47
105
|
"success",
|
|
48
106
|
] as const;
|
|
49
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The diagnosis as a JSON schema: the six keys, all required. Sent as the
|
|
110
|
+
* response format to OpenAI-compatible servers that constrain decoding to a
|
|
111
|
+
* schema (vLLM, SGLang, llama.cpp). Asked for JSON in the prompt alone, the
|
|
112
|
+
* calibration model now and then closes the object after the first key or
|
|
113
|
+
* nests quotes that break it; constrained decoding rules both out.
|
|
114
|
+
*/
|
|
115
|
+
export const DIAGNOSIS_SCHEMA = {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
technical_description: { type: "string" },
|
|
119
|
+
experimental_conclusion: { type: "string" },
|
|
120
|
+
experimental_significance: { type: "string" },
|
|
121
|
+
fit_quality: { type: "string" },
|
|
122
|
+
parameter_extraction: {
|
|
123
|
+
anyOf: [{ type: "object", additionalProperties: true }, { type: "string" }, { type: "null" }],
|
|
124
|
+
},
|
|
125
|
+
success: { type: "boolean" },
|
|
126
|
+
},
|
|
127
|
+
required: [...DIAGNOSIS_SECTIONS],
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
} as const;
|
|
130
|
+
|
|
131
|
+
/** `response_format` of a chat completion constrained to the diagnosis schema. */
|
|
132
|
+
export const RESPONSE_FORMAT = {
|
|
133
|
+
type: "json_schema",
|
|
134
|
+
json_schema: { name: "calibration_diagnosis", schema: DIAGNOSIS_SCHEMA },
|
|
135
|
+
} as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether the model's API takes `response_format` in the request body. Pi
|
|
139
|
+
* merges `samplingParams` into the body of chat completions only; the
|
|
140
|
+
* responses API spells the format differently and other APIs ignore it.
|
|
141
|
+
*/
|
|
142
|
+
export function canConstrain(model: Pick<Model<Api>, "api">): boolean {
|
|
143
|
+
return model.api === "openai-completions";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The diagnosis keys an answer lacks; all six when there is no diagnosis. */
|
|
147
|
+
export function missingKeys(diagnosis: Record<string, unknown> | undefined): string[] {
|
|
148
|
+
if (!diagnosis) return [...DIAGNOSIS_SECTIONS];
|
|
149
|
+
return DIAGNOSIS_SECTIONS.filter((key) => !(key in diagnosis));
|
|
150
|
+
}
|
|
151
|
+
|
|
50
152
|
export const SYSTEM_PROMPT = [
|
|
51
153
|
"You evaluate superconducting qubit calibration experiment plots.",
|
|
52
154
|
"Answer with one JSON object and nothing else, with these keys:",
|
|
@@ -56,28 +158,114 @@ export const SYSTEM_PROMPT = [
|
|
|
56
158
|
' "fit_quality": "good" | "marginal" | "poor" and why;',
|
|
57
159
|
' "parameter_extraction": extracted values with units, or null;',
|
|
58
160
|
' "success": true | false, whether the experiment achieved its goal.',
|
|
59
|
-
"Base every statement on the plot and the given context only.",
|
|
161
|
+
"Base every statement on the plot, the reference, and the given context only.",
|
|
60
162
|
].join("\n");
|
|
61
163
|
|
|
164
|
+
/** One labeled example: its figure(s) followed by the expert's diagnosis. */
|
|
165
|
+
export interface LabeledExample {
|
|
166
|
+
label: string;
|
|
167
|
+
images: ReadonlyArray<{ data: string; mimeType: string }>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type UserContentBlock =
|
|
171
|
+
| { type: "text"; text: string }
|
|
172
|
+
| { type: "image"; data: string; mimeType: string };
|
|
173
|
+
|
|
62
174
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
175
|
+
* The single user turn sent to the evaluation model, as the QCalEval papers
|
|
176
|
+
* lay it out: the reference for the experiment, then each labeled example as
|
|
177
|
+
* its figure followed by its diagnosis, then the context and question, then
|
|
178
|
+
* the figure to judge. Everything is one ordered content array; images are
|
|
179
|
+
* native blocks, never placeholders in the text.
|
|
180
|
+
*
|
|
181
|
+
* `prompt` is the same turn as text, with `[figure]` where an image was, for
|
|
182
|
+
* the record of what the model saw.
|
|
66
183
|
*/
|
|
67
|
-
export function
|
|
184
|
+
export function buildUserContent(
|
|
68
185
|
context: string,
|
|
69
186
|
question: string | undefined,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
187
|
+
images: ReadonlyArray<{ data: string; mimeType: string }>,
|
|
188
|
+
knowledge?: string,
|
|
189
|
+
examples: ReadonlyArray<LabeledExample> = [],
|
|
190
|
+
): { content: UserContentBlock[]; prompt: string; examplesUsed: number } {
|
|
191
|
+
const content: UserContentBlock[] = [];
|
|
192
|
+
const shown = fitExamples(examples);
|
|
193
|
+
const text = (lines: string[]) => {
|
|
194
|
+
content.push({ type: "text", text: lines.join("\n") });
|
|
195
|
+
};
|
|
196
|
+
const image = (figure: { data: string; mimeType: string }) => {
|
|
197
|
+
content.push({ type: "image", data: figure.data, mimeType: figure.mimeType });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const opening: string[] = [];
|
|
201
|
+
if (knowledge?.trim()) {
|
|
202
|
+
opening.push("Reference for this experiment (expected plot, outcomes, parameters):", knowledge.trim(), "");
|
|
203
|
+
}
|
|
204
|
+
if (shown.length > 0) {
|
|
205
|
+
opening.push(
|
|
206
|
+
`${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.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (opening.length > 0) text(opening);
|
|
210
|
+
|
|
211
|
+
shown.forEach((example, index) => {
|
|
212
|
+
text(["", `Example ${index + 1} of ${shown.length}:`]);
|
|
213
|
+
for (const figure of example.images) image(figure);
|
|
214
|
+
text([`Diagnosis of example ${index + 1}:`, example.label.trim()]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const closing: string[] = [];
|
|
218
|
+
if (shown.length > 0) closing.push("", "Now the figure to judge, evaluated independently of the examples.");
|
|
219
|
+
closing.push("Experiment context:", context.trim() || "(none)");
|
|
220
|
+
if (images.length > 1) {
|
|
221
|
+
closing.push(
|
|
75
222
|
"",
|
|
76
|
-
`${
|
|
223
|
+
`${images.length} figures follow, in the order the context describes; the last one is the measured result to judge.`,
|
|
77
224
|
);
|
|
78
225
|
}
|
|
79
|
-
if (question?.trim())
|
|
80
|
-
|
|
226
|
+
if (question?.trim()) closing.push("", "Specific question:", question.trim());
|
|
227
|
+
// Repeated here because a long reference ahead of the plot pulls the model
|
|
228
|
+
// towards prose; the last instruction it reads is the output format.
|
|
229
|
+
closing.push("", "Answer with the JSON object described in the system prompt, and nothing else.");
|
|
230
|
+
text(closing);
|
|
231
|
+
for (const figure of images) image(figure);
|
|
232
|
+
|
|
233
|
+
const prompt = content
|
|
234
|
+
.map((block) => (block.type === "text" ? block.text : "[figure]"))
|
|
235
|
+
.join("\n")
|
|
236
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
237
|
+
.trim();
|
|
238
|
+
return { content, prompt, examplesUsed: shown.length };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Keep examples in order within the example and figure budgets; later ones are dropped. */
|
|
242
|
+
export function fitExamples(examples: ReadonlyArray<LabeledExample>): LabeledExample[] {
|
|
243
|
+
const kept: LabeledExample[] = [];
|
|
244
|
+
let figures = 0;
|
|
245
|
+
for (const example of examples) {
|
|
246
|
+
if (kept.length >= MAX_EXAMPLES) break;
|
|
247
|
+
if (!example.label?.trim() || !example.images?.length) continue;
|
|
248
|
+
if (figures + example.images.length > MAX_EXAMPLE_IMAGES) break;
|
|
249
|
+
figures += example.images.length;
|
|
250
|
+
kept.push(example);
|
|
251
|
+
}
|
|
252
|
+
return kept;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The user turn as text only (no examples), kept for callers that render the prompt. */
|
|
256
|
+
export function buildUserPrompt(
|
|
257
|
+
context: string,
|
|
258
|
+
question: string | undefined,
|
|
259
|
+
imageCount = 1,
|
|
260
|
+
knowledge?: string,
|
|
261
|
+
): string {
|
|
262
|
+
const placeholder = { data: "", mimeType: "image/png" };
|
|
263
|
+
return buildUserContent(context, question, Array(imageCount).fill(placeholder), knowledge)
|
|
264
|
+
.content.filter((block) => block.type === "text")
|
|
265
|
+
.map((block) => (block as { text: string }).text)
|
|
266
|
+
.join("\n")
|
|
267
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
268
|
+
.trim();
|
|
81
269
|
}
|
|
82
270
|
|
|
83
271
|
/** Pull the text blocks of the assistant answer, dropping thinking. */
|
|
@@ -133,11 +321,99 @@ function parseObject(json: string): Record<string, unknown> | undefined {
|
|
|
133
321
|
return undefined;
|
|
134
322
|
}
|
|
135
323
|
|
|
324
|
+
/** One answer of the evaluation model, read for the record. */
|
|
325
|
+
export interface Answer {
|
|
326
|
+
text: string;
|
|
327
|
+
diagnosis?: Record<string, unknown>;
|
|
328
|
+
/** Diagnosis keys the answer lacks; empty when it is complete. */
|
|
329
|
+
missing: string[];
|
|
330
|
+
/** Model calls made for this answer: 1, or 2 after an incomplete or refused first one. */
|
|
331
|
+
attempts: number;
|
|
332
|
+
/** Whether decoding was constrained to the diagnosis schema. */
|
|
333
|
+
structured: boolean;
|
|
334
|
+
stopReason: AssistantMessage["stopReason"];
|
|
335
|
+
usage?: Usage;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function readAnswer(message: AssistantMessage, structured: boolean): Answer {
|
|
339
|
+
const text = answerText(message);
|
|
340
|
+
const diagnosis = parseDiagnosis(text);
|
|
341
|
+
return {
|
|
342
|
+
text,
|
|
343
|
+
diagnosis,
|
|
344
|
+
missing: missingKeys(diagnosis),
|
|
345
|
+
attempts: 1,
|
|
346
|
+
structured,
|
|
347
|
+
stopReason: message.stopReason,
|
|
348
|
+
usage: message.usage,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Token usage of two calls together, so a retried answer is billed as both. */
|
|
353
|
+
export function addUsage(a: Usage | undefined, b: Usage | undefined): Usage | undefined {
|
|
354
|
+
if (!a || !b) return a ?? b;
|
|
355
|
+
const sum = (x: number | undefined, y: number | undefined) => (x ?? 0) + (y ?? 0);
|
|
356
|
+
return {
|
|
357
|
+
...a,
|
|
358
|
+
input: sum(a.input, b.input),
|
|
359
|
+
output: sum(a.output, b.output),
|
|
360
|
+
cacheRead: sum(a.cacheRead, b.cacheRead),
|
|
361
|
+
cacheWrite: sum(a.cacheWrite, b.cacheWrite),
|
|
362
|
+
...(a.reasoning === undefined && b.reasoning === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { reasoning: sum(a.reasoning, b.reasoning) }),
|
|
365
|
+
totalTokens: sum(a.totalTokens, b.totalTokens),
|
|
366
|
+
cost: {
|
|
367
|
+
input: sum(a.cost?.input, b.cost?.input),
|
|
368
|
+
output: sum(a.cost?.output, b.cost?.output),
|
|
369
|
+
cacheRead: sum(a.cost?.cacheRead, b.cost?.cacheRead),
|
|
370
|
+
cacheWrite: sum(a.cost?.cacheWrite, b.cost?.cacheWrite),
|
|
371
|
+
total: sum(a.cost?.total, b.cost?.total),
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Ask the model for the diagnosis, in at most two calls. The first call is
|
|
378
|
+
* constrained to the schema when the API allows it; a server that refuses the
|
|
379
|
+
* response format (an older vLLM, a gateway) gets the plain request instead.
|
|
380
|
+
* An answer that lacks diagnosis keys is asked for once more, since the model
|
|
381
|
+
* usually completes the object on a second sample, and the more complete of
|
|
382
|
+
* the two answers is kept. Usage counts every call made.
|
|
383
|
+
*/
|
|
384
|
+
export async function askForDiagnosis(
|
|
385
|
+
call: (structured: boolean) => Promise<AssistantMessage>,
|
|
386
|
+
constrain: boolean,
|
|
387
|
+
): Promise<Answer | { error: string }> {
|
|
388
|
+
let structured = constrain;
|
|
389
|
+
let first = await call(structured);
|
|
390
|
+
let attempts = 1;
|
|
391
|
+
let usage: Usage | undefined = first.usage;
|
|
392
|
+
if (first.stopReason === "error" && structured) {
|
|
393
|
+
structured = false;
|
|
394
|
+
first = await call(structured);
|
|
395
|
+
attempts += 1;
|
|
396
|
+
usage = addUsage(usage, first.usage);
|
|
397
|
+
}
|
|
398
|
+
if (first.stopReason === "error") return { error: first.errorMessage ?? "model error" };
|
|
399
|
+
const answer = { ...readAnswer(first, structured), attempts, usage };
|
|
400
|
+
if (answer.missing.length === 0 || attempts === 2) return answer;
|
|
401
|
+
const second = await call(structured);
|
|
402
|
+
const retried = second.stopReason === "error" ? undefined : readAnswer(second, structured);
|
|
403
|
+
const kept = retried && retried.missing.length <= answer.missing.length ? retried : answer;
|
|
404
|
+
return { ...kept, attempts: 2, usage: addUsage(first.usage, second.usage) };
|
|
405
|
+
}
|
|
406
|
+
|
|
136
407
|
const imageSchema = Type.Object({
|
|
137
408
|
data: Type.String({ description: "Base64 image bytes (no data: prefix)" }),
|
|
138
409
|
mimeType: Type.String({ description: "image/png or image/jpeg" }),
|
|
139
410
|
});
|
|
140
411
|
|
|
412
|
+
const exampleSchema = Type.Object({
|
|
413
|
+
label: Type.String({ description: "The expert's diagnosis of the example figure(s)" }),
|
|
414
|
+
images: Type.Array(imageSchema, { minItems: 1 }),
|
|
415
|
+
});
|
|
416
|
+
|
|
141
417
|
const parameters = Type.Object({
|
|
142
418
|
context: Type.String({
|
|
143
419
|
description:
|
|
@@ -146,6 +422,18 @@ const parameters = Type.Object({
|
|
|
146
422
|
question: Type.Optional(
|
|
147
423
|
Type.String({ description: "A specific question to answer about the plot" }),
|
|
148
424
|
),
|
|
425
|
+
task_name: Type.Optional(
|
|
426
|
+
Type.String({
|
|
427
|
+
description:
|
|
428
|
+
"The calibration task's name as the host knows it (e.g. RandomizedBenchmarking). Lets the host supply its reference knowledge for the experiment.",
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
knowledge: Type.Optional(
|
|
432
|
+
Type.String({
|
|
433
|
+
description:
|
|
434
|
+
"Normally omitted: the host supplies the reference for `task_name` (expected result, review questions, failure patterns). Fill only to override it with text you were given.",
|
|
435
|
+
}),
|
|
436
|
+
),
|
|
149
437
|
images: Type.Optional(
|
|
150
438
|
Type.Array(imageSchema, {
|
|
151
439
|
description:
|
|
@@ -160,18 +448,52 @@ const parameters = Type.Object({
|
|
|
160
448
|
"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`.",
|
|
161
449
|
}),
|
|
162
450
|
),
|
|
451
|
+
use_examples: Type.Optional(
|
|
452
|
+
Type.Boolean({
|
|
453
|
+
description:
|
|
454
|
+
"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.",
|
|
455
|
+
}),
|
|
456
|
+
),
|
|
457
|
+
examples: Type.Optional(
|
|
458
|
+
Type.Array(exampleSchema, {
|
|
459
|
+
maxItems: MAX_EXAMPLES,
|
|
460
|
+
description:
|
|
461
|
+
"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.",
|
|
462
|
+
}),
|
|
463
|
+
),
|
|
163
464
|
});
|
|
164
465
|
|
|
165
466
|
/** Stored on the tool result; the host reads `diagnosis` without reparsing text. */
|
|
166
467
|
export interface EvaluationDetails {
|
|
167
|
-
model
|
|
468
|
+
/** The model that judged the plot; absent when none could be resolved. */
|
|
469
|
+
model?: ModelSpec;
|
|
470
|
+
/** True when no calibration VLM was configured and the conversation's model judged instead. */
|
|
471
|
+
fallback?: boolean;
|
|
168
472
|
diagnosis?: Record<string, unknown> | null;
|
|
169
|
-
/** The model's answer when it was not the requested JSON. */
|
|
473
|
+
/** The model's answer when it was not the requested JSON, or not all of it. */
|
|
170
474
|
raw?: string;
|
|
475
|
+
/** Diagnosis keys the parsed answer lacks, when it is incomplete. */
|
|
476
|
+
missing_keys?: string[];
|
|
477
|
+
/** Model calls made: 1, or 2 after an incomplete or refused first answer. */
|
|
478
|
+
attempts?: number;
|
|
479
|
+
/** Whether decoding was constrained to the diagnosis schema. */
|
|
480
|
+
structured?: boolean;
|
|
481
|
+
/** Why the model stopped, as pi reports it. */
|
|
482
|
+
stop_reason?: string;
|
|
483
|
+
/** The user turn the model was sent (reference, examples, context, question), so the host can show what it saw. */
|
|
484
|
+
prompt?: string;
|
|
485
|
+
/** How many labeled examples were shown before the figure. */
|
|
486
|
+
examples_used?: number;
|
|
487
|
+
/** The system prompt the model was sent, so the host can show the whole request. */
|
|
488
|
+
system_prompt?: string;
|
|
171
489
|
}
|
|
172
490
|
|
|
173
|
-
function failure(spec: ModelSpec, text: string): AgentToolResult<EvaluationDetails> {
|
|
174
|
-
return {
|
|
491
|
+
function failure(spec: ModelSpec | undefined, text: string): AgentToolResult<EvaluationDetails> {
|
|
492
|
+
return {
|
|
493
|
+
content: [{ type: "text", text: `qcal_evaluate: ${text}` }],
|
|
494
|
+
details: spec ? { model: spec } : {},
|
|
495
|
+
isError: true,
|
|
496
|
+
};
|
|
175
497
|
}
|
|
176
498
|
|
|
177
499
|
export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
@@ -184,6 +506,8 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
184
506
|
promptGuidelines: [
|
|
185
507
|
"Use qcal_evaluate for every calibration figure you need to judge, instead of judging it yourself.",
|
|
186
508
|
"Pass the task name, target, and expected result as context so the diagnosis is specific.",
|
|
509
|
+
"Pass task_name when you know it: the host then supplies its reference knowledge for that task.",
|
|
510
|
+
"Set use_examples true only when the user asked for a judgement against labeled reference examples.",
|
|
187
511
|
"Treat the returned diagnosis as the evaluation of record; add your own reasoning only on top of it.",
|
|
188
512
|
],
|
|
189
513
|
parameters,
|
|
@@ -194,11 +518,10 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
194
518
|
_onUpdate: unknown,
|
|
195
519
|
ctx: ExtensionToolContext,
|
|
196
520
|
): Promise<AgentToolResult<EvaluationDetails>> {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
521
|
+
const resolved = resolveEvaluationModel(ctx.modelRegistry, ctx.model, process.env.QCALEVAL_MODEL);
|
|
522
|
+
if (!resolved.model) return failure(undefined, resolved.error);
|
|
523
|
+
const { model, fallback } = resolved;
|
|
524
|
+
const spec: ModelSpec = { provider: model.provider, modelId: model.id };
|
|
202
525
|
const images: ImageContent[] = (params.images ?? []).map((image) => ({
|
|
203
526
|
type: "image",
|
|
204
527
|
data: image.data,
|
|
@@ -208,39 +531,58 @@ export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
|
208
531
|
return failure(spec, "no plot was given. Pass the figure in `images` or fetch it first.");
|
|
209
532
|
}
|
|
210
533
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
534
|
+
const { content, prompt, examplesUsed } = buildUserContent(
|
|
535
|
+
params.context,
|
|
536
|
+
params.question,
|
|
537
|
+
images.map((image) => ({ data: image.data, mimeType: image.mimeType })),
|
|
538
|
+
params.knowledge,
|
|
539
|
+
params.examples ?? [],
|
|
540
|
+
);
|
|
541
|
+
const answer = await askForDiagnosis(
|
|
542
|
+
(structured) =>
|
|
543
|
+
ctx.modelRegistry.complete(
|
|
544
|
+
model,
|
|
216
545
|
{
|
|
217
|
-
|
|
218
|
-
|
|
546
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
547
|
+
messages: [
|
|
219
548
|
{
|
|
220
|
-
|
|
221
|
-
|
|
549
|
+
role: "user",
|
|
550
|
+
content: content.map((block) =>
|
|
551
|
+
block.type === "text"
|
|
552
|
+
? { type: "text" as const, text: block.text }
|
|
553
|
+
: { type: "image" as const, data: block.data, mimeType: block.mimeType },
|
|
554
|
+
),
|
|
555
|
+
timestamp: Date.now(),
|
|
222
556
|
},
|
|
223
|
-
...images,
|
|
224
557
|
],
|
|
225
|
-
timestamp: Date.now(),
|
|
226
558
|
},
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
559
|
+
{
|
|
560
|
+
signal,
|
|
561
|
+
maxTokens: outputBudget(model),
|
|
562
|
+
...(structured ? { samplingParams: { response_format: RESPONSE_FORMAT } } : {}),
|
|
563
|
+
},
|
|
564
|
+
),
|
|
565
|
+
canConstrain(model),
|
|
230
566
|
);
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const text = answerText(message);
|
|
235
|
-
const diagnosis = parseDiagnosis(text);
|
|
567
|
+
if ("error" in answer) return failure(spec, answer.error);
|
|
568
|
+
const { text, diagnosis, missing } = answer;
|
|
569
|
+
const complete = diagnosis !== undefined && missing.length === 0;
|
|
236
570
|
return {
|
|
237
571
|
content: [{ type: "text", text: diagnosis ? JSON.stringify(diagnosis, null, 2) : text }],
|
|
238
572
|
details: {
|
|
239
573
|
model: spec,
|
|
574
|
+
...(fallback ? { fallback: true } : {}),
|
|
240
575
|
diagnosis: diagnosis ?? null,
|
|
241
|
-
...(
|
|
576
|
+
...(complete ? {} : { raw: text }),
|
|
577
|
+
...(diagnosis && !complete ? { missing_keys: missing } : {}),
|
|
578
|
+
attempts: answer.attempts,
|
|
579
|
+
structured: answer.structured,
|
|
580
|
+
stop_reason: answer.stopReason,
|
|
581
|
+
prompt,
|
|
582
|
+
system_prompt: SYSTEM_PROMPT,
|
|
583
|
+
...(examplesUsed ? { examples_used: examplesUsed } : {}),
|
|
242
584
|
},
|
|
243
|
-
...(
|
|
585
|
+
...(answer.usage ? { usage: answer.usage } : {}),
|
|
244
586
|
};
|
|
245
587
|
},
|
|
246
588
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangekame3/pi-qcaleval",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2-beta.10",
|
|
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
|
@@ -18,11 +18,23 @@ Ising-Calibration by default) and returns a JSON diagnosis with
|
|
|
18
18
|
the task name, the target (qubit or coupling), the swept parameter, and what a
|
|
19
19
|
successful result looks like. Add `question` when a specific point matters
|
|
20
20
|
(for example "is the chevron centred on the current frequency?").
|
|
21
|
+
- Pass `task_name` whenever you know the task (for example `CheckRabi`): the
|
|
22
|
+
host then supplies its own reference knowledge for that experiment, which is
|
|
23
|
+
more reliable than a summary you write yourself. Keep `context` for what the
|
|
24
|
+
reference cannot know: the target, the swept range, and previous values.
|
|
21
25
|
- When the conversation holds an expected reference figure followed by the
|
|
22
26
|
measured result (a task review opens this way), pass `max_images: 2` and say
|
|
23
27
|
in `context` that the first figure is the reference and the last the
|
|
24
28
|
measurement. The host then supplies both; the default is the newest figure only.
|
|
25
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
|
+
|
|
26
38
|
## How to use the result
|
|
27
39
|
|
|
28
40
|
- Treat the diagnosis as the evaluation of record; reason on top of it instead
|