@orangekame3/pi-qcaleval 0.0.0-stage → 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/LICENSE +201 -0
- package/README.md +97 -2
- package/extensions/qcaleval.ts +589 -0
- package/package.json +50 -4
- package/skills/qcaleval/SKILL.md +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright yyyy OQTOPUS Team
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-qcaleval
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [pi](https://github.com/badlogic/pi-mono) extension that sends superconducting
|
|
4
|
+
qubit calibration plots to a dedicated calibration-evaluation VLM and returns a
|
|
5
|
+
structured diagnosis. The default model is
|
|
6
|
+
[NVIDIA Ising-Calibration](https://build.nvidia.com/nvidia/ising-calibration-1.5-31b/modelcard)
|
|
7
|
+
served by vLLM.
|
|
8
|
+
|
|
9
|
+
The planner model keeps the conversation; `qcal_evaluate` is the tool it calls
|
|
10
|
+
whenever a calibration figure needs a fit-quality judgement, parameter
|
|
11
|
+
extraction, or a success verdict. The extension has no QDash dependency, so it
|
|
12
|
+
works from the pi CLI and from the QDash Copilot runtime alike.
|
|
13
|
+
|
|
14
|
+
## Tool
|
|
15
|
+
|
|
16
|
+
`qcal_evaluate(context, question?, task_name?, knowledge?, images?, max_images?)` returns a JSON object with
|
|
17
|
+
`technical_description`, `experimental_conclusion`, `experimental_significance`,
|
|
18
|
+
`fit_quality`, `parameter_extraction`, and `success`. The parsed object is also
|
|
19
|
+
stored in the tool result's `details.diagnosis`; when the model did not answer
|
|
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`.
|
|
29
|
+
|
|
30
|
+
`images` are base64 image blocks. A host may fill them in from the most recent
|
|
31
|
+
figures of the conversation, so the planner does not have to pass image bytes;
|
|
32
|
+
`max_images` (default 1, up to 4) says how many of the newest figures to supply,
|
|
33
|
+
for example 2 when an expected reference figure precedes the measured result.
|
|
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
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
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).
|
|
53
|
+
|
|
54
|
+
The provider must be declared in pi's `models.json` with its base URL and
|
|
55
|
+
credential, for example:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"providers": {
|
|
60
|
+
"vllm": {
|
|
61
|
+
"baseUrl": "https://llm.example/v1",
|
|
62
|
+
"api": "openai-completions",
|
|
63
|
+
"apiKey": "$VLLM_API_KEY",
|
|
64
|
+
"models": [{ "id": "nvidia/Ising-Calibration-1.5-31B-NVFP4", "contextWindow": 16384, "maxTokens": 2048, "input": ["text", "image"] }]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Only the plot, a short system prompt, and the `context` text are sent, so the
|
|
71
|
+
evaluation fits comfortably in a 16k-token deployment.
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pi install npm:@orangekame3/pi-qcaleval
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Releases are tagged `vX.Y.Z` and published to npm as `latest`; every push to
|
|
80
|
+
`main` also publishes a `beta` prerelease (`npm:@orangekame3/pi-qcaleval@beta`)
|
|
81
|
+
for hosts that want an unreleased build. Installing from git
|
|
82
|
+
(`pi install git:github.com/orangekame3/pi-qcaleval`) also works.
|
|
83
|
+
|
|
84
|
+
For development against the QDash Agent Runtime, clone this repository under
|
|
85
|
+
`agent-runtime/extensions/` in the QDash checkout and start the runtime with
|
|
86
|
+
`compose.extensions.yaml`; see the QDash Copilot architecture docs.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bun install
|
|
92
|
+
bun run check # tsc --noEmit
|
|
93
|
+
bun run test # node --test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
Apache-2.0
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-qcaleval: evaluate calibration plots with a dedicated calibration VLM.
|
|
3
|
+
*
|
|
4
|
+
* The extension has no QDash dependency. It takes a plot (plus the experiment
|
|
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
|
|
7
|
+
* configured separately from the conversation's model, for a diagnosis. The
|
|
8
|
+
* planner decides what to do with it; whether a calibration loop continues is
|
|
9
|
+
* the host's policy, not this tool's.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Api, AssistantMessage, ImageContent, Model, Usage } from "@earendil-works/pi-ai";
|
|
13
|
+
import type {
|
|
14
|
+
AgentToolResult,
|
|
15
|
+
ExtensionAPI,
|
|
16
|
+
ExtensionToolContext,
|
|
17
|
+
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Type, type Static } from "typebox";
|
|
19
|
+
|
|
20
|
+
/** Figures per evaluation; a 16k-token deployment holds a few plots comfortably. */
|
|
21
|
+
export const MAX_IMAGES = 4;
|
|
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
|
+
|
|
34
|
+
export interface ModelSpec {
|
|
35
|
+
provider: string;
|
|
36
|
+
modelId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
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;
|
|
43
|
+
const slash = spec.indexOf("/");
|
|
44
|
+
if (slash <= 0 || slash === spec.length - 1) {
|
|
45
|
+
throw new Error(`QCALEVAL_MODEL must be provider/modelId, got "${spec}"`);
|
|
46
|
+
}
|
|
47
|
+
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
48
|
+
}
|
|
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
|
+
|
|
98
|
+
/** Categories the Ising-Calibration model card describes; asked for in this order. */
|
|
99
|
+
export const DIAGNOSIS_SECTIONS = [
|
|
100
|
+
"technical_description",
|
|
101
|
+
"experimental_conclusion",
|
|
102
|
+
"experimental_significance",
|
|
103
|
+
"fit_quality",
|
|
104
|
+
"parameter_extraction",
|
|
105
|
+
"success",
|
|
106
|
+
] as const;
|
|
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
|
+
|
|
152
|
+
export const SYSTEM_PROMPT = [
|
|
153
|
+
"You evaluate superconducting qubit calibration experiment plots.",
|
|
154
|
+
"Answer with one JSON object and nothing else, with these keys:",
|
|
155
|
+
' "technical_description": what the plot shows (axes, traces, fit);',
|
|
156
|
+
' "experimental_conclusion": what the data says about the experiment;',
|
|
157
|
+
' "experimental_significance": what it means for the calibration;',
|
|
158
|
+
' "fit_quality": "good" | "marginal" | "poor" and why;',
|
|
159
|
+
' "parameter_extraction": extracted values with units, or null;',
|
|
160
|
+
' "success": true | false, whether the experiment achieved its goal.',
|
|
161
|
+
"Base every statement on the plot, the reference, and the given context only.",
|
|
162
|
+
].join("\n");
|
|
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
|
+
|
|
174
|
+
/**
|
|
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.
|
|
183
|
+
*/
|
|
184
|
+
export function buildUserContent(
|
|
185
|
+
context: string,
|
|
186
|
+
question: string | undefined,
|
|
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(
|
|
222
|
+
"",
|
|
223
|
+
`${images.length} figures follow, in the order the context describes; the last one is the measured result to judge.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
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();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Pull the text blocks of the assistant answer, dropping thinking. */
|
|
272
|
+
export function answerText(message: AssistantMessage): string {
|
|
273
|
+
return message.content
|
|
274
|
+
.map((block) => (block.type === "text" ? block.text : ""))
|
|
275
|
+
.join("")
|
|
276
|
+
.trim();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Parse the model's JSON object, tolerating a code fence, a "thought" preamble
|
|
281
|
+
* or trailing commentary around it: the first balanced `{...}` is taken.
|
|
282
|
+
*/
|
|
283
|
+
export function parseDiagnosis(text: string): Record<string, unknown> | undefined {
|
|
284
|
+
const start = text.indexOf("{");
|
|
285
|
+
if (start < 0) return undefined;
|
|
286
|
+
let depth = 0;
|
|
287
|
+
let inString = false;
|
|
288
|
+
for (let i = start; i < text.length; i++) {
|
|
289
|
+
const ch = text[i];
|
|
290
|
+
if (inString) {
|
|
291
|
+
if (ch === "\\") i++;
|
|
292
|
+
else if (ch === '"') inString = false;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (ch === '"') inString = true;
|
|
296
|
+
else if (ch === "{") depth++;
|
|
297
|
+
else if (ch === "}" && --depth === 0) {
|
|
298
|
+
return parseObject(text.slice(start, i + 1));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Parse one JSON object. The model writes LaTeX such as `\pi` inside strings,
|
|
306
|
+
* which is an invalid JSON escape, so a failed strict parse is retried with
|
|
307
|
+
* every backslash that does not start a JSON escape doubled.
|
|
308
|
+
*/
|
|
309
|
+
function parseObject(json: string): Record<string, unknown> | undefined {
|
|
310
|
+
for (const candidate of [json, json.replace(/\\(?!["\\/bfnrtu])/g, "\\\\")]) {
|
|
311
|
+
try {
|
|
312
|
+
const parsed: unknown = JSON.parse(candidate);
|
|
313
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
314
|
+
return parsed as Record<string, unknown>;
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
} catch {
|
|
318
|
+
// Try the lenient candidate next.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
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
|
+
|
|
407
|
+
const imageSchema = Type.Object({
|
|
408
|
+
data: Type.String({ description: "Base64 image bytes (no data: prefix)" }),
|
|
409
|
+
mimeType: Type.String({ description: "image/png or image/jpeg" }),
|
|
410
|
+
});
|
|
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
|
+
|
|
417
|
+
const parameters = Type.Object({
|
|
418
|
+
context: Type.String({
|
|
419
|
+
description:
|
|
420
|
+
"Experiment context: task name, target qubit/coupling, parameters, and what a successful result looks like",
|
|
421
|
+
}),
|
|
422
|
+
question: Type.Optional(
|
|
423
|
+
Type.String({ description: "A specific question to answer about the plot" }),
|
|
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
|
+
),
|
|
437
|
+
images: Type.Optional(
|
|
438
|
+
Type.Array(imageSchema, {
|
|
439
|
+
description:
|
|
440
|
+
"Normally omitted: the host supplies the newest figures of the conversation. Fill only with real base64 image bytes you were given; never with placeholders or descriptions.",
|
|
441
|
+
}),
|
|
442
|
+
),
|
|
443
|
+
max_images: Type.Optional(
|
|
444
|
+
Type.Integer({
|
|
445
|
+
minimum: 1,
|
|
446
|
+
maximum: MAX_IMAGES,
|
|
447
|
+
description:
|
|
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`.",
|
|
449
|
+
}),
|
|
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
|
+
),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
/** Stored on the tool result; the host reads `diagnosis` without reparsing text. */
|
|
467
|
+
export interface EvaluationDetails {
|
|
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;
|
|
472
|
+
diagnosis?: Record<string, unknown> | null;
|
|
473
|
+
/** The model's answer when it was not the requested JSON, or not all of it. */
|
|
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;
|
|
489
|
+
}
|
|
490
|
+
|
|
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
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export default function qcalevalExtension(pi: ExtensionAPI) {
|
|
500
|
+
pi.registerTool<typeof parameters, EvaluationDetails>({
|
|
501
|
+
name: "qcal_evaluate",
|
|
502
|
+
label: "Evaluate Calibration Plot",
|
|
503
|
+
description:
|
|
504
|
+
"Send a calibration experiment plot to the calibration-evaluation VLM and return its diagnosis: technical description, conclusion, significance, fit quality, extracted parameters, and success.",
|
|
505
|
+
promptSnippet: "Diagnose a calibration plot with the dedicated calibration VLM",
|
|
506
|
+
promptGuidelines: [
|
|
507
|
+
"Use qcal_evaluate for every calibration figure you need to judge, instead of judging it yourself.",
|
|
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.",
|
|
511
|
+
"Treat the returned diagnosis as the evaluation of record; add your own reasoning only on top of it.",
|
|
512
|
+
],
|
|
513
|
+
parameters,
|
|
514
|
+
async execute(
|
|
515
|
+
_toolCallId: string,
|
|
516
|
+
params: Static<typeof parameters>,
|
|
517
|
+
signal: AbortSignal | undefined,
|
|
518
|
+
_onUpdate: unknown,
|
|
519
|
+
ctx: ExtensionToolContext,
|
|
520
|
+
): Promise<AgentToolResult<EvaluationDetails>> {
|
|
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 };
|
|
525
|
+
const images: ImageContent[] = (params.images ?? []).map((image) => ({
|
|
526
|
+
type: "image",
|
|
527
|
+
data: image.data,
|
|
528
|
+
mimeType: image.mimeType,
|
|
529
|
+
}));
|
|
530
|
+
if (images.length === 0) {
|
|
531
|
+
return failure(spec, "no plot was given. Pass the figure in `images` or fetch it first.");
|
|
532
|
+
}
|
|
533
|
+
|
|
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,
|
|
545
|
+
{
|
|
546
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
547
|
+
messages: [
|
|
548
|
+
{
|
|
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(),
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
signal,
|
|
561
|
+
maxTokens: outputBudget(model),
|
|
562
|
+
...(structured ? { samplingParams: { response_format: RESPONSE_FORMAT } } : {}),
|
|
563
|
+
},
|
|
564
|
+
),
|
|
565
|
+
canConstrain(model),
|
|
566
|
+
);
|
|
567
|
+
if ("error" in answer) return failure(spec, answer.error);
|
|
568
|
+
const { text, diagnosis, missing } = answer;
|
|
569
|
+
const complete = diagnosis !== undefined && missing.length === 0;
|
|
570
|
+
return {
|
|
571
|
+
content: [{ type: "text", text: diagnosis ? JSON.stringify(diagnosis, null, 2) : text }],
|
|
572
|
+
details: {
|
|
573
|
+
model: spec,
|
|
574
|
+
...(fallback ? { fallback: true } : {}),
|
|
575
|
+
diagnosis: diagnosis ?? null,
|
|
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 } : {}),
|
|
584
|
+
},
|
|
585
|
+
...(answer.usage ? { usage: answer.usage } : {}),
|
|
586
|
+
};
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangekame3/pi-qcaleval",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2-beta.10",
|
|
4
|
+
"description": "pi extension that sends calibration plots to a calibration-evaluation VLM such as NVIDIA Ising-Calibration and returns a structured diagnosis",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/orangekame3/pi-qcaleval.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/orangekame3/pi-qcaleval#readme",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi-package",
|
|
13
|
+
"pi-extension",
|
|
14
|
+
"calibration",
|
|
15
|
+
"qdash"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"files": [
|
|
25
|
+
"extensions",
|
|
26
|
+
"skills"
|
|
27
|
+
],
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./extensions/qcaleval.ts"
|
|
31
|
+
],
|
|
32
|
+
"skills": [
|
|
33
|
+
"./skills"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"check": "tsc --noEmit",
|
|
38
|
+
"test": "rm -rf .test-dist && tsc --noEmit false --outDir .test-dist && node --test tests/*.test.mjs; status=$?; rm -rf .test-dist; exit $status"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@earendil-works/pi-ai": "*",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"typebox": "*"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@earendil-works/pi-ai": "1.0.3",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "1.0.3",
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"typebox": "^1.0.55",
|
|
50
|
+
"typescript": "^5.8.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qcaleval
|
|
3
|
+
description: Evaluate superconducting qubit calibration plots with the dedicated calibration VLM through the pi-qcaleval extension. Use when a calibration figure (Rabi, Ramsey, chevron, T1/T2, readout, spectroscopy, two-qubit tune-up) needs a fit-quality judgement, parameter extraction, or a success verdict.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Whether a calibration loop continues after an evaluation is the host's policy. This skill describes the tool, not when to stop. -->
|
|
7
|
+
|
|
8
|
+
# Calibration plot evaluation
|
|
9
|
+
|
|
10
|
+
`qcal_evaluate` sends a plot to a calibration-evaluation model (NVIDIA
|
|
11
|
+
Ising-Calibration by default) and returns a JSON diagnosis with
|
|
12
|
+
`technical_description`, `experimental_conclusion`, `experimental_significance`,
|
|
13
|
+
`fit_quality`, `parameter_extraction`, and `success`.
|
|
14
|
+
|
|
15
|
+
## Preferred tools
|
|
16
|
+
|
|
17
|
+
- `qcal_evaluate`: judge one or more calibration plots. Always give `context`:
|
|
18
|
+
the task name, the target (qubit or coupling), the swept parameter, and what a
|
|
19
|
+
successful result looks like. Add `question` when a specific point matters
|
|
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.
|
|
25
|
+
- When the conversation holds an expected reference figure followed by the
|
|
26
|
+
measured result (a task review opens this way), pass `max_images: 2` and say
|
|
27
|
+
in `context` that the first figure is the reference and the last the
|
|
28
|
+
measurement. The host then supplies both; the default is the newest figure only.
|
|
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
|
+
|
|
38
|
+
## How to use the result
|
|
39
|
+
|
|
40
|
+
- Treat the diagnosis as the evaluation of record; reason on top of it instead
|
|
41
|
+
of re-judging the plot yourself.
|
|
42
|
+
- Report `fit_quality` and `success` verbatim, then the extracted parameters.
|
|
43
|
+
- When `success` is false, say what the diagnosis identified as the cause before
|
|
44
|
+
proposing a next step.
|
|
45
|
+
- The model only sees the plot and the context you pass. If a conclusion needs
|
|
46
|
+
data that is not in the plot (previous values, device limits), fetch it and
|
|
47
|
+
include it in `context`.
|