Skip to content

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 ​

pascal
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.md

See The documentation index for the options and for how the pages should be shaped.

Released under the Apache License 2.0