fix(captain): filter FAQ signals with business context

This commit is contained in:
aakashb95
2026-07-14 23:13:03 +05:30
parent b95e75f4ca
commit cf1fdff04b
2 changed files with 22 additions and 8 deletions
@@ -6,6 +6,7 @@ class Captain::Llm::ConversationFaqPromptsService
Only generate an FAQ when the conversation contains durable, reusable knowledge that would help many future customers.
## Source rules
- The input starts with trusted business context. Use it to reject conversations about other businesses or topics, but never use it as the source of an FAQ answer.
- The conversation history contains only customer messages and human support agent messages.
- Base every FAQ strictly on information stated in the human support agent messages. Do not infer, generalize, or add external knowledge.
- A human support agent must state every fact used in the FAQ answer. Customer messages cannot supply missing answer facts.
@@ -25,6 +25,8 @@ class Captain::Llm::ConversationFaqService < Llm::BaseAiService
def conversation_faq_content
[
'Business Context:',
JSON.pretty_generate(business_context),
"Conversation ID: ##{conversation.display_id}",
"Channel: #{conversation.inbox.channel.name}",
'Message History:',
@@ -74,9 +76,7 @@ class Captain::Llm::ConversationFaqService < Llm::BaseAiService
def route_candidate(faq)
embedding = embedding_service.get_embedding(candidate_text(faq))
if matching_record(approved_faqs_for_language, faq, embedding)
return discard_observation(faq)
end
return discard_observation(faq) if matching_record(approved_faqs_for_language, faq, embedding)
suggestion = matching_record(open_suggestions_for_language, faq, embedding)
suggestion ||= assistant.faq_suggestions.create!(
@@ -96,10 +96,13 @@ class Captain::Llm::ConversationFaqService < Llm::BaseAiService
def likely_matches(relation, embedding)
return [] unless relation.exists?
relation
.nearest_neighbors(:embedding, embedding, distance: 'cosine')
.limit(MATCH_LIMIT)
.select { |record| record.neighbor_distance < DISTANCE_THRESHOLD }
ApplicationRecord.transaction do
ApplicationRecord.connection.execute("SET LOCAL ivfflat.iterative_scan = 'relaxed_order'")
relation
.nearest_neighbors(:embedding, embedding, distance: 'cosine')
.limit(MATCH_LIMIT)
.select { |record| record.neighbor_distance < DISTANCE_THRESHOLD }
end
end
def same_faq?(candidate, existing_record)
@@ -136,7 +139,7 @@ class Captain::Llm::ConversationFaqService < Llm::BaseAiService
language: faq_language,
status: :attached
)
suggestion.update!(source_count: suggestion.observations.attached.count)
suggestion.update!(source_count: suggestion.source_count + 1)
observation
end
end
@@ -214,6 +217,16 @@ class Captain::Llm::ConversationFaqService < Llm::BaseAiService
Captain::Llm::ConversationFaqPromptsService.generator(language_name(faq_language))
end
def business_context
{
product_name: assistant.config['product_name'],
assistant_description: assistant.description,
instructions: assistant.config['instructions'],
response_guidelines: assistant.response_guidelines,
guardrails: assistant.guardrails
}.compact
end
def faq_language
@faq_language ||= normalize_language(conversation.language.presence || conversation.account.locale.presence || I18n.default_locale.to_s)
end