====== chat_llm: LLM-backed chat engine ======
''chat_llm'' is the JavaScript chat engine behind Synchronet's LLM-powered
Guru. It turns a large language model (running locally under Ollama, or any
OpenAI-compatible endpoint) into a conversational Guru that can answer
questions, search your message and file bases for grounding (RAG), call
tools, and remember each caller across sessions.
It is **transport-agnostic** and serves three callers from one engine:
* **private** — the classic 1:1 Guru page from a terminal session.
* **multinode** — Guru participation in multi-node channel chat.
* **irc** — the [[module:chat_llm_irc|IRC bot adapter]].
The engine lives in ''exec/chat_llm.js''. Its companions are
[[config:chat_llm.ini]] (configuration), the [[module:llm_tools|tool
registry]], and the [[module:llm_index|RAG index builder]]. For a
start-to-finish setup walk-through, see [[howto:llm-guru|Setting up the LLM
Guru]].
===== Enabling it =====
Two steps, no recompile:
- In [[util:SCFG]] → [[config:chat_features#artificial_gurus|Chat Features → Artificial Gurus]], edit your Guru and set the **Module** field to ''chat_llm''. An empty Module field keeps the legacy ELIZA-style pattern engine driven by the Guru's ''.dat'' answer file.
- Configure the backend (endpoint, model, prompts) in [[config:chat_llm.ini]].
That is all that is required for a working LLM Guru. Retrieval (RAG) and the
bundled tools are optional layers configured separately.
===== Entry points =====
A chat module is a JavaScript file under ''exec/'' that exposes the functions
the BBS calls. ''chat_llm'' exposes two **high-level** entries — these are
the contract every chat module must satisfy:
* ''chat_session(input, ctx)'' — process one caller turn; returns the reply string (or ''null'' to stay silent). Loads the caller's memory, dispatches to the model, persists the updated memory.
* ''open_session(ctx)'' — produce an opening greeting at the start of a session (or ''null'' for none).
It also exposes lower-level helpers callers may use directly:
* ''llm_chat(input, ctx, opts)'' / ''llm_open(ctx, opts)'' — pure dispatch, **no** memory load/save. For unusual flows that manage their own history.
* ''ctx_from_user(useron, persona_code, persona_name, supports_utf8)'' — the standard way to build a ''ctx'' (below) from a Synchronet ''User''.
===== The ctx object =====
Every entry point takes a ''ctx'' (chat context) object. It carries who is
talking, the transport mode, the conversation so far, and a few output knobs.
A custom module receives this same object.
There are three **modes**: ''private'' (1:1 Guru page), ''multinode'' (channel
chat), and ''irc'' (IRC bot). Most fields are shared; a few only matter in
channel modes.
==== Caller-supplied fields ====
^ Field ^ Type ^ Meaning ^
| ''persona'' | ''{code, name}'' | Bot identity. ''code'' selects the [[config:chat_llm.ini]] section **and** the memory-file namespace; ''name'' is the display name. |
| ''speaker'' | ''{id, alias, attrs}'' | Who is talking. ''id'' is namespaced (''%%"user:42"%%'', ''%%"irc:vert/frosty"%%'') and becomes the memory filename. ''alias'' is the display name. ''attrs'' holds ''{real_name, level, location, lang}'' for BBS users, or ''{}''. |
| ''mode'' | string | ''%%"private"%%'', ''%%"multinode"%%'', or ''%%"irc"%%''. Channel modes prefix each turn with the speaker's name so the model can attribute multi-party lines. |
| ''transcript'' | array | Conversation history, newest last; each turn is ''{who, text, ts, bot}'' (''bot:true'' marks the bot's own turns). ''chat_session''/''open_session'' auto-load this from memory, so callers normally pass ''%%[]%%''. |
| ''participants'' | array | ''{id, alias}'' roster for **multinode**; empty otherwise. |
| ''addressed'' | boolean | In channel modes, ''false'' makes the engine return ''null'' (stay silent). Always ''true'' in private mode. |
| ''supports_utf8'' | boolean | Gates the language directive and output charset; forces English to non-UTF-8 terminals for non-Latin scripts. |
| ''channel'' | string | Channel name (channel modes); used to validate relay recipients. |
| ''channel_context'' | string | Recent room chatter, injected so the model can follow cross-conversations. |
| ''channel_members'' | object | IRC: current channel roster, used to validate relay recipients. |
| ''seen_members'' | object | IRC: nicks seen before, used for deferred ("tell them when you see them") relays. |
| ''typing_speed_factor'' | number | Per-character output speed for the terminal typing animation; ''0'' disables it (IRC uses ''0''). |
| ''simulate_typos'' | boolean | Whether the terminal typing animation includes fat-finger/transposition typos. |
==== Engine-set fields (read-only) ====
The engine writes a few fields **back** onto ''ctx'' during a call. A module
author treats these as outputs — do not set them:
* ''ctx._profile'' — a one-line diagnostic summary (persona, mode, retrieval stats), suitable for a chat log. Read it **after** ''chat_session()'' returns.
* ''ctx._streamed'' — ''true'' when the engine already streamed its reply to the terminal itself, so the caller should not re-emit the returned string.
* The engine also attaches retrieval diagnostics (''ctx._rag_*'') and other internal scratch fields; treat anything beginning with ''_'' as read-only engine state.
==== Examples ====
**Private (1:1 Guru page)** — the standard BBS path:
var ctx = ctx_from_user(user, "guru", "The Guru",
console.term_supports(USER_UTF8));
var greeting = open_session(ctx); // optional opening line, or null
var reply = chat_session("what subs cover C programming?", ctx);
log(ctx._profile); // read AFTER the call
**Multinode channel turn:**
var ctx = ctx_from_user(user, "guru", "The Guru", true);
ctx.mode = "multinode";
ctx.addressed = true;
ctx.participants = [{id:"user:42", alias:"Frosty"},
{id:"user:7", alias:"Digital Man"}];
ctx.channel_context = "...recent channel lines...";
var reply = chat_session("anyone know the FidoNet zone for Europe?", ctx);
**IRC (built by hand, no User object):**
var ctx = {
persona: { code: "guru:irc", name: "The Guru" },
speaker: { id: "irc:vert/frosty", alias: "Frosty", attrs: {} },
participants: [],
transcript: [],
mode: "irc",
supports_utf8: true,
addressed: true,
typing_speed_factor: 0, // no terminal: no per-character animation
simulate_typos: false,
channel: "#synchronet",
channel_context: format_channel_context("#synchronet"),
channel_members: chan_members("#synchronet"),
seen_members: chan_seen("#synchronet")
};
var reply = chat_session("Frosty: what's new?", ctx);
To test from the command line without a live session, run the engine under
[[util:jsexec]] and build a ''ctx'' from a stored user — pass ''new User(N)''
to ''ctx_from_user'', or ''null'' for an anonymous tester context.
===== How a turn is processed =====
For each caller turn, ''chat_session'':
- **Checks for a "forget me" command** (e.g. ''%%/forget me%%'', ''%%forget everything%%'') and, if matched, wipes that caller's stored memory and replies with a canned confirmation.
- **Classifies intent** to decide cheaply whether retrieval and tools are even worth invoking for this input.
- **Retrieves grounding (RAG)** from the configured [[module:llm_index|BM25 index]], injecting the top hits — but only when they clear a relevance gate, so off-topic questions don't pull in noise.
- **Runs the tool loop**, offering the model the registered [[module:llm_tools|tools]] and feeding their results back until it produces a final answer.
- **Post-processes** the reply: strips control codes that could trigger hang-up/quit, and appends a verified wiki URL when the answer was grounded in a wiki page.
===== Persistent memory =====
''chat_session''/''open_session'' keep a per-(speaker, persona) memory file
under ''data/chat/''. Each file holds a rolling window of recent turns plus
an LLM-compressed long-term summary, and survives BBS recycles. A caller can
erase their own memory at any time by saying ''%%/forget me%%''.
Memory behavior is tuned in [[config:chat_llm.ini]] (''memory_persist'',
''history_window'', ''summarize_threshold'', ''memory_max_age_days'', and
related knobs). Set ''memory_persist = false'' for public contexts where you
don't want to retain anything about strangers.
===== Writing your own chat module =====
You are not limited to ''chat_llm''. Any JavaScript file under ''exec/'' that
defines ''chat_session(input, ctx)'' and ''open_session(ctx)'' — and accepts
the ''ctx'' object above — can be named in the Guru **Module** field. This
lets you back a Guru with a different engine, a scripted persona, or an
entirely custom integration, without touching Synchronet's C/C++ code.
===== See Also =====
* [[config:chat_llm.ini]] — engine and chat configuration
* [[module:llm_tools]] — the tool registry and bundled tools
* [[module:llm_index]] — building the RAG index
* [[module:chat_llm_irc]] — the IRC bot adapter
* [[howto:llm-guru]] — start-to-finish setup
* [[config:chat_features#artificial_gurus|Artificial Gurus]] — the SCFG menu
{{tag>chat guru llm chat_llm ai}}