Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions content/guides/calls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions examples/read-transcript.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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"));
}
29 changes: 29 additions & 0 deletions examples/read_transcript.py
Original file line number Diff line number Diff line change
@@ -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))
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down