Add 'llm' bot: OpenAI-compatible chat (Ollama-ready)

New 'llm' bot type that takes a startup context (system prompt) and replies to
each message via an OpenAI-compatible endpoint — works with a local Ollama
(ollama serve, http://localhost:11434/v1), OpenAI, Grok, etc. Generalize the
support LLM handler into _handle_llm_message (shared by support + llm) with a
per-bot default prompt. Create form reuses the LLM fields (URL/key/model/context)
for both support and llm. Adds llm_test.py (mock OpenAI backend) — passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-06-05 18:58:54 +01:00
parent 3f0338041c
commit aaf3c23a18
3 changed files with 186 additions and 22 deletions

View File

@@ -47,6 +47,7 @@
<h2 style="font-size:15px;margin-bottom:12px;">Available bot types</h2>
<table>
<tr><td><span class="tag">echo</span></td><td class="muted">Repeats every message back to the sender — handy for testing a connection end to end.</td></tr>
<tr><td><span class="tag">llm</span></td><td class="muted">Chat with a local or remote LLM (OpenAI-compatible, e.g. Ollama). Give it context, it replies to your messages.</td></tr>
<tr><td><span class="tag">broadcast</span></td><td class="muted">Relays messages from authorized publishers out to all of the bot's contacts.</td></tr>
<tr><td><span class="tag">support</span></td><td class="muted">Business inbox — auto-replies with a welcome message and collects incoming inquiries.</td></tr>
<tr><td><span class="tag">directory</span></td><td class="muted">Directory service for discovering and listing groups or contacts.</td></tr>
@@ -150,24 +151,26 @@
<div id="support-fields" style="display:none;">
<div style="border-top:1px solid var(--border);margin:4px 0 14px;padding-top:14px;">
<p class="muted" style="margin-bottom:12px;">
LLM backend (OpenAI-compatible). Leave the URL blank for a static welcome-only bot.
LLM backend (OpenAI-compatible — works with a local Ollama via <code>ollama serve</code>,
OpenAI, Grok…). The LLM bot needs the URL; support bots may leave it blank for a
welcome-only inbox.
</p>
</div>
<div class="field">
<label>API Base URL</label>
<input type="text" name="api_base" placeholder="https://api.x.ai/v1 (Ollama: http://localhost:11434/v1)">
<input type="text" name="api_base" placeholder="http://localhost:11434/v1 (Ollama) · https://api.x.ai/v1">
</div>
<div class="field">
<label>API Key</label>
<input type="password" name="api_key" placeholder="xai-… (any value for Ollama)">
<label>API Key <span class="muted" style="font-weight:400;">(any value for Ollama)</span></label>
<input type="password" name="api_key" placeholder="ollama · xai-…">
</div>
<div class="field">
<label>Model</label>
<input type="text" name="model" placeholder="grok-2 (Ollama: llama3.2)">
<input type="text" name="model" placeholder="llama3.2 (Ollama) · grok-2">
</div>
<div class="field">
<label>System Prompt</label>
<textarea name="system_prompt" rows="3" placeholder="You are a helpful customer-support assistant…"></textarea>
<label>Context <span class="muted" style="font-weight:400;">(system prompt given on start-up)</span></label>
<textarea name="system_prompt" rows="3" placeholder="You are a helpful assistant…"></textarea>
</div>
</div>
<div id="deadmans-fields" style="display:none;">
@@ -289,7 +292,7 @@ function copyAddr(ev, btn, addr) {
function onTypeChange() {
const val = document.getElementById('type-select').value;
document.getElementById('welcome-field').style.display = (val === 'echo') ? 'none' : '';
document.getElementById('support-fields').style.display = (val === 'support') ? 'block' : 'none';
document.getElementById('support-fields').style.display = (val === 'support' || val === 'llm') ? 'block' : 'none';
document.getElementById('deadmans-fields').style.display = (val === 'deadmans') ? 'block' : 'none';
document.getElementById('directory-fields').style.display = (val === 'directory') ? 'block' : 'none';
document.getElementById('broadcast-fields').style.display = (val === 'broadcast') ? 'block' : 'none';
@@ -307,7 +310,7 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
const config = {};
const welcome = fd.get('welcome_message');
if (welcome) config.welcome_message = welcome;
if (botType === 'support') {
if (botType === 'support' || botType === 'llm') {
const apiBase = (fd.get('api_base') || '').trim();
if (apiBase) config.api_base = apiBase;
const apiKey = (fd.get('api_key') || '').trim();