@oqtopus-team/pi-qdash 0.1.11-beta.19 → 0.1.11-beta.20
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/lib/qcal-evidence.ts +164 -0
- package/extensions/qdash.ts +42 -20
- package/package.json +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence text for pi-qcaleval's `qcal_evaluate`: what a task result's
|
|
3
|
+
* measurement settings were, read from the run parameters QDash records.
|
|
4
|
+
*
|
|
5
|
+
* The evaluation model is meant to read the plot, so the context names the
|
|
6
|
+
* task, the target and the sweep (what was varied, over which range, in how
|
|
7
|
+
* many points), not the fitted results. The numbers come from the recorded
|
|
8
|
+
* parameters, so they are exact and need no hand-written summary.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** One parameter as QDash records it on a task result. */
|
|
12
|
+
export interface RecordedParameter {
|
|
13
|
+
value: unknown;
|
|
14
|
+
unit: string;
|
|
15
|
+
value_type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A swept parameter: the axis a plot was taken along. */
|
|
20
|
+
export interface Sweep {
|
|
21
|
+
name: string;
|
|
22
|
+
unit: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** How the points are spaced. */
|
|
25
|
+
spacing: "linear" | "log" | "list";
|
|
26
|
+
/** First and last point, in the parameter's unit. */
|
|
27
|
+
start: number;
|
|
28
|
+
stop: number;
|
|
29
|
+
points: number;
|
|
30
|
+
/** Spacing between points for a linear sweep. */
|
|
31
|
+
step?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The recorded parameters of a task result, as a name → record map. */
|
|
35
|
+
export function recordedParameters(value: unknown): Record<string, RecordedParameter> {
|
|
36
|
+
const output: Record<string, RecordedParameter> = {};
|
|
37
|
+
const input = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
38
|
+
for (const [name, raw] of Object.entries(input)) {
|
|
39
|
+
if (raw && typeof raw === "object" && "value" in raw) {
|
|
40
|
+
const record = raw as Record<string, unknown>;
|
|
41
|
+
output[name] = {
|
|
42
|
+
value: record.value,
|
|
43
|
+
unit: typeof record.unit === "string" ? record.unit : "",
|
|
44
|
+
value_type: typeof record.value_type === "string" ? record.value_type : "",
|
|
45
|
+
description: typeof record.description === "string" ? record.description : "",
|
|
46
|
+
};
|
|
47
|
+
} else if (typeof raw === "number" || typeof raw === "string" || typeof raw === "boolean" || Array.isArray(raw)) {
|
|
48
|
+
output[name] = { value: raw, unit: "", value_type: Array.isArray(raw) ? "list" : typeof raw, description: "" };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function numbers(value: unknown): number[] | undefined {
|
|
55
|
+
if (!Array.isArray(value)) return undefined;
|
|
56
|
+
const list = value.map((item) => (typeof item === "number" ? item : typeof item === "string" ? Number(item) : Number.NaN));
|
|
57
|
+
return list.length > 0 && list.every((item) => Number.isFinite(item)) ? list : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The sweeps among the recorded parameters, in record order, read the way
|
|
62
|
+
* QDash's `get_value` expands them: `range` / `np.arange` as (start, stop,
|
|
63
|
+
* step), `np.linspace` as (start, stop, num), `np.logspace` as (log10 start,
|
|
64
|
+
* log10 stop, num), and `list` as the points themselves.
|
|
65
|
+
*/
|
|
66
|
+
export function sweepsOf(parameters: Record<string, RecordedParameter>): Sweep[] {
|
|
67
|
+
const sweeps: Sweep[] = [];
|
|
68
|
+
for (const [name, parameter] of Object.entries(parameters)) {
|
|
69
|
+
const values = numbers(parameter.value);
|
|
70
|
+
if (!values) continue;
|
|
71
|
+
const base = { name, unit: parameter.unit, description: parameter.description };
|
|
72
|
+
const type = parameter.value_type;
|
|
73
|
+
if ((type === "range" || type === "np.arange") && values.length === 3) {
|
|
74
|
+
const [start, stop, step] = values as [number, number, number];
|
|
75
|
+
if (step === 0 || (stop - start) / step <= 0) continue;
|
|
76
|
+
const points = Math.ceil((stop - start) / step - 1e-9);
|
|
77
|
+
sweeps.push({ ...base, spacing: "linear", start, stop: start + (points - 1) * step, points, step });
|
|
78
|
+
} else if (type === "np.linspace" && values.length === 3) {
|
|
79
|
+
const [start, stop, num] = values as [number, number, number];
|
|
80
|
+
const points = Math.max(1, Math.round(num));
|
|
81
|
+
sweeps.push({ ...base, spacing: "linear", start, stop, points, ...(points > 1 ? { step: (stop - start) / (points - 1) } : {}) });
|
|
82
|
+
} else if (type === "np.logspace" && values.length === 3) {
|
|
83
|
+
const [start, stop, num] = values as [number, number, number];
|
|
84
|
+
sweeps.push({ ...base, spacing: "log", start: 10 ** start, stop: 10 ** stop, points: Math.max(1, Math.round(num)) });
|
|
85
|
+
} else if (type === "list" || type === "" || values.length !== 3) {
|
|
86
|
+
if (values.length < 2) continue;
|
|
87
|
+
sweeps.push({ ...base, spacing: "list", start: Math.min(...values), stop: Math.max(...values), points: values.length });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return sweeps;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A number for the model: up to four significant digits, no trailing zeros. */
|
|
94
|
+
export function formatNumber(value: number): string {
|
|
95
|
+
if (!Number.isFinite(value)) return String(value);
|
|
96
|
+
if (Number.isInteger(value) && Math.abs(value) < 1e15) return String(value);
|
|
97
|
+
const text = Math.abs(value) >= 1e6 || Math.abs(value) < 1e-4 ? value.toExponential(3) : value.toPrecision(4);
|
|
98
|
+
return text.includes("e") ? text.replace(/\.?0+e/, "e") : text.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function withUnit(value: number, unit: string): string {
|
|
102
|
+
const text = formatNumber(value);
|
|
103
|
+
return unit && unit !== "a.u." ? `${text} ${unit}` : text;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One sweep as text, e.g. "time_range 0 to 400 ns in steps of 8 ns (51 points)". */
|
|
107
|
+
export function formatSweep(sweep: Sweep): string {
|
|
108
|
+
const range = `${sweep.name} ${formatNumber(sweep.start)} to ${withUnit(sweep.stop, sweep.unit)}`;
|
|
109
|
+
if (sweep.spacing === "log") return `${range}, ${sweep.points} points log-spaced`;
|
|
110
|
+
if (sweep.spacing === "list") return `${range}, ${sweep.points} points`;
|
|
111
|
+
return sweep.step === undefined ? `${range}, ${sweep.points} points` : `${range} in steps of ${withUnit(sweep.step, sweep.unit)} (${sweep.points} points)`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Scalar settings as "name value unit" items, numbers and short strings only. */
|
|
115
|
+
export function formatSettings(parameters: Record<string, RecordedParameter>, skip: ReadonlySet<string> = new Set()): string[] {
|
|
116
|
+
const items: string[] = [];
|
|
117
|
+
for (const [name, parameter] of Object.entries(parameters)) {
|
|
118
|
+
if (skip.has(name)) continue;
|
|
119
|
+
const value = parameter.value;
|
|
120
|
+
if (typeof value === "number") items.push(`${name} ${withUnit(value, parameter.unit)}`);
|
|
121
|
+
else if (typeof value === "boolean") items.push(`${name} ${value}`);
|
|
122
|
+
else if (typeof value === "string" && value.length <= 40) items.push(`${name} ${value}`);
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface QcalContext {
|
|
128
|
+
taskName?: string;
|
|
129
|
+
status?: string;
|
|
130
|
+
targetKind?: string;
|
|
131
|
+
targetId?: string;
|
|
132
|
+
chipId?: string;
|
|
133
|
+
runParameters: Record<string, RecordedParameter>;
|
|
134
|
+
/** Calibration inputs (previous values), shown only on request. */
|
|
135
|
+
inputParameters?: Record<string, RecordedParameter>;
|
|
136
|
+
/** Fitted results, shown only on request. */
|
|
137
|
+
metrics?: Record<string, RecordedParameter>;
|
|
138
|
+
message?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The context for `qcal_evaluate`: the task and target, then the sweep and
|
|
143
|
+
* the other run settings. Inputs and fitted results are added only when the
|
|
144
|
+
* caller asks, since the model is meant to read the plot.
|
|
145
|
+
*/
|
|
146
|
+
export function qcalContextText(context: QcalContext): string {
|
|
147
|
+
let head = `Task ${context.taskName ?? "unknown"}`;
|
|
148
|
+
if (context.targetId) {
|
|
149
|
+
head += ` on ${context.targetKind && context.targetKind !== "system" ? `${context.targetKind} ${context.targetId}` : context.targetId}`;
|
|
150
|
+
}
|
|
151
|
+
if (context.chipId) head += ` (chip ${context.chipId})`;
|
|
152
|
+
if (context.status) head += `, status ${context.status}`;
|
|
153
|
+
const lines = [`${head}.`];
|
|
154
|
+
const sweeps = sweepsOf(context.runParameters);
|
|
155
|
+
if (sweeps.length > 0) lines.push(`Sweep: ${sweeps.map(formatSweep).join("; ")}.`);
|
|
156
|
+
const settings = formatSettings(context.runParameters, new Set(sweeps.map((sweep) => sweep.name)));
|
|
157
|
+
if (settings.length > 0) lines.push(`Settings: ${settings.join("; ")}.`);
|
|
158
|
+
const inputs = context.inputParameters ? formatSettings(context.inputParameters) : [];
|
|
159
|
+
if (inputs.length > 0) lines.push(`Calibration inputs: ${inputs.join("; ")}.`);
|
|
160
|
+
const metrics = context.metrics ? formatSettings(context.metrics) : [];
|
|
161
|
+
if (metrics.length > 0) lines.push(`Fitted results: ${metrics.join("; ")}.`);
|
|
162
|
+
if (context.message?.trim()) lines.push(`Task message: ${context.message.trim()}`);
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
package/extensions/qdash.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { toFigureToolResult, toTextToolResult, toToolResult } from "./lib/result
|
|
|
25
25
|
import { ansi, boxLinesToWidth, boxed, formatNumber, textComponent } from "./lib/render.js";
|
|
26
26
|
import { plotSeriesLines, timeseriesPlotComponent, timeseriesPoints } from "./lib/timeseries-plot.js";
|
|
27
27
|
import { installQDashWriteGate } from "./lib/write-gate.js";
|
|
28
|
+
import { qcalContextText, recordedParameters, sweepsOf, type RecordedParameter, type Sweep } from "./lib/qcal-evidence.js";
|
|
28
29
|
import { registerCalibrationPipelineTools } from "./tools/calibration-pipeline.js";
|
|
29
30
|
import { registerTaskKnowledgeTool } from "./tools/task-knowledge.js";
|
|
30
31
|
import { registerTimeseriesComparisonTool } from "./tools/timeseries.js";
|
|
@@ -405,6 +406,14 @@ type CalibrationEvidence = {
|
|
|
405
406
|
task?: Record<string, unknown>;
|
|
406
407
|
metrics?: Record<string, number | string | boolean | null>;
|
|
407
408
|
parameters?: Record<string, number | string | boolean | null>;
|
|
409
|
+
/** The run parameters as recorded, with units and value types. */
|
|
410
|
+
runParameters?: Record<string, RecordedParameter>;
|
|
411
|
+
/** The calibration inputs as recorded (previous values), with units. */
|
|
412
|
+
inputParameters?: Record<string, RecordedParameter>;
|
|
413
|
+
/** The output parameters as recorded (fitted results), with units. */
|
|
414
|
+
metricRecords?: Record<string, RecordedParameter>;
|
|
415
|
+
/** The swept parameters among them: the axes the plot was taken along. */
|
|
416
|
+
sweeps?: Sweep[];
|
|
408
417
|
resultJson?: unknown;
|
|
409
418
|
figures?: Array<Record<string, unknown>>;
|
|
410
419
|
logs?: string[];
|
|
@@ -431,23 +440,24 @@ function firstStringField(object: Record<string, unknown>, keys: string[]): stri
|
|
|
431
440
|
return undefined;
|
|
432
441
|
}
|
|
433
442
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
"",
|
|
443
|
-
"
|
|
444
|
-
|
|
445
|
-
"",
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
443
|
+
/**
|
|
444
|
+
* The evidence text, used as the context of `qcal_evaluate`: the task, the
|
|
445
|
+
* target, the sweep and the run settings. Fitted results and calibration
|
|
446
|
+
* inputs are left out unless asked for, since the evaluation model is meant
|
|
447
|
+
* to read them off the plot.
|
|
448
|
+
*/
|
|
449
|
+
function qcalEvidenceText(evidence: CalibrationEvidence, include: { inputs?: boolean; metrics?: boolean } = {}): string {
|
|
450
|
+
return qcalContextText({
|
|
451
|
+
taskName: typeof evidence.task?.name === "string" ? evidence.task.name : undefined,
|
|
452
|
+
status: typeof evidence.task?.status === "string" ? evidence.task.status : undefined,
|
|
453
|
+
targetKind: typeof evidence.target?.kind === "string" ? evidence.target.kind : undefined,
|
|
454
|
+
targetId: typeof evidence.target?.id === "string" ? evidence.target.id : undefined,
|
|
455
|
+
chipId: typeof evidence.target?.chipId === "string" ? evidence.target.chipId : undefined,
|
|
456
|
+
runParameters: evidence.runParameters ?? {},
|
|
457
|
+
...(include.inputs ? { inputParameters: recordedParameters(evidence.inputParameters) } : {}),
|
|
458
|
+
...(include.metrics ? { metrics: recordedParameters(evidence.metricRecords) } : {}),
|
|
459
|
+
message: typeof evidence.task?.message === "string" ? evidence.task.message : undefined,
|
|
460
|
+
});
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
function cleanQuery(values: Record<string, unknown> = {}): Record<string, unknown> {
|
|
@@ -2574,7 +2584,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2574
2584
|
pi.registerTool({
|
|
2575
2585
|
name: "qdash_build_qcal_evidence",
|
|
2576
2586
|
label: "QDash Build QCal Evidence",
|
|
2577
|
-
description: "Summarize a QDash task result (target,
|
|
2587
|
+
description: "Summarize a QDash task result (target, sweep and run settings) as evidence for pi-qcaleval's qcal_evaluate, returning the task figure as image content. Fitted results are left out unless includeMetrics is set.",
|
|
2578
2588
|
promptSnippet: "Summarize a QDash calibration task result as evidence for qcal_evaluate",
|
|
2579
2589
|
promptGuidelines: [
|
|
2580
2590
|
"Use qdash_build_qcal_evidence before qcal_evaluate when the user asks to evaluate a QDash calibration task with pi-qcaleval; pass the returned summary as the context of qcal_evaluate.",
|
|
@@ -2588,8 +2598,10 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2588
2598
|
includeFigureAnalysis: Type.Optional(Type.Boolean({ description: "Analyze Plotly JSON figures and include summaries/warnings in notes. Defaults to true." })),
|
|
2589
2599
|
maxFigures: Type.Optional(Type.Number({ description: "Maximum image figures to embed. Defaults to 1." })),
|
|
2590
2600
|
includeRawTaskResult: Type.Optional(Type.Boolean({ description: "Include the full raw task result in resultJson. Defaults to false." })),
|
|
2601
|
+
includeInputs: Type.Optional(Type.Boolean({ description: "Add the calibration inputs (previous values) to the evidence text. Defaults to false: the evaluation model reads the plot." })),
|
|
2602
|
+
includeMetrics: Type.Optional(Type.Boolean({ description: "Add the fitted output parameters to the evidence text. Defaults to false: the evaluation model reads the plot." })),
|
|
2591
2603
|
}),
|
|
2592
|
-
async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean }) {
|
|
2604
|
+
async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean; includeInputs?: boolean; includeMetrics?: boolean }) {
|
|
2593
2605
|
const client = await makeClient(params);
|
|
2594
2606
|
const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
|
|
2595
2607
|
const figures: Array<Record<string, unknown>> = [];
|
|
@@ -2634,6 +2646,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2634
2646
|
|
|
2635
2647
|
const couplingId = firstStringField(task, ["coupling_id", "couplingId", "qid"]);
|
|
2636
2648
|
const qid = firstStringField(task, ["qid"]);
|
|
2649
|
+
const runParameters = recordedParameters(task.run_parameters);
|
|
2637
2650
|
const evidence: CalibrationEvidence = {
|
|
2638
2651
|
id: params.taskId,
|
|
2639
2652
|
source: "external",
|
|
@@ -2651,9 +2664,14 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2651
2664
|
startedAt: firstStringField(task, ["start_at", "startAt"]),
|
|
2652
2665
|
completedAt: firstStringField(task, ["end_at", "endAt"]),
|
|
2653
2666
|
status: firstStringField(task, ["status"]),
|
|
2667
|
+
message: typeof task.message === "string" && task.message.trim() ? task.message.trim() : undefined,
|
|
2654
2668
|
},
|
|
2655
2669
|
metrics: parameterValueRecord(task.output_parameters),
|
|
2656
2670
|
parameters: { ...parameterValueRecord(task.input_parameters), ...parameterValueRecord(task.run_parameters) },
|
|
2671
|
+
runParameters,
|
|
2672
|
+
sweeps: sweepsOf(runParameters),
|
|
2673
|
+
inputParameters: recordedParameters(task.input_parameters),
|
|
2674
|
+
metricRecords: recordedParameters(task.output_parameters),
|
|
2657
2675
|
figures: figures.length ? figures : undefined,
|
|
2658
2676
|
notes: [
|
|
2659
2677
|
typeof task.message === "string" ? `Task message: ${task.message}` : undefined,
|
|
@@ -2669,7 +2687,11 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2669
2687
|
},
|
|
2670
2688
|
};
|
|
2671
2689
|
|
|
2672
|
-
const result = toTextToolResult(
|
|
2690
|
+
const result = toTextToolResult(
|
|
2691
|
+
qcalEvidenceText(evidence, { inputs: params.includeInputs === true, metrics: params.includeMetrics === true }),
|
|
2692
|
+
{ evidence },
|
|
2693
|
+
{ tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence },
|
|
2694
|
+
);
|
|
2673
2695
|
return { ...result, content: [...result.content, ...images] };
|
|
2674
2696
|
},
|
|
2675
2697
|
});
|