@oqtopus-team/pi-qdash 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/extensions/lib/forum-images.ts +82 -0
- package/extensions/lib/investigation-context.ts +169 -0
- package/extensions/lib/timeseries-analysis.ts +609 -0
- package/extensions/lib/write-gate.ts +1 -0
- package/extensions/qdash.ts +222 -52
- package/extensions/tools/timeseries.ts +211 -0
- package/package.json +7 -3
- package/skills/qdash/SKILL.md +48 -7
- package/skills/qdash-calibration-agent/SKILL.md +4 -1
- package/skills/qdash-timeseries-diagnosis/SKILL.md +155 -0
- package/skills/qdash-two-qubit-calibration-diagnosis/SKILL.md +4 -3
package/extensions/qdash.ts
CHANGED
|
@@ -4,19 +4,28 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
QDashClient,
|
|
9
|
+
defaultConfigPath,
|
|
10
|
+
type CooldownResponse,
|
|
11
|
+
type DownloadedFile,
|
|
12
|
+
type TaskResultFigureOptions,
|
|
13
|
+
} from "@oqtopus-team/qdash-client";
|
|
8
14
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
15
|
import { Type } from "typebox";
|
|
10
16
|
|
|
11
17
|
import { analyzePlotlyFigure, figureAnalysisText } from "./lib/figure-analysis.js";
|
|
12
18
|
import { fetchFigureDetails, figureComponent, figureResultText, mediaTypeForPath, type FigureDetails } from "./lib/figures.js";
|
|
19
|
+
import { forumImageReplyContent, forumImageReplyPreview, loadLocalForumImages, type LocalForumImage } from "./lib/forum-images.js";
|
|
13
20
|
import { forumDetailComponent, forumDetailLines, forumListLines, forumPostTitle, forumPostsFromPayload } from "./lib/forum-render.js";
|
|
21
|
+
import { investigationContextSummary, validateInvestigationContext, type TimeseriesInvestigationContext } from "./lib/investigation-context.js";
|
|
14
22
|
import { qdashObjectLinks, qdashWebBaseUrl, qdashWebUrl, safeConfig, withQDashLinks } from "./lib/links.js";
|
|
15
23
|
import { arrayFromPayload, compactItems, firstNumber, firstString, formatItem, payloadTotal, statusIcon } from "./lib/payload.js";
|
|
16
24
|
import { toTextToolResult, toToolResult } from "./lib/results.js";
|
|
17
25
|
import { ansi, boxLinesToWidth, boxed, formatNumber, textComponent } from "./lib/render.js";
|
|
18
26
|
import { plotSeriesLines, timeseriesPlotComponent, timeseriesPoints } from "./lib/timeseries-plot.js";
|
|
19
27
|
import { installQDashWriteGate } from "./lib/write-gate.js";
|
|
28
|
+
import { registerTimeseriesComparisonTool } from "./tools/timeseries.js";
|
|
20
29
|
import { analyzeWiringMarkdown, wiringInsightLines, type WiringInsights } from "./lib/wiring-analysis.js";
|
|
21
30
|
|
|
22
31
|
type QDashQueryParams = {
|
|
@@ -131,6 +140,7 @@ type QDashContextState = {
|
|
|
131
140
|
taskName?: string;
|
|
132
141
|
lastExecutionId?: string;
|
|
133
142
|
lastTaskId?: string;
|
|
143
|
+
investigation?: TimeseriesInvestigationContext;
|
|
134
144
|
};
|
|
135
145
|
|
|
136
146
|
const CONTEXT_ENTRY_TYPE = "qdash-context";
|
|
@@ -151,7 +161,8 @@ function contextSummary(): string {
|
|
|
151
161
|
const chip = currentContext.chipId ?? "auto-chip";
|
|
152
162
|
const session = currentContext.agentSessionId ? ` session:${shortId(currentContext.agentSessionId)}` : "";
|
|
153
163
|
const target = currentContext.qid ? ` q${currentContext.qid}` : currentContext.couplingId ? ` c${currentContext.couplingId}` : "";
|
|
154
|
-
|
|
164
|
+
const investigation = currentContext.investigation?.name ? ` investigation:${currentContext.investigation.name}` : "";
|
|
165
|
+
return `qdash ${profile} ${chip}${target}${session}${investigation}`;
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
function contextStatusLine(theme?: Theme): string {
|
|
@@ -179,8 +190,16 @@ function contextStatusLine(theme?: Theme): string {
|
|
|
179
190
|
function isQDashContextState(value: unknown): value is QDashContextState {
|
|
180
191
|
if (!value || typeof value !== "object") return false;
|
|
181
192
|
const context = value as QDashContextState;
|
|
182
|
-
|
|
193
|
+
const stringsValid = [context.profile, context.chipId, context.agentSessionId, context.qid, context.couplingId, context.taskName, context.lastExecutionId, context.lastTaskId]
|
|
183
194
|
.every((item) => item === undefined || typeof item === "string");
|
|
195
|
+
if (!stringsValid) return false;
|
|
196
|
+
if (context.investigation === undefined) return true;
|
|
197
|
+
try {
|
|
198
|
+
validateInvestigationContext(context.investigation);
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
184
203
|
}
|
|
185
204
|
|
|
186
205
|
function loadGlobalContext(): QDashContextState {
|
|
@@ -194,8 +213,9 @@ function loadGlobalContext(): QDashContextState {
|
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
function saveGlobalContext(context: QDashContextState): void {
|
|
216
|
+
const { investigation: _investigation, ...globalContext } = context;
|
|
197
217
|
mkdirSync(dirname(GLOBAL_CONTEXT_PATH), { recursive: true });
|
|
198
|
-
writeFileSync(GLOBAL_CONTEXT_PATH, `${JSON.stringify(
|
|
218
|
+
writeFileSync(GLOBAL_CONTEXT_PATH, `${JSON.stringify(globalContext, null, 2)}\n`, "utf8");
|
|
199
219
|
}
|
|
200
220
|
|
|
201
221
|
function adoptContextFromToolInput(input: unknown): boolean {
|
|
@@ -383,6 +403,9 @@ async function defaultChipId(client: QDashClient, chipId?: string): Promise<stri
|
|
|
383
403
|
return chipId ?? currentContext.chipId ?? client.getDefaultChipId();
|
|
384
404
|
}
|
|
385
405
|
|
|
406
|
+
// Keep arbitrary GET access isolated for qdash_raw_get and legacy filters that are
|
|
407
|
+
// not represented in the generated OpenAPI surface. Normal operations should use
|
|
408
|
+
// QDashClient's high-level methods or client.api so upgrades remain type-checked.
|
|
386
409
|
async function rawGet<T>(client: QDashClient, path: string, query?: Record<string, unknown>): Promise<T> {
|
|
387
410
|
return (client as unknown as { get<T>(path: string, query?: Record<string, unknown>): Promise<T> }).get(path, cleanQuery(query));
|
|
388
411
|
}
|
|
@@ -458,19 +481,6 @@ function requireValue(value: string | undefined, name: string): string {
|
|
|
458
481
|
return value;
|
|
459
482
|
}
|
|
460
483
|
|
|
461
|
-
type CooldownRecord = {
|
|
462
|
-
cooldown_id: string;
|
|
463
|
-
cryo_id: string;
|
|
464
|
-
description?: string;
|
|
465
|
-
started_at: string;
|
|
466
|
-
ended_at?: string | null;
|
|
467
|
-
chip_ids?: string[];
|
|
468
|
-
wiring_info?: string;
|
|
469
|
-
wiring_blocks?: Record<string, unknown>[];
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
type CooldownListResponse = { cooldowns?: CooldownRecord[] };
|
|
473
|
-
|
|
474
484
|
type CooldownWiringParams = {
|
|
475
485
|
profile?: string;
|
|
476
486
|
configPath?: string;
|
|
@@ -488,7 +498,7 @@ type WiringInsightParams = CooldownWiringParams & {
|
|
|
488
498
|
color?: boolean;
|
|
489
499
|
};
|
|
490
500
|
|
|
491
|
-
function newestCooldown(cooldowns:
|
|
501
|
+
function newestCooldown(cooldowns: CooldownResponse[]): CooldownResponse | undefined {
|
|
492
502
|
return [...cooldowns].sort((a, b) => {
|
|
493
503
|
const activeDifference = Number(a.ended_at != null) - Number(b.ended_at != null);
|
|
494
504
|
if (activeDifference !== 0) return activeDifference;
|
|
@@ -500,15 +510,15 @@ async function resolveCooldownWiring(params: CooldownWiringParams) {
|
|
|
500
510
|
params = applyQDashContext(params);
|
|
501
511
|
const client = await makeClient(params);
|
|
502
512
|
let selection: string;
|
|
503
|
-
let cooldown:
|
|
513
|
+
let cooldown: CooldownResponse;
|
|
504
514
|
let resolvedChipId = params.chipId ?? currentContext.chipId;
|
|
505
515
|
|
|
506
516
|
if (params.cooldownId) {
|
|
507
|
-
cooldown = await
|
|
517
|
+
cooldown = await client.api.getCooldown(params.cooldownId);
|
|
508
518
|
selection = "explicit cooldownId";
|
|
509
519
|
} else {
|
|
510
520
|
resolvedChipId = params.chipId ?? (params.cryoId ? undefined : await defaultChipId(client));
|
|
511
|
-
const response = await
|
|
521
|
+
const response = await client.api.listCooldowns({
|
|
512
522
|
cryo_id: params.cryoId,
|
|
513
523
|
chip_id: resolvedChipId,
|
|
514
524
|
});
|
|
@@ -517,7 +527,7 @@ async function resolveCooldownWiring(params: CooldownWiringParams) {
|
|
|
517
527
|
const scope = params.cryoId ? `cryo ${params.cryoId}` : `chip ${resolvedChipId}`;
|
|
518
528
|
throw new Error(`No cooldown containing wiring information was found for ${scope}`);
|
|
519
529
|
}
|
|
520
|
-
cooldown = await
|
|
530
|
+
cooldown = await client.api.getCooldown(selected.cooldown_id);
|
|
521
531
|
selection = params.cryoId ? "active/newest cooldown for cryo" : "active/newest cooldown for chip";
|
|
522
532
|
}
|
|
523
533
|
|
|
@@ -525,14 +535,14 @@ async function resolveCooldownWiring(params: CooldownWiringParams) {
|
|
|
525
535
|
const wiringInfo = typeof cooldown.wiring_info === "string" ? cooldown.wiring_info.trim() : "";
|
|
526
536
|
const includeBlocks = params.includeBlocks === true || !wiringInfo;
|
|
527
537
|
const history = params.includeHistory
|
|
528
|
-
? await
|
|
538
|
+
? await client.api.listCooldownWiringEvents(cooldown.cooldown_id, {
|
|
529
539
|
limit: params.historyLimit ?? 20,
|
|
530
540
|
skip: 0,
|
|
531
541
|
})
|
|
532
542
|
: undefined;
|
|
533
543
|
let cryostat: unknown;
|
|
534
544
|
try {
|
|
535
|
-
cryostat = await
|
|
545
|
+
cryostat = await client.api.getCryostat(cooldown.cryo_id);
|
|
536
546
|
} catch {
|
|
537
547
|
// Wiring remains useful on servers where cryostat detail is not accessible.
|
|
538
548
|
}
|
|
@@ -580,9 +590,9 @@ async function buildDashboard(params: { profile?: string; configPath?: string; u
|
|
|
580
590
|
const chipId = await defaultChipId(client, params.chipId);
|
|
581
591
|
const [chips, openIssues, recentExecutions, failedTaskResults, provenanceStats] = await Promise.allSettled([
|
|
582
592
|
client.listChips(),
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
client.api.listIssues({ is_closed: false, limit }),
|
|
594
|
+
client.api.listExecutions({ chip_id: chipId, limit }),
|
|
595
|
+
client.api.listTaskResults({ chip_id: chipId, status: "failed", limit }),
|
|
586
596
|
client.getProvenanceStats(),
|
|
587
597
|
]);
|
|
588
598
|
const value = <T>(result: PromiseSettledResult<T>): T | { error: string } => result.status === "fulfilled" ? result.value : { error: result.reason instanceof Error ? result.reason.message : String(result.reason) };
|
|
@@ -724,7 +734,7 @@ async function buildRecentCalibrationSummary(params: { profile?: string; configP
|
|
|
724
734
|
const chipId = await defaultChipId(client, params.chipId);
|
|
725
735
|
const end = new Date();
|
|
726
736
|
const start = new Date(end.getTime() - (params.withinHours ?? 24) * 3600_000);
|
|
727
|
-
const payload = await
|
|
737
|
+
const payload = await client.api.listTaskResults({ chip_id: chipId, start_from: start.toISOString(), start_to: end.toISOString(), limit: params.limit ?? 30 });
|
|
728
738
|
const items = arrayFromPayload(payload).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
|
|
729
739
|
const byTarget = new Map<string, Record<string, unknown>[]>();
|
|
730
740
|
for (const item of items) {
|
|
@@ -925,7 +935,7 @@ async function buildTargetOperationsReport(params: { profile?: string; configPat
|
|
|
925
935
|
const [latestTarget, results, issues, forumPosts] = await Promise.all([
|
|
926
936
|
kind === "qubit" ? client.getChipQubit(chipId, id) : client.getChipCoupling(chipId, id),
|
|
927
937
|
client.listTaskResults({ chipId, qid, couplingId, startAt: start.toISOString(), endAt: end.toISOString(), limit }),
|
|
928
|
-
|
|
938
|
+
client.api.listIssues({ is_closed: false, limit: 100 }),
|
|
929
939
|
client.listForumPosts({ chipId, status: "open", targetType: kind, targetId: id, limit }),
|
|
930
940
|
]);
|
|
931
941
|
const recentResults = arrayFromPayload(results).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
|
|
@@ -1109,7 +1119,7 @@ async function buildCalibrationValidation(params: { profile?: string; configPath
|
|
|
1109
1119
|
const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
|
|
1110
1120
|
const status = firstString(task, ["status"])?.toLowerCase() ?? "unknown";
|
|
1111
1121
|
const figurePaths = taskFigurePaths(task, 20);
|
|
1112
|
-
const issuesPayload = await
|
|
1122
|
+
const issuesPayload = await client.api.getTaskResultIssues(params.taskId).catch(() => []);
|
|
1113
1123
|
const issues = arrayFromPayload(issuesPayload).filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object");
|
|
1114
1124
|
const afterExecutionId = firstString(task, ["execution_id", "executionId"]);
|
|
1115
1125
|
const comparison = params.beforeExecutionId && afterExecutionId
|
|
@@ -1242,10 +1252,10 @@ async function buildDashboardInsights(params: { profile?: string; configPath?: s
|
|
|
1242
1252
|
const start = new Date(end.getTime() - (params.withinHours ?? 168) * 3600_000);
|
|
1243
1253
|
const [forums, issues, failed, metrics, aiInsights] = await Promise.allSettled([
|
|
1244
1254
|
client.listForumPosts({ chipId, status: "open", limit }),
|
|
1245
|
-
|
|
1246
|
-
|
|
1255
|
+
client.api.listIssues({ is_closed: false, limit }),
|
|
1256
|
+
client.api.listTaskResults({ chip_id: chipId, status: "failed", start_from: start.toISOString(), start_to: end.toISOString(), limit }),
|
|
1247
1257
|
client.getChipMetrics(chipId),
|
|
1248
|
-
|
|
1258
|
+
client.api.getDashboardAiInsights(chipId, { latest_only: true, start_at: start.toISOString(), end_at: end.toISOString() }),
|
|
1249
1259
|
]);
|
|
1250
1260
|
const value = <T>(result: PromiseSettledResult<T>): T | undefined => result.status === "fulfilled" ? result.value : undefined;
|
|
1251
1261
|
const insightMap = new Map<string, DashboardInsight>();
|
|
@@ -1340,6 +1350,56 @@ function taskFigurePaths(task: Record<string, unknown>, maxFigures: number): str
|
|
|
1340
1350
|
return Array.isArray(paths) ? paths.filter((item): item is string => typeof item === "string").slice(0, maxFigures) : [];
|
|
1341
1351
|
}
|
|
1342
1352
|
|
|
1353
|
+
type ForumImageReplyParams = {
|
|
1354
|
+
parentPostId: string;
|
|
1355
|
+
imagePaths: string[];
|
|
1356
|
+
interpretation: string;
|
|
1357
|
+
title?: string;
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
type PreparedForumImageReply = {
|
|
1361
|
+
parent: Record<string, unknown>;
|
|
1362
|
+
images: LocalForumImage[];
|
|
1363
|
+
title: string;
|
|
1364
|
+
preview: string;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
async function prepareForumImageReply(client: QDashClient, params: ForumImageReplyParams): Promise<PreparedForumImageReply> {
|
|
1368
|
+
const parent = await client.getForumPost(params.parentPostId) as unknown as Record<string, unknown>;
|
|
1369
|
+
const images = loadLocalForumImages(params.imagePaths);
|
|
1370
|
+
const title = params.title ?? `${new Date().toISOString().slice(0, 10)} 追加分析画像`;
|
|
1371
|
+
const preview = forumImageReplyPreview(title, images, params.interpretation);
|
|
1372
|
+
return { parent, images, title, preview };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
async function createForumImageReply(client: QDashClient, params: ForumImageReplyParams, prepared: PreparedForumImageReply) {
|
|
1376
|
+
const uploaded = [];
|
|
1377
|
+
for (const image of prepared.images) {
|
|
1378
|
+
const result = await client.uploadForumImage(image.blob, image.filename);
|
|
1379
|
+
uploaded.push({ ...image, url: result.url });
|
|
1380
|
+
}
|
|
1381
|
+
const content = forumImageReplyContent(prepared.title, uploaded, params.interpretation);
|
|
1382
|
+
const parent = prepared.parent;
|
|
1383
|
+
const request = {
|
|
1384
|
+
category: firstString(parent, ["category"]) ?? "other",
|
|
1385
|
+
title: null,
|
|
1386
|
+
content,
|
|
1387
|
+
content_blocks: [],
|
|
1388
|
+
parent_id: params.parentPostId,
|
|
1389
|
+
chip_id: firstString(parent, ["chip_id", "chipId"]),
|
|
1390
|
+
target_type: firstString(parent, ["target_type", "targetType"]),
|
|
1391
|
+
target_id: firstString(parent, ["target_id", "targetId"]),
|
|
1392
|
+
cooldown_id: firstString(parent, ["cooldown_id", "cooldownId"]),
|
|
1393
|
+
status: firstString(parent, ["status"]) ?? "open",
|
|
1394
|
+
};
|
|
1395
|
+
const reply = await client.createForumPost(request as never);
|
|
1396
|
+
return {
|
|
1397
|
+
reply,
|
|
1398
|
+
content,
|
|
1399
|
+
images: uploaded.map(({ blob: _blob, ...image }) => image),
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1343
1403
|
async function buildForumEvidenceReply(client: QDashClient, params: ForumEvidenceReplyParams) {
|
|
1344
1404
|
const [parent, task] = await Promise.all([
|
|
1345
1405
|
client.getForumPost(params.parentPostId) as unknown as Promise<Record<string, unknown>>,
|
|
@@ -1421,19 +1481,19 @@ async function executeQuery(params: QDashQueryParams) {
|
|
|
1421
1481
|
case "chip_qubit": return client.getChipQubit(await defaultChipId(client, params.chipId), requireValue(params.qid, "qid"));
|
|
1422
1482
|
case "chip_couplings": return client.listChipCouplings(await defaultChipId(client, params.chipId), { limit: params.limit, offset: params.offset });
|
|
1423
1483
|
case "chip_coupling": return client.getChipCoupling(await defaultChipId(client, params.chipId), requireValue(params.couplingId, "couplingId"));
|
|
1424
|
-
case "cryostats": return
|
|
1425
|
-
case "cryostat": return
|
|
1426
|
-
case "cooldowns": return
|
|
1427
|
-
case "cooldown": return
|
|
1428
|
-
case "cooldown_wiring_events": return
|
|
1484
|
+
case "cryostats": return client.api.listCryostats();
|
|
1485
|
+
case "cryostat": return client.api.getCryostat(requireValue(params.cryoId, "cryoId"));
|
|
1486
|
+
case "cooldowns": return client.api.listCooldowns({ cryo_id: params.cryoId, chip_id: params.chipId });
|
|
1487
|
+
case "cooldown": return client.api.getCooldown(requireValue(params.cooldownId, "cooldownId"));
|
|
1488
|
+
case "cooldown_wiring_events": return client.api.listCooldownWiringEvents(requireValue(params.cooldownId, "cooldownId"), { limit: params.limit, skip: params.skip });
|
|
1429
1489
|
case "timeseries": return client.getTaskResultsTimeseries({ chipId: await defaultChipId(client, params.chipId), parameter: requireValue(params.parameter, "parameter"), tag: params.tag, qid: params.qid, startAt: requireValue(params.startAt, "startAt"), endAt: requireValue(params.endAt, "endAt") });
|
|
1430
1490
|
case "task_results": return rawGet(client, "/task-results", { chip_id: params.chipId, task_name: params.taskName, qid: params.qid, coupling_id: params.couplingId, execution_id: params.executionId, username: params.username, status: params.status, start_from: params.startFrom ?? params.startAt, start_to: params.startTo ?? params.endAt, message_contains: params.messageContains, limit: params.limit, skip: params.skip });
|
|
1431
1491
|
case "task_result": return client.getTaskResult(requireValue(params.taskId, "taskId"));
|
|
1432
|
-
case "task_note": return
|
|
1433
|
-
case "task_result_issues": return
|
|
1434
|
-
case "qubit_latest": return
|
|
1492
|
+
case "task_note": return client.api.getTaskNote(requireValue(params.taskId, "taskId"));
|
|
1493
|
+
case "task_result_issues": return client.api.getTaskResultIssues(requireValue(params.taskId, "taskId"));
|
|
1494
|
+
case "qubit_latest": return client.api.getLatestQubitTaskResults({ chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task") });
|
|
1435
1495
|
case "qubit_history": return rawGet(client, `/task-results/qubits/${pathPart(requireValue(params.qid, "qid"))}/history`, { chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task"), date: requireValue(params.date, "date") });
|
|
1436
|
-
case "coupling_latest": return
|
|
1496
|
+
case "coupling_latest": return client.api.getLatestCouplingTaskResults({ chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task") });
|
|
1437
1497
|
case "coupling_history": return rawGet(client, `/task-results/couplings/${pathPart(requireValue(params.couplingId, "couplingId"))}/history`, { chip_id: await defaultChipId(client, params.chipId), task: requireValue(params.task ?? params.taskName, "task"), date: requireValue(params.date, "date") });
|
|
1438
1498
|
case "tasks": return client.listTasks(params.backend);
|
|
1439
1499
|
case "task_knowledge": return params.taskName ? client.getTaskKnowledge(params.taskName) : client.listTaskKnowledge();
|
|
@@ -1443,23 +1503,26 @@ async function executeQuery(params: QDashQueryParams) {
|
|
|
1443
1503
|
case "files_tree": return client.getFilesTree();
|
|
1444
1504
|
case "file_content": return client.getFileContent(requireValue(params.path, "path"));
|
|
1445
1505
|
case "git_status": return client.getGitStatus();
|
|
1446
|
-
case "issues": return
|
|
1447
|
-
case "issue_knowledge": return
|
|
1506
|
+
case "issues": return client.api.listIssues({ task_id: params.taskId, is_closed: params.isClosed, limit: params.limit, skip: params.skip });
|
|
1507
|
+
case "issue_knowledge": return client.api.listIssueKnowledge({ status: params.status, task_name: params.taskName, limit: params.limit, skip: params.skip });
|
|
1448
1508
|
case "flows": return client.listFlows();
|
|
1449
1509
|
case "flow": return client.getFlow(requireValue(params.flowName, "flowName"));
|
|
1450
1510
|
case "flow_templates": return client.listFlowTemplates();
|
|
1451
1511
|
case "flow_template": return client.getFlowTemplate(requireValue(params.templateId, "templateId"));
|
|
1452
|
-
case "flow_helper_files": return
|
|
1453
|
-
case "flow_helper_file": return
|
|
1512
|
+
case "flow_helper_files": return client.api.listFlowHelperFiles();
|
|
1513
|
+
case "flow_helper_file": return client.api.getFlowHelperFile(requireValue(params.filename, "filename"));
|
|
1454
1514
|
case "executions": return rawGet(client, "/executions", { chip_id: params.chipId, flow_name: params.flowName, status: params.status, skip: params.skip, limit: params.limit });
|
|
1455
1515
|
case "execution": return client.getExecution(requireValue(params.executionId, "executionId"));
|
|
1456
|
-
case "ai_reviews": return
|
|
1457
|
-
case "ai_review_runs": return
|
|
1458
|
-
case "ai_review_run": return
|
|
1516
|
+
case "ai_reviews": return client.api.listTaskResultAiReviews({ chip_id: params.chipId, task_name: params.taskName, status: params.status, decision: params.decision, latest_only: params.latestOnly, skip: params.skip, limit: params.limit });
|
|
1517
|
+
case "ai_review_runs": return client.api.listTaskResultAiReviewRuns({ chip_id: params.chipId, task_name: params.taskName, skip: params.skip, limit: params.limit });
|
|
1518
|
+
case "ai_review_run": return client.api.getTaskResultAiReviewRun(requireValue(params.reviewRunId, "reviewRunId"));
|
|
1459
1519
|
case "forum_posts": return client.listForumPosts({ status: params.status, chipId: params.chipId, limit: params.limit, skip: params.skip });
|
|
1460
1520
|
case "provenance_stats": return client.getProvenanceStats();
|
|
1461
|
-
case "provenance_history": return
|
|
1462
|
-
case "provenance_changes":
|
|
1521
|
+
case "provenance_history": return client.api.getParameterHistory({ parameter_name: requireValue(params.parameterName ?? params.parameter, "parameterName"), qid: requireValue(params.qid, "qid"), limit: params.limit });
|
|
1522
|
+
case "provenance_changes": {
|
|
1523
|
+
const parameterName = params.parameterName ?? params.parameter;
|
|
1524
|
+
return client.getRecentChanges({ parameterNames: parameterName ? [parameterName] : undefined, withinHours: params.withinHours, limit: params.limit });
|
|
1525
|
+
}
|
|
1463
1526
|
case "provenance_lineage": return client.getProvenanceLineage(requireValue(params.entityId, "entityId"));
|
|
1464
1527
|
case "provenance_impact": return client.getProvenanceImpact(requireValue(params.entityId, "entityId"));
|
|
1465
1528
|
}
|
|
@@ -1728,6 +1791,12 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
1728
1791
|
},
|
|
1729
1792
|
});
|
|
1730
1793
|
|
|
1794
|
+
registerTimeseriesComparisonTool(pi, {
|
|
1795
|
+
makeClient,
|
|
1796
|
+
defaultChipId,
|
|
1797
|
+
investigationContext: () => currentContext.investigation,
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1731
1800
|
registerQueryTool({
|
|
1732
1801
|
name: "qdash_list_task_results",
|
|
1733
1802
|
label: "QDash List Task Results",
|
|
@@ -2239,6 +2308,65 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2239
2308
|
},
|
|
2240
2309
|
});
|
|
2241
2310
|
|
|
2311
|
+
pi.registerTool({
|
|
2312
|
+
name: "qdash_preview_forum_image_reply",
|
|
2313
|
+
label: "QDash Preview Forum Image Reply",
|
|
2314
|
+
description: "Preview a QDash forum reply that will upload and embed locally generated analysis images. Read-only; no image or post is created.",
|
|
2315
|
+
promptSnippet: "Preview local analysis images and forum reply content before uploading",
|
|
2316
|
+
promptGuidelines: [
|
|
2317
|
+
"Use qdash_preview_forum_image_reply before uploading locally generated analysis images to a forum thread.",
|
|
2318
|
+
"Show the image paths and reply content, then require explicit confirmation before publishing.",
|
|
2319
|
+
],
|
|
2320
|
+
parameters: Type.Object({
|
|
2321
|
+
...connectionParams,
|
|
2322
|
+
parentPostId: Type.String(),
|
|
2323
|
+
imagePaths: Type.Array(Type.String(), { minItems: 1, maxItems: 8 }),
|
|
2324
|
+
interpretation: Type.String({ description: "Human-readable interpretation or context shown after the images." }),
|
|
2325
|
+
title: Type.Optional(Type.String()),
|
|
2326
|
+
}),
|
|
2327
|
+
async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; parentPostId: string; imagePaths: string[]; interpretation: string; title?: string }) {
|
|
2328
|
+
const client = await makeClient(params);
|
|
2329
|
+
const prepared = await prepareForumImageReply(client, params);
|
|
2330
|
+
return toTextToolResult(prepared.preview, {
|
|
2331
|
+
preview: prepared.preview,
|
|
2332
|
+
parent: prepared.parent,
|
|
2333
|
+
images: prepared.images.map(({ blob: _blob, ...image }) => image),
|
|
2334
|
+
}, { tool: "qdash_preview_forum_image_reply" });
|
|
2335
|
+
},
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
pi.registerTool({
|
|
2339
|
+
name: "qdash_create_forum_image_reply",
|
|
2340
|
+
label: "QDash Create Forum Image Reply",
|
|
2341
|
+
description: "Upload locally generated analysis images and create a QDash forum reply embedding them. This is a write operation and requires confirmation.",
|
|
2342
|
+
promptSnippet: "Upload local analysis images and create a forum reply after explicit confirmation",
|
|
2343
|
+
promptGuidelines: [
|
|
2344
|
+
"Use qdash_create_forum_image_reply only after previewing the exact image paths and reply content with qdash_preview_forum_image_reply.",
|
|
2345
|
+
"Require explicit confirmation before uploading images or creating the reply.",
|
|
2346
|
+
],
|
|
2347
|
+
parameters: Type.Object({
|
|
2348
|
+
...connectionParams,
|
|
2349
|
+
parentPostId: Type.String(),
|
|
2350
|
+
imagePaths: Type.Array(Type.String(), { minItems: 1, maxItems: 8 }),
|
|
2351
|
+
interpretation: Type.String({ description: "Human-readable interpretation or context shown after the images." }),
|
|
2352
|
+
title: Type.Optional(Type.String()),
|
|
2353
|
+
confirmWrite: Type.Optional(Type.Boolean()),
|
|
2354
|
+
}),
|
|
2355
|
+
async execute(_toolCallId, params: ConfirmableParams & { profile?: string; configPath?: string; useEnv?: boolean; parentPostId: string; imagePaths: string[]; interpretation: string; title?: string }, _signal, _onUpdate, ctx) {
|
|
2356
|
+
const client = await makeClient(params);
|
|
2357
|
+
const prepared = await prepareForumImageReply(client, params);
|
|
2358
|
+
if (!params.confirmWrite) {
|
|
2359
|
+
const confirmed = ctx.hasUI && await ctx.ui.confirm("Upload images and create QDash forum reply?", prepared.preview.slice(0, 1800));
|
|
2360
|
+
if (!confirmed) throw new Error("qdash_create_forum_image_reply requires explicit confirmation");
|
|
2361
|
+
}
|
|
2362
|
+
const created = await createForumImageReply(client, params, prepared);
|
|
2363
|
+
const postId = firstString(created.reply as unknown as Record<string, unknown>, ["id"]);
|
|
2364
|
+
const parentUrl = qdashWebUrl(client, `/forum/posts/${encodeURIComponent(params.parentPostId)}`);
|
|
2365
|
+
const text = [`created forum image reply${postId ? ` ${postId}` : ""}`, `parent ${parentUrl}`, "", created.content].join("\n");
|
|
2366
|
+
return toTextToolResult(text, created, { tool: "qdash_create_forum_image_reply", url: parentUrl });
|
|
2367
|
+
},
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2242
2370
|
pi.registerTool({
|
|
2243
2371
|
name: "qdash_create_forum_evidence_reply",
|
|
2244
2372
|
label: "QDash Create Forum Evidence Reply",
|
|
@@ -3067,8 +3195,49 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
3067
3195
|
},
|
|
3068
3196
|
});
|
|
3069
3197
|
|
|
3198
|
+
pi.registerCommand("qdash-investigation-setup", {
|
|
3199
|
+
description: "Create or edit a reusable generic timeseries investigation context; accepts optional JSON",
|
|
3200
|
+
handler: async (args, ctx) => {
|
|
3201
|
+
const template: TimeseriesInvestigationContext = currentContext.investigation ?? {
|
|
3202
|
+
name: "timeseries-investigation",
|
|
3203
|
+
timezoneOffsetMinutes: 540,
|
|
3204
|
+
qdashSeries: currentContext.qid ? [
|
|
3205
|
+
{ parameter: "metric_a", qid: currentContext.qid },
|
|
3206
|
+
{ parameter: "metric_b", qid: currentContext.qid },
|
|
3207
|
+
] : [
|
|
3208
|
+
{ parameter: "metric_a" },
|
|
3209
|
+
{ parameter: "metric_b" },
|
|
3210
|
+
],
|
|
3211
|
+
analysis: { resampleMinutes: 1, detrend: "none", normalize: "none" },
|
|
3212
|
+
};
|
|
3213
|
+
const source = args.trim() || await ctx.ui.editor("QDash investigation context (JSON)", JSON.stringify(template, null, 2));
|
|
3214
|
+
if (!source) return;
|
|
3215
|
+
try {
|
|
3216
|
+
const investigation = validateInvestigationContext(JSON.parse(source));
|
|
3217
|
+
currentContext = { ...currentContext, investigation };
|
|
3218
|
+
persistContext();
|
|
3219
|
+
refreshContextUi(ctx);
|
|
3220
|
+
ctx.ui.setWidget("qdash", ["QDash Investigation", investigationContextSummary(investigation)]);
|
|
3221
|
+
ctx.ui.notify(`Investigation context saved: ${investigationContextSummary(investigation)}`, "info");
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
ctx.ui.notify(`Invalid investigation context: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3224
|
+
}
|
|
3225
|
+
},
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3228
|
+
pi.registerCommand("qdash-clear-investigation", {
|
|
3229
|
+
description: "Clear the reusable timeseries investigation context without changing profile/chip context",
|
|
3230
|
+
handler: async (_args, ctx) => {
|
|
3231
|
+
const { investigation: _investigation, ...rest } = currentContext;
|
|
3232
|
+
currentContext = rest;
|
|
3233
|
+
persistContext();
|
|
3234
|
+
refreshContextUi(ctx);
|
|
3235
|
+
ctx.ui.notify("QDash investigation context cleared", "info");
|
|
3236
|
+
},
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3070
3239
|
pi.registerCommand("qdash-clear-context", {
|
|
3071
|
-
description: "Clear the current QDash profile/chip/agent-session context",
|
|
3240
|
+
description: "Clear the current QDash profile/chip/agent-session/investigation context",
|
|
3072
3241
|
handler: async (_args, ctx) => {
|
|
3073
3242
|
currentContext = {};
|
|
3074
3243
|
persistContext();
|
|
@@ -3180,6 +3349,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
3180
3349
|
`last execution ${currentContext.lastExecutionId ?? "none"}`,
|
|
3181
3350
|
`last task ${currentContext.lastTaskId ?? "none"}`,
|
|
3182
3351
|
`agent session ${currentContext.agentSessionId ?? "none"}`,
|
|
3352
|
+
`investigation ${currentContext.investigation ? investigationContextSummary(currentContext.investigation) : "none"}`,
|
|
3183
3353
|
]);
|
|
3184
3354
|
ctx.ui.notify(contextSummary(), "info");
|
|
3185
3355
|
},
|