Files
moreminimore-chat/app/services/chatbot/config_service.rb
Moreminimore 2495239187 feat(chatbot): OSS self-contained guardrail + knowledge-base chatbot
Built-in AI chatbot (no EE, uses Llm::Resolver) that answers in-scope chats
from a knowledge base and hands off to a human when needed. Selected per
inbox via an Integrations::Hook with app_id 'chatbot'.

- Integrations::Chatbot::ProcessorService (mirrors Dialogflow/Captain)
  wired via HookListener + HookJob + apps.yml(chatbot, inbox)
- Chatbot::DecisionService: 1-call default ({in_scope/refuse/handoff}),
  2-call option; thread-safe prompt threading
- Chatbot::KnowledgeRetriever: keyword top-k over KB (+ embedding reserved)
- KnowledgeBaseFaq + import service (md per-heading + front-matter tags)
- Chatbot::ConfigService + admin chatbot_config endpoint
- refs off-topic (e.g. fortune-telling); handoff = bot_handoff! (pending->open)
2026-08-25 16:09:47 +07:00

87 lines
3.4 KiB
Ruby

# Per-account chatbot configuration for the self-contained OSS chatbot.
#
# Reads/writes the account's chatbot settings in Account#custom_attributes (jsonb).
# The chatbot processor + decision service read from here so there's a single source
# of truth for how the bot behaves for a given account.
#
# Keys (namespaced 'chatbot_*'):
# chatbot_enabled [bool] gates whether the chatbot replies (default false)
# chatbot_system_prompt [String] the persona/system instructions for the answer LLM
# chatbot_guardrail_prompt [String] the allowed-scope / guardrail instructions
# chatbot_out_of_scope_reply [String] templated refusal reply for out-of-scope messages
# chatbot_call_mode [Integer] 1 (one call) or 2 (guardrail + answer) — default 1
#
# Guardrail + out-of-scope reply have safe Thai defaults; system prompt and call mode
# fall back to built-in values when unset.
module Chatbot::ConfigService
KEYS = %w[
chatbot_enabled chatbot_system_prompt chatbot_guardrail_prompt
chatbot_out_of_scope_reply chatbot_call_mode
].freeze
DEFAULT_SYSTEM_PROMPT = <<~PROMPT.freeze
You are a helpful customer-service assistant for this business. Answer using ONLY the
provided knowledge base and conversation history. Be concise, accurate and polite.
If the knowledge base does not contain the answer, say you are not sure and offer to
connect the customer with a support agent. Do not invent facts.
PROMPT
DEFAULT_GUARDRAIL_PROMPT = <<~PROMPT.freeze
The bot answers questions about this business's PRODUCTS, SERVICES, and related support
topics only. It must NOT answer unrelated or off-topic requests (e.g. personal advice,
fortune-telling, horoscopes, unrelated general knowledge).
PROMPT
DEFAULT_OUT_OF_SCOPE_REPLY = ' '
module_function
def enabled?(account)
account.custom_attributes['chatbot_enabled'] == true
end
def system_prompt(account)
value_or_default(account, 'chatbot_system_prompt', DEFAULT_SYSTEM_PROMPT)
end
def guardrail_prompt(account)
value_or_default(account, 'chatbot_guardrail_prompt', DEFAULT_GUARDRAIL_PROMPT)
end
def out_of_scope_reply(account)
value_or_default(account, 'chatbot_out_of_scope_reply', DEFAULT_OUT_OF_SCOPE_REPLY)
end
def call_mode(account)
configured = account.custom_attributes['chatbot_call_mode'].to_i
[1, 2].include?(configured) ? configured : 1
end
# Apply a params hash of allowed keys to the account's custom_attributes and persist.
# Returns the resulting config hash. Ignores/merges only known keys.
def update!(account, params)
attrs = account.custom_attributes || {}
KEYS.each do |key|
attrs[key] = params[key] if params.key?(key)
end
account.update!(custom_attributes: attrs)
config(account)
end
# @return [Hash] full current config
def config(account)
{
enabled: enabled?(account),
system_prompt: system_prompt(account),
guardrail_prompt: guardrail_prompt(account),
out_of_scope_reply: out_of_scope_reply(account),
call_mode: call_mode(account)
}
end
def value_or_default(account, key, default)
value = account.custom_attributes[key]
value.presence || default
end
end