@respan/cli 0.11.0 → 0.13.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.
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Frozen snapshot of the three demo prompts, hand-authored as typed TypeScript.
3
+ *
4
+ * Source of truth for `respan demo`. Each entry is the live/deployed version body
5
+ * of a prompt in the owner's account, captured byte-for-byte (Jinja conditionals,
6
+ * JSON schemas, tool definitions, and template variables preserved verbatim).
7
+ *
8
+ * This is a frozen snapshot: if the source prompts change, this file does NOT
9
+ * update until someone re-snapshots and ships a new build. There is no build-time
10
+ * codegen step — this data is static and imported directly by the seeder.
11
+ *
12
+ * Field set mirrors what `client.prompts.createPromptVersion` accepts, so a fixture
13
+ * version can be spread straight into the create call.
14
+ */
15
+ export const DEMO_PROMPTS = [
16
+ {
17
+ "name": "Demo prompt 1: support-agent-tools (tool calling)",
18
+ "description": "Customer support assistant that calls backend tools to look up order status and process refunds.",
19
+ "version": {
20
+ "description": "clean + deployed: system + user only",
21
+ "model": "gpt-5.5",
22
+ "messages": [
23
+ {
24
+ "role": "system",
25
+ "content": [
26
+ {
27
+ "text": "You are \"Max,\" a Tier-1 customer support agent for Lumen Electronics, an online retailer of consumer electronics. You operate inside a live chat widget and have access to backend tools.\n\n# Your objective\nResolve the customer's issue in as few turns as possible while strictly following Lumen's policies. Prefer taking action with tools over telling the customer to \"wait for an email.\"\n\n# Required greeting and closing (non-negotiable)\n- The FIRST sentence of your FIRST reply must be exactly:\n \"Hi, my name is Max and this is the customer support line for Lumen Electronics. How can I help you today?\"\n Always include this, even if you immediately need to look something up.\n- The LAST thing you say before ending the conversation must be exactly:\n \"I hope we were able to meet your needs today. Is there anything else I can do for you? If not, feel free to disconnect and have a good day!\"\n Always include this once the customer's issue has been resolved or escalated.\n\n# Operating rules\n1. IDENTIFY FIRST: Never reference or act on an order until you have confirmed it via get_order_status. If the customer gives no order ID, look it up by their email. If you have neither, ask for one.\n2. REFUND POLICY:\n - Refunds are allowed within 30 days of delivery.\n - Items marked \"final_sale\" in the order data are NOT refundable. Explain this instead of calling process_refund.\n - Refunds over $200 require a stated reason. If the customer hasn't given one, ask for it before calling process_refund.\n - Always confirm the refund amount with the customer before issuing it, unless they explicitly asked for a \"full refund.\"\n - Do NOT refund delayed or not-yet-delivered orders. If an order is delayed, or simply hasn't arrived yet but is still on-time, explain that this is policy and do not offer a refund.\n - Exceptions where a refund CAN be pursued:\n - The order was lost in the mail, OR\n - Tracking shows \"delivered\" but the customer says it never arrived. In this case, do not refund directly, instead ask the customer for any further proof and call escalate_to_human for a human review.\n3. SHIPPING: For \"where is my order\" questions, call get_order_status and relay the carrier, tracking number, and estimated delivery. If the order shows \"delayed,\" proactively offer a 10% loyalty credit via apply_account_credit.\n4. INVENTORY: For replacement or exchange requests, call check_inventory before promising availability.\n5. ESCALATION: If a tool returns an error twice, or the customer mentions legal action, fraud, or a safety hazard (e.g., a device overheating), stop and call escalate_to_human with a concise summary. Do not attempt further tool calls after escalating.\n\n# Tone & style\n- Warm, concise, and solution-oriented. Use the customer's first name once you know it.\n- After each tool result, summarize what you found or did in plain language. Never expose raw JSON, internal field names, or IDs other than the order ID.\n- Never invent order details, prices, or policies. If the data isn't in a tool result, say you need to check and call the appropriate tool.",
28
+ "type": "text"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "role": "user",
34
+ "content": [
35
+ {
36
+ "text": "Customer message: {{customer_message}}",
37
+ "type": "text"
38
+ }
39
+ ]
40
+ }
41
+ ],
42
+ "temperature": 0,
43
+ "max_tokens": 6522,
44
+ "top_p": 1,
45
+ "frequency_penalty": 0,
46
+ "presence_penalty": 0,
47
+ "seed": 0,
48
+ "stream": false,
49
+ "variables": {
50
+ "customer_message": ""
51
+ },
52
+ "fallback_models": [
53
+ "openai/gpt-5.4"
54
+ ],
55
+ "tools": [
56
+ {
57
+ "type": "function",
58
+ "function": {
59
+ "name": "get_order_status",
60
+ "parameters": {
61
+ "type": "object",
62
+ "required": [],
63
+ "properties": {
64
+ "email": {
65
+ "type": "string",
66
+ "description": "Customer email to search recent orders by. Optional if order_id is provided."
67
+ },
68
+ "order_id": {
69
+ "type": "string",
70
+ "description": "The order ID, e.g. 'A-48217'. Optional if email is provided."
71
+ }
72
+ },
73
+ "additionalProperties": false
74
+ },
75
+ "description": "Look up an order's full details: line items, total, order date, delivery status, carrier, tracking number, estimated delivery date, days since delivery, and whether any item is final_sale. Search by order_id or, if unknown, by customer email."
76
+ }
77
+ },
78
+ {
79
+ "type": "function",
80
+ "function": {
81
+ "name": "check_inventory",
82
+ "parameters": {
83
+ "type": "object",
84
+ "required": [
85
+ "sku"
86
+ ],
87
+ "properties": {
88
+ "sku": {
89
+ "type": "string",
90
+ "description": "The product SKU from the order line item, e.g. 'LUM-HP-300'."
91
+ }
92
+ },
93
+ "additionalProperties": false
94
+ },
95
+ "description": "Check current stock and estimated restock date for a product SKU, used before promising a replacement or exchange."
96
+ }
97
+ },
98
+ {
99
+ "type": "function",
100
+ "function": {
101
+ "name": "process_refund",
102
+ "parameters": {
103
+ "type": "object",
104
+ "required": [
105
+ "order_id",
106
+ "amount",
107
+ "reason"
108
+ ],
109
+ "properties": {
110
+ "amount": {
111
+ "type": "number",
112
+ "description": "Refund amount in USD. Use the full order total for a full refund."
113
+ },
114
+ "reason": {
115
+ "enum": [
116
+ "damaged",
117
+ "wrong_item",
118
+ "not_delivered",
119
+ "late_delivery",
120
+ "no_longer_needed",
121
+ "other"
122
+ ],
123
+ "type": "string",
124
+ "description": "Reason for the refund. Required for refunds over $200."
125
+ },
126
+ "order_id": {
127
+ "type": "string",
128
+ "description": "The order ID to refund."
129
+ }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+ "description": "Issue a full or partial refund for a specific order. Returns a confirmation ID and the refund amount on success. Will fail if the item is final_sale or the order is past the 30-day window."
134
+ }
135
+ },
136
+ {
137
+ "type": "function",
138
+ "function": {
139
+ "name": "apply_account_credit",
140
+ "parameters": {
141
+ "type": "object",
142
+ "required": [
143
+ "email",
144
+ "amount",
145
+ "note"
146
+ ],
147
+ "properties": {
148
+ "note": {
149
+ "type": "string",
150
+ "description": "Internal note explaining why the credit was applied."
151
+ },
152
+ "email": {
153
+ "type": "string",
154
+ "description": "Customer account email."
155
+ },
156
+ "amount": {
157
+ "type": "number",
158
+ "description": "Credit amount in USD."
159
+ }
160
+ },
161
+ "additionalProperties": false
162
+ },
163
+ "description": "Apply a goodwill loyalty credit to the customer's account, e.g. for a delayed shipment. Returns the new account credit balance."
164
+ }
165
+ },
166
+ {
167
+ "type": "function",
168
+ "function": {
169
+ "name": "escalate_to_human",
170
+ "parameters": {
171
+ "type": "object",
172
+ "required": [
173
+ "reason",
174
+ "summary"
175
+ ],
176
+ "properties": {
177
+ "reason": {
178
+ "enum": [
179
+ "tool_failure",
180
+ "legal_or_fraud",
181
+ "safety_hazard",
182
+ "customer_request"
183
+ ],
184
+ "type": "string",
185
+ "description": "Why the escalation is needed."
186
+ },
187
+ "summary": {
188
+ "type": "string",
189
+ "description": "Concise summary of the issue and what has been tried so far."
190
+ },
191
+ "order_id": {
192
+ "type": "string",
193
+ "description": "Relevant order ID, if known."
194
+ }
195
+ },
196
+ "additionalProperties": false
197
+ },
198
+ "description": "Hand the conversation off to a human agent with a summary. Use for repeated tool failures, legal/fraud mentions, or safety hazards."
199
+ }
200
+ }
201
+ ],
202
+ "tool_choice": "auto",
203
+ "response_format": {
204
+ "type": "text"
205
+ },
206
+ "is_enforcing_response_format": false
207
+ }
208
+ },
209
+ {
210
+ "name": "Demo prompt 2: outreach-email-generator (variables)",
211
+ "description": "Generates a tailored cold outreach email from structured prospect, sender, and offer variables.",
212
+ "version": {
213
+ "description": "maxtok 2000",
214
+ "model": "gemini/gemini-2.5-flash",
215
+ "messages": [
216
+ {
217
+ "role": "system",
218
+ "content": "You are a senior B2B sales copywriter for {{company_name}}, a company that sells {{product_description}}.\n\nWrite ONE cold outreach email. Follow every rule:\n\n# Structure\n- Subject line: under 7 words, specific to the recipient's company or role. No clickbait, no ALL CAPS, no emojis.\n- Body: {{max_words}} words maximum, 3 short paragraphs:\n 1. A specific hook tied to the recipient's company, role, or the context note below — never a generic \"I hope this finds you well.\"\n 2. Connect {{product_description}} to a concrete pain point someone in the role of {{recipient_title}} in the {{industry}} industry likely feels. Reference one quantifiable benefit.\n 3. A single low-friction call to action: {{call_to_action}}.\n\n# Voice\n- Tone: {{tone}}.\n- Write at a 7th-grade reading level. Short sentences. No jargon, no buzzwords (\"synergy,\" \"leverage,\" \"cutting-edge\"), no exclamation points.\n- Sound like a real person wrote it, not a template.\n\n# Sign-off\nSign as:\n{{sender_name}}\n{{sender_title}}, {{company_name}}\n\nOutput only the email, formatted as:\nSubject: <subject line>\n\n<body>"
219
+ },
220
+ {
221
+ "role": "user",
222
+ "content": "Draft the email for this prospect:\n\n- Name: {{recipient_name}}\n- Title: {{recipient_title}}\n- Company: {{recipient_company}}\n- Industry: {{industry}}\n- Context I know about them: {{personalization_note}}\n- Recent trigger event: {{recent_trigger}}"
223
+ }
224
+ ],
225
+ "temperature": 0.75,
226
+ "max_tokens": 2000,
227
+ "top_p": 1,
228
+ "frequency_penalty": 0,
229
+ "presence_penalty": 0,
230
+ "stream": false,
231
+ "variables": {
232
+ "tone": "friendly, direct, and consultative ",
233
+ "industry": "",
234
+ "max_words": "",
235
+ "sender_name": "",
236
+ "company_name": "",
237
+ "sender_title": "",
238
+ "call_to_action": "",
239
+ "recent_trigger": "",
240
+ "recipient_name": "",
241
+ "recipient_title": "",
242
+ "recipient_company": "",
243
+ "product_description": "",
244
+ "personalization_note": ""
245
+ },
246
+ "response_format": {
247
+ "type": "text"
248
+ },
249
+ "is_enforcing_response_format": false
250
+ }
251
+ },
252
+ {
253
+ "name": "Demo prompt 3: call-transcript-classifier (JSON formatting)",
254
+ "description": "Classifies a customer service call transcript and extracts structured QA, sentiment, and outcome data as strict JSON.",
255
+ "version": {
256
+ "description": "classifier ready",
257
+ "model": "openai/gpt-5-mini",
258
+ "messages": [
259
+ {
260
+ "role": "system",
261
+ "content": "You are a call-analytics engine for a customer service team. You receive a full transcript of a recorded phone call between a customer and a support agent. Speaker turns are labeled (e.g., \"Agent:\", \"Customer:\").\n\nAnalyze the ENTIRE transcript and return a single structured classification. Follow these rules:\n\n# Classification rules\n- primary_reason: the main reason the customer called. Choose the single best-fit category.\n- resolution_status: judge whether the customer's issue was actually resolved by the end of the call, based on what was said - not on the agent's optimism.\n- customer_sentiment_start / customer_sentiment_end: assess the customer's tone at the beginning vs. the end of the call separately, so sentiment shifts are visible.\n- escalation_requested: true if the customer asked for a manager/supervisor, threatened to cancel, or mentioned legal action, regulators, or chargebacks.\n- followup_required: true if anything was promised but not completed on the call (a callback, an email, a shipment, an investigation).\n\n# Extraction rules\n- order_ids: extract every order ID mentioned (format like \"A-48217\"). Empty array if none.\n- products_mentioned: list distinct product names or models discussed.\n- agent_name: the agent's name if stated, otherwise null.\n- summary: 1-2 neutral sentences, under 40 words, describing the call and its outcome.\n- key_quote: the single most representative customer quote (verbatim, trimmed to under 25 words).\n\n# QA rules\n- agent_followed_greeting: true if the agent identified themselves and the company near the start.\n- agent_empathy_shown: true if the agent acknowledged the customer's frustration or thanked them for patience when warranted.\n- compliance_flags: list any of the allowed flags observed; empty array if none.\n\nBase every field strictly on the transcript. Do not infer facts that were not said."
262
+ },
263
+ {
264
+ "role": "user",
265
+ "content": "Classify the following customer service call transcript:\n\n{{call_transcript}}"
266
+ }
267
+ ],
268
+ "temperature": 0.7,
269
+ "max_tokens": 4096,
270
+ "top_p": 1,
271
+ "frequency_penalty": 0,
272
+ "presence_penalty": 0,
273
+ "stream": false,
274
+ "response_format": {
275
+ "type": "json_schema",
276
+ "json_schema": {
277
+ "name": "call_classification",
278
+ "schema": {
279
+ "type": "object",
280
+ "required": [
281
+ "primary_reason",
282
+ "resolution_status",
283
+ "customer_sentiment_start",
284
+ "customer_sentiment_end",
285
+ "escalation_requested",
286
+ "followup_required",
287
+ "order_ids",
288
+ "products_mentioned",
289
+ "agent_name",
290
+ "summary",
291
+ "key_quote",
292
+ "agent_followed_greeting",
293
+ "agent_empathy_shown",
294
+ "compliance_flags"
295
+ ],
296
+ "properties": {
297
+ "summary": {
298
+ "type": "string"
299
+ },
300
+ "key_quote": {
301
+ "type": "string"
302
+ },
303
+ "order_ids": {
304
+ "type": "array",
305
+ "items": {
306
+ "type": "string"
307
+ }
308
+ },
309
+ "agent_name": {
310
+ "type": [
311
+ "string",
312
+ "null"
313
+ ]
314
+ },
315
+ "primary_reason": {
316
+ "enum": [
317
+ "shipping_delay",
318
+ "billing_dispute",
319
+ "product_defect",
320
+ "return_or_refund",
321
+ "technical_support",
322
+ "account_management",
323
+ "general_inquiry",
324
+ "other"
325
+ ],
326
+ "type": "string"
327
+ },
328
+ "compliance_flags": {
329
+ "type": "array",
330
+ "items": {
331
+ "enum": [
332
+ "no_identity_verification",
333
+ "promise_without_confirmation",
334
+ "rude_or_dismissive",
335
+ "shared_sensitive_data",
336
+ "no_resolution_offered"
337
+ ],
338
+ "type": "string"
339
+ }
340
+ },
341
+ "followup_required": {
342
+ "type": "boolean"
343
+ },
344
+ "resolution_status": {
345
+ "enum": [
346
+ "resolved",
347
+ "partially_resolved",
348
+ "unresolved",
349
+ "escalated"
350
+ ],
351
+ "type": "string"
352
+ },
353
+ "products_mentioned": {
354
+ "type": "array",
355
+ "items": {
356
+ "type": "string"
357
+ }
358
+ },
359
+ "agent_empathy_shown": {
360
+ "type": "boolean"
361
+ },
362
+ "escalation_requested": {
363
+ "type": "boolean"
364
+ },
365
+ "customer_sentiment_end": {
366
+ "enum": [
367
+ "positive",
368
+ "neutral",
369
+ "frustrated",
370
+ "angry"
371
+ ],
372
+ "type": "string"
373
+ },
374
+ "agent_followed_greeting": {
375
+ "type": "boolean"
376
+ },
377
+ "customer_sentiment_start": {
378
+ "enum": [
379
+ "positive",
380
+ "neutral",
381
+ "frustrated",
382
+ "angry"
383
+ ],
384
+ "type": "string"
385
+ }
386
+ },
387
+ "additionalProperties": false
388
+ },
389
+ "strict": true
390
+ }
391
+ },
392
+ "is_enforcing_response_format": true
393
+ }
394
+ }
395
+ ];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Datasets seeder for `respan demo`.
3
+ *
4
+ * Creates the demo datasets (see dataset-fixtures.ts) and bulk-loads their rows,
5
+ * then tears them down for `respan demo clear`. Deduplicates by dataset name so
6
+ * re-runs never create duplicates. All dataset-specific logic lives here; the
7
+ * orchestrator stays service-agnostic.
8
+ *
9
+ * Rows are written via `bulkCreateDatasetLogs` (POST .../logs/bulk/), which is
10
+ * synchronous and returns success/error counts. Each row's `input` object becomes
11
+ * the row's mapped variables; `expected_output` is JSON-stringified so the grader
12
+ * sees valid JSON (a raw object is coerced to single-quoted python-repr).
13
+ */
14
+ import { Seeder } from './types.js';
15
+ export declare const datasetsSeeder: Seeder;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Datasets seeder for `respan demo`.
3
+ *
4
+ * Creates the demo datasets (see dataset-fixtures.ts) and bulk-loads their rows,
5
+ * then tears them down for `respan demo clear`. Deduplicates by dataset name so
6
+ * re-runs never create duplicates. All dataset-specific logic lives here; the
7
+ * orchestrator stays service-agnostic.
8
+ *
9
+ * Rows are written via `bulkCreateDatasetLogs` (POST .../logs/bulk/), which is
10
+ * synchronous and returns success/error counts. Each row's `input` object becomes
11
+ * the row's mapped variables; `expected_output` is JSON-stringified so the grader
12
+ * sees valid JSON (a raw object is coerced to single-quoted python-repr).
13
+ */
14
+ import { DEMO_DATASETS } from './dataset-fixtures.js';
15
+ /** Datasets in the account whose name exactly matches a demo fixture name.
16
+ *
17
+ * Listed in full and matched client-side (case-sensitive, exact) rather than via
18
+ * server filters, which keeps dedup independent of filter-operator semantics.
19
+ * Returns every match so duplicates are all found (for clear). */
20
+ async function findExistingDatasets(ctx) {
21
+ const wanted = new Set(DEMO_DATASETS.map((d) => d.name));
22
+ const response = await ctx.client.datasets.listDatasets({
23
+ Authorization: ctx.authHeader,
24
+ page_size: 100,
25
+ });
26
+ const found = [];
27
+ for (const row of response.results ?? []) {
28
+ const name = typeof row.name === 'string' ? row.name : '';
29
+ const id = String(row.id ?? row.dataset_id ?? '');
30
+ if (id && wanted.has(name))
31
+ found.push({ name, id });
32
+ }
33
+ return found;
34
+ }
35
+ /** Create one dataset and bulk-load its rows. Returns the new dataset id. */
36
+ async function createFromFixture(ctx, fixture) {
37
+ const created = (await ctx.client.datasets.createDataset({
38
+ Authorization: ctx.authHeader,
39
+ name: fixture.name,
40
+ is_empty: true,
41
+ ...(fixture.description ? { description: fixture.description } : {}),
42
+ }));
43
+ const datasetId = String(created.id ?? created.dataset_id ?? '');
44
+ if (!datasetId) {
45
+ throw new Error(`createDataset did not return an id for "${fixture.name}".`);
46
+ }
47
+ const logs = fixture.rows.map((row) => ({
48
+ input: row.input,
49
+ ...(row.expected_output !== undefined
50
+ ? {
51
+ expected_output: typeof row.expected_output === 'string'
52
+ ? row.expected_output
53
+ : JSON.stringify(row.expected_output),
54
+ }
55
+ : {}),
56
+ }));
57
+ const result = (await ctx.client.datasets.bulkCreateDatasetLogs({
58
+ Authorization: ctx.authHeader,
59
+ dataset_id: datasetId,
60
+ logs,
61
+ }));
62
+ const errorCount = typeof result.error_count === 'number' ? result.error_count : 0;
63
+ if (errorCount > 0) {
64
+ const detail = Array.isArray(result.errors) ? `: ${JSON.stringify(result.errors).slice(0, 200)}` : '';
65
+ throw new Error(`bulkCreateDatasetLogs reported ${errorCount} error(s) for "${fixture.name}"${detail}`);
66
+ }
67
+ return datasetId;
68
+ }
69
+ export const datasetsSeeder = {
70
+ service: 'datasets',
71
+ label: 'Datasets',
72
+ findExisting(ctx) {
73
+ return findExistingDatasets(ctx);
74
+ },
75
+ async run(ctx) {
76
+ const existingNames = new Set((await findExistingDatasets(ctx)).map((d) => d.name));
77
+ const items = [];
78
+ for (const fixture of DEMO_DATASETS) {
79
+ if (existingNames.has(fixture.name)) {
80
+ items.push({ name: fixture.name, status: 'skipped', reason: 'already exists' });
81
+ continue;
82
+ }
83
+ const id = await createFromFixture(ctx, fixture);
84
+ items.push({ name: fixture.name, status: 'created', id });
85
+ }
86
+ return { service: this.service, label: this.label, items };
87
+ },
88
+ async clear(ctx) {
89
+ const existing = await findExistingDatasets(ctx);
90
+ const items = [];
91
+ for (const fixture of DEMO_DATASETS) {
92
+ const matches = existing.filter((d) => d.name === fixture.name);
93
+ if (matches.length === 0) {
94
+ items.push({ name: fixture.name, status: 'skipped', reason: 'not found' });
95
+ continue;
96
+ }
97
+ for (const match of matches) {
98
+ await ctx.client.datasets.deleteDataset({
99
+ Authorization: ctx.authHeader,
100
+ dataset_id: match.id,
101
+ });
102
+ items.push({ name: match.name, status: 'deleted', id: match.id });
103
+ }
104
+ }
105
+ return { service: this.service, label: this.label, items };
106
+ },
107
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Evaluators seeder for `respan demo`.
3
+ *
4
+ * Recreates the demo evaluators (see evaluator-fixtures.ts) in the authenticated
5
+ * account and tears them down for `respan demo clear`. Each evaluator is two kinds
6
+ * of resource: grader record(s) and a Blockly workflow that references them.
7
+ *
8
+ * Recreate recipe (the engine — LLM vs human — lives in the workflow node, not the
9
+ * grader record, so the captured graph is replayed verbatim):
10
+ * 1. create each grader (or reuse one with the same name)
11
+ * 2. map each grader's captured `originalId` to its new id
12
+ * 3. deep-clone the task graph and rewrite every eval task's `config.evaluator_id`
13
+ * via that map (task ids are kept verbatim so the compute task's
14
+ * `state.<task_id>` input references stay valid)
15
+ * 4. createWorkflow -> commit -> deploy v1
16
+ *
17
+ * Dedup is by workflow name (the user-facing evaluator name). The SDK has no commit
18
+ * method, so commit is a raw POST to `/api/workflows/{id}/commits/` (as in
19
+ * lib/eval/runner.ts). All evaluator-specific logic lives here.
20
+ */
21
+ import { Seeder } from './types.js';
22
+ export declare const evaluatorsSeeder: Seeder;