@oqtopus-team/pi-qdash 0.1.3 → 0.1.4

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.
@@ -393,6 +393,58 @@ type TaskResultFigureFile = DownloadedFile & {
393
393
  jsonFigurePaths: string[];
394
394
  };
395
395
 
396
+ type CalibrationEvidence = {
397
+ id?: string;
398
+ source: string;
399
+ target?: Record<string, unknown>;
400
+ task?: Record<string, unknown>;
401
+ metrics?: Record<string, number | string | boolean | null>;
402
+ parameters?: Record<string, number | string | boolean | null>;
403
+ resultJson?: unknown;
404
+ figures?: Array<Record<string, unknown>>;
405
+ logs?: string[];
406
+ notes?: string;
407
+ history?: Array<Record<string, unknown>>;
408
+ };
409
+
410
+ function parameterValueRecord(value: unknown): Record<string, number | string | boolean | null> {
411
+ const output: Record<string, number | string | boolean | null> = {};
412
+ const input = value && typeof value === "object" ? value as Record<string, unknown> : {};
413
+ for (const [key, raw] of Object.entries(input)) {
414
+ const object = raw && typeof raw === "object" ? raw as Record<string, unknown> : undefined;
415
+ const candidate = object && "value" in object ? object.value : raw;
416
+ if (typeof candidate === "number" || typeof candidate === "string" || typeof candidate === "boolean" || candidate === null) output[key] = candidate;
417
+ }
418
+ return output;
419
+ }
420
+
421
+ function firstStringField(object: Record<string, unknown>, keys: string[]): string | undefined {
422
+ for (const key of keys) {
423
+ const value = object[key];
424
+ if (typeof value === "string" && value) return value;
425
+ }
426
+ return undefined;
427
+ }
428
+
429
+ function qcalEvidenceText(evidence: CalibrationEvidence): string {
430
+ const metrics = Object.entries(evidence.metrics ?? {}).slice(0, 8).map(([key, value]) => ` - ${key}: ${String(value)}`);
431
+ const figures = evidence.figures ?? [];
432
+ return boxed("QDash QCal Evidence", [
433
+ `id ${evidence.id ?? "n/a"}`,
434
+ `source ${evidence.source}`,
435
+ `target ${evidence.target?.id ?? evidence.target?.couplingId ?? evidence.target?.qids ?? "n/a"}`,
436
+ `task ${evidence.task?.name ?? "n/a"} (${evidence.task?.status ?? "unknown"})`,
437
+ "",
438
+ "Metrics",
439
+ ...(metrics.length ? metrics : [" - none"]),
440
+ "",
441
+ `Figures ${figures.length}`,
442
+ ...figures.map((figure, index) => ` - #${index + 1} ${String(figure.role ?? "plot")} ${String(figure.path ?? figure.url ?? "")}`),
443
+ "",
444
+ "Use qcal_evaluate_bundle with details.evidence to evaluate this evidence with pi-qcal.",
445
+ ]).join("\n");
446
+ }
447
+
396
448
  function cleanQuery(values: Record<string, unknown> = {}): Record<string, unknown> {
397
449
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null));
398
450
  }
@@ -2462,6 +2514,105 @@ export default function qdashExtension(pi: ExtensionAPI) {
2462
2514
  },
2463
2515
  });
2464
2516
 
