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)
This commit is contained in:
Moreminimore
2026-08-25 16:09:47 +07:00
parent b86b0c59f6
commit 2495239187
16 changed files with 870 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
# == Schema Information
#
# Table name: knowledge_base_faqs
#
# id :bigint not null, primary key
# account_id :bigint not null
# title :string not null
# content :text not null
# topic_tags :jsonb default: [] not null
# source_filename :string
# embedding :vector(1536)
# created_at :datetime not null
# updated_at :datetime not null
#
class KnowledgeBaseFaq < ApplicationRecord
belongs_to :account
# pgvector KNN support (matches repo pattern: has_neighbors + nearest_neighbors)
has_neighbors :embedding, normalize: true
validates :title, presence: true
validates :content, presence: true
# topic_tags stored as jsonb array; expose string-list helpers for import/retrieval.
def topic_tag_list
Array(topic_tags)
end
def topic_tag_list=(value)
self.topic_tags = Array(value).map(&:strip).reject(&:blank?)
end
scope :for_account, ->(account) { where(account_id: account.id) }
scope :with_embedding, -> { where.not(embedding: nil) }
end