@ishlabs/cli 0.10.0 → 0.12.0
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/dist/commands/ask.js +52 -0
- package/dist/commands/chat.js +76 -10
- package/dist/lib/chat-endpoint-formatters.d.ts +37 -2
- package/dist/lib/chat-endpoint-formatters.js +58 -8
- package/dist/lib/chat-endpoint-templates.d.ts +35 -0
- package/dist/lib/chat-endpoint-templates.js +210 -0
- package/dist/lib/docs.js +145 -3
- package/dist/lib/output.js +4 -1
- package/dist/lib/skill-content.d.ts +18 -0
- package/dist/lib/skill-content.js +93 -4
- package/dist/lib/types.d.ts +4 -0
- package/package.json +2 -2
package/dist/commands/ask.js
CHANGED
|
@@ -347,6 +347,7 @@ Examples:
|
|
|
347
347
|
allFlagName: "--all-simulatable",
|
|
348
348
|
allFlagDescription: "Use every simulatable AI profile matching the filters",
|
|
349
349
|
})
|
|
350
|
+
.option("--no-dispatch", "Create the ask in DRAFT status without billing or dispatching the round. Hand the draft id back to the user, then start it with `ish ask dispatch <id>`. Audience flags are still required because the testers are materialized at create time. Mutually exclusive with --wait (nothing to wait for).")
|
|
350
351
|
.option("--wait", "Wait until the first round completes (or errors)")
|
|
351
352
|
.option("--timeout <s>", "Wait timeout in seconds (default 300)")
|
|
352
353
|
.addHelpText("after", `
|
|
@@ -389,6 +390,9 @@ Picks come back with a \`pick_confidence\` (0..1) score per tester when
|
|
|
389
390
|
`)
|
|
390
391
|
.action(async (opts, cmd) => {
|
|
391
392
|
await withClient(cmd, async (client, globals) => {
|
|
393
|
+
if (opts.dispatch === false && opts.wait) {
|
|
394
|
+
throw new Error("--no-dispatch and --wait are incompatible — a draft ask has nothing to wait for. Drop --wait, or run `ish ask dispatch <id> --wait` after the draft is created.");
|
|
395
|
+
}
|
|
392
396
|
const wid = resolveWorkspace(opts.workspace);
|
|
393
397
|
const testerIds = await resolveAudienceProfileIds(client, wid, audienceFlags(opts), { requireSimulatable: true, allFlagName: "--all-simulatable" });
|
|
394
398
|
const round = await buildRoundInput(client, wid, opts, !!globals.quiet);
|
|
@@ -398,6 +402,7 @@ Picks come back with a \`pick_confidence\` (0..1) score per tester when
|
|
|
398
402
|
language: opts.language,
|
|
399
403
|
tester_profile_ids: testerIds,
|
|
400
404
|
first_round: round,
|
|
405
|
+
...(opts.dispatch === false && { dispatch: false }),
|
|
401
406
|
};
|
|
402
407
|
let data = await client.post(`/products/${wid}/asks`, body);
|
|
403
408
|
if (data.id) {
|
|
@@ -414,11 +419,58 @@ Picks come back with a \`pick_confidence\` (0..1) score per tester when
|
|
|
414
419
|
result.alias = tagAlias(ALIAS_PREFIX.ask, String(result.id));
|
|
415
420
|
formatAskDetail(result, globals.json);
|
|
416
421
|
if (!globals.json && data.id) {
|
|
422
|
+
if (opts.dispatch === false) {
|
|
423
|
+
const askAlias = tagAlias(ALIAS_PREFIX.ask, data.id);
|
|
424
|
+
process.stderr.write(`\n Draft created. Start it with: ish ask dispatch ${askAlias}\n`);
|
|
425
|
+
}
|
|
417
426
|
const url = getWebUrl(globals, `/${wid}/asks/${data.id}`);
|
|
418
427
|
process.stderr.write(`\n ${terminalLink(url, "Open in browser ↗")}\n\n`);
|
|
419
428
|
}
|
|
420
429
|
});
|
|
421
430
|
});
|
|
431
|
+
// ---- dispatch -----------------------------------------------------------
|
|
432
|
+
// Pattern B-dispatch: flip a DRAFT ask to RUNNING and enqueue the worker.
|
|
433
|
+
// Idempotent on the server (409 on non-DRAFT) — surface that as a usage
|
|
434
|
+
// error rather than a transient failure.
|
|
435
|
+
ask
|
|
436
|
+
.command("dispatch")
|
|
437
|
+
.description("Dispatch a draft ask — bills credits and starts the round")
|
|
438
|
+
.argument("[id]", "Ask alias or UUID (defaults to active ask)")
|
|
439
|
+
.option("--ask <id>", "Ask ID; alternative to positional argument")
|
|
440
|
+
.option("--wait", "Wait until the first round completes (or errors)")
|
|
441
|
+
.option("--timeout <s>", "Wait timeout in seconds (default 300)")
|
|
442
|
+
.addHelpText("after", `
|
|
443
|
+
Use after \`ish ask create --no-dispatch\` to start a draft once the user has
|
|
444
|
+
reviewed it. The dispatch is BILLABLE — credits are charged when responses
|
|
445
|
+
land, the same as a normal create.
|
|
446
|
+
|
|
447
|
+
Examples:
|
|
448
|
+
# Dispatch the active draft and wait for results:
|
|
449
|
+
$ ish ask dispatch --wait
|
|
450
|
+
|
|
451
|
+
# Dispatch a specific draft, JSON output:
|
|
452
|
+
$ ish ask dispatch a-6ec --json
|
|
453
|
+
|
|
454
|
+
A non-DRAFT ask returns a 409 (\`already dispatched\`). The CLI maps that to a
|
|
455
|
+
usage error so re-running this command is safe — no duplicate run.`)
|
|
456
|
+
.action(async (id, opts, cmd) => {
|
|
457
|
+
await withClient(cmd, async (client, globals) => {
|
|
458
|
+
const aid = resolveAsk(pickAskRef(id, opts.ask));
|
|
459
|
+
let data = await client.post(`/asks/${aid}/dispatch`, {});
|
|
460
|
+
if (opts.wait) {
|
|
461
|
+
const timeoutMs = parseWaitTimeout(opts.timeout);
|
|
462
|
+
data = await pollUntilRoundDone(client, aid, 0, timeoutMs, !!globals.quiet);
|
|
463
|
+
}
|
|
464
|
+
const result = data;
|
|
465
|
+
if (result.id)
|
|
466
|
+
result.alias = tagAlias(ALIAS_PREFIX.ask, String(result.id));
|
|
467
|
+
formatAskDetail(result, globals.json);
|
|
468
|
+
if (!globals.json && data.product_id) {
|
|
469
|
+
const url = getWebUrl(globals, `/${data.product_id}/asks/${aid}`);
|
|
470
|
+
process.stderr.write(`\n ${terminalLink(url, "Open in browser ↗")}\n\n`);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
});
|
|
422
474
|
// ---- get ----------------------------------------------------------------
|
|
423
475
|
ask
|
|
424
476
|
.command("get")
|
package/dist/commands/chat.js
CHANGED
|
@@ -17,6 +17,7 @@ import { loadConfig, saveConfig } from "../config.js";
|
|
|
17
17
|
import { ApiError } from "../lib/api-client.js";
|
|
18
18
|
import { output } from "../lib/output.js";
|
|
19
19
|
import { formatChatEndpointList, formatChatEndpointDetail, envelopeFromRow, } from "../lib/chat-endpoint-formatters.js";
|
|
20
|
+
import { getChatEndpointTemplate, TEMPLATE_NAMES, } from "../lib/chat-endpoint-templates.js";
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Helpers
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
@@ -57,7 +58,7 @@ function urlLooksLocal(url) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
function inferredToConfig(inferred) {
|
|
60
|
-
|
|
61
|
+
const cfg = {
|
|
61
62
|
transport: inferred.transport,
|
|
62
63
|
outgoing: {
|
|
63
64
|
url: inferred.outgoing.url ?? undefined,
|
|
@@ -70,6 +71,10 @@ function inferredToConfig(inferred) {
|
|
|
70
71
|
incoming: inferred.incoming,
|
|
71
72
|
asyncPoll: inferred.asyncPoll ?? null,
|
|
72
73
|
};
|
|
74
|
+
if (inferred.streaming) {
|
|
75
|
+
cfg.streaming = inferred.streaming;
|
|
76
|
+
}
|
|
77
|
+
return cfg;
|
|
73
78
|
}
|
|
74
79
|
async function tunnelGuard(client) {
|
|
75
80
|
try {
|
|
@@ -197,7 +202,7 @@ endpoint, apply the override, and PUT the merged result. Field flags win over
|
|
|
197
202
|
$ ish chat endpoint update ep-abc --name "Production"
|
|
198
203
|
$ ish chat endpoint update ep-abc --url https://api.example.com/v2/chat
|
|
199
204
|
$ ish chat endpoint get ep-abc --verbose \\
|
|
200
|
-
| jq '.config.incoming.
|
|
205
|
+
| jq '.config.incoming.slots += [{"containerPath": "response.options", "kind": "alternatives"}]' \\
|
|
201
206
|
| ish chat endpoint update ep-abc --endpoint-config -`)
|
|
202
207
|
.action(async (id, opts, cmd) => {
|
|
203
208
|
await withClient(cmd, async (client, globals) => {
|
|
@@ -355,32 +360,93 @@ endpoint, apply the override, and PUT the merged result. Field flags win over
|
|
|
355
360
|
function attachChatEndpointInit(parent) {
|
|
356
361
|
parent
|
|
357
362
|
.command("init")
|
|
358
|
-
.description("Author an endpoint from a curl/JSON sample via auto-detect-shape")
|
|
363
|
+
.description("Author an endpoint from a curl/JSON sample via auto-detect-shape, or from a known-good template")
|
|
359
364
|
.option("--from-curl <file>", 'Path to a curl example file (or "-" for stdin)')
|
|
360
365
|
.option("--from-json <file>", 'Path to a JSON request/response sample (or "-" for stdin)')
|
|
366
|
+
.option("--template <name>", `Start from a known-good template (one of: ${TEMPLATE_NAMES.join(", ")})`)
|
|
361
367
|
.option("--name <name>", "Save the inferred config under this display name")
|
|
362
368
|
.option("--no-save", "Infer the shape without persisting it")
|
|
363
369
|
.option("--workspace <id>", "Workspace ID")
|
|
364
370
|
.option("--tunnel-backed", "Force isTunnelBacked=true (overrides localhost auto-detect)")
|
|
365
371
|
.option("--no-tunnel-backed", "Force isTunnelBacked=false (overrides localhost auto-detect)")
|
|
366
372
|
.addHelpText("after", `
|
|
367
|
-
Pass exactly one of --from-curl
|
|
373
|
+
Pass exactly one of --from-curl, --from-json, or --template. --from-curl and
|
|
374
|
+
--from-json accept "-" for stdin. --template <name> emits a hand-curated
|
|
375
|
+
ChatbotEndpointConfig from public docs (no LLM round-trip), substituting
|
|
376
|
+
{{secret:NAME}} placeholders for auth tokens.
|
|
377
|
+
|
|
378
|
+
Available templates:
|
|
379
|
+
${TEMPLATE_NAMES.map((n) => ` ${n}`).join("\n")}
|
|
368
380
|
|
|
369
381
|
isTunnelBacked decision: explicit flag wins; else true when the inferred URL
|
|
370
|
-
points at localhost / 127.0.0.1 / 0.0.0.0.
|
|
382
|
+
points at localhost / 127.0.0.1 / 0.0.0.0 (templates always default to false).
|
|
371
383
|
|
|
372
384
|
Examples:
|
|
373
385
|
$ ish chat endpoint init --from-curl ./bot.curl --name my-bot
|
|
374
|
-
$ ish chat endpoint init --from-json ./shape.json --no-save | jq '.config'
|
|
386
|
+
$ ish chat endpoint init --from-json ./shape.json --no-save | jq '.config'
|
|
387
|
+
$ ish chat endpoint init --template openai --name "OpenAI"
|
|
388
|
+
$ ish chat endpoint init --template anthropic --no-save | jq '.config'`)
|
|
375
389
|
.action(async (opts, cmd) => {
|
|
376
390
|
await withClient(cmd, async (client, globals) => {
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
const sources = [opts.fromCurl, opts.fromJson, opts.template].filter((s) => s !== undefined).length;
|
|
392
|
+
if (sources === 0) {
|
|
393
|
+
throw new Error("Pass exactly one of --from-curl <file>, --from-json <file>, or --template <name>.");
|
|
379
394
|
}
|
|
380
|
-
if (
|
|
381
|
-
throw new Error("Pass
|
|
395
|
+
if (sources > 1) {
|
|
396
|
+
throw new Error("Pass exactly one of --from-curl, --from-json, or --template — not multiple.");
|
|
382
397
|
}
|
|
383
398
|
const ws = resolveWorkspace(opts.workspace);
|
|
399
|
+
// Template path — fully local, no auto-detect call.
|
|
400
|
+
if (opts.template !== undefined) {
|
|
401
|
+
const tmpl = getChatEndpointTemplate(opts.template);
|
|
402
|
+
if (!tmpl) {
|
|
403
|
+
throw new Error(`Unknown template "${opts.template}". Available: ${TEMPLATE_NAMES.join(", ")}.`);
|
|
404
|
+
}
|
|
405
|
+
const config = JSON.parse(JSON.stringify(tmpl.config));
|
|
406
|
+
const inferredUrl = (typeof config.outgoing?.url === "string" ? config.outgoing.url : null) ?? null;
|
|
407
|
+
const detectedTunnel = urlLooksLocal(inferredUrl);
|
|
408
|
+
let tunnelBacked;
|
|
409
|
+
if (opts.tunnelBacked === true)
|
|
410
|
+
tunnelBacked = true;
|
|
411
|
+
else if (opts.tunnelBacked === false)
|
|
412
|
+
tunnelBacked = false;
|
|
413
|
+
else
|
|
414
|
+
tunnelBacked = detectedTunnel;
|
|
415
|
+
config.isTunnelBacked = tunnelBacked;
|
|
416
|
+
let endpointId = null;
|
|
417
|
+
let endpointAlias = null;
|
|
418
|
+
let saved = false;
|
|
419
|
+
const saveExplicitlyDisabled = opts.save === false;
|
|
420
|
+
const proposedName = opts.name ?? `template:${tmpl.name}`;
|
|
421
|
+
if (!saveExplicitlyDisabled) {
|
|
422
|
+
const created = await client.post(`/products/${ws}/chatbot-endpoints`, { name: proposedName, config, isTunnelBacked: tunnelBacked });
|
|
423
|
+
if (created.id) {
|
|
424
|
+
endpointId = created.id;
|
|
425
|
+
endpointAlias = tagAlias(ALIAS_PREFIX.chatEndpoint, created.id);
|
|
426
|
+
saved = true;
|
|
427
|
+
if (!globals.quiet) {
|
|
428
|
+
console.error(`Created endpoint ${endpointAlias}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const result = {
|
|
433
|
+
success: true,
|
|
434
|
+
saved,
|
|
435
|
+
endpoint_id: endpointId,
|
|
436
|
+
alias: endpointAlias,
|
|
437
|
+
config,
|
|
438
|
+
tunnel_backed: tunnelBacked,
|
|
439
|
+
tunnel_backed_detected: detectedTunnel,
|
|
440
|
+
template: tmpl.name,
|
|
441
|
+
description: tmpl.description,
|
|
442
|
+
warnings: [
|
|
443
|
+
"Templates use {{secret:NAME}} placeholders for auth — set the matching workspace secrets via `ish secret set` before testing.",
|
|
444
|
+
],
|
|
445
|
+
};
|
|
446
|
+
output(result, globals.json, { writePath: true });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Auto-detect path (curl or JSON paste).
|
|
384
450
|
const path = (opts.fromCurl ?? opts.fromJson);
|
|
385
451
|
const paste = await readFileOrStdin(path);
|
|
386
452
|
const inferRes = await client.post(`/products/${ws}/chat/auto-detect-shape`, { paste }, { timeout: 120_000 });
|
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* The backend returns a nested camelCase shape (id, name, productId, config,
|
|
5
5
|
* isTunnelBacked, createdAt, updatedAt). The lean projection keeps only the
|
|
6
6
|
* fields an agent typically branches on: id/alias/name, transport, the
|
|
7
|
-
* outgoing url + method, the incoming messagePath,
|
|
7
|
+
* outgoing url + method, the incoming messagePath, slot/reference counts, and
|
|
8
8
|
* isTunnelBacked. `--verbose` (or piped) passes the raw response.
|
|
9
|
+
*
|
|
10
|
+
* Slots-only model: `incoming.slots` and `incoming.references` are typed
|
|
11
|
+
* binding lists. Each slot carries `{containerPath, kind, labelPath, idPath}`;
|
|
12
|
+
* each reference carries `{containerPath, labelPath, urlPath}`. Legacy fields
|
|
13
|
+
* (`optionsPath`, `formRequestPath`, `cardsPath`, `artifactsPath`,
|
|
14
|
+
* `suggestedFollowupsPath`, plus the parallel `slotsContainerPaths` /
|
|
15
|
+
* `slotsKindHints` / `slotsLabelPaths` / `slotsIdPaths` /
|
|
16
|
+
* `referencesContainerPaths` arrays) are gone — anything interactive is a
|
|
17
|
+
* slot tagged with `kind`; anything passive is a reference.
|
|
9
18
|
*/
|
|
10
19
|
export interface OutgoingHttp {
|
|
11
20
|
url?: string;
|
|
@@ -13,15 +22,41 @@ export interface OutgoingHttp {
|
|
|
13
22
|
mode?: string;
|
|
14
23
|
[key: string]: unknown;
|
|
15
24
|
}
|
|
25
|
+
export interface SlotBinding {
|
|
26
|
+
containerPath: string;
|
|
27
|
+
kind?: "alternatives" | "form" | "text" | null;
|
|
28
|
+
labelPath?: string | null;
|
|
29
|
+
idPath?: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ReferenceBinding {
|
|
32
|
+
containerPath: string;
|
|
33
|
+
labelPath?: string | null;
|
|
34
|
+
urlPath?: string | null;
|
|
35
|
+
}
|
|
16
36
|
export interface IncomingHttp {
|
|
17
37
|
messagePath?: string;
|
|
18
|
-
|
|
38
|
+
conversationIdPath?: string | null;
|
|
39
|
+
endOfConversationPath?: string | null;
|
|
40
|
+
errorPath?: string | null;
|
|
41
|
+
toolCallsPath?: string | null;
|
|
42
|
+
tokenUsagePath?: string | null;
|
|
43
|
+
slots?: SlotBinding[];
|
|
44
|
+
references?: ReferenceBinding[];
|
|
45
|
+
responseStub?: unknown;
|
|
19
46
|
[key: string]: unknown;
|
|
20
47
|
}
|
|
48
|
+
export interface StreamingSettings {
|
|
49
|
+
eventFormat?: "openai" | "anthropic" | "raw";
|
|
50
|
+
deltaPath?: string | null;
|
|
51
|
+
terminalEvent?: string | null;
|
|
52
|
+
maxWaitSeconds?: number;
|
|
53
|
+
}
|
|
21
54
|
export interface ChatbotEndpointConfig {
|
|
22
55
|
transport?: string;
|
|
23
56
|
outgoing?: OutgoingHttp;
|
|
24
57
|
incoming?: IncomingHttp;
|
|
58
|
+
streaming?: StreamingSettings | null;
|
|
59
|
+
asyncPoll?: unknown;
|
|
25
60
|
isTunnelBacked?: boolean;
|
|
26
61
|
[key: string]: unknown;
|
|
27
62
|
}
|
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* The backend returns a nested camelCase shape (id, name, productId, config,
|
|
5
5
|
* isTunnelBacked, createdAt, updatedAt). The lean projection keeps only the
|
|
6
6
|
* fields an agent typically branches on: id/alias/name, transport, the
|
|
7
|
-
* outgoing url + method, the incoming messagePath,
|
|
7
|
+
* outgoing url + method, the incoming messagePath, slot/reference counts, and
|
|
8
8
|
* isTunnelBacked. `--verbose` (or piped) passes the raw response.
|
|
9
|
+
*
|
|
10
|
+
* Slots-only model: `incoming.slots` and `incoming.references` are typed
|
|
11
|
+
* binding lists. Each slot carries `{containerPath, kind, labelPath, idPath}`;
|
|
12
|
+
* each reference carries `{containerPath, labelPath, urlPath}`. Legacy fields
|
|
13
|
+
* (`optionsPath`, `formRequestPath`, `cardsPath`, `artifactsPath`,
|
|
14
|
+
* `suggestedFollowupsPath`, plus the parallel `slotsContainerPaths` /
|
|
15
|
+
* `slotsKindHints` / `slotsLabelPaths` / `slotsIdPaths` /
|
|
16
|
+
* `referencesContainerPaths` arrays) are gone — anything interactive is a
|
|
17
|
+
* slot tagged with `kind`; anything passive is a reference.
|
|
9
18
|
*/
|
|
10
19
|
import { tagAlias, ALIAS_PREFIX } from "./alias-store.js";
|
|
11
20
|
import { output, printTable } from "./output.js";
|
|
@@ -29,10 +38,10 @@ function leanRow(row) {
|
|
|
29
38
|
out.mode = cfg.outgoing.mode;
|
|
30
39
|
if (cfg.incoming?.messagePath)
|
|
31
40
|
out.message_path = cfg.incoming.messagePath;
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
out.slots = Array.isArray(cfg.incoming?.slots) ? cfg.incoming.slots.length : 0;
|
|
42
|
+
out.references = Array.isArray(cfg.incoming?.references)
|
|
43
|
+
? cfg.incoming.references.length
|
|
34
44
|
: 0;
|
|
35
|
-
out.slots_paths = slotsCount;
|
|
36
45
|
return out;
|
|
37
46
|
}
|
|
38
47
|
/** Return the round-trippable envelope used by `endpoint get --verbose`. */
|
|
@@ -68,6 +77,25 @@ export function formatChatEndpointList(rows, json, verbose) {
|
|
|
68
77
|
r.is_tunnel_backed ? "yes" : "no",
|
|
69
78
|
]));
|
|
70
79
|
}
|
|
80
|
+
function formatSlotLine(slot) {
|
|
81
|
+
const kindLabel = slot.kind ?? "auto";
|
|
82
|
+
const subParts = [];
|
|
83
|
+
if (slot.labelPath)
|
|
84
|
+
subParts.push(`label=${slot.labelPath}`);
|
|
85
|
+
if (slot.idPath)
|
|
86
|
+
subParts.push(`id=${slot.idPath}`);
|
|
87
|
+
const tail = subParts.length > 0 ? ` (${subParts.join(", ")})` : "";
|
|
88
|
+
return ` ${slot.containerPath} [${kindLabel}]${tail}`;
|
|
89
|
+
}
|
|
90
|
+
function formatReferenceLine(ref) {
|
|
91
|
+
const subParts = [];
|
|
92
|
+
if (ref.labelPath)
|
|
93
|
+
subParts.push(`label=${ref.labelPath}`);
|
|
94
|
+
if (ref.urlPath)
|
|
95
|
+
subParts.push(`url=${ref.urlPath}`);
|
|
96
|
+
const tail = subParts.length > 0 ? ` (${subParts.join(", ")})` : "";
|
|
97
|
+
return ` ${ref.containerPath}${tail}`;
|
|
98
|
+
}
|
|
71
99
|
export function formatChatEndpointDetail(row, json, verbose) {
|
|
72
100
|
if (json) {
|
|
73
101
|
if (verbose) {
|
|
@@ -96,9 +124,31 @@ export function formatChatEndpointDetail(row, json, verbose) {
|
|
|
96
124
|
if (cfg.incoming) {
|
|
97
125
|
console.log("");
|
|
98
126
|
console.log(` Message path ${cfg.incoming.messagePath ?? "-"}`);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
if (cfg.incoming.conversationIdPath) {
|
|
128
|
+
console.log(` Session path ${cfg.incoming.conversationIdPath}`);
|
|
129
|
+
}
|
|
130
|
+
if (cfg.incoming.errorPath) {
|
|
131
|
+
console.log(` Error path ${cfg.incoming.errorPath}`);
|
|
132
|
+
}
|
|
133
|
+
const slots = Array.isArray(cfg.incoming.slots) ? cfg.incoming.slots : [];
|
|
134
|
+
console.log(` Slots ${slots.length}`);
|
|
135
|
+
for (const slot of slots) {
|
|
136
|
+
console.log(formatSlotLine(slot));
|
|
137
|
+
}
|
|
138
|
+
const refs = Array.isArray(cfg.incoming.references) ? cfg.incoming.references : [];
|
|
139
|
+
console.log(` References ${refs.length}`);
|
|
140
|
+
for (const ref of refs) {
|
|
141
|
+
console.log(formatReferenceLine(ref));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (cfg.transport === "streaming" && cfg.streaming) {
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log(` Streaming ${cfg.streaming.eventFormat ?? "openai"}`);
|
|
147
|
+
if (cfg.streaming.deltaPath) {
|
|
148
|
+
console.log(` delta_path ${cfg.streaming.deltaPath}`);
|
|
149
|
+
}
|
|
150
|
+
if (cfg.streaming.terminalEvent) {
|
|
151
|
+
console.log(` terminal ${cfg.streaming.terminalEvent}`);
|
|
152
|
+
}
|
|
103
153
|
}
|
|
104
154
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-curated `ChatbotEndpointConfig` templates for `ish chat endpoint init
|
|
3
|
+
* --template <name>`.
|
|
4
|
+
*
|
|
5
|
+
* Each template is a known-good wire shape an agent can drop straight into
|
|
6
|
+
* `create_chatbot_endpoint` — derived from public docs for the named
|
|
7
|
+
* provider. Auth / API key values are placeholder secret refs
|
|
8
|
+
* (`{{secret:NAME}}`); the agent stores the real value via
|
|
9
|
+
* `ish secret set` before testing.
|
|
10
|
+
*
|
|
11
|
+
* Templates are intentionally minimal:
|
|
12
|
+
* - `transport`, `outgoing`, `incoming` (slots-only).
|
|
13
|
+
* - No `retry` block — defaults are fine.
|
|
14
|
+
* - No `asyncPoll` — none of the listed providers use it.
|
|
15
|
+
* - `streaming` only when the provider's default flow is SSE-shaped
|
|
16
|
+
* (we currently keep all templates in `sync` mode; agents who want
|
|
17
|
+
* streaming flip the transport themselves after init).
|
|
18
|
+
*
|
|
19
|
+
* Slots / references are populated when the provider documents a stable
|
|
20
|
+
* structured-output container (e.g. Bot Framework's `suggestedActions`,
|
|
21
|
+
* Watson Assistant's `output.generic[]`). Where the provider is pure text
|
|
22
|
+
* (vanilla OpenAI / Anthropic chat-completions), the lists stay empty —
|
|
23
|
+
* the runtime auto-classifier handles any structured content the bot
|
|
24
|
+
* decides to emit per turn.
|
|
25
|
+
*/
|
|
26
|
+
import type { ChatbotEndpointConfig } from "./chat-endpoint-formatters.js";
|
|
27
|
+
export type ChatEndpointTemplateName = "openai" | "anthropic" | "voiceflow" | "dialogflow-cx" | "botframework";
|
|
28
|
+
export interface ChatEndpointTemplate {
|
|
29
|
+
name: ChatEndpointTemplateName;
|
|
30
|
+
description: string;
|
|
31
|
+
config: ChatbotEndpointConfig;
|
|
32
|
+
}
|
|
33
|
+
export declare const TEMPLATE_NAMES: ChatEndpointTemplateName[];
|
|
34
|
+
export declare function getChatEndpointTemplate(name: string): ChatEndpointTemplate | undefined;
|
|
35
|
+
export declare function listChatEndpointTemplates(): ChatEndpointTemplate[];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-curated `ChatbotEndpointConfig` templates for `ish chat endpoint init
|
|
3
|
+
* --template <name>`.
|
|
4
|
+
*
|
|
5
|
+
* Each template is a known-good wire shape an agent can drop straight into
|
|
6
|
+
* `create_chatbot_endpoint` — derived from public docs for the named
|
|
7
|
+
* provider. Auth / API key values are placeholder secret refs
|
|
8
|
+
* (`{{secret:NAME}}`); the agent stores the real value via
|
|
9
|
+
* `ish secret set` before testing.
|
|
10
|
+
*
|
|
11
|
+
* Templates are intentionally minimal:
|
|
12
|
+
* - `transport`, `outgoing`, `incoming` (slots-only).
|
|
13
|
+
* - No `retry` block — defaults are fine.
|
|
14
|
+
* - No `asyncPoll` — none of the listed providers use it.
|
|
15
|
+
* - `streaming` only when the provider's default flow is SSE-shaped
|
|
16
|
+
* (we currently keep all templates in `sync` mode; agents who want
|
|
17
|
+
* streaming flip the transport themselves after init).
|
|
18
|
+
*
|
|
19
|
+
* Slots / references are populated when the provider documents a stable
|
|
20
|
+
* structured-output container (e.g. Bot Framework's `suggestedActions`,
|
|
21
|
+
* Watson Assistant's `output.generic[]`). Where the provider is pure text
|
|
22
|
+
* (vanilla OpenAI / Anthropic chat-completions), the lists stay empty —
|
|
23
|
+
* the runtime auto-classifier handles any structured content the bot
|
|
24
|
+
* decides to emit per turn.
|
|
25
|
+
*/
|
|
26
|
+
const OPENAI = {
|
|
27
|
+
name: "openai",
|
|
28
|
+
description: "OpenAI chat-completions wire shape. Stateless by default; ish ships the rolled-up history per turn. Drop in any OpenAI-compatible bot (Groq / Together / vLLM / OpenRouter / LiteLLM).",
|
|
29
|
+
config: {
|
|
30
|
+
transport: "sync",
|
|
31
|
+
outgoing: {
|
|
32
|
+
url: "https://api.openai.com/v1/chat/completions",
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json",
|
|
36
|
+
Authorization: "Bearer {{secret:OPENAI_API_KEY}}",
|
|
37
|
+
},
|
|
38
|
+
bodyTemplate: {
|
|
39
|
+
model: "gpt-4o-mini",
|
|
40
|
+
messages: "{{history_with_current}}",
|
|
41
|
+
},
|
|
42
|
+
mode: "stateless",
|
|
43
|
+
roleAliases: {},
|
|
44
|
+
},
|
|
45
|
+
incoming: {
|
|
46
|
+
messagePath: "choices[0].message.content",
|
|
47
|
+
toolCallsPath: "choices[0].message.tool_calls",
|
|
48
|
+
tokenUsagePath: "usage",
|
|
49
|
+
slots: [],
|
|
50
|
+
references: [],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const ANTHROPIC = {
|
|
55
|
+
name: "anthropic",
|
|
56
|
+
description: "Anthropic Messages API. Stateless: each request carries the full history and a fresh system prompt. Default model is the latest Sonnet.",
|
|
57
|
+
config: {
|
|
58
|
+
transport: "sync",
|
|
59
|
+
outgoing: {
|
|
60
|
+
url: "https://api.anthropic.com/v1/messages",
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
"anthropic-version": "2023-06-01",
|
|
65
|
+
"x-api-key": "{{secret:ANTHROPIC_API_KEY}}",
|
|
66
|
+
},
|
|
67
|
+
bodyTemplate: {
|
|
68
|
+
model: "claude-sonnet-4-20250514",
|
|
69
|
+
max_tokens: 1024,
|
|
70
|
+
messages: "{{history_with_current}}",
|
|
71
|
+
},
|
|
72
|
+
mode: "stateless",
|
|
73
|
+
roleAliases: {},
|
|
74
|
+
},
|
|
75
|
+
incoming: {
|
|
76
|
+
messagePath: "content[0].text",
|
|
77
|
+
tokenUsagePath: "usage",
|
|
78
|
+
slots: [],
|
|
79
|
+
references: [],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const VOICEFLOW = {
|
|
84
|
+
name: "voiceflow",
|
|
85
|
+
description: "Voiceflow Dialog Manager API. Stateful — the URL embeds the user/session id, and Voiceflow returns a list of trace objects. Body is `{action: {type: 'text', payload: <text>}}` per turn.",
|
|
86
|
+
config: {
|
|
87
|
+
transport: "sync",
|
|
88
|
+
outgoing: {
|
|
89
|
+
url: "https://general-runtime.voiceflow.com/state/user/{{conversation_id}}/interact",
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
Authorization: "{{secret:VOICEFLOW_API_KEY}}",
|
|
94
|
+
versionID: "production",
|
|
95
|
+
},
|
|
96
|
+
bodyTemplate: {
|
|
97
|
+
action: {
|
|
98
|
+
type: "text",
|
|
99
|
+
payload: "{{action.text}}",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
mode: "stateful",
|
|
103
|
+
roleAliases: {},
|
|
104
|
+
},
|
|
105
|
+
incoming: {
|
|
106
|
+
// Voiceflow's runtime returns an array of trace objects; the bot's
|
|
107
|
+
// textual reply lives in the first `speak` / `text` trace's payload.
|
|
108
|
+
// Agents typically tighten this path after a probe turn.
|
|
109
|
+
messagePath: "[0].payload.message",
|
|
110
|
+
slots: [
|
|
111
|
+
{
|
|
112
|
+
containerPath: "[0].payload.choices",
|
|
113
|
+
kind: "alternatives",
|
|
114
|
+
labelPath: "name",
|
|
115
|
+
idPath: "intent",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
references: [],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const DIALOGFLOW_CX = {
|
|
123
|
+
name: "dialogflow-cx",
|
|
124
|
+
description: "Google Dialogflow CX `detectIntent`. Stateful: the URL embeds the agent + session id and the response carries the next `currentPage` plus a list of fulfillment messages.",
|
|
125
|
+
config: {
|
|
126
|
+
transport: "sync",
|
|
127
|
+
outgoing: {
|
|
128
|
+
url: "https://dialogflow.googleapis.com/v3/projects/{{secret:GCP_PROJECT}}/locations/global/agents/{{secret:DIALOGFLOW_AGENT_ID}}/sessions/{{conversation_id}}:detectIntent",
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
Authorization: "Bearer {{secret:GOOGLE_ACCESS_TOKEN}}",
|
|
133
|
+
},
|
|
134
|
+
bodyTemplate: {
|
|
135
|
+
queryInput: {
|
|
136
|
+
text: { text: "{{action.text}}" },
|
|
137
|
+
languageCode: "en",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
mode: "stateful",
|
|
141
|
+
roleAliases: {},
|
|
142
|
+
},
|
|
143
|
+
incoming: {
|
|
144
|
+
messagePath: "queryResult.responseMessages[0].text.text[0]",
|
|
145
|
+
conversationIdPath: "responseId",
|
|
146
|
+
slots: [
|
|
147
|
+
{
|
|
148
|
+
containerPath: "queryResult.responseMessages",
|
|
149
|
+
kind: null,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
references: [],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const BOTFRAMEWORK = {
|
|
157
|
+
name: "botframework",
|
|
158
|
+
description: "Microsoft Bot Framework Direct Line `conversations/{id}/activities`. Stateful via the conversation id; activities[] carry the bot's reply plus suggestedActions for choice slots.",
|
|
159
|
+
config: {
|
|
160
|
+
transport: "sync",
|
|
161
|
+
outgoing: {
|
|
162
|
+
url: "https://directline.botframework.com/v3/directline/conversations/{{conversation_id}}/activities",
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: {
|
|
165
|
+
"content-type": "application/json",
|
|
166
|
+
Authorization: "Bearer {{secret:DIRECTLINE_SECRET}}",
|
|
167
|
+
},
|
|
168
|
+
bodyTemplate: {
|
|
169
|
+
type: "message",
|
|
170
|
+
from: { id: "{{tester.name}}" },
|
|
171
|
+
text: "{{action.text}}",
|
|
172
|
+
},
|
|
173
|
+
mode: "stateful",
|
|
174
|
+
roleAliases: {},
|
|
175
|
+
},
|
|
176
|
+
incoming: {
|
|
177
|
+
messagePath: "activities[0].text",
|
|
178
|
+
conversationIdPath: "activities[0].conversation.id",
|
|
179
|
+
slots: [
|
|
180
|
+
{
|
|
181
|
+
containerPath: "activities[0].suggestedActions.actions",
|
|
182
|
+
kind: "alternatives",
|
|
183
|
+
labelPath: "title",
|
|
184
|
+
idPath: "value",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
references: [
|
|
188
|
+
{
|
|
189
|
+
containerPath: "activities[0].attachments",
|
|
190
|
+
labelPath: "name",
|
|
191
|
+
urlPath: "contentUrl",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const TEMPLATES = {
|
|
198
|
+
openai: OPENAI,
|
|
199
|
+
anthropic: ANTHROPIC,
|
|
200
|
+
voiceflow: VOICEFLOW,
|
|
201
|
+
"dialogflow-cx": DIALOGFLOW_CX,
|
|
202
|
+
botframework: BOTFRAMEWORK,
|
|
203
|
+
};
|
|
204
|
+
export const TEMPLATE_NAMES = Object.keys(TEMPLATES);
|
|
205
|
+
export function getChatEndpointTemplate(name) {
|
|
206
|
+
return TEMPLATES[name];
|
|
207
|
+
}
|
|
208
|
+
export function listChatEndpointTemplates() {
|
|
209
|
+
return TEMPLATE_NAMES.map((n) => TEMPLATES[n]);
|
|
210
|
+
}
|
package/dist/lib/docs.js
CHANGED
|
@@ -574,6 +574,49 @@ ish ask results a-6ec
|
|
|
574
574
|
ish ask results a-6ec --json | jq '.rounds[0].aggregates'
|
|
575
575
|
\`\`\`
|
|
576
576
|
|
|
577
|
+
## Status field
|
|
578
|
+
|
|
579
|
+
Asks carry a top-level \`status\`:
|
|
580
|
+
|
|
581
|
+
- \`draft\` — created but not dispatched yet. No credits charged. Created
|
|
582
|
+
by \`ish ask create --no-dispatch\`.
|
|
583
|
+
- \`running\` — dispatched; the round is executing or queued.
|
|
584
|
+
- \`completed\` — round 1 (or the most recent round) finished.
|
|
585
|
+
- \`cancelled\` — terminated explicitly.
|
|
586
|
+
|
|
587
|
+
Surfaces in \`ish ask list\` (table column) and \`ish ask get\` (header
|
|
588
|
+
metadata line and JSON \`status\` field). Lean JSON keeps the field
|
|
589
|
+
intact — no \`--verbose\` needed to see it.
|
|
590
|
+
|
|
591
|
+
## Stage-then-dispatch (draft asks)
|
|
592
|
+
|
|
593
|
+
When you want a human to review the audience and prompt **before** any
|
|
594
|
+
credits are spent, separate creation from dispatch:
|
|
595
|
+
|
|
596
|
+
\`\`\`
|
|
597
|
+
# 1. Stage — materializes testers, no worker enqueue, no bill yet
|
|
598
|
+
ish ask create --workspace w-6ec --name "tagline AB" \\
|
|
599
|
+
--prompt "Which sounds better?" \\
|
|
600
|
+
--variant text:"Short and punchy." \\
|
|
601
|
+
--variant text:"A longer, descriptive line." \\
|
|
602
|
+
--sample 30 --wants-pick \\
|
|
603
|
+
--no-dispatch
|
|
604
|
+
|
|
605
|
+
# Returns an ask with status="draft". Hand the alias back to the user.
|
|
606
|
+
|
|
607
|
+
# 2. Dispatch — flips DRAFT → RUNNING and enqueues the round (BILLABLE)
|
|
608
|
+
ish ask dispatch a-6ec --wait
|
|
609
|
+
\`\`\`
|
|
610
|
+
|
|
611
|
+
\`--no-dispatch\` requires audience flags (testers are still materialized
|
|
612
|
+
at create time — only the worker enqueue and billing are deferred). It
|
|
613
|
+
is incompatible with \`--wait\` since there is nothing to wait for.
|
|
614
|
+
|
|
615
|
+
\`ish ask dispatch\` is idempotent on the server: a non-DRAFT ask returns
|
|
616
|
+
HTTP 409 (\`already dispatched\`) which the CLI maps to a usage error, so
|
|
617
|
+
re-running the command is safe. The user who calls \`dispatch\` is the
|
|
618
|
+
billing principal — keep that in mind for shared workspaces.
|
|
619
|
+
|
|
577
620
|
## Reading the verdict
|
|
578
621
|
|
|
579
622
|
For \`--wants-pick\` / \`--wants-ratings\` rounds, \`ask results --json\`
|
|
@@ -1783,8 +1826,7 @@ ish chat endpoint get ep-abc --verbose \\
|
|
|
1783
1826
|
| ish chat endpoint update ep-abc --endpoint-config -
|
|
1784
1827
|
|
|
1785
1828
|
ish chat endpoint get ep-abc --verbose \\
|
|
1786
|
-
| jq '.config.incoming.
|
|
1787
|
-
| .config.incoming.slotsKindHints["response.options"] = "alternatives"' \\
|
|
1829
|
+
| jq '.config.incoming.slots += [{"containerPath": "response.options", "kind": "alternatives"}]' \\
|
|
1788
1830
|
| ish chat endpoint update ep-abc --endpoint-config -
|
|
1789
1831
|
\`\`\`
|
|
1790
1832
|
|
|
@@ -1820,6 +1862,106 @@ The renderer expands these tokens at request time:
|
|
|
1820
1862
|
}
|
|
1821
1863
|
\`\`\`
|
|
1822
1864
|
|
|
1865
|
+
### Slot bindings (interactive containers)
|
|
1866
|
+
|
|
1867
|
+
The bot's response shape is described by two typed lists on
|
|
1868
|
+
\`incoming\`:
|
|
1869
|
+
|
|
1870
|
+
- \`incoming.slots[]\` — INTERACTIVE containers the persona must
|
|
1871
|
+
respond to. Each entry is
|
|
1872
|
+
\`{containerPath, kind?, labelPath?, idPath?}\`.
|
|
1873
|
+
- \`incoming.references[]\` — PASSIVE containers (citations,
|
|
1874
|
+
followups, file artifacts, related links). Each entry is
|
|
1875
|
+
\`{containerPath, labelPath?, urlPath?}\`.
|
|
1876
|
+
|
|
1877
|
+
\`kind\` is one of \`alternatives\` (pick from a list), \`form\`
|
|
1878
|
+
(fill named fields), or \`text\` (free text). Leaving \`kind\` unset
|
|
1879
|
+
(the default) means "auto-classify per-turn from the live shape" —
|
|
1880
|
+
ish inspects the container value at parse time and dispatches on the
|
|
1881
|
+
shape it sees. Use that whenever the bot returns different slot
|
|
1882
|
+
kinds across turns.
|
|
1883
|
+
|
|
1884
|
+
\`labelPath\` / \`idPath\` (alternatives) and \`labelPath\` /
|
|
1885
|
+
\`urlPath\` (references) are optional sub-paths within each item.
|
|
1886
|
+
When omitted, ish falls back to \`label\` / \`text\` / \`title\` for
|
|
1887
|
+
labels, \`id\` / \`value\` / \`payload\` for ids, and
|
|
1888
|
+
\`url\` / \`uri\` / \`href\` for urls.
|
|
1889
|
+
|
|
1890
|
+
The legacy per-affordance fields (\`optionsPath\`,
|
|
1891
|
+
\`formRequestPath\`, \`cardsPath\`, \`artifactsPath\`,
|
|
1892
|
+
\`suggestedFollowupsPath\`) are gone. Anything interactive is a
|
|
1893
|
+
slot tagged with \`kind\`; anything passive is a reference. New
|
|
1894
|
+
affordance shapes ship as a new \`kind\` value, no schema migration.
|
|
1895
|
+
|
|
1896
|
+
\`auto-detect-shape\` (the engine behind \`init\`) populates these
|
|
1897
|
+
lists from the response stub via a shape-based classifier:
|
|
1898
|
+
\`list[{label, id?}]\` becomes \`alternatives\`,
|
|
1899
|
+
\`{fields: [...]}\` becomes \`form\`,
|
|
1900
|
+
\`list[{label, url}]\` becomes a reference, and
|
|
1901
|
+
\`list[str]\` becomes a string-list reference. The classifier never
|
|
1902
|
+
emits \`kind=text\` — that's a hand-set tag for free-text follow-ups.
|
|
1903
|
+
|
|
1904
|
+
### Streaming endpoints
|
|
1905
|
+
|
|
1906
|
+
When a bot speaks Server-Sent Events (Accept: text/event-stream,
|
|
1907
|
+
\`-N\` / \`--no-buffer\` curl flags, or \`"stream": true\` in the
|
|
1908
|
+
body), \`init\` flips \`transport\` to \`"streaming"\` and seeds a
|
|
1909
|
+
\`streaming\` block:
|
|
1910
|
+
|
|
1911
|
+
\`\`\`json
|
|
1912
|
+
{
|
|
1913
|
+
"transport": "streaming",
|
|
1914
|
+
"streaming": {
|
|
1915
|
+
"eventFormat": "openai",
|
|
1916
|
+
"deltaPath": null,
|
|
1917
|
+
"terminalEvent": null,
|
|
1918
|
+
"maxWaitSeconds": 120
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
\`\`\`
|
|
1922
|
+
|
|
1923
|
+
\`eventFormat\` is one of:
|
|
1924
|
+
|
|
1925
|
+
- \`"openai"\` — chunks shaped like
|
|
1926
|
+
\`{choices: [{delta: {content: "..."}}]}\`; ends on \`[DONE]\` or
|
|
1927
|
+
\`finish_reason != null\`. Matches OpenAI / Groq / vLLM / LiteLLM /
|
|
1928
|
+
OpenRouter / Chainlit.
|
|
1929
|
+
- \`"anthropic"\` — \`event: content_block_delta\` chunks carrying
|
|
1930
|
+
\`{delta: {type: "text_delta", text: "..."}}\`; ends on
|
|
1931
|
+
\`event: message_stop\`.
|
|
1932
|
+
- \`"raw"\` — body-text concatenation; no JSON decoding; closes on
|
|
1933
|
+
connection drop.
|
|
1934
|
+
|
|
1935
|
+
Override the format-specific defaults via \`deltaPath\` (e.g. an
|
|
1936
|
+
OpenAI-compatible proxy that nests delta under
|
|
1937
|
+
\`message.delta.content\`) and \`terminalEvent\`. \`maxWaitSeconds\`
|
|
1938
|
+
caps the streaming-loop deadline.
|
|
1939
|
+
|
|
1940
|
+
### From a template
|
|
1941
|
+
|
|
1942
|
+
For well-known providers, skip auto-detect and start from a
|
|
1943
|
+
hand-curated config:
|
|
1944
|
+
|
|
1945
|
+
\`\`\`
|
|
1946
|
+
ish chat endpoint init --template openai --name "OpenAI"
|
|
1947
|
+
ish chat endpoint init --template anthropic --no-save | jq '.config'
|
|
1948
|
+
ish chat endpoint init --template voiceflow --name "Sales bot"
|
|
1949
|
+
ish chat endpoint init --template dialogflow-cx --name "Support"
|
|
1950
|
+
ish chat endpoint init --template botframework --name "Concierge"
|
|
1951
|
+
\`\`\`
|
|
1952
|
+
|
|
1953
|
+
Templates use \`{{secret:NAME}}\` placeholders for auth tokens; set
|
|
1954
|
+
the matching workspace secrets before testing:
|
|
1955
|
+
|
|
1956
|
+
\`\`\`
|
|
1957
|
+
printf %s "$OPENAI_API_KEY" | ish secret set OPENAI_API_KEY --value-stdin
|
|
1958
|
+
\`\`\`
|
|
1959
|
+
|
|
1960
|
+
Available templates: openai, anthropic, voiceflow, dialogflow-cx,
|
|
1961
|
+
botframework. Each is derived from the provider's public docs and
|
|
1962
|
+
is intentionally minimal — agents typically tighten the message
|
|
1963
|
+
path / model / slot bindings after one round-trip with the real bot.
|
|
1964
|
+
|
|
1823
1965
|
### Auth via workspace secrets
|
|
1824
1966
|
|
|
1825
1967
|
For bots behind an API key, store the value as a workspace secret
|
|
@@ -2087,7 +2229,7 @@ const PAGES = [
|
|
|
2087
2229
|
{
|
|
2088
2230
|
slug: "guides/chat",
|
|
2089
2231
|
title: "guide: chat-modality studies",
|
|
2090
|
-
description: "Configure a chatbot endpoint, smoke test it, run a chat-modality study.",
|
|
2232
|
+
description: "Configure a chatbot endpoint (slots-only model), smoke test it, run a chat-modality study. Covers slot bindings, streaming endpoints, and built-in templates.",
|
|
2091
2233
|
body: GUIDE_CHAT,
|
|
2092
2234
|
},
|
|
2093
2235
|
];
|
package/dist/lib/output.js
CHANGED
|
@@ -1452,9 +1452,10 @@ export function formatAskList(asks, json) {
|
|
|
1452
1452
|
return;
|
|
1453
1453
|
}
|
|
1454
1454
|
const aliasMap = getAliasMap(ALIAS_PREFIX.ask);
|
|
1455
|
-
printTable(["#", "NAME", "AUDIENCE", "ROUNDS", "LAST ROUND", "ARCHIVED"], asks.map((a) => [
|
|
1455
|
+
printTable(["#", "NAME", "STATUS", "AUDIENCE", "ROUNDS", "LAST ROUND", "ARCHIVED"], asks.map((a) => [
|
|
1456
1456
|
aliasMap.get(String(a.id)) || String(a.id || ""),
|
|
1457
1457
|
String(a.name || ""),
|
|
1458
|
+
String(a.status || "-"),
|
|
1458
1459
|
String(a.audience_count ?? "0"),
|
|
1459
1460
|
String(a.round_count ?? "0"),
|
|
1460
1461
|
formatDate(a.last_round_at),
|
|
@@ -1533,6 +1534,8 @@ export function formatAskDetail(ask, json) {
|
|
|
1533
1534
|
if (ask.description)
|
|
1534
1535
|
console.log(String(ask.description));
|
|
1535
1536
|
const meta = [];
|
|
1537
|
+
if (ask.status)
|
|
1538
|
+
meta.push(String(ask.status));
|
|
1536
1539
|
if (ask.is_archived)
|
|
1537
1540
|
meta.push("archived");
|
|
1538
1541
|
meta.push(formatDate(ask.created_at));
|
|
@@ -29,3 +29,21 @@ export interface SkillTargetSpec {
|
|
|
29
29
|
consumers: string[];
|
|
30
30
|
}
|
|
31
31
|
export declare const SKILL_TARGETS: SkillTargetSpec[];
|
|
32
|
+
/**
|
|
33
|
+
* Walks from `startDir` upward (inclusive of the home directory, capped at
|
|
34
|
+
* the filesystem root) looking for an installed ish skill at any of
|
|
35
|
+
* SKILL_TARGETS. Returns the first hit, identified by the presence of a
|
|
36
|
+
* SKILL.md file. Used by `ish status` to nudge agents toward `ish init`
|
|
37
|
+
* when the project doesn't have the skill installed yet.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findInstalledSkill(startDir: string, fs: {
|
|
40
|
+
existsSync: (p: string) => boolean;
|
|
41
|
+
}, path: {
|
|
42
|
+
join: (...p: string[]) => string;
|
|
43
|
+
dirname: (p: string) => string;
|
|
44
|
+
resolve: (p: string) => string;
|
|
45
|
+
}, homeDir: string): {
|
|
46
|
+
target: SkillTargetSpec;
|
|
47
|
+
root: string;
|
|
48
|
+
skillMdPath: string;
|
|
49
|
+
} | null;
|
|
@@ -138,6 +138,11 @@ ish study create --modality chat --endpoint my-bot --assignment "Sign up:Try to
|
|
|
138
138
|
ish study run --sample 5 --country SE --wait
|
|
139
139
|
ish ask run --new --name "..." --prompt "..." --variant text:"A" --variant text:"B" --sample 30 --wants-pick --wait
|
|
140
140
|
|
|
141
|
+
# Stage an ask for human review, then dispatch (no credits charged on stage)
|
|
142
|
+
ish ask create --name "..." --prompt "..." --variant text:"A" --variant text:"B" \
|
|
143
|
+
--sample 30 --wants-pick --no-dispatch
|
|
144
|
+
ish ask dispatch a-6ec --wait
|
|
145
|
+
|
|
141
146
|
# Results
|
|
142
147
|
ish study results
|
|
143
148
|
ish ask results a-6ec --round 1
|
|
@@ -236,6 +241,15 @@ implies \`--quiet\` so the bare value is the only thing on stdout.
|
|
|
236
241
|
follow-up question to a completed round preserves prior comments,
|
|
237
242
|
picks, and ratings; only the new question is dispatched. Pass
|
|
238
243
|
\`--redispatch-all\` for the legacy reset-and-rerun behavior.
|
|
244
|
+
- **\`ask create --no-dispatch\` stages a draft, no bill yet.** Pair
|
|
245
|
+
with \`ish ask dispatch <id>\` to flip DRAFT → RUNNING and start
|
|
246
|
+
the round. Use this when the user wants to review the audience or
|
|
247
|
+
prompt before any credits are charged. Audience flags are still
|
|
248
|
+
required (testers materialize at create time); only the worker
|
|
249
|
+
enqueue and billing are deferred. Asks now carry a top-level
|
|
250
|
+
\`status\` (\`draft | running | completed | cancelled\`) visible in
|
|
251
|
+
\`ask list\` and \`ask get\`. \`dispatch\` is idempotent — a
|
|
252
|
+
non-DRAFT ask returns 409 mapped to a usage error.
|
|
239
253
|
- **\`ask results --json\` adds \`cross_round_summary\` for 2+ rounds.**
|
|
240
254
|
Top-level field with per-round picks/winner snapshots and
|
|
241
255
|
\`picks_delta\` (R1 → last). Don't diff two \`ask results\` calls by
|
|
@@ -520,8 +534,20 @@ ish iteration create --url "$URL"
|
|
|
520
534
|
|
|
521
535
|
Goal: configure a customer chatbot endpoint, smoke test it, and run
|
|
522
536
|
a chat-modality study end to end. The CLI talks to the endpoint
|
|
523
|
-
through whatever transport it's configured for (sync / async-poll
|
|
524
|
-
local bots reach ish via \`ish connect\`.
|
|
537
|
+
through whatever transport it's configured for (sync / async-poll /
|
|
538
|
+
streaming); local bots reach ish via \`ish connect\`.
|
|
539
|
+
|
|
540
|
+
Endpoint shape is slots-only: \`incoming.slots[]\` lists interactive
|
|
541
|
+
containers (\`{containerPath, kind?, labelPath?, idPath?}\`,
|
|
542
|
+
\`kind\` ∈ \`alternatives\` / \`form\` / \`text\` or unset to
|
|
543
|
+
auto-classify per-turn) and \`incoming.references[]\` lists passive
|
|
544
|
+
containers (citations / followups / artifacts). Auto-detect derives
|
|
545
|
+
both from the response stub via shape rules — no markers to learn.
|
|
546
|
+
|
|
547
|
+
For well-known providers, skip auto-detect and start from a
|
|
548
|
+
hand-curated template:
|
|
549
|
+
\`ish chat endpoint init --template <openai|anthropic|voiceflow|dialogflow-cx|botframework>\`.
|
|
550
|
+
Templates use \`{{secret:NAME}}\` placeholders for auth.
|
|
525
551
|
|
|
526
552
|
\`\`\`bash
|
|
527
553
|
# 1. Author the endpoint from a curl example (or a ChatbotEndpointConfig file).
|
|
@@ -540,7 +566,7 @@ ish chat endpoint test "$ID" -m "Hello"
|
|
|
540
566
|
# one-liner shorthand. Mirrors the editor dialog's PUT contract.
|
|
541
567
|
ish chat endpoint update "$ID" --name "Production support bot"
|
|
542
568
|
ish chat endpoint get "$ID" --verbose \\
|
|
543
|
-
| jq '.config.incoming.
|
|
569
|
+
| jq '.config.incoming.slots += [{"containerPath": "response.options", "kind": "alternatives"}]' \\
|
|
544
570
|
| ish chat endpoint update "$ID" --endpoint-config -
|
|
545
571
|
|
|
546
572
|
# 4. Run a chat-modality study referencing the endpoint. Audience size
|
|
@@ -596,7 +622,41 @@ you can branch on plan caps before \`study create\` returns
|
|
|
596
622
|
The full reference is at \`ish docs get-page guides/chat\`,
|
|
597
623
|
secrets are at \`ish docs get-page concepts/secret\`.
|
|
598
624
|
|
|
599
|
-
## 8.
|
|
625
|
+
## 8. Stage an ask for human review, then dispatch
|
|
626
|
+
|
|
627
|
+
Goal: prepare a billable A/B but let the user inspect and approve the
|
|
628
|
+
audience + prompt before any credits are spent. Two-step flow with a
|
|
629
|
+
DRAFT status in between.
|
|
630
|
+
|
|
631
|
+
\`\`\`bash
|
|
632
|
+
# 1. Stage. No worker enqueued, no bill. Audience flags are still
|
|
633
|
+
# required — testers materialize at create time.
|
|
634
|
+
ASK=$(ish ask create --name "tagline AB" \\
|
|
635
|
+
--prompt "Which sounds better?" \\
|
|
636
|
+
--variant text:"Short and punchy." \\
|
|
637
|
+
--variant text:"A longer, descriptive line." \\
|
|
638
|
+
--sample 30 --wants-pick \\
|
|
639
|
+
--no-dispatch \\
|
|
640
|
+
--get alias)
|
|
641
|
+
|
|
642
|
+
# Hand the alias back to the user. They can inspect it:
|
|
643
|
+
# ish ask get "$ASK" # status: draft
|
|
644
|
+
# ish ask get "$ASK" --json | jq '.testers | length'
|
|
645
|
+
|
|
646
|
+
# 2. Dispatch once approved (BILLABLE). Idempotent: a non-DRAFT ask
|
|
647
|
+
# returns 409 mapped to exit 2, so re-running is safe.
|
|
648
|
+
ish ask dispatch "$ASK" --wait
|
|
649
|
+
\`\`\`
|
|
650
|
+
|
|
651
|
+
The \`status\` field on the ask reflects lifecycle (\`draft\` → \`running\`
|
|
652
|
+
→ \`completed\`); \`is_archived\` is orthogonal. \`ish ask list\` shows
|
|
653
|
+
status as a column.
|
|
654
|
+
|
|
655
|
+
\`--no-dispatch\` is incompatible with \`--wait\` — there is nothing to
|
|
656
|
+
wait for. Pass \`--wait\` to \`ish ask dispatch\` instead if you want to
|
|
657
|
+
block until the round settles.
|
|
658
|
+
|
|
659
|
+
## 9. Display-vs-capture: a script that does both
|
|
600
660
|
|
|
601
661
|
Goal: drive an A/B in a script, capture aliases without \`jq\`, and
|
|
602
662
|
still show the human a readable result table at the end.
|
|
@@ -837,3 +897,32 @@ export const SKILL_TARGETS = [
|
|
|
837
897
|
consumers: ["Codex", "Cursor", "Cline", "Roo Code"],
|
|
838
898
|
},
|
|
839
899
|
];
|
|
900
|
+
/**
|
|
901
|
+
* Walks from `startDir` upward (inclusive of the home directory, capped at
|
|
902
|
+
* the filesystem root) looking for an installed ish skill at any of
|
|
903
|
+
* SKILL_TARGETS. Returns the first hit, identified by the presence of a
|
|
904
|
+
* SKILL.md file. Used by `ish status` to nudge agents toward `ish init`
|
|
905
|
+
* when the project doesn't have the skill installed yet.
|
|
906
|
+
*/
|
|
907
|
+
export function findInstalledSkill(startDir, fs, path, homeDir) {
|
|
908
|
+
let dir = path.resolve(startDir);
|
|
909
|
+
const home = path.resolve(homeDir);
|
|
910
|
+
// Walk until the parent stops changing (filesystem root). Include `home`
|
|
911
|
+
// itself in the search so a global ~/.claude/skills/ish counts, but stop
|
|
912
|
+
// right after — don't claim a skill installed somewhere above $HOME.
|
|
913
|
+
while (true) {
|
|
914
|
+
for (const target of SKILL_TARGETS) {
|
|
915
|
+
const root = path.join(dir, target.path);
|
|
916
|
+
const skillMdPath = path.join(root, "SKILL.md");
|
|
917
|
+
if (fs.existsSync(skillMdPath)) {
|
|
918
|
+
return { target, root, skillMdPath };
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (dir === home)
|
|
922
|
+
return null;
|
|
923
|
+
const parent = path.dirname(dir);
|
|
924
|
+
if (parent === dir)
|
|
925
|
+
return null;
|
|
926
|
+
dir = parent;
|
|
927
|
+
}
|
|
928
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -245,6 +245,7 @@ export interface SimulationConfig {
|
|
|
245
245
|
}
|
|
246
246
|
export type AskVariantKind = "image" | "text" | "audio" | "video" | "document";
|
|
247
247
|
export declare const ASK_VARIANT_KINDS: AskVariantKind[];
|
|
248
|
+
export type AskStatus = "draft" | "running" | "completed" | "cancelled";
|
|
248
249
|
export type AskRoundStatus = "running" | "completed" | "errored";
|
|
249
250
|
export type AskResponseStatus = "pending" | "completed" | "errored";
|
|
250
251
|
export interface AskVariant {
|
|
@@ -293,6 +294,7 @@ export interface AskCreateInput {
|
|
|
293
294
|
language?: string;
|
|
294
295
|
tester_profile_ids: string[];
|
|
295
296
|
first_round: AskRoundInput;
|
|
297
|
+
dispatch?: boolean;
|
|
296
298
|
}
|
|
297
299
|
export interface AskUpdateInput {
|
|
298
300
|
name?: string;
|
|
@@ -355,6 +357,7 @@ export interface Ask {
|
|
|
355
357
|
name: string;
|
|
356
358
|
description?: string | null;
|
|
357
359
|
is_archived: boolean;
|
|
360
|
+
status?: AskStatus;
|
|
358
361
|
testers: AskAudienceTester[];
|
|
359
362
|
rounds: AskRound[];
|
|
360
363
|
created_at: string;
|
|
@@ -367,6 +370,7 @@ export interface AskListItem {
|
|
|
367
370
|
name: string;
|
|
368
371
|
description?: string | null;
|
|
369
372
|
is_archived: boolean;
|
|
373
|
+
status?: AskStatus;
|
|
370
374
|
audience_count: number;
|
|
371
375
|
round_count: number;
|
|
372
376
|
last_round_at?: string | null;
|
package/package.json
CHANGED