Actions: an assistant that acts
Answering is the default; acting is something you switch on deliberately. You register the actions your application allows, the assistant calls them when the user asks for something to be done, and it tells the user what happened.
User: "open the Leads board and prepare a proposal for Gary Orange:
12 hours of consulting at 150 each and 4 licences at 480 each"
→ open_screen(name = "Leads")
→ draft_proposal(lead = "Gary Orange",
items = "Consulting x 12 @ 150\nLicence x 4 @ 480") [confirmed]
Assistant: "I opened the Leads board and filled the draft for Hera Blue, Inc.
with two line items, $3720 in total: check it and press Preview."That is the CRM sample application talking, and you can run it.
Registering an action
uses HelpChatAI.Actions;
HelpChatAI.Actions.Add('open_screen',
'Brings one of the application screens to the front.',
[THelpChatActionParam.Create('name',
'Screen name: Dashboard, Accounts, Leads, Sales, Marketing, Calendar or Users.')],
procedure(const AArgs: THelpChatActionArgs; var AResult: string)
var
LTab: TTabSheet;
begin
LTab := FindTab(AArgs.AsString('name'));
if LTab = nil then
raise Exception.CreateFmt('There is no "%s" screen.', // the model is told
[AArgs.AsString('name')]);
PageControl.ActivePage := LTab;
AResult := 'Opened the ' + LTab.Caption + ' screen.'; // and so is this
end);The description and the parameter descriptions are the API the model reads: write them the way you would explain the action to a new colleague, and say what the parameter values look like ("PDF, Excel oppure CSV"). A parameter can be apkString, apkInteger, apkNumber or apkBoolean, required or not.
Reading parameters never raises: AsString, AsInteger, AsFloat, AsBoolean and Has return your default when a model omits an optional one or misspells a name.
Three rules
Only what is registered exists
There is no generic "run this" entry point. The model sees the names, descriptions and parameters you declared and can call nothing else. Keep destructive operations out of the registry, or behind a confirmation.
An action runs on the main thread
Handlers open forms and fill fields, while the model call runs on a worker thread. HelpChatAI queues the call, wakes the host, and the component executes it on the main thread and sends the answer back. You do not deal with any of that — but it is why an action must not block for minutes: the default timeout is 60 seconds (Actions.TimeoutMs), after which the model is told the action did not answer. Cancellation is honoured: a user who stops the reply does not leave a worker parked.
Anything that changes data asks first
HelpChatAI.Actions.Add('draft_proposal', '...', [...],
procedure(const AArgs: THelpChatActionArgs; var AResult: string)
begin
...
end,
True); // ← ConfirmWith Confirm, the host asks before running it: your OnActionConfirm, or — in the VCL, with no handler — a standard dialog naming the action and its parameters. In FMX there is no dialog that can be waited for on every platform, so an action needing confirmation withoutOnActionConfirm is refused (see The FMX component).
procedure TMainForm.HelpChatActionConfirm(Sender: TObject; const AName: string;
const AArgs: THelpChatActionArgs; var AAllow: Boolean);
begin
AAllow := not FReadOnlyMode; // your policy, not a dialog
end;When something goes wrong
Everything that can happen is told to the model in plain words, so that it explains itself to the user or tries something else:
| Outcome | The model is told |
|---|---|
| done | your AResult, or "the action was executed" |
| denied | the user did not allow it |
| unknown | there is no action with that name |
| failed | the message of the exception your handler raised |
| timeout | it did not answer in time |
| cancelled | the reply was cancelled |
An exception inside a handler is information, not a crash: raise Exception.Create('Il documento è aperto in modifica') is a perfectly good way to tell the assistant why it could not do what it was asked.
Watching what happens
procedure TMainForm.HelpChatActionExecuted(Sender: TObject; const AName: string;
const AOutcome: THelpChatActionOutcome; const AResult: string);
begin
StatusBar.Panels[3].Text := Format('Azione %s: %s', [AName, GetEnumName(TypeInfo(THelpChatActionOutcome), Ord(AOutcome))]);
Log(AName, AOutcome, AResult);
end;ActionsEnabled := False hides every action from the model without unregistering anything — useful for a read-only mode, a demo, or a user profile without the right to act.
What it needs
- A model provider.
claude,openaiandgeminiall call tools;stubanddocsearchignore them. Up toMaxToolRoundsmodel calls per reply (4 by default), which is what lets the assistant chain two actions and then comment. The three APIs deliver a call differently - typed blocks, fragments tied by an index, a whole part - and your handler never sees the difference. - The local engine. With a remote server the model runs there and the actions live in the client: the definitions and the calls do not travel yet. Registering actions with a remote engine currently does nothing.
Designing good actions
- One action, one intention.
draft_proposalis better thanset_field(name, value)called five times. - Prepare, do not commit. The CRM sample fills the draft with the customer and the line items and computes the total, then stops: Preview and Save Proposal stay with the user.
create_leadis the same idea — the record is written and its window opens, so correcting what the assistant understood is a normal edit. - Return something useful.
AResultis what the model reads to describe what happened; "Opened the Leads screen" produces a better sentence than an empty string. - Say no clearly. An exception message is read by the model and usually reaches the user in its own words.