The engine from a command line
The engine is RTL only — no VCL, no FireMonkey, no window. That is a design rule of the library (Architecture), and the easiest way to see it is to drive the engine from a console program: a few units, no form, and the same answers the chat panel shows.
It is worth doing for three things: trying a provider before wiring it into an application, checking that a documentation index answers the questions your users actually ask, and running a smoke test in a build pipeline where no UI exists.
The whole thing
program AskHelp;
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.Classes,
HelpChatAI.Types, HelpChatAI.Settings, HelpChatAI.Engine,
// A provider exists only if its unit is linked: it registers itself in the
// initialization. Link the ones you may configure.
HelpChatAI.Provider.DocSearch,
HelpChatAI.Provider.Claude;
procedure StreamReply(const AEngine: THelpChatEngine; const AId: string);
var
LMessage: THelpChatMessage;
LPrinted: Integer;
begin
// Nothing blocks on the model: Send returns at once with a pending message
// that the worker fills. Read it until it stops being pending.
LPrinted := 0;
Write('assistant> ');
repeat
if not AEngine.TryGetMessage(AId, LMessage) then
Break;
if LMessage.Content.Length > LPrinted then
begin
Write(LMessage.Content.Substring(LPrinted));
LPrinted := LMessage.Content.Length;
end;
if LMessage.Pending then
TThread.Sleep(100);
until not LMessage.Pending;
Writeln;
if LMessage.IsError then
Writeln('[error] ', LMessage.Content);
end;
var
LSettings: THelpChatSettings;
LEngine: THelpChatEngine;
LContext: THelpChatContext;
LResult: THelpChatSendResult;
LLine: string;
begin
LSettings := THelpChatSettings.Create;
try
// helpchatai.json, merged in order: the shared user file, the one next to
// the executable, the per-application user file. The API key usually lives
// in the first one.
LSettings.LoadFromDefaultFiles;
LSettings.Provider := 'claude';
LSettings.DocIndexFile := 'help-index.json';
LEngine := THelpChatEngine.Create(LSettings);
try
LEngine.Start;
if LEngine.DocIndex.IsAvailable then
Writeln('Index: ', LEngine.DocIndex.Count, ' pages')
else
Writeln('Index: not found at ', LSettings.ResolveDocIndexFile);
Writeln('assistant> ', LEngine.Greeting);
// The screen the user would be on: it is what makes an answer specific.
LContext := Default(THelpChatContext);
LContext.ScreenLabel := 'Leads';
while True do
begin
Write('you> ');
Readln(LLine);
if SameText(Trim(LLine), '/quit') then
Break;
LResult := LEngine.Send(LLine, LContext);
if LResult.Sent then
StreamReply(LEngine, LResult.AssistantMessage.Id);
end;
LEngine.Stop;
finally
LEngine.Free;
end;
finally
LSettings.Free;
end;
end.What to notice
Send does not wait. It returns a pending assistant message and a worker fills it; the loop above polls it, which is what a console can do. An application subscribes to OnMessageChanged instead — it is fired on the worker thread, so a UI marshals it with TThread.Queue.
The provider is a name, and the name has to be linked. Settings.Provider := 'claude' only works if HelpChatAI.Provider.Claude is in the uses of the program: providers register themselves in their unit's initialization. A name nobody registered produces an error message saying which unit to add — never a silent fallback. See Providers.
The context is worth setting. With ScreenLabel the retriever favours the pages about that screen, exactly as the component does when you press F1 — see Screen context.
Where the key comes from. LoadFromDefaultFiles reads helpchatai.json from three places, the first being the one shared by every HelpChatAI application of the user (%APPDATA%\HelpChatAI). Nothing has to be compiled into the program; with no key the provider says so and the rest keeps working. See Configuration.
Building the index first
The engine answers from an index, not from the Markdown. Build it with the tool that ships with the library, which is itself a console program over the same units:
HelpIndexBuilder --source=<folder with the .md> --out=help-index.json --toc=Index.mdSee The documentation index for the options and for how the pages should be shaped.