@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.
@@ -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
+ }
@@ -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
- function qcalEvidenceText(evidence: CalibrationEvidence): string {
435
- const metrics = Object.entries(evidence.metrics ?? {}).slice(0, 8).map(([key, value]) => ` - ${key}: ${String(value)}`);
436
- const figures = evidence.figures ?? [];
437
- return boxed("QDash QCal Evidence", [
438
- `id ${evidence.id ?? "n/a"}`,
439
- `source ${evidence.source}`,
440
- `target ${evidence.target?.id ?? evidence.target?.couplingId ?? evidence.target?.qids ?? "n/a"}`,
441
- `task ${evidence.task?.name ?? "n/a"} (${evidence.task?.status ?? "unknown"})`,
442
- "",
443
- "Metrics",
444
- ...(metrics.length ? metrics : [" - none"]),
445
- "",
446
- `Figures ${figures.length}`,
447
- ...figures.map((figure, index) => ` - #${index + 1} ${String(figure.role ?? "plot")} ${String(figure.path ?? figure.url ?? "")}`),
448
- "",
449
- "Pass this summary as the context of qcal_evaluate; the embedded figure is the measured result to judge.",
450
- ]).join("\n");
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, parameters, metrics, figure analysis) as evidence for pi-qcaleval's qcal_evaluate, returning the task figure as image content.",
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(qcalEvidenceText(evidence), { evidence }, { tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence });
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oqtopus-team/pi-qdash",
3
- "version": "0.1.11-beta.19",
3
+ "version": "0.1.11-beta.20",
4
4
  "description": "Pi extension for querying QDash via @oqtopus-team/qdash-client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",