Use the workbench from your own code
Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS, so you can script it from any language: batch a backlog of open NCRs, wire it into a QMS export, or re-run an assessment nightly as new inspection data lands. This page walks through each call with examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"ok":true,"data":…} on success,
{"error":{"code","message"}} on failure.
There is no /apps/ncr-workbench/ path segment. The app is bound to the token
itself, which is minted at POST /v1/app-api/guest with the slug — every
later call is just /me, /estimate, /run or
/run-stream.
| Code | Meaning | What to do |
|---|---|---|
unauthorized | Missing, stale or revoked token | Mint a new guest token, or sign in for a personal one |
not_found | The route does not exist | Check for a stray /apps/<slug>/ segment — that is the usual cause |
payment_required | Balance below the run minimum | Top up; /estimate tells you the reserve and the minimum in advance |
validation_error | Body is not the input object | Send the fields at the top level, not wrapped in {"input": …} |
rate_limited | Too many requests | Back off and retry; do not tight-loop |
/me, /estimate and /guest are free. /run
and /run-stream spend credits.
The input this app sends
These are the exact fields the app's own run path submits.
| Field | Type | Notes |
|---|---|---|
ncr | string, required | The nonconformance material as pasted: form dump, inspection results, an SPC alert, a complaint thread. The app cuts the middle at 40,000 characters and keeps both ends, announcing the cut in-band; do the same rather than truncating the tail, because the ask and the history live at the end. |
context | string, optional | Up to 6,000 characters. The framework in force, the program, the decision needed, who reads the answer. Also where the app folds in answers to a previous run's open questions. |
spc | string, optional | A plain-text I-MR block: n, mean, MRbar, sigma, control limits, Western Electric hits, Cp/Cpk and the raw values. Treated by the prompt as a hint to verify, never as fact. The app computes it in the browser; you can compute it yourself or omit it. |
retry_note | string, optional | Only used on the app's one automatic reformat retry: a verbatim restatement of the output shape, sent when the first reply failed to parse. Omit it on a first attempt. |
Idempotency-Key header on every run. The app derives it from a hash of
the input plus the attempt number, so a dropped connection replays the same job instead of
billing a second one.
The output contract
The reply is plain text — no JSON, no code fence around the whole response. A reply that breaks any of these rules is rejected by the app's parser, which then retries once.
SEVERITY: Critical | Major | Minor DISPOSITION: Use as is | Rework | Repair | Scrap | Return to supplier | Insufficient information CONFIDENCE: <integer 0-100, bare, no percent sign> SUMMARY: <2-4 sentences, may wrap, ends at the first blank line> ## Containment - … ## Root cause analysis - … ## Disposition rationale - … ## CAPA plan - … ## Regulatory and escalation - … ## Open questions - … (or the single bullet "- None.")
- bullet; a bullet may wrap onto indented continuation lines. A
section with nothing to report carries the single bullet - None.
1. Get a token
A guest token is one call and needs no account. For a personal token that bills your own credits, open the token page and sign in — it shows the token, copies it, and copies a ready-made shell export. You never need the browser developer console.
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}"
# Or mint a guest token, no account needed:
TOKEN=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug":"ncr-workbench"}' | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["token"])')
echo "${TOKEN:0:8}..."
import json, urllib.request
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # or leave blank and mint a guest token below
def call(path, body=None, token=None, method=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method or ("POST" if data else "GET"))
req.add_header("Content-Type", "application/json")
if token:
req.add_header("Authorization", "Bearer " + token)
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]
if TOKEN == "YOUR_TOKEN":
TOKEN = call("/guest", {"slug": "ncr-workbench"})["token"]
print(TOKEN[:8] + "...")
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // or mint a guest token below
async function call(path, body, method) {
const res = await fetch(API + path, {
method: method || (body ? "POST" : "GET"),
headers: {
"Content-Type": "application/json",
...(TOKEN && TOKEN !== "YOUR_TOKEN" ? { Authorization: "Bearer " + TOKEN } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.code + ": " + json.error?.message);
return json.data;
}
if (TOKEN === "YOUR_TOKEN") {
TOKEN = (await call("/guest", { slug: "ncr-workbench" })).token;
}
console.log(TOKEN.slice(0, 8) + "...");
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN"
func call(path string, body any, out any) error {
var buf *bytes.Buffer = bytes.NewBuffer(nil)
method := "GET"
if body != nil {
method = "POST"
b, _ := json.Marshal(body)
buf = bytes.NewBuffer(b)
}
req, _ := http.NewRequest(method, API+path, buf)
req.Header.Set("Content-Type", "application/json")
if token != "YOUR_TOKEN" {
req.Header.Set("Authorization", "Bearer "+token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
return json.Unmarshal(env.Data, out)
}
func main() {
var guest struct{ Token string `json:"token"` }
if token == "YOUR_TOKEN" {
_ = call("/guest", map[string]string{"slug": "ncr-workbench"}, &guest)
token = guest.Token
}
fmt.Println(token[:8] + "...")
}
import java.net.URI;
import java.net.http.*;
public class Workbench {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String body) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
.header("Content-Type", "application/json");
if (!token.equals("YOUR_TOKEN")) b.header("Authorization", "Bearer " + token);
b = body == null ? b.GET() : b.POST(HttpRequest.BodyPublishers.ofString(body));
return HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
if (token.equals("YOUR_TOKEN")) {
String res = call("/guest", "{\"slug\":\"ncr-workbench\"}");
token = res.replaceAll(".*\"token\"\\s*:\\s*\"([^\"]+)\".*", "$1");
}
System.out.println(token.substring(0, 8) + "...");
}
}
require "json"
require "net/http"
API = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN"
def call(path, body = nil, token: nil, method: nil)
uri = URI(API.to_s + path)
req = if body || method == "POST"
Net::HTTP::Post.new(uri)
else
Net::HTTP::Get.new(uri)
end
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}" if token
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)["data"]
end
token = TOKEN == "YOUR_TOKEN" ? call("/guest", { slug: "ncr-workbench" })["token"] : TOKEN
puts token[0, 8] + "..."
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
function call(string $path, ?array $body = null, ?string $token = null, string $method = null): array {
$headers = ["Content-Type: application/json"];
if ($token) { $headers[] = "Authorization: Bearer $token"; }
$ch = curl_init(API . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
} elseif ($method) {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
}
$raw = curl_exec($ch);
curl_close($ch);
return json_decode($raw, true)["data"];
}
if ($token === "YOUR_TOKEN") {
$token = call("/guest", ["slug" => "ncr-workbench"])["token"];
}
echo substr($token, 0, 8) . "...\n";
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string API = "https://api.skillsafe.ai/v1/app-api";
string token = "YOUR_TOKEN";
var http = new HttpClient();
async Task<JsonElement> Call(string path, object? body = null, string? method = null) {
var req = new HttpRequestMessage(
body != null ? HttpMethod.Post : new HttpMethod(method ?? "GET"), API + path);
if (body != null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
if (token != "YOUR_TOKEN")
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var res = await http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("data");
}
if (token == "YOUR_TOKEN") {
var guest = await Call("/guest", new { slug = "ncr-workbench" });
token = guest.GetProperty("token").GetString()!;
}
Console.WriteLine(token[..8] + "...");
2. Check who you are and what you can spend
Free. Returns subject_type (user or guest), subject_id and credits.
curl -s https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer $TOKEN"
# {"ok":true,"data":{"subject_type":"guest","subject_id":"gst_...","credits":0}}
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"])
const me = await call("/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = call("/me", nil, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("/me", null));
me = call("/me", token: token)
puts "#{me['subject_type']} #{me['credits']}"
$me = call("/me", null, $token);
echo $me["subject_type"] . " " . $me["credits"] . "\n";
var me = await Call("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
3. Estimate before you spend
Free, creates no job and charges nothing. hold_credits is what gets
reserved, not the price — the actual charge is usually far lower, because the
hold prices the full output cap. If your balance sits between min_credits and
hold_credits the run still executes with a reduced cap and comes back with
truncated: true. Compare the balance from step 2 against
hold_credits before you submit; a 402 after the fact is avoidable.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"ncr":"NCR-25-0642 bore diameter drifting on 118 pcs ...","context":"IATF 16949, special characteristic","spc":""}'
# {"hold_credits":1578,"min_credits":149,"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000}
payload = {
"ncr": "NCR-25-0642 bore diameter drifting on 118 pcs ...",
"context": "IATF 16949, special characteristic",
"spc": "",
}
est = call("/estimate", payload, token=TOKEN)
print(est["hold_credits"], est["min_credits"], est["model"])
const payload = {
ncr: "NCR-25-0642 bore diameter drifting on 118 pcs ...",
context: "IATF 16949, special characteristic",
spc: "",
};
const est = await call("/estimate", payload);
console.log(est.hold_credits, est.min_credits, est.model);
payload := map[string]string{
"ncr": "NCR-25-0642 bore diameter drifting on 118 pcs ...",
"context": "IATF 16949, special characteristic",
"spc": "",
}
var est struct {
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
Model string `json:"model"`
}
_ = call("/estimate", payload, &est)
fmt.Println(est.HoldCredits, est.MinCredits, est.Model)
String payload = "{\"ncr\":\"NCR-25-0642 bore diameter drifting on 118 pcs ...\","
+ "\"context\":\"IATF 16949, special characteristic\",\"spc\":\"\"}";
System.out.println(call("/estimate", payload));
payload = {
ncr: "NCR-25-0642 bore diameter drifting on 118 pcs ...",
context: "IATF 16949, special characteristic",
spc: ""
}
est = call("/estimate", payload, token: token)
puts "#{est['hold_credits']} #{est['min_credits']} #{est['model']}"
$payload = [
"ncr" => "NCR-25-0642 bore diameter drifting on 118 pcs ...",
"context" => "IATF 16949, special characteristic",
"spc" => "",
];
$est = call("/estimate", $payload, $token);
echo "{$est['hold_credits']} {$est['min_credits']} {$est['model']}\n";
var payload = new {
ncr = "NCR-25-0642 bore diameter drifting on 118 pcs ...",
context = "IATF 16949, special characteristic",
spc = ""
};
var est = await Call("/estimate", payload);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("model")}");
4. Run it, and poll for the result
This spends credits. The body is the input object directly —
not wrapped in {"input": …}. Send an Idempotency-Key: reuse
the same key and the same job comes back instead of a second charge. /run
returns a job_id; poll /jobs/{id} until status is
succeeded or failed, then read
output.output and parse it against the contract above.
KEY="ncr-workbench-$(printf '%s' "$NCR_TEXT" | shasum | cut -c1-8)-1" JOB=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d @payload.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])') until curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \ -H "Authorization: Bearer $TOKEN" | grep -q '"status":"succeeded"'; do sleep 2; done curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \ | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time, urllib.request
key = "ncr-workbench-" + hashlib.sha256(payload["ncr"].encode()).hexdigest()[:8] + "-1"
req = urllib.request.Request(API + "/run", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call("/jobs/" + job_id, token=TOKEN)
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
print(job["output"]["output"])
const enc = new TextEncoder().encode(payload.ncr);
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", enc))]
.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
const key = `ncr-workbench-${digest}-1`;
const res = await fetch(API + "/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + TOKEN,
"Idempotency-Key": key,
},
body: JSON.stringify(payload),
});
const { data } = await res.json();
let job;
do {
await new Promise((r) => setTimeout(r, 2000));
job = await call("/jobs/" + data.job_id);
} while (job.status !== "succeeded" && job.status !== "failed");
console.log(job.output.output);
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "ncr-workbench-"+fingerprint+"-1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var env struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&env)
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
for {
_ = call("/jobs/"+env.Data.JobID, nil, &job)
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(2 * time.Second)
}
fmt.Println(job.Output.Output)
HttpRequest run = HttpRequest.newBuilder(URI.create(API + "/run"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "ncr-workbench-" + fingerprint + "-1")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
String created = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = created.replaceAll(".*\"job_id\"\\s*:\\s*\"([^\"]+)\".*", "$1");
String job;
do {
Thread.sleep(2000);
job = call("/jobs/" + jobId, null);
} while (!job.contains("\"status\":\"succeeded\"") && !job.contains("\"status\":\"failed\""));
System.out.println(job);
require "digest"
key = "ncr-workbench-#{Digest::SHA256.hexdigest(payload[:ncr])[0, 8]}-1"
uri = URI("#{API}/run")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("/jobs/#{job_id}", token: token)
if %w[succeeded failed].include?(job["status"])
puts job.dig("output", "output")
break
end
sleep 2
end
$key = "ncr-workbench-" . substr(hash("sha256", $payload["ncr"]), 0, 8) . "-1";
$ch = curl_init(API . "/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer $token",
"Idempotency-Key: $key",
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
do {
sleep(2);
$job = call("/jobs/$jobId", null, $token);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
echo $job["output"]["output"] . "\n";
using System.Security.Cryptography;
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload.ncr)))[..8].ToLower();
var runReq = new HttpRequestMessage(HttpMethod.Post, API + "/run") {
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
runReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
runReq.Headers.Add("Idempotency-Key", $"ncr-workbench-{hash}-1");
var created = JsonDocument.Parse(await (await http.SendAsync(runReq)).Content.ReadAsStringAsync());
var jobId = created.RootElement.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
do {
await Task.Delay(2000);
job = await Call("/jobs/" + jobId);
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));
Console.WriteLine(job.GetProperty("output").GetProperty("output").GetString());
5. Stream it instead
This spends credits. /run-stream returns
text/event-stream: delta events carry output as it generates and a
final done event carries charged_credits, truncated
and the authoritative full output. Prefer the done payload over
the concatenated deltas — the stream can drop the tail. The same
Idempotency-Key rule applies. In the browser, the vendored SDK wraps this:
ss.runStream(input, { idempotencyKey, onDelta }).
curl -N -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @payload.json
# event: delta
# data: {"text":"SEVERITY: Major\n"}
# ...
# event: done
# data: {"job_id":"job_...","charged_credits":412,"truncated":false,"output":{"output":"SEVERITY: ..."}}
req = urllib.request.Request(API + "/run-stream", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
full = ""
with urllib.request.urlopen(req) as stream:
for raw in stream:
line = raw.decode().strip()
if line.startswith("data:"):
evt = json.loads(line[5:].strip())
if "text" in evt:
full += evt["text"]
elif "output" in evt:
full = evt["output"]["output"] # authoritative
print(full)
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + TOKEN,
"Idempotency-Key": key,
},
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", full = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const evt = JSON.parse(line.slice(5).trim());
if (evt.text) full += evt.text;
else if (evt.output) full = evt.output.output; // authoritative
}
}
console.log(full);
b, _ = json.Marshal(payload)
req, _ = http.NewRequest("POST", API+"/run-stream", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", key)
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()
full := ""
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var evt struct {
Text string `json:"text"`
Output *struct {
Output string `json:"output"`
} `json:"output"`
}
_ = json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &evt)
if evt.Output != nil {
full = evt.Output.Output
} else {
full += evt.Text
}
}
fmt.Println(full)
HttpRequest stream = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
StringBuilder full = new StringBuilder();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body()
.filter(l -> l.startsWith("data:"))
.forEach(l -> full.append(l.substring(5).trim()).append("\n"));
System.out.println(full); // parse each line as JSON; prefer the final done event
uri = URI("#{API}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
full = ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
evt = JSON.parse(line[5..].strip) rescue next
full = evt.dig("output", "output") || (full + evt.fetch("text", ""))
end
end
end
end
puts full
$full = "";
$ch = curl_init(API . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer $token",
"Idempotency-Key: $key",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$full) {
foreach (explode("\n", $chunk) as $line) {
if (strpos($line, "data:") !== 0) { continue; }
$evt = json_decode(trim(substr($line, 5)), true);
if (isset($evt["output"]["output"])) { $full = $evt["output"]["output"]; }
elseif (isset($evt["text"])) { $full .= $evt["text"]; }
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
echo $full . "\n";
var streamReq = new HttpRequestMessage(HttpMethod.Post, API + "/run-stream") {
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
streamReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
streamReq.Headers.Add("Idempotency-Key", key);
var resp = await http.SendAsync(streamReq, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await resp.Content.ReadAsStreamAsync());
var full = "";
while (await reader.ReadLineAsync() is string line) {
if (!line.StartsWith("data:")) continue;
var evt = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (evt.TryGetProperty("output", out var o))
full = o.GetProperty("output").GetString()!;
else if (evt.TryGetProperty("text", out var t))
full += t.GetString();
}
Console.WriteLine(full);
6. Parse the reply
The four tag lines come first, then the six sections. A minimal, faithful parser: read
SEVERITY, DISPOSITION and CONFIDENCE from the leading
lines, take SUMMARY up to the first blank line, then split on
## headings and read - bullets. Reject the reply rather than
guessing if a tag or a heading is missing — that is what the app does, and it is why
the retry exists.
# Reject anything that does not open with the contract: head -1 reply.txt | grep -Eq '^SEVERITY: (Critical|Major|Minor)$' || echo "contract violated" grep -c '^## ' reply.txt # must be 6
import re
SECTIONS = ["Containment", "Root cause analysis", "Disposition rationale",
"CAPA plan", "Regulatory and escalation", "Open questions"]
def parse(text):
sev = re.search(r"^SEVERITY:\s*(Critical|Major|Minor)\s*$", text, re.M)
dis = re.search(r"^DISPOSITION:\s*(.+?)\s*$", text, re.M)
con = re.search(r"^CONFIDENCE:\s*(\d{1,3})\s*$", text, re.M)
summ = re.search(r"^SUMMARY:\s*(.*?)(?:\n\s*\n)", text, re.M | re.S)
if not (sev and dis and con and summ):
return None
out = {"severity": sev.group(1), "disposition": dis.group(1),
"confidence": int(con.group(1)), "summary": " ".join(summ.group(1).split()),
"sections": {}}
for name in SECTIONS:
body = re.search(r"^##\s+" + re.escape(name) + r"\s*$(.*?)(?=^##\s|\Z)",
text, re.M | re.S)
if body is None:
return None
bullets = [b.strip() for b in re.findall(r"^-\s+(.*)$", body.group(1), re.M)]
out["sections"][name] = [] if bullets == ["None."] else bullets
return out
const SECTIONS = ["Containment", "Root cause analysis", "Disposition rationale",
"CAPA plan", "Regulatory and escalation", "Open questions"];
function parse(text) {
const sev = /^SEVERITY:\s*(Critical|Major|Minor)\s*$/m.exec(text);
const dis = /^DISPOSITION:\s*(.+?)\s*$/m.exec(text);
const con = /^CONFIDENCE:\s*(\d{1,3})\s*$/m.exec(text);
const summ = /^SUMMARY:\s*([\s\S]*?)\n\s*\n/m.exec(text);
if (!sev || !dis || !con || !summ) return null;
const sections = {};
for (const name of SECTIONS) {
const re = new RegExp("^##\\s+" + name + "\\s*$([\\s\\S]*?)(?=^##\\s|$)", "m");
const body = re.exec(text);
if (!body) return null;
const bullets = [...body[1].matchAll(/^-\s+(.*)$/gm)].map((m) => m[1].trim());
sections[name] = bullets.length === 1 && /^none\.?$/i.test(bullets[0]) ? [] : bullets;
}
return { severity: sev[1], disposition: dis[1], confidence: +con[1],
summary: summ[1].replace(/\s+/g, " ").trim(), sections };
}
// Split on the six headings, then read "- " bullets out of each block.
sections := regexp.MustCompile(`(?m)^##\s+(.+)$`).Split(reply, -1)
names := regexp.MustCompile(`(?m)^##\s+(.+)$`).FindAllStringSubmatch(reply, -1)
if len(names) != 6 {
return errors.New("contract violated: expected six sections")
}
for i, n := range names {
bullets := regexp.MustCompile(`(?m)^-\s+(.*)$`).FindAllStringSubmatch(sections[i+1], -1)
fmt.Println(n[1], len(bullets))
}
String[] names = {"Containment", "Root cause analysis", "Disposition rationale",
"CAPA plan", "Regulatory and escalation", "Open questions"};
for (String n : names) {
if (!reply.contains("## " + n)) throw new IllegalStateException("missing section: " + n);
}
java.util.regex.Matcher m =
java.util.regex.Pattern.compile("^SEVERITY:\\s*(Critical|Major|Minor)$",
java.util.regex.Pattern.MULTILINE).matcher(reply);
if (!m.find()) throw new IllegalStateException("no SEVERITY line");
System.out.println(m.group(1));
SECTIONS = ["Containment", "Root cause analysis", "Disposition rationale",
"CAPA plan", "Regulatory and escalation", "Open questions"].freeze
def parse(text)
sev = text[/^SEVERITY:\s*(Critical|Major|Minor)\s*$/, 1]
dis = text[/^DISPOSITION:\s*(.+?)\s*$/, 1]
con = text[/^CONFIDENCE:\s*(\d{1,3})\s*$/, 1]
return nil unless sev && dis && con
sections = SECTIONS.to_h do |name|
body = text[/^##\s+#{Regexp.escape(name)}\s*$(.*?)(?=^##\s|\z)/m, 1]
return nil unless body
bullets = body.scan(/^-\s+(.*)$/).flatten.map(&:strip)
[name, bullets == ["None."] ? [] : bullets]
end
{ severity: sev, disposition: dis, confidence: con.to_i, sections: sections }
end
$sections = ["Containment", "Root cause analysis", "Disposition rationale",
"CAPA plan", "Regulatory and escalation", "Open questions"];
preg_match('/^SEVERITY:\s*(Critical|Major|Minor)\s*$/m', $reply, $sev);
preg_match('/^DISPOSITION:\s*(.+?)\s*$/m', $reply, $dis);
preg_match('/^CONFIDENCE:\s*(\d{1,3})\s*$/m', $reply, $con);
if (!$sev || !$dis || !$con) { throw new RuntimeException("contract violated"); }
$out = [];
foreach ($sections as $name) {
$re = '/^##\s+' . preg_quote($name, '/') . '\s*$(.*?)(?=^##\s|\z)/ms';
if (!preg_match($re, $reply, $body)) { throw new RuntimeException("missing $name"); }
preg_match_all('/^-\s+(.*)$/m', $body[1], $bullets);
$out[$name] = $bullets[1];
}
using System.Text.RegularExpressions;
string[] names = { "Containment", "Root cause analysis", "Disposition rationale",
"CAPA plan", "Regulatory and escalation", "Open questions" };
var sev = Regex.Match(reply, @"^SEVERITY:\s*(Critical|Major|Minor)\s*$", RegexOptions.Multiline);
if (!sev.Success) throw new InvalidOperationException("contract violated");
foreach (var name in names) {
var body = Regex.Match(reply,
$@"^##\s+{Regex.Escape(name)}\s*$(.*?)(?=^##\s|\z)",
RegexOptions.Multiline | RegexOptions.Singleline);
if (!body.Success) throw new InvalidOperationException($"missing section: {name}");
var bullets = Regex.Matches(body.Groups[1].Value, @"^-\s+(.*)$", RegexOptions.Multiline);
Console.WriteLine($"{name}: {bullets.Count}");
}
Insufficient information is a real answer — treat it as a result to act
on, not as a failure to retry.