Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Build an assistant that uses tools

After this lesson: I can build a small assistant that uses my own functions.

75 min lesson3 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

A small assistant that uses your functions

An assistant that uses tools is a program that lets a model call your own functions to do a job. It matters because an assistant is only safe when all the parts you have built so far work together.

It needs tools the model can name and a loop that runs them. It also needs a step limit, errors passed back as results and a person to approve risky calls. This lesson puts those parts together in one small program.

By the end of this lesson

Note

I can build a small assistant that uses my own functions.

Key words

WordWhat it means
Assistanta program that uses a model and tools to do a job
Loopsteps repeated until an answer or a limit
Routercode that sends each request to its function
Approvala person's yes before a write tool runs
Guarda check that stops a call from running

The cells students fill in are left blank in this preview.

An assistant is five parts working together

Each part is one you have already built. The tools are your functions and their descriptions. The loop asks, runs and asks again. The router sends each request to its function.

The guards are the step limit, the error results and the approval for writes. Take any one away and a small failure can become real damage.

Worked example: what each part protects against

PartWhat it doesWhat goes wrong without it
Step limitStops after three stepsA loop that never ends
Router tableRefuses an unknown toolA request runs the wrong thing
Error resultsPasses a failure backA crash instead of a recovery
ApprovalAsks before a writeA mistake changes your data
LogRecords every callNobody knows what happened

The cells students fill in are left blank in this preview.

Save the model and approval helpers

Command
docker exec -w /home/node/project agent node -e "const ask=async(m,t,c)=>{const r=await fetch((process.env.AI_URL||'http://ai:4000')+'/v1/chat/completions',{method:'POST',headers:{'content-type':'application/json',authorization:'Bearer lab'},body:JSON.stringify({model:'lab',messages:m,tools:t,tool_choice:c,max_tokens:120})});const j=await r.json();if(r.ok)return j.choices[0].message;throw Error(r.status+' '+j.error.message)};const tool=(n,d,k)=>({type:'function',function:{name:n,description:d,parameters:{type:'object',properties:{[k]:{type:'string'}}}}});const mk=()=>{const rl=require('readline').createInterface({input:process.stdin});const it=rl[Symbol.asyncIterator]();return{yes:async q=>{process.stdout.write(q+' (y/n) ');const l=await it.next();const ok=l.value?.trim()=='y';console.log(ok?'y':'n');return ok},close:()=>rl.close()}};require('fs').writeFileSync('agent.cjs','exports.ask='+ask+';exports.tool='+tool+';exports.mk='+mk+';');console.log('Saved agent.cjs')"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Save the shop tools

Command
docker exec -w /home/node/project agent node -e "const make=()=>{const{tool}=require('./agent.cjs');const N=[];const k=(i,n,o)=>({role:'assistant',tool_calls:[{id:i,function:{name:n,arguments:JSON.stringify(o)}}]});return{N,T:[tool('get_price','Get a shop item price.','item'),tool('add_note','Save a short note.','text')],F:{get_price:a=>({apple:40,pear:45})[a.item]||'Error: not found',add_note:a=>N.push(a.text)},FK:[k('c1','get_price',{item:'pear'}),k('c2','add_note',{text:'pear 45p'}),{role:'assistant',content:'Saved.'}]}};require('fs').writeFileSync('tools.cjs','exports.make='+make+';');console.log('Saved tools.cjs')"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Check a call in a fixed order

For each request, ask the same questions in the same order. Is the name in the table? Does it need approval? Did the person say y? Run it, and turn any error into a result.

A call that fails a question never reaches the next one. Every outcome, run or not, becomes a tool message and a log entry.

Run the whole assistant

Command
printf 'y\n' | docker exec -i -w /home/node/project agent node -e "const{ask,mk}=require('./agent.cjs');const{T,F,N,FK}=require('./tools.cjs').make();const{yes,close}=mk();let rec=0;(async()=>{const m=[{role:'user',content:'Find the price of a pear, then save it as a note.'}];for(let s=1;s<4;s++){let a;try{if(rec)throw 0;a=await ask(m,T)}catch(e){rec||console.log('No model ('+e.message+'). Recorded sample:');rec=1;a=FK[s-1]}m.push(a);if(a.tool_calls==null){console.log('Answer: '+a.content);break}for(const c of a.tool_calls){const n=c.function.name;let r='Error: unknown tool';if(Object.hasOwn(F,n))r=n=='add_note'&&(await yes('Allow '+n+' '+c.function.arguments+'?'))===false?'Refused by the person':String(F[n](JSON.parse(c.function.arguments)));console.log('Step '+s+': '+n+' -> '+r);m.push({role:'tool',tool_call_id:c.id,content:r})}}console.log('Notes: '+N.join(', '));close()})()"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A common mistake

Warning

Many people think an assistant is safe once the model is good. A good model still gets things wrong. The safety is in your code: the table of tools, the step limit, the approval and the log. Build them first, and trust the model later.

Going further: change the job

Change the question in the assistant Run to Save a note: buy bread. Run it twice, once with y and once with n fed in.

Compare the two notes lines. Write down which part of the program made the difference.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.