Work return cases from your own code
Everything the Returns Desk app does goes through the SkillSafe App API: plain JSON over HTTPS, one bearer token, no SDK required. Paste a case in, get back a worked assessment — the case classified, the item graded, one next action called, the fraud signals read, the disposition argued and the refund figure stated.
This page is the whole surface: the routes, the exact fields the app itself sends, the output contract the reply is parsed from, and the error codes. Every step below is shown in eight languages — pick one and the whole page follows.
Base URL and the envelope
Base URL: https://api.skillsafe.ai/v1/app-api. Send
Authorization: Bearer <token> on every call except
/guest, and Content-Type: application/json
on every body.
Every response is wrapped the same way:
{"ok": true, "data": { ... }, "meta": { ... }}
{"ok": false, "error": {"code": "...", "message": "...", "details": { ... }}}
Check ok, then read data. The examples
below all unwrap it in their client helper.
The routes
There is no /apps/{slug}/ path
segment. The slug is bound to the token once, at
POST /guest; after that the token is the app. If you find
yourself building a URL with returns-desk in the path, that is the
404 you are about to get.
| Route | What it does |
|---|---|
| POST /v1/app-api/guest | Body {"slug":"returns-desk"}. Returns token, guest_id. No auth header needed — this is what mints one. |
| GET /v1/app-api/me | subject_type ("user" or "guest"), subject_id, credits. |
| POST /v1/app-api/estimate | Free. No job, no charge, no hold. Returns model, model_alias, markup_bps, hold_credits, min_credits, sponsor_enabled. |
| POST /v1/app-api/run | Returns job_id. Accepts Idempotency-Key. |
| GET /v1/app-api/jobs/{job_id} | Poll until status is succeeded or failed. |
| POST /v1/app-api/run-stream | The same run, answered as Server-Sent Events. Accepts Idempotency-Key. |
The input object
The body of /estimate and /run
is the input object directly — it is not wrapped in
{"input": …}.
| Field | Type | What it carries |
|---|---|---|
| case | string, required | The pasted return case: the RMA ticket, the customer emails, the order lines and
values, the receiving and inspection notes, serials, the account's return history, the
warranty terms. Messy dumps beat polished summaries — the dates, serials and
values are what the decision is argued from. The app clips this at 40,000 characters by
dropping the middle and keeping both ends, leaving an in-band
[case truncated: N characters removed from the middle…]
marker so the model knows what it cannot see. Do the same rather than a blind head
slice. |
| context | string, optional | Policy in force (window, fees, receipt rules), channel and category, customer standing, and what decision is actually needed. Clipped the same way at 6,000 characters. Whatever you leave out comes back as an open question rather than an assumption. |
| facts | string, optional | Deterministic text the app's in-browser calculator produces: the category and grade selected, the item value, the restocking fee and net refund, the ten-signal fraud score with its band, the standard disposition, the vendor recovery band, and any input sitting near a threshold. It is a hint, not a fact — it reflects what somebody ticked, and the assessment is free to contradict it (the app surfaces exactly that disagreement in its reconciliation panel). |
| retry_note | string, optional | App-internal. The app sends it only on its automatic one-shot reformat retry, when the first reply failed to parse; it restates the required output shape. A direct API caller normally omits it. |
Error codes
Failures come back as {"ok": false, "error": {"code", "message",
"details"}}. Branch on code, not on the message text.
| Code | HTTP | What to do |
|---|---|---|
| unauthorized | 401 | Missing or expired token. Mint a new guest token, or re-copy your personal one from the token page. |
| not_found | 404 | Wrong route. You probably inserted an /apps/{slug}/ segment that does not exist — or the job id is wrong. |
| payment_required | 402 | Not enough credits for the hold. Top up, or check min_credits from /estimate first. |
| validation_error | 400 | Bad input shape. Usually a missing case, or the input wrapped in {"input": …} when it should be sent directly. |
| rate_limited | 429 | Back off and retry — exponential, with jitter. Reuse the same Idempotency-Key so the retry cannot double-bill. |
| sponsor_exhausted | 402 | The day's sponsored guest allowance is used up. Sign in and run on your own credits. |
1. A tiny client helper
Every call below is one HTTP request with a bearer token and a JSON body, so start with a small helper that adds the header, sends the body and unwraps the envelope. Later steps reuse it. Browsers enforce CORS on this API — run these from a server, a script or a terminal, not from another site's frontend.
API="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # step 2 mints one
# Every call is one of these two shapes:
# curl -s "$API/me" -H "Authorization: Bearer $TOKEN"
# curl -s -X POST "$API/run" -H "Authorization: Bearer $TOKEN" \
# -H "Content-Type: application/json" -d @input.json
#
# Success: {"ok":true,"data":{ ... },"meta":{ ... }}
# Failure: {"ok":false,"error":{"code":"...","message":"...","details":{ ... }}}
#
# jq '.data' pulls the payload out of the envelope.
import json, urllib.request, urllib.error
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # step 2 mints one
def api(method, path, body=None, headers=None):
data = None if body is None else json.dumps(body).encode("utf-8")
req = urllib.request.Request(API + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req) as res:
payload = json.load(res)
except urllib.error.HTTPError as exc:
payload = json.load(exc)
if not payload.get("ok"):
err = payload.get("error") or {}
raise RuntimeError(err.get("code", "error") + ": " + err.get("message", ""))
return payload["data"]
// Node 18+ (built-in fetch), or any server-side runtime you control.
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // step 2 mints one
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
...extraHeaders,
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await res.json();
if (!payload.ok) {
const err = payload.error || {};
throw new Error(`${err.code || res.status}: ${err.message || res.statusText}`);
}
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const (
API = "https://api.skillsafe.ai/v1/app-api"
Token = "YOUR_TOKEN" // step 2 mints one
)
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(method, path string, body, out any, extra map[string]string) error {
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}
}
req, err := http.NewRequest(method, API+path, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+Token)
req.Header.Set("Content-Type", "application/json")
for k, v := range extra {
req.Header.Set(k, v)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return err
}
if !env.OK {
return fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair it with your JSON library of choice
// (Jackson, Gson, org.json) to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
public final class ReturnsDesk {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // step 2 mints one
static final HttpClient HTTP = HttpClient.newHttpClient();
/** Returns the raw envelope: {"ok":true,"data":{...}} or {"ok":false,"error":{...}}. */
static String api(String method, String path, String jsonBody, Map<String, String> extra)
throws Exception {
var builder = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody));
extra.forEach(builder::header);
var res = HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) {
throw new RuntimeException("api " + method + " " + path + ": " + res.body());
}
return res.body();
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # step 2 mints one
def api(method, path, body = nil, extra = {})
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
extra.each { |k, v| req[k] = v }
req.body = JSON.generate(body) if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
unless payload["ok"]
err = payload["error"] || {}
raise "#{err["code"]}: #{err["message"]}"
end
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // step 2 mints one
function api(string $method, string $path, ?array $body = null, array $extra = []): mixed {
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array_merge([
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
], $extra),
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException(($payload["error"]["code"] ?? "error") . ": " .
($payload["error"]["message"] ?? "request failed"));
}
return $payload["data"];
}
// .NET 8+
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
static class ReturnsDesk
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN"; // step 2 mints one
static readonly HttpClient Http = new();
static ReturnsDesk() =>
Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
public static async Task<JsonElement> ApiAsync(
HttpMethod method, string path, object? body = null,
(string Name, string Value)? extra = null)
{
var req = new HttpRequestMessage(method, Api + path);
if (body != null) req.Content = JsonContent.Create(body);
if (extra.HasValue) req.Headers.Add(extra.Value.Name, extra.Value.Value);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var err = payload.GetProperty("error");
throw new Exception($"{err.GetProperty("code")}: {err.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
2. Get a token
Two ways, and neither involves a browser console.
- Your own token — open the
token page, sign in with SkillSafe, then press
Copy shell export. It puts
export SKILLSAFE_TOKEN="…"on your clipboard. Runs made with it bill your account. Treat it like a password. - A guest token —
POST /guest, no browser at all. Guests can always read/meand/estimate; whether a guest can afford a run depends on the day's sponsorship budget (sponsor_enabledin the estimate).
The slug is bound to the token here. That is the only place
returns-desk appears in the whole API — there is no
/apps/{slug}/ path segment on any other route.
# Guest token - the slug is bound to the token at this call and nowhere else.
TOKEN=$(curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"returns-desk"}' | jq -r '.data.token')
echo "${TOKEN:0:8}..." # never echo the whole thing into a log
# Personal token instead: https://returns-desk.skillsafe.ai/tokens.html
# -> sign in -> "Copy shell export" -> paste the export line into this shell.
import json, urllib.request
req = urllib.request.Request(
API + "/guest",
data=json.dumps({"slug": "returns-desk"}).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as res:
guest = json.load(res)["data"]
TOKEN = guest["token"] # rebind the module constant used by api()
print("guest id:", guest["guest_id"])
# For a personal token: https://returns-desk.skillsafe.ai/tokens.html
# -> sign in -> "Copy token", and paste it into TOKEN instead.
// /guest takes no Authorization header - it is what mints one.
const res = await fetch(`${API}/guest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "returns-desk" }),
});
const { ok, data: guest, error } = await res.json();
if (!ok) throw new Error(`${error.code}: ${error.message}`);
console.log(guest.guest_id);
// guest.token is the bearer token every later call sends.
// Personal token instead: https://returns-desk.skillsafe.ai/tokens.html
var guest struct {
Token string `json:"token"`
GuestID string `json:"guest_id"`
}
// /guest ignores the Authorization header call() sends; that is harmless.
if err := call("POST", "/guest", map[string]string{"slug": "returns-desk"}, &guest, nil); err != nil {
log.Fatal(err)
}
fmt.Println("guest id:", guest.GuestID)
// guest.Token is the bearer token. For a personal token, visit
// https://returns-desk.skillsafe.ai/tokens.html and copy it from there.
String envelope = ReturnsDesk.api("POST", "/guest",
"{\"slug\":\"returns-desk\"}", Map.of());
// data.token - the bearer token every later call sends
// data.guest_id - identifies the guest wallet
//
// Personal token instead: https://returns-desk.skillsafe.ai/tokens.html
// -> sign in -> "Copy token", and use it as ReturnsDesk.TOKEN.
guest = api("POST", "/guest", { slug: "returns-desk" })
token = guest["token"]
puts "guest id: #{guest["guest_id"]}"
# Personal token instead: https://returns-desk.skillsafe.ai/tokens.html
# -> sign in -> "Copy shell export".
$guest = api("POST", "/guest", ["slug" => "returns-desk"]);
$token = $guest["token"]; // the bearer token
echo "guest id: {$guest["guest_id"]}\n";
// Personal token instead: https://returns-desk.skillsafe.ai/tokens.html
var guest = await ReturnsDesk.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "returns-desk" });
var token = guest.GetProperty("token").GetString();
Console.WriteLine($"guest id: {guest.GetProperty("guest_id")}");
// Personal token instead: https://returns-desk.skillsafe.ai/tokens.html
3. Check who you are and what you can spend
GET /me tells you which kind of token you are holding and what it
can spend: subject_type is "user" for a
signed-in account or "guest" for a guest token, and
credits is the balance. Worth checking before a batch.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
# {"subject_type":"guest","subject_id":"gst_...","credits":0}
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
if me["subject_type"] == "guest":
print("guest runs depend on the day's sponsorship budget - see step 4")
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
if (me.subject_type === "guest") {
console.log("guest runs depend on the day's sponsorship budget - see step 4");
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits float64 `json:"credits"`
}
if err := call("GET", "/me", nil, &me, nil); err != nil {
log.Fatal(err)
}
fmt.Printf("%s %s: %.2f credits\n", me.SubjectType, me.SubjectID, me.Credits)
String envelope = ReturnsDesk.api("GET", "/me", null, Map.of());
// data.subject_type - "user" or "guest"
// data.subject_id
// data.credits - spendable balance
System.out.println(envelope);
me = api("GET", "/me")
puts "#{me["subject_type"]} #{me["subject_id"]}: #{me["credits"]} credits"
puts "guest runs depend on the day's sponsorship budget" if me["subject_type"] == "guest"
$me = api("GET", "/me");
echo "{$me["subject_type"]}: {$me["credits"]} credits\n";
if ($me["subject_type"] === "guest") {
echo "guest runs depend on the day's sponsorship budget - see step 4\n";
}
var me = await ReturnsDesk.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");
4. Price the run before you make it
POST /estimate takes the same body as
/run. It is free: it creates no job, charges
nothing, and holds nothing. Use it to see which model you are pointed at and what the run
would cost before you commit to it.
| Response field | What it is |
|---|---|
| model | The exact model id the run would use. |
| model_alias | The stable alias the app asked for. |
| markup_bps | Platform markup in basis points. |
| hold_credits | Worst-case cost — what a run would place on hold. |
| min_credits | Floor you must have available for the run to start. |
| sponsor_enabled | True when a guest token can run on the day's sponsored allowance. |
cat > input.json <<'JSON'
{
"case": "RMA-88213. Ordered 2026-03-02, delivered 2026-03-09, return opened 2026-03-11.\nCustomer: \"camera is defective, shutter sticks\". Receiving notes: body serial VR-8841203 does not match the shipped serial VR-8839117; 21,438 shutter actuations logged; box opened, foam missing.\nOrder line: 1 x mirrorless body, $1,249.00. Account has 6 returns in 12 months.",
"context": "30-day window, 15% restocking fee outside 14 days, fee waived on genuine defects. Channel: web. Category: electronics. Customer is a 2-year account, $8k lifetime. Decision needed: refund or deny by Friday.",
"facts": "Category: Electronics. Grade: Grade C. Item value: $1,249.00. Restocking fee 15% = $187.35, net refund $1,061.65. Fraud score 72 of 100 (flag for review). Standard disposition: refurbish. Vendor recovery band: always pursue (over $500)."
}
JSON
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @input.json \
| jq '.data | {model, model_alias, markup_bps, hold_credits, min_credits, sponsor_enabled}'
# Free: no job is created and nothing is charged.
INPUT = {
# required - paste the record, not a summary
"case": open("case.txt", encoding="utf-8").read(),
# optional - policy in force, channel, category, customer standing, decision needed
"context": "30-day window, 15% restocking fee outside 14 days, waived on genuine defects. "
"Electronics, web channel, 2-year account. Refund or deny by Friday.",
# optional - a HINT, not a fact. The app fills this from its in-browser calculator.
"facts": "Category: Electronics. Grade: Grade C. Item value: $1,249.00. "
"Fraud score 72 of 100 (flag for review). Standard disposition: refurbish.",
}
est = api("POST", "/estimate", INPUT)
assert est["model"], "no model resolved"
assert est["model_alias"], "no model alias resolved"
assert isinstance(est["markup_bps"], int), "markup_bps should be an integer"
print(est["model"], "via", est["model_alias"], "markup", est["markup_bps"], "bps")
print("hold:", est["hold_credits"], "min:", est["min_credits"],
"sponsored:", est["sponsor_enabled"])
# Nothing was charged and no job exists - /estimate is free.
const input = {
// required - the pasted return case
case: await readFile("case.txt", "utf8"),
// optional - policy in force, channel and category, customer standing, decision needed
context:
"30-day window, 15% restocking fee outside 14 days, waived on genuine defects. " +
"Electronics, web channel, 2-year account. Refund or deny by Friday.",
// optional - a HINT, not a fact: the app derives it from its in-browser calculator
facts:
"Category: Electronics. Grade: Grade C. Item value: $1,249.00. " +
"Fraud score 72 of 100 (flag for review). Standard disposition: refurbish.",
};
const est = await api("POST", "/estimate", input);
if (!est.model || !est.model_alias) throw new Error("no model resolved");
console.log(est.model, est.model_alias, est.markup_bps);
console.log("hold", est.hold_credits, "min", est.min_credits, "sponsored", est.sponsor_enabled);
// Free: no job, no charge, no hold.
type Input struct {
Case string `json:"case"`
Context string `json:"context,omitempty"`
Facts string `json:"facts,omitempty"`
RetryNote string `json:"retry_note,omitempty"` // app-internal; leave empty
}
input := Input{
Case: string(caseBytes),
Context: "30-day window, 15% restocking fee outside 14 days, waived on genuine defects.",
Facts: "Category: Electronics. Grade: Grade C. Fraud score 72 of 100 (flag for review).",
}
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
HoldCredits float64 `json:"hold_credits"`
MinCredits float64 `json:"min_credits"`
SponsorEnabled bool `json:"sponsor_enabled"`
}
if err := call("POST", "/estimate", input, &est, nil); err != nil {
log.Fatal(err)
}
if est.Model == "" || est.ModelAlias == "" {
log.Fatal("no model resolved")
}
fmt.Printf("%s (%s) markup %d bps, hold %.2f\n",
est.Model, est.ModelAlias, est.MarkupBps, est.HoldCredits)
// Free: /estimate creates no job and charges nothing.
// Build the body with your JSON library; the input object goes in DIRECTLY,
// it is not wrapped in {"input": ...}.
String body = """
{
"case": %s,
"context": %s,
"facts": %s
}""".formatted(json(caseText), json(contextText), json(factsText));
String envelope = ReturnsDesk.api("POST", "/estimate", body, Map.of());
// data.model - exact model id
// data.model_alias - stable alias
// data.markup_bps - platform markup, basis points
// data.hold_credits - worst-case cost
// data.min_credits - floor needed to start
// data.sponsor_enabled - guest tokens can run free today
//
// /estimate is free: no job is created and nothing is charged.
System.out.println(envelope);
input = {
# required
"case" => File.read("case.txt"),
# optional
"context" => "30-day window, 15% restocking fee outside 14 days, waived on genuine defects. " \
"Electronics, web channel, 2-year account. Refund or deny by Friday.",
# optional - a HINT, not a fact
"facts" => "Category: Electronics. Grade: Grade C. Item value: $1,249.00. " \
"Fraud score 72 of 100 (flag for review)."
}
est = api("POST", "/estimate", input)
raise "no model resolved" unless est["model"] && est["model_alias"]
puts "#{est["model"]} (#{est["model_alias"]}) markup #{est["markup_bps"]} bps"
puts "hold #{est["hold_credits"]} min #{est["min_credits"]} sponsored #{est["sponsor_enabled"]}"
# Free: no job, no charge.
$input = [
// required
"case" => file_get_contents("case.txt"),
// optional
"context" => "30-day window, 15% restocking fee outside 14 days, waived on genuine defects. "
. "Electronics, web channel, 2-year account. Refund or deny by Friday.",
// optional - a HINT, not a fact
"facts" => "Category: Electronics. Grade: Grade C. Item value: $1,249.00. "
. "Fraud score 72 of 100 (flag for review).",
];
$est = api("POST", "/estimate", $input);
if (empty($est["model"]) || empty($est["model_alias"])) {
throw new RuntimeException("no model resolved");
}
echo "{$est["model"]} ({$est["model_alias"]}) markup {$est["markup_bps"]} bps\n";
echo "hold {$est["hold_credits"]} min {$est["min_credits"]}\n";
// Free: no job is created and nothing is charged.
var input = new {
// required
@case = File.ReadAllText("case.txt"),
// optional
context = "30-day window, 15% restocking fee outside 14 days, waived on genuine defects. " +
"Electronics, web channel, 2-year account. Refund or deny by Friday.",
// optional - a HINT, not a fact
facts = "Category: Electronics. Grade: Grade C. Item value: $1,249.00. " +
"Fraud score 72 of 100 (flag for review).",
};
var est = await ReturnsDesk.ApiAsync(HttpMethod.Post, "/estimate", input);
if (est.GetProperty("model").GetString() is not { Length: > 0 } model ||
est.GetProperty("model_alias").GetString() is not { Length: > 0 } alias)
{
throw new Exception("no model resolved");
}
Console.WriteLine($"{model} ({alias}) markup {est.GetProperty("markup_bps")} bps");
Console.WriteLine($"hold {est.GetProperty("hold_credits")} min {est.GetProperty("min_credits")}");
// Free: /estimate creates no job and charges nothing.
@case / "case" is a
reserved word in several languages — quote or escape the key rather than renaming it.
The wire field is case.
5. Run it, then poll the job
POST /run takes the same body, places a credit hold and returns a
job_id. Poll GET /jobs/{job_id}
every second or two until status is
succeeded or failed. The assessment text
lands at output.output as a plain string — no code fence, no
JSON wrapper.
Always send an Idempotency-Key header. The app derives its key from
a hash of case + context + facts plus an attempt counter, so a
dropped connection or its own one-shot reformat retry can never bill twice. Do the same:
one stable key per logical run.
KEY="returns-desk-$(shasum -a 256 input.json | cut -c1-16)-a1" JOB_ID=$(curl -s -X POST "$API/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d @input.json | jq -r '.data.job_id') while :; do JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN") STATUS=$(echo "$JOB" | jq -r '.data.status') case "$STATUS" in succeeded|failed) break ;; esac sleep 2 done echo "$JOB" | jq -r '.data.output.output' # the assessment, as plain text
import hashlib, json, time
basis = INPUT["case"] + " " + INPUT.get("context", "") + " " + INPUT.get("facts", "")
key = "returns-desk-" + hashlib.sha256(basis.encode("utf-8")).hexdigest()[:16] + "-a1"
started = api("POST", "/run", INPUT, {"Idempotency-Key": key})
job_id = started["job_id"]
while True:
job = api("GET", "/jobs/" + job_id)
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error") or "run failed")
out = job.get("output") or {}
text = out.get("output") if isinstance(out, dict) else out
print(text) # the assessment - see step 7 for parsing
print("charged:", job.get("charged_credits"))
import { createHash } from "node:crypto";
const basis = [input.case, input.context ?? "", input.facts ?? ""].join(" ");
const key = "returns-desk-" + createHash("sha256").update(basis).digest("hex").slice(0, 16) + "-a1";
const { job_id } = await api("POST", "/run", input, { "Idempotency-Key": key });
let job;
for (;;) {
job = await api("GET", `/jobs/${job_id}`);
if (job.status === "succeeded" || job.status === "failed") break;
await new Promise((r) => setTimeout(r, 1500));
}
if (job.status === "failed") throw new Error(job.error ?? "run failed");
const text = typeof job.output === "string" ? job.output : job.output?.output ?? "";
console.log(text); // the assessment - see step 7 for parsing
console.log("charged", job.charged_credits);
basis := input.Case + " " + input.Context + " " + input.Facts
sum := sha256.Sum256([]byte(basis))
key := fmt.Sprintf("returns-desk-%x-a1", sum[:8])
var started struct {
JobID string `json:"job_id"`
}
if err := call("POST", "/run", input, &started,
map[string]string{"Idempotency-Key": key}); err != nil {
log.Fatal(err)
}
var job struct {
Status string `json:"status"`
Error string `json:"error"`
ChargedCredits float64 `json:"charged_credits"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
for {
if err := call("GET", "/jobs/"+started.JobID, nil, &job, nil); err != nil {
log.Fatal(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(1500 * time.Millisecond)
}
if job.Status == "failed" {
log.Fatal(job.Error)
}
fmt.Println(job.Output.Output) // the assessment - see step 7 for parsing
var digest = java.security.MessageDigest.getInstance("SHA-256");
var basis = caseText + " " + contextText + " " + factsText;
var hex = java.util.HexFormat.of().formatHex(digest.digest(basis.getBytes("UTF-8")));
var key = "returns-desk-" + hex.substring(0, 16) + "-a1";
String started = ReturnsDesk.api("POST", "/run", body, Map.of("Idempotency-Key", key));
String jobId = /* data.job_id, via your JSON library */ null;
String job;
String status;
while (true) {
job = ReturnsDesk.api("GET", "/jobs/" + jobId, null, Map.of());
status = /* data.status */ null;
if ("succeeded".equals(status) || "failed".equals(status)) break;
Thread.sleep(1500);
}
// On success the assessment text is at data.output.output - a plain string.
// On failure data.error carries the reason.
require "digest"
basis = [input["case"], input["context"], input["facts"]].join(" ")
key = "returns-desk-#{Digest::SHA256.hexdigest(basis)[0, 16]}-a1"
started = api("POST", "/run", input, { "Idempotency-Key" => key })
job = nil
loop do
job = api("GET", "/jobs/#{started["job_id"]}")
break if %w[succeeded failed].include?(job["status"])
sleep 1.5
end
raise(job["error"] || "run failed") if job["status"] == "failed"
text = job["output"].is_a?(Hash) ? job["output"]["output"] : job["output"]
puts text # the assessment - see step 7 for parsing
puts "charged: #{job["charged_credits"]}"
$basis = $input["case"] . " " . ($input["context"] ?? "") . " " . ($input["facts"] ?? "");
$key = "returns-desk-" . substr(hash("sha256", $basis), 0, 16) . "-a1";
$started = api("POST", "/run", $input, ["Idempotency-Key: $key"]);
do {
sleep(2);
$job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
if ($job["status"] === "failed") {
throw new RuntimeException($job["error"] ?? "run failed");
}
$text = is_array($job["output"]) ? ($job["output"]["output"] ?? "") : $job["output"];
echo $text; // the assessment - see step 7 for parsing
echo "\ncharged: {$job["charged_credits"]}\n";
var basis = string.Join(" ", input.@case, input.context, input.facts);
var hash = Convert.ToHexString(
System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(basis)));
var key = $"returns-desk-{hash[..16].ToLowerInvariant()}-a1";
var started = await ReturnsDesk.ApiAsync(HttpMethod.Post, "/run", input,
("Idempotency-Key", key));
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await ReturnsDesk.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status is "succeeded" or "failed") break;
await Task.Delay(1500);
}
if (job.GetProperty("status").GetString() == "failed")
throw new Exception(job.GetProperty("error").GetString());
var text = job.GetProperty("output").GetProperty("output").GetString();
Console.WriteLine(text); // the assessment - see step 7 for parsing
6. Or stream it
POST /run-stream is the same call with the same body and the same
Idempotency-Key, answered as
text/event-stream. That is what the app itself uses, which is why
the verdict appears line by line. Events:
| Event | Payload |
|---|---|
| job | {"job_id": "…"} — fired once, as soon as the job exists. |
| delta | {"text": "…"} — append it; the concatenation is the whole reply. |
| done | {"job_id", "status", "charged_credits", "truncated", "output"} |
| error | {"code", "message", "job_id"} — the stream ends here. |
An idempotent replay is answered as ordinary JSON rather than SSE, so check the
response's Content-Type before you start parsing events.
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @input.json
# event: job
# data: {"job_id":"job_01H..."}
#
# event: delta
# data: {"text":"CASE: Fraud review\n"}
#
# event: done
# data: {"job_id":"job_01H...","status":"succeeded","charged_credits":18.4,"output":{"output":"CASE: ..."}}
# pip install requests
import json, requests
with requests.post(API + "/run-stream", json=INPUT, stream=True, headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": key,
}) as res:
if "text/event-stream" not in res.headers.get("content-type", ""):
# idempotent replay - a plain JSON envelope, not a stream
text = res.json()["data"]["output"]["output"]
else:
event, chunks, done = "message", [], None
for line in res.iter_lines(decode_unicode=True):
if line is None or line == "":
event = "message"
continue
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if event == "delta":
chunks.append(payload.get("text", ""))
elif event == "done":
done = payload
elif event == "error":
raise RuntimeError(payload["code"] + ": " + payload["message"])
text = "".join(chunks)
print(text)
const res = await fetch(`${API}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(input),
});
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) {
// idempotent replay - a plain JSON envelope, not a stream
const payload = await res.json();
console.log(payload.data.output.output);
} else {
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let text = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let i;
while ((i = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, i);
buffer = buffer.slice(i + 2);
let event = "message";
let data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
const payload = JSON.parse(data);
if (event === "delta") text += payload.text || "";
else if (event === "error") throw new Error(`${payload.code}: ${payload.message}`);
}
}
console.log(text);
}
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(bodyJSON))
req.Header.Set("Authorization", "Bearer "+Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if !strings.Contains(res.Header.Get("Content-Type"), "text/event-stream") {
// idempotent replay - a plain JSON envelope, not a stream
log.Fatal("replayed; decode the envelope instead")
}
var text strings.Builder
event := "message"
scanner := bufio.NewScanner(res.Body)
scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for scanner.Scan() {
line := scanner.Text()
switch {
case line == "":
event = "message"
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var payload struct {
Text string `json:"text"`
Code string `json:"code"`
Message string `json:"message"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &payload)
switch event {
case "delta":
text.WriteString(payload.Text)
case "error":
log.Fatalf("%s: %s", payload.Code, payload.Message)
}
}
}
fmt.Println(text.String())
var req = HttpRequest.newBuilder(URI.create(ReturnsDesk.API + "/run-stream"))
.header("Authorization", "Bearer " + ReturnsDesk.TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var res = ReturnsDesk.HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
var text = new StringBuilder();
var event = new String[] { "message" };
res.body().forEach(line -> {
if (line.isEmpty()) {
event[0] = "message";
} else if (line.startsWith("event:")) {
event[0] = line.substring(6).trim();
} else if (line.startsWith("data:")) {
String data = line.substring(5).trim();
// parse `data` with your JSON library
if (event[0].equals("delta")) text.append(/* payload.text */ "");
else if (event[0].equals("error")) throw new RuntimeException(data);
}
});
System.out.println(text);
require "net/http"
require "json"
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(input)
text = +""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
unless res["content-type"].to_s.include?("text/event-stream")
# idempotent replay - a plain JSON envelope, not a stream
text = JSON.parse(res.body).dig("data", "output", "output").to_s
next
end
event = "message"
buffer = +""
res.read_body do |chunk|
buffer << chunk
while (i = buffer.index("\n"))
line = buffer.slice!(0, i + 1).chomp
if line.empty?
event = "message"
elsif line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:")
payload = JSON.parse(line[5..].strip)
case event
when "delta" then text << payload["text"].to_s
when "error" then raise "#{payload["code"]}: #{payload["message"]}"
end
end
end
end
end
end
puts text
$text = "";
$event = "message";
$buffer = "";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: $key",
"Accept: text/event-stream",
],
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$text, &$event, &$buffer) {
$buffer .= $chunk;
while (($i = strpos($buffer, "\n")) !== false) {
$line = rtrim(substr($buffer, 0, $i), "\r");
$buffer = substr($buffer, $i + 1);
if ($line === "") {
$event = "message";
} elseif (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:")) {
$payload = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") {
$text .= $payload["text"] ?? "";
} elseif ($event === "error") {
throw new RuntimeException($payload["code"] . ": " . $payload["message"]);
}
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
echo $text;
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream")
{
Content = JsonContent.Create(input),
};
req.Headers.Add("Idempotency-Key", key);
using var res = await ReturnsDesk.Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
if (res.Content.Headers.ContentType?.MediaType != "text/event-stream")
{
// idempotent replay - a plain JSON envelope, not a stream
var replay = await res.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(replay.GetProperty("data").GetProperty("output").GetProperty("output"));
return;
}
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var text = new System.Text.StringBuilder();
var evt = "message";
while (await reader.ReadLineAsync() is { } line)
{
if (line.Length == 0) { evt = "message"; continue; }
if (line.StartsWith("event:")) { evt = line[6..].Trim(); continue; }
if (!line.StartsWith("data:")) continue;
var payload = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (evt == "delta") text.Append(payload.GetProperty("text").GetString());
else if (evt == "error")
throw new Exception($"{payload.GetProperty("code")}: {payload.GetProperty("message")}");
}
Console.WriteLine(text);
7. Parse the reply back into fields
The reply is plain text in a fixed shape — not JSON, and never wrapped in a code
fence. All five header lines and all six sections are required, in this order. Each section
carries - bullets and nothing else, except
## Open questions, which may be the single line
None.
CASE: <Standard return | Policy exception | Fraud review | Warranty claim | Vendor recovery | Recall return> GRADE: <Grade A | Grade B | Grade C | Grade D | Not graded> ACTION: <Approve the refund | Approve exchange or store credit | Approve with restocking fee | Deny the return | Hold for fraud review | Route to warranty | File a vendor claim | Insufficient information> CONFIDENCE: <integer 0-100> SUMMARY: <2 to 4 sentences> ## Immediate actions - ... ## Eligibility and policy - ... ## Grading and disposition - ... ## Fraud check - ... ## Refund and recovery - ... ## Open questions - ... (or the single line: None.)
Parsing it is two regexes: one for the header lines, one to split on
^## . If a header line is missing, treat the reply as malformed
rather than guessing — that is exactly when the app sends its one-shot
retry_note reformat request.
# Header lines:
echo "$TEXT" | sed -n 's/^CASE: //p'
echo "$TEXT" | sed -n 's/^GRADE: //p'
echo "$TEXT" | sed -n 's/^ACTION: //p'
echo "$TEXT" | sed -n 's/^CONFIDENCE: //p'
# SUMMARY may wrap over several lines, so take everything up to the first "## ":
echo "$TEXT" | awk '/^SUMMARY: /{f=1} /^## /{f=0} f'
# One section, bullets only:
echo "$TEXT" | awk '/^## Immediate actions$/{f=1;next} /^## /{f=0} f'
# All six section headings, in order:
echo "$TEXT" | grep '^## '
import re
HEADERS = ("CASE", "GRADE", "ACTION", "CONFIDENCE")
SECTIONS = ("Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions")
def parse(text):
out = {}
for name in HEADERS:
m = re.search(r"^" + name + r":[ \t]*(.+)$", text, re.M)
if not m:
raise ValueError("malformed reply: no " + name + " line")
out[name.lower()] = m.group(1).strip()
out["confidence"] = int(out["confidence"])
m = re.search(r"^SUMMARY:[ \t]*(.*?)(?=\n##\s|\Z)", text, re.M | re.S)
if not m:
raise ValueError("malformed reply: no SUMMARY line")
out["summary"] = " ".join(m.group(1).split())
body = {}
for heading, chunk in re.findall(r"^##[ \t]+(.+?)[ \t]*\n(.*?)(?=\n##\s|\Z)",
text, re.M | re.S):
lines = [ln.strip() for ln in chunk.strip().splitlines() if ln.strip()]
body[heading] = [ln[2:].strip() for ln in lines if ln.startswith("- ")] or lines
missing = [s for s in SECTIONS if s not in body]
if missing:
raise ValueError("malformed reply: missing " + ", ".join(missing))
out["sections"] = body
return out
verdict = parse(text)
print(verdict["case"], "/", verdict["grade"], "/", verdict["action"],
"(" + str(verdict["confidence"]) + ")")
for bullet in verdict["sections"]["Immediate actions"]:
print(" -", bullet)
const HEADERS = ["CASE", "GRADE", "ACTION", "CONFIDENCE"];
const SECTIONS = [
"Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions",
];
function parse(text) {
const out = {};
for (const name of HEADERS) {
const m = text.match(new RegExp(`^${name}:[ \t]*(.+)$`, "m"));
if (!m) throw new Error(`malformed reply: no ${name} line`);
out[name.toLowerCase()] = m[1].trim();
}
out.confidence = Number.parseInt(out.confidence, 10);
const sm = text.match(/^SUMMARY:[ \t]*([\s\S]*?)(?=\n##\s|$)/m);
if (!sm) throw new Error("malformed reply: no SUMMARY line");
out.summary = sm[1].trim().replace(/\s+/g, " ");
out.sections = {};
const re = /^##[ \t]+(.+?)[ \t]*\n([\s\S]*?)(?=\n##\s|$)/gm;
for (const m of text.matchAll(re)) {
const lines = m[2].trim().split("\n").map((s) => s.trim()).filter(Boolean);
const bullets = lines.filter((s) => s.startsWith("- ")).map((s) => s.slice(2).trim());
out.sections[m[1]] = bullets.length ? bullets : lines;
}
const missing = SECTIONS.filter((s) => !(s in out.sections));
if (missing.length) throw new Error(`malformed reply: missing ${missing.join(", ")}`);
return out;
}
const verdict = parse(text);
console.log(verdict.case, verdict.grade, verdict.action, verdict.confidence);
console.log(verdict.sections["Immediate actions"]);
var (
headerRe = regexp.MustCompile(`(?m)^(CASE|GRADE|ACTION|CONFIDENCE):[ \t]*(.+)$`)
sectionRe = regexp.MustCompile(`(?ms)^##[ \t]+(.+?)[ \t]*\n(.*?)(?:\n##\s|\z)`)
)
var wanted = []string{
"Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions",
}
type Verdict struct {
Case, Grade, Action, Summary string
Confidence int
Sections map[string][]string
}
func parse(text string) (*Verdict, error) {
v := &Verdict{Sections: map[string][]string{}}
for _, m := range headerRe.FindAllStringSubmatch(text, -1) {
switch m[1] {
case "CASE":
v.Case = strings.TrimSpace(m[2])
case "GRADE":
v.Grade = strings.TrimSpace(m[2])
case "ACTION":
v.Action = strings.TrimSpace(m[2])
case "CONFIDENCE":
v.Confidence, _ = strconv.Atoi(strings.TrimSpace(m[2]))
}
}
if v.Case == "" || v.Grade == "" || v.Action == "" {
return nil, errors.New("malformed reply: missing a header line")
}
for _, m := range sectionRe.FindAllStringSubmatch(text, -1) {
for _, line := range strings.Split(strings.TrimSpace(m[2]), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "- ") {
v.Sections[m[1]] = append(v.Sections[m[1]], strings.TrimSpace(line[2:]))
}
}
}
for _, name := range wanted {
if _, ok := v.Sections[name]; !ok {
return nil, fmt.Errorf("malformed reply: missing section %q", name)
}
}
return v, nil
}
import java.util.*;
import java.util.regex.*;
record Verdict(String caseType, String grade, String action, int confidence,
String summary, Map<String, List<String>> sections) {}
static final List<String> SECTIONS = List.of(
"Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions");
static String header(String text, String name) {
var m = Pattern.compile("(?m)^" + name + ":[ \\t]*(.+)$").matcher(text);
if (!m.find()) throw new IllegalArgumentException("malformed reply: no " + name + " line");
return m.group(1).trim();
}
static Verdict parse(String text) {
var sections = new LinkedHashMap<String, List<String>>();
var m = Pattern.compile("(?ms)^##[ \\t]+(.+?)[ \\t]*\\n(.*?)(?=\\n##\\s|\\z)").matcher(text);
while (m.find()) {
var bullets = new ArrayList<String>();
for (String line : m.group(2).strip().split("\\n")) {
line = line.strip();
if (line.startsWith("- ")) bullets.add(line.substring(2).strip());
}
sections.put(m.group(1), bullets);
}
for (String name : SECTIONS) {
if (!sections.containsKey(name)) {
throw new IllegalArgumentException("malformed reply: missing " + name);
}
}
var summary = Pattern.compile("(?ms)^SUMMARY:[ \\t]*(.*?)(?=\\n##\\s|\\z)").matcher(text);
return new Verdict(header(text, "CASE"), header(text, "GRADE"), header(text, "ACTION"),
Integer.parseInt(header(text, "CONFIDENCE")),
summary.find() ? summary.group(1).replaceAll("\\s+", " ").strip() : "",
sections);
}
SECTIONS = [
"Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions"
].freeze
def parse(text)
verdict = {}
%w[CASE GRADE ACTION CONFIDENCE].each do |name|
m = text[/^#{name}:[ \t]*(.+)$/, 1]
raise "malformed reply: no #{name} line" unless m
verdict[name.downcase.to_sym] = m.strip
end
verdict[:confidence] = verdict[:confidence].to_i
summary = text[/^SUMMARY:[ \t]*(.*?)(?=\n\#\#\s|\z)/m, 1]
raise "malformed reply: no SUMMARY line" unless summary
verdict[:summary] = summary.split.join(" ")
sections = {}
text.scan(/^\#\#[ \t]+(.+?)[ \t]*\n(.*?)(?=\n\#\#\s|\z)/m) do |heading, chunk|
sections[heading] = chunk.lines.map(&:strip).reject(&:empty?)
.select { |l| l.start_with?("- ") }
.map { |l| l[2..].strip }
end
missing = SECTIONS.reject { |s| sections.key?(s) }
raise "malformed reply: missing #{missing.join(", ")}" unless missing.empty?
verdict[:sections] = sections
verdict
end
v = parse(text)
puts "#{v[:case]} / #{v[:grade]} / #{v[:action]} (#{v[:confidence]})"
v[:sections]["Immediate actions"].each { |b| puts " - #{b}" }
<?php
const SECTIONS = [
"Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions",
];
function parseVerdict(string $text): array {
$verdict = [];
foreach (["CASE", "GRADE", "ACTION", "CONFIDENCE"] as $name) {
if (!preg_match("/^$name:[ \t]*(.+)$/m", $text, $m)) {
throw new RuntimeException("malformed reply: no $name line");
}
$verdict[strtolower($name)] = trim($m[1]);
}
$verdict["confidence"] = (int) $verdict["confidence"];
if (!preg_match('/^SUMMARY:[ \t]*(.*?)(?=\n##\s|\z)/ms', $text, $m)) {
throw new RuntimeException("malformed reply: no SUMMARY line");
}
$verdict["summary"] = preg_replace('/\s+/', " ", trim($m[1]));
$sections = [];
preg_match_all('/^##[ \t]+(.+?)[ \t]*\n(.*?)(?=\n##\s|\z)/ms', $text, $all, PREG_SET_ORDER);
foreach ($all as $m) {
$bullets = [];
foreach (preg_split('/\n/', trim($m[2])) as $line) {
$line = trim($line);
if (str_starts_with($line, "- ")) $bullets[] = trim(substr($line, 2));
}
$sections[$m[1]] = $bullets;
}
$missing = array_diff(SECTIONS, array_keys($sections));
if ($missing) {
throw new RuntimeException("malformed reply: missing " . implode(", ", $missing));
}
$verdict["sections"] = $sections;
return $verdict;
}
$v = parseVerdict($text);
echo "{$v["case"]} / {$v["grade"]} / {$v["action"]} ({$v["confidence"]})\n";
using System.Text.RegularExpressions;
static readonly string[] Sections = {
"Immediate actions", "Eligibility and policy", "Grading and disposition",
"Fraud check", "Refund and recovery", "Open questions",
};
record Verdict(string Case, string Grade, string Action, int Confidence,
string Summary, Dictionary<string, List<string>> Sections);
static string Header(string text, string name)
{
var m = Regex.Match(text, $"^{name}:[ \t]*(.+)$", RegexOptions.Multiline);
if (!m.Success) throw new Exception($"malformed reply: no {name} line");
return m.Groups[1].Value.Trim();
}
static Verdict Parse(string text)
{
var sections = new Dictionary<string, List<string>>();
foreach (Match m in Regex.Matches(text, @"^\#\#[ \t]+(.+?)[ \t]*\n(.*?)(?=\n\#\#\s|\z)",
RegexOptions.Multiline | RegexOptions.Singleline))
{
var bullets = m.Groups[2].Value.Trim()
.Split('\n').Select(s => s.Trim())
.Where(s => s.StartsWith("- ")).Select(s => s[2..].Trim()).ToList();
sections[m.Groups[1].Value] = bullets;
}
var missing = Sections.Where(s => !sections.ContainsKey(s)).ToList();
if (missing.Count > 0) throw new Exception("malformed reply: missing " + string.Join(", ", missing));
var sm = Regex.Match(text, @"^SUMMARY:[ \t]*(.*?)(?=\n\#\#\s|\z)",
RegexOptions.Multiline | RegexOptions.Singleline);
return new Verdict(Header(text, "CASE"), Header(text, "GRADE"), Header(text, "ACTION"),
int.Parse(Header(text, "CONFIDENCE")),
sm.Success ? Regex.Replace(sm.Groups[1].Value.Trim(), @"\s+", " ") : "",
sections);
}