2517
+ pi.registerTool({
2518
+ name: "qdash_build_qcal_evidence",
2519
+ label: "QDash Build QCal Evidence",
2520
+ description: "Build a provider-neutral CalibrationEvidence bundle from a QDash task result for evaluation by pi-qcal.",
2521
+ promptSnippet: "Convert QDash calibration task results into pi-qcal CalibrationEvidence",
2522
+ promptGuidelines: [
2523
+ "Use qdash_build_qcal_evidence before qcal_evaluate_bundle when the user asks to evaluate QDash calibration data with pi-qcal.",
2524
+ "qdash_build_qcal_evidence is read-only and only converts QDash data into a generic evidence bundle; it does not evaluate, execute, commit, or apply calibration parameters.",
2525
+ "After qdash_build_qcal_evidence, pass details.evidence to qcal_evaluate_bundle and treat the result as advisory evidence.",
2526
+ ],
2527
+ parameters: Type.Object({
2528
+ ...connectionParams,
2529
+ taskId: Type.String(),
2530
+ includeFigures: Type.Optional(Type.Boolean({ description: "Embed task image figures as data URLs. Defaults to true." })),
2531
+ includeFigureAnalysis: Type.Optional(Type.Boolean({ description: "Analyze Plotly JSON figures and include summaries/warnings in notes. Defaults to true." })),
2532
+ maxFigures: Type.Optional(Type.Number({ description: "Maximum image figures to embed. Defaults to 1." })),
2533
+ includeRawTaskResult: Type.Optional(Type.Boolean({ description: "Include the full raw task result in resultJson. Defaults to false." })),
2534
+ }),
2535
+ async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean }) {
2536
+ const client = await makeClient(params);
2537
+ const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
2538
+ const figures: Array<Record<string, unknown>> = [];
2539
+ const notes: string[] = [];
2540
+ const figurePaths = Array.isArray(task.figure_path) ? task.figure_path.filter((item): item is string => typeof item === "string") : [];
2541
+ const jsonFigurePaths = Array.isArray(task.json_figure_path) ? task.json_figure_path.filter((item): item is string => typeof item === "string") : [];
2542
+
2543
+ if (params.includeFigureAnalysis !== false) {
2544
+ const limit = Math.min(jsonFigurePaths.length, params.maxFigures ?? 1);
2545
+ for (let index = 0; index < limit; index += 1) {
2546
+ try {
2547
+ const file: TaskResultFigureFile = await client.getTaskResultFigure(params.taskId, { index, preferJson: true });
2548
+ const figure = JSON.parse(Buffer.from(file.data).toString("utf8")) as unknown;
2549
+ const analysis = analyzePlotlyFigure(file.path ?? jsonFigurePaths[index] ?? "", figure, params.taskId);
2550
+ notes.push(`Figure JSON analysis #${index + 1}:`, ...analysis.summaries.map((line) => `- ${line}`));
2551
+ if (analysis.warnings.length) notes.push("Warnings:", ...analysis.warnings.map((line) => `- ${line}`));
2552
+ figures.push({ path: file.path ?? jsonFigurePaths[index], role: "plotly", data: figure });
2553
+ } catch (error) {
2554
+ notes.push(`Figure JSON analysis #${index + 1} failed: ${error instanceof Error ? error.message : String(error)}`);
2555
+ }
2556
+ }
2557
+ }
2558
+
2559
+ if (params.includeFigures !== false) {
2560
+ const limit = Math.min(figurePaths.length, params.maxFigures ?? 1);
2561
+ for (let index = 0; index < limit; index += 1) {
2562
+ try {
2563
+ const file: TaskResultFigureFile = await client.getTaskResultFigure(params.taskId, { index, preferJson: false });
2564
+ const bytes = Buffer.from(file.data);
2565
+ const mediaType = file.mediaType || mediaTypeForPath(file.path ?? figurePaths[index] ?? "");
2566
+ if (mediaType.startsWith("image/")) {
2567
+ figures.push({ path: file.path ?? figurePaths[index], url: `data:${mediaType};base64,${bytes.toString("base64")}`, mimeType: mediaType, role: "plot" });
2568
+ }
2569
+ } catch (error) {
2570
+ notes.push(`Figure image #${index + 1} embedding failed: ${error instanceof Error ? error.message : String(error)}`);
2571
+ }
2572
+ }
2573
+ }
2574
+
2575
+ const couplingId = firstStringField(task, ["coupling_id", "couplingId", "qid"]);
2576
+ const qid = firstStringField(task, ["qid"]);
2577
+ const evidence: CalibrationEvidence = {
2578
+ id: params.taskId,
2579
+ source: "external",
2580
+ target: {
2581
+ kind: couplingId && couplingId.includes("-") ? "coupling" : qid ? "qubit" : "system",
2582
+ id: couplingId ?? qid,
2583
+ couplingId: couplingId && couplingId.includes("-") ? couplingId : undefined,
2584
+ qids: qid && !qid.includes("-") ? [qid] : undefined,
2585
+ chipId: firstStringField(task, ["chip_id", "chipId"]),
2586
+ },
2587
+ task: {
2588
+ name: firstStringField(task, ["task_name", "taskName", "name"]),
2589
+ objective: `Evaluate QDash calibration task ${firstStringField(task, ["task_name", "taskName", "name"]) ?? params.taskId}`,
2590
+ backend: firstStringField(task, ["backend", "chip_id", "chipId"]),
2591
+ startedAt: firstStringField(task, ["start_at", "startAt"]),
2592
+ completedAt: firstStringField(task, ["end_at", "endAt"]),
2593
+ status: firstStringField(task, ["status"]),
2594
+ },
2595
+ metrics: parameterValueRecord(task.output_parameters),
2596
+ parameters: { ...parameterValueRecord(task.input_parameters), ...parameterValueRecord(task.run_parameters) },
2597
+ figures: figures.length ? figures : undefined,
2598
+ notes: [
2599
+ typeof task.message === "string" ? `Task message: ${task.message}` : undefined,
2600
+ notes.length ? notes.join("\n") : undefined,
2601
+ ].filter(Boolean).join("\n\n") || undefined,
2602
+ resultJson: params.includeRawTaskResult ? task : {
2603
+ task_id: task.task_id,
2604
+ execution_id: task.execution_id,
2605
+ flow_name: task.flow_name,
2606
+ figure_path: figurePaths,
2607
+ json_figure_path: jsonFigurePaths,
2608
+ links: task._links,
2609
+ },
2610
+ };
2611
+
2612
+ return toTextToolResult(qcalEvidenceText(evidence), { evidence }, { tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence });
2613
+ },
2614
+ });
2615
+
2465
2616
  registerQueryTool({
2466
2617
  name: "qdash_get_provenance_stats",
2467
2618
  label: "QDash Provenance Stats",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oqtopus-team/pi-qdash",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Pi extension for querying QDash via @oqtopus-team/qdash-client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,7 +22,7 @@ Use the pi-qdash tools instead of scraping the UI or hand-writing auth headers.
22
22
  - `qdash_list_executions`, `qdash_get_execution`
23
23
  - `qdash_list_ai_reviews`, `qdash_get_provenance_stats`
24
24
  - `qdash_list_forum_posts`, `qdash_get_forum_post`, `qdash_list_forum_replies`
25
- - `qdash_get_figure`, `qdash_get_task_figures`, `qdash_recent_calibration_figure`, `qdash_analyze_figure_json`
25
+ - `qdash_get_figure`, `qdash_get_task_figures`, `qdash_recent_calibration_figure`, `qdash_analyze_figure_json`, `qdash_build_qcal_evidence`
26
26
  - `qdash_create_forum_evidence_reply` for confirmed evidence curation replies with visible task figures
27
27
  3. Use harness overview tools for status and triage:
28
28
  - `qdash_investigate` for natural-language requests to investigate or compare recent calibration, target history, figures, failures, issues, and Forum context
@@ -108,4 +108,5 @@ When the user wants to preserve an investigated observation in QDash forum/notes
108
108
  - Use `qdash_compare_calibration` when the user asks what changed between recent calibration experiments; treat differences as evidence, not automatic approval to commit/apply parameters.
109
109
  - Use `qdash_recent_calibration_figure` when the user asks to see recent experiment images without a task ID.
110
110
  - Use `qdash_analyze_figure_json` for read-only numeric summaries of Plotly JSON calibration figures; keep domain-specific interpretation in the matching skill.
111
+ - Use `qdash_build_qcal_evidence` when the user wants to evaluate a QDash calibration task with pi-qcal. It only converts QDash data into provider-neutral `CalibrationEvidence`; pass `details.evidence` to `qcal_evaluate_bundle` for the actual evaluation.
111
112
  - Prefer summarizing large responses with counts, IDs, time ranges, and notable values.