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:
@@ -78,11 +78,15 @@ def mark_all_read() -> None:
|
||||
for n in _notifications:
|
||||
n["read"] = True
|
||||
|
||||
# Default system prompt for LLM-backed support bots when none is configured.
|
||||
# Default system prompts when none is configured.
|
||||
DEFAULT_SUPPORT_PROMPT = (
|
||||
"You are a helpful customer-support assistant. Answer concisely and politely. "
|
||||
"If you don't know something, say so rather than guessing."
|
||||
)
|
||||
DEFAULT_LLM_PROMPT = (
|
||||
"You are a helpful assistant. Answer concisely. "
|
||||
"If you don't know something, say so rather than guessing."
|
||||
)
|
||||
|
||||
|
||||
async def llm_chat(
|
||||
@@ -132,7 +136,7 @@ def group_member_count(g: dict) -> int:
|
||||
return g.get("groupSummary", {}).get("currentMembers", 0)
|
||||
|
||||
|
||||
BOT_TYPES = ["echo", "broadcast", "support", "directory", "deadmans"]
|
||||
BOT_TYPES = ["echo", "llm", "broadcast", "support", "directory", "deadmans"]
|
||||
USER_TYPES = ["user"]
|
||||
BUSINESS_TYPES = ["business"] # cli accounts with a business address (per-customer group chats)
|
||||
ALL_TYPES = BOT_TYPES + USER_TYPES + BUSINESS_TYPES
|
||||
@@ -483,7 +487,7 @@ async def _run_bot(
|
||||
elif bot_type == "broadcast":
|
||||
# auto-reply greets each new contact (default lists allowed publishers)
|
||||
settings["autoReply"] = {"type": "text", "text": _bc_welcome(config, name)}
|
||||
elif bot_type in ("echo", "directory", "deadmans"):
|
||||
elif bot_type in ("echo", "llm", "directory", "deadmans"):
|
||||
welcome = config.get("welcome_message", f"Connected to {name}.")
|
||||
settings["autoReply"] = {"type": "text", "text": welcome}
|
||||
|
||||
@@ -593,8 +597,13 @@ async def _run_bot(
|
||||
pass
|
||||
|
||||
elif bot_type == "support" and text:
|
||||
await _handle_support_message(
|
||||
b, chat, config, item, chat_info, text
|
||||
await _handle_llm_message(
|
||||
b, chat, config, item, chat_info, text, DEFAULT_SUPPORT_PROMPT
|
||||
)
|
||||
|
||||
elif bot_type == "llm" and text:
|
||||
await _handle_llm_message(
|
||||
b, chat, config, item, chat_info, text, DEFAULT_LLM_PROMPT
|
||||
)
|
||||
|
||||
elif bot_type == "directory" and text:
|
||||
@@ -765,20 +774,22 @@ async def _fire_deadmans(chat: Any, user_id: int, config: dict) -> int:
|
||||
return sent
|
||||
|
||||
|
||||
async def _handle_support_message(
|
||||
b: RunningBot, chat: Any, config: dict, item: dict, chat_info: dict, text: str
|
||||
async def _handle_llm_message(
|
||||
b: RunningBot, chat: Any, config: dict, item: dict, chat_info: dict, text: str,
|
||||
default_prompt: str = DEFAULT_LLM_PROMPT,
|
||||
) -> None:
|
||||
"""Answer an incoming support message via the configured OpenAI-compatible LLM.
|
||||
"""Reply to an incoming message via the configured OpenAI-compatible LLM
|
||||
(works with Ollama's `ollama serve` at http://localhost:11434/v1, OpenAI, Grok…).
|
||||
|
||||
If no api_base is configured the bot stays silent (the static welcome auto-reply
|
||||
has already greeted the contact) — so support bots without an LLM behave as before.
|
||||
`system_prompt` (the startup context), `api_base` (the API URL), `model` and an
|
||||
optional `api_key` come from config. If no api_base is set the bot stays silent.
|
||||
"""
|
||||
api_base = (config.get("api_base") or "").strip()
|
||||
if not api_base:
|
||||
return # no LLM configured for this bot
|
||||
|
||||
contact_id = chat_info.get("contact", {}).get("contactId")
|
||||
system_prompt = config.get("system_prompt") or DEFAULT_SUPPORT_PROMPT
|
||||
system_prompt = config.get("system_prompt") or default_prompt
|
||||
model = config.get("model") or "grok-2"
|
||||
api_key = config.get("api_key") or ""
|
||||
|
||||
@@ -792,7 +803,7 @@ async def _handle_support_message(
|
||||
try:
|
||||
reply = await llm_chat(api_base, api_key, model, messages)
|
||||
except Exception as e:
|
||||
log.error("support LLM error: %s", e)
|
||||
log.error("LLM error: %s", e)
|
||||
_append_log(b, f"LLM error: {e}")
|
||||
try:
|
||||
await chat.api_send_text_reply(
|
||||
@@ -807,7 +818,7 @@ async def _handle_support_message(
|
||||
try:
|
||||
await chat.api_send_text_reply(item, reply)
|
||||
except Exception:
|
||||
log.exception("failed to send support reply")
|
||||
log.exception("failed to send LLM reply")
|
||||
|
||||
|
||||
async def global_status() -> dict:
|
||||
|
||||
Reference in New Issue
Block a user