diff --git a/content/guides/calls.mdx b/content/guides/calls.mdx index 1b844d3..7735d93 100644 --- a/content/guides/calls.mdx +++ b/content/guides/calls.mdx @@ -528,6 +528,37 @@ When the terminal `structured_result` is `null`, CALL-E did not produce a schema Each recipient attempt can include `transcript_turns`, an ordered list of structured transcript turns for that dial attempt. Each turn has `offset_seconds`, `speaker`, and `text`; `speaker` is `bot`, `user`, or `unknown`. The array is empty when no transcript is available. +### Read transcript turns + +Read `recipients[].attempts[].transcript_turns` in Python or HTTP responses, +and `recipients[].attempts[].transcriptTurns` in the TypeScript SDK. Each turn +keeps the `offset_seconds` field in both SDKs. A null offset means the timestamp +is unavailable; zero means the start of the attempt. + +The runnable [Python reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read_transcript.py) +and [TypeScript reader](https://github.com/CALLE-AI/calle-docs/blob/main/examples/read-transcript.ts) +label each recipient and attempt, keep `unknown` separate from `user`, and display +null offsets as `time unavailable` without changing the input. An unknown speaker +is not evidence that the recipient answered. Interpret the result using +[Task completion](#task-completion). + +From a checkout of the docs repository, run the synthetic checks with Python 3.11+ +or Node.js 22.18+ (native TypeScript support): + +```bash +python examples/read_transcript.py +node examples/read-transcript.ts +``` + +These commands make no API requests. They cover bot, user, unknown, zero and null +offsets, and an empty transcript. To read a real result in your application, pass +the completed Python/HTTP call object to `transcript_lines`, or the completed +TypeScript SDK call object to `transcriptLines`, and iterate the returned lines. +Transcript text may contain private data; keep the output private. + +### Poll for results + + TypeScript: ```ts diff --git a/examples/read-transcript.ts b/examples/read-transcript.ts new file mode 100644 index 0000000..d75e33a --- /dev/null +++ b/examples/read-transcript.ts @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { pathToFileURL } from "node:url"; + +type Turn = { + offset_seconds: number | null; + speaker: "bot" | "user" | "unknown"; + text: string; +}; +type TranscriptCall = { + recipients: { attempts: { transcriptTurns: Turn[] }[] }[]; +}; + +export function* transcriptLines(call: TranscriptCall): Generator { + for (const [recipientIndex, recipient] of call.recipients.entries()) { + for (const [attemptIndex, attempt] of recipient.attempts.entries()) { + for (const turn of attempt.transcriptTurns) { + const timestamp = turn.offset_seconds === null + ? "time unavailable" + : `${turn.offset_seconds}s`; + yield `recipient ${recipientIndex + 1}, attempt ${attemptIndex + 1}: [${timestamp}] ${turn.speaker}: ${turn.text}`; + } + } + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const sample: TranscriptCall = { recipients: [{ attempts: [{ transcriptTurns: [ + { offset_seconds: 0, speaker: "bot", text: "Hello." }, + { offset_seconds: 2, speaker: "user", text: "Hi." }, + { offset_seconds: null, speaker: "unknown", text: "Unattributed speech." }, + ] }, { transcriptTurns: [] }] }] }; + const lines = [...transcriptLines(sample)]; + assert.deepEqual(lines, [ + "recipient 1, attempt 1: [0s] bot: Hello.", + "recipient 1, attempt 1: [2s] user: Hi.", + "recipient 1, attempt 1: [time unavailable] unknown: Unattributed speech.", + ]); + assert.equal(sample.recipients[0].attempts[0].transcriptTurns[2].offset_seconds, null); + console.log(lines.join("\n")); +} diff --git a/examples/read_transcript.py b/examples/read_transcript.py new file mode 100644 index 0000000..6ccb17e --- /dev/null +++ b/examples/read_transcript.py @@ -0,0 +1,29 @@ +"""Read Python SDK / HTTP transcript turns. Run directly for synthetic checks.""" + + +def transcript_lines(call): + for recipient_index, recipient in enumerate(call["recipients"], 1): + for attempt_index, attempt in enumerate(recipient["attempts"], 1): + for turn in attempt["transcript_turns"]: + offset = turn["offset_seconds"] + timestamp = "time unavailable" if offset is None else f"{offset}s" + yield ( + f"recipient {recipient_index}, attempt {attempt_index}: " + f"[{timestamp}] {turn['speaker']}: {turn['text']}" + ) + + +if __name__ == "__main__": + sample = {"recipients": [{"attempts": [{"transcript_turns": [ + {"offset_seconds": 0, "speaker": "bot", "text": "Hello."}, + {"offset_seconds": 2, "speaker": "user", "text": "Hi."}, + {"offset_seconds": None, "speaker": "unknown", "text": "Unattributed speech."}, + ]}, {"transcript_turns": []}]}]} + lines = list(transcript_lines(sample)) + assert lines == [ + "recipient 1, attempt 1: [0s] bot: Hello.", + "recipient 1, attempt 1: [2s] user: Hi.", + "recipient 1, attempt 1: [time unavailable] unknown: Unattributed speech.", + ] + assert sample["recipients"][0]["attempts"][0]["transcript_turns"][2]["offset_seconds"] is None + print("\n".join(lines)) diff --git a/package.json b/package.json index 4877774..ca74581 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ "dev": "pnpm run sync:openapi && pnpm run sync:regions && zudoku dev", "build": "pnpm run sync:openapi && pnpm run sync:regions && zudoku build && node scripts/augment-llms.mjs", "verify:dist": "node scripts/verify-dist.mjs", - "validate": "pnpm run typecheck && pnpm run build && pnpm run verify:dist && pnpm run test:deploy && pnpm run test", + "validate": "pnpm run typecheck && pnpm run build && pnpm run verify:dist && pnpm run test:deploy && pnpm run test:transcripts && pnpm run test", "preview": "zudoku preview", "test": "playwright test", + "test:transcripts": "python3 examples/read_transcript.py && node --experimental-strip-types examples/read-transcript.ts", "test:deploy": "python3 -m unittest scripts/test_deploy_docs_to_oss.py", "typecheck": "tsc -p tsconfig.json --noEmit" },