Asking for a person, and getting a loop
A customer types "let me talk to a person". The chatbot replies "I can help with that!" and asks the same question again. The customer closes the app and rings the shop.
A good feature knows when to stop. The hand over is a part of the design, not a failure of it.
By the end of this lesson
Note
I can design the point where a person takes over.
Key words
| Word | What it means |
|---|---|
| Hand over | passing a conversation to a person |
| Trigger | a rule that starts a hand over |
| Context note | what the person is told so nobody repeats |
| Queue | the list of people waiting for a person |
| Return | giving the conversation back to the model |
The cells students fill in are left blank in this preview.
Four triggers decide when to hand over
Write the triggers as rules, so they can be tested. Four are common. The person asked for a human. The topic is one a model must not decide, such as a refund. The model's confidence is low. The chat has run too many turns.
The first one must always work. Nobody should have to argue with a machine to reach a person.
The note and the wait
The person who takes over should not ask the customer to start again. Hand across a short note: the reasons, the last message and the number of turns.
Tell the customer how long the wait is, using a real number. When the person finishes, decide whether the chat returns to the model or closes.
A common mistake
Warning
Many people think handing over means the AI feature failed, so they make it hard to reach a person. It is the other way. A quick, clear hand over keeps the customer. Make it easy to ask, and send the context across.
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the trigger checker, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const RULES = { maxTurns: 6, minConfidence: 50, topics: [\"refund\", \"complaint\"] };' 'const CHATS = [' ' { id: \"c1\", turns: 3, confidence: 90, topic: \"hours\", asked: false, last: \"When do you open?\" },' ' { id: \"c2\", turns: 4, confidence: 80, topic: \"refund\", asked: false, last: \"I want my money back.\" },' ' { id: \"c3\", turns: 8, confidence: 40, topic: \"delivery\", asked: false, last: \"It still has not come.\" },' ' { id: \"c4\", turns: 2, confidence: 95, topic: \"hours\", asked: true, last: \"Let me talk to a person.\" },' '];' > handover.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the trigger checker, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function reasons(c) {' ' const out = [];' ' if (c.asked) out.push(\"asked\");' ' if (RULES.topics.includes(c.topic)) out.push(\"topic\");' ' if (c.confidence < RULES.minConfidence) out.push(\"confidence\");' ' if (c.turns > RULES.maxTurns) out.push(\"turns\");' ' return out;' '}' >> handover.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the trigger checker, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' for (const c of CHATS) {' ' const why = reasons(c);' ' console.log(c.id + \" \" + (why.length > 0 ? \"hand over \" + why.join(\"+\") : \"stay with the model\"));' ' }' ' const note = CHATS[2];' ' console.log(\"note \" + JSON.stringify({ id: note.id, reasons: reasons(note), last: note.last, turns: note.turns }));' ' const msg = \"Write one short line telling a customer a person is taking over and they need not repeat themselves.\";' ' const reply = await ask([{ role: \"user\", content: msg }], \"A person is taking over now. You do not need to repeat anything.\", 30);' ' console.log(\"customer sees: \" + reply.text);' '}' 'main();' >> handover.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the trigger checker
docker exec -w /home/node/project agent node handover.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
One possible output
| Line | What it shows |
|---|---|
| c1 stay with the model | No trigger fired |
| c2 hand over topic | A refund is a topic for a person |
| c3 hand over confidence+turns | Two triggers at once |
| c4 hand over asked | The customer asked, so always |
| note {"id":"c3",...} | What the person is handed |
The cells students fill in are left blank in this preview.