Forum taxonomy injected into instructions block + source-derived CTA category
This commit is contained in:
parent
a24ae90ecb
commit
216f4391d9
2 changed files with 92 additions and 27 deletions
|
|
@ -171,7 +171,8 @@ module DepiedencapAiCitations
|
|||
advice) that are not literally in the excerpts.
|
||||
2) You may cautiously point to the closest thread(s) with [[n]] — framed as
|
||||
"peut-être en rapport", never as a real answer.
|
||||
3) Invite the user to post in a fitting category or rephrase.
|
||||
3) Invite the user to post in a fitting category — name the most relevant
|
||||
one from the « Forum categories » list below.
|
||||
4) All citation and role-limit rules from the main prompt still apply.
|
||||
|
||||
### Evidence
|
||||
|
|
@ -182,7 +183,8 @@ module DepiedencapAiCitations
|
|||
## STABLE FORUM EVIDENCE
|
||||
No sufficiently relevant forum threads were retrieved.
|
||||
Reply briefly in the user's language that you lack solid forum evidence for this question.
|
||||
Invite them to post in a fitting category. Do NOT invent threads, brands, or /t/ URLs.
|
||||
Invite them to post in the most fitting category — name it from the
|
||||
« Forum categories » list below. Do NOT invent threads, brands, or /t/ URLs.
|
||||
Do NOT write ### Sources. Do NOT call tools.
|
||||
TXT
|
||||
|
||||
|
|
@ -244,18 +246,50 @@ module DepiedencapAiCitations
|
|||
|
||||
def self.instructions_block(question)
|
||||
pack = retrieve_pack(question)
|
||||
return legacy_instructions_block(pack[:sources]) if pack[:zone] == :legacy
|
||||
block =
|
||||
if pack[:zone] == :legacy
|
||||
legacy_instructions_block(pack[:sources])
|
||||
elsif pack[:sources].blank? || pack[:zone] == :empty
|
||||
NO_EVIDENCE_BLOCK.strip
|
||||
else
|
||||
lines =
|
||||
pack[:sources].each_with_index.map do |s, i|
|
||||
cat = s[:category].present? ? " [cat:#{s[:category]}]" : ""
|
||||
excerpt = s[:excerpt].present? ? "\n excerpt: #{s[:excerpt]}" : ""
|
||||
"#{i + 1}. title: #{s[:title]} [via:#{s[:via]}]#{cat}#{excerpt}"
|
||||
end
|
||||
|
||||
return NO_EVIDENCE_BLOCK.strip if pack[:sources].blank? || pack[:zone] == :empty
|
||||
|
||||
lines =
|
||||
pack[:sources].each_with_index.map do |s, i|
|
||||
excerpt = s[:excerpt].present? ? "\n excerpt: #{s[:excerpt]}" : ""
|
||||
"#{i + 1}. title: #{s[:title]} [via:#{s[:via]}]#{excerpt}"
|
||||
tpl = pack[:zone] == :weak ? WEAK_EVIDENCE_TEMPLATE : EVIDENCE_TEMPLATE
|
||||
format(tpl, lines: lines.join("\n")).strip
|
||||
end
|
||||
|
||||
tpl = pack[:zone] == :weak ? WEAK_EVIDENCE_TEMPLATE : EVIDENCE_TEMPLATE
|
||||
format(tpl, lines: lines.join("\n")).strip
|
||||
"#{block}\n\n#{forum_taxonomy_block}".strip
|
||||
end
|
||||
|
||||
# Taxonomie publique du forum, injectée dans le bloc d'instructions : le LLM
|
||||
# peut nommer la bonne (sous-)catégorie quand il invite à poster, même sans
|
||||
# evidence probante. Cachée 12 h — les catégories bougent rarement.
|
||||
def self.forum_taxonomy_block
|
||||
Discourse.cache.fetch("dpec_taxonomy", expires_in: 12.hours) do
|
||||
cats =
|
||||
Category
|
||||
.where(read_restricted: false)
|
||||
.where.not(id: SiteSetting.uncategorized_category_id)
|
||||
.order(:position)
|
||||
.to_a
|
||||
by_parent = cats.group_by(&:parent_category_id)
|
||||
|
||||
lines = ["## Forum categories (public — where discussions live)"]
|
||||
by_parent[nil].to_a.each do |parent|
|
||||
pdesc = parent.description.to_s.gsub(/<[^>]+>/, " ").squish
|
||||
lines << "#{parent.name}#{pdesc.present? ? " — #{pdesc}" : ""}"
|
||||
by_parent[parent.id].to_a.each do |child|
|
||||
cdesc = child.description.to_s.gsub(/<[^>]+>/, " ").squish
|
||||
lines << " - #{child.name}#{cdesc.present? ? " — #{cdesc}" : ""}"
|
||||
end
|
||||
end
|
||||
lines.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
def self.for_question(question)
|
||||
|
|
@ -445,6 +479,8 @@ module DepiedencapAiCitations
|
|||
title: h[:title],
|
||||
url: "/t/#{h[:slug]}/#{h[:topic_id]}/#{h[:post_number]}",
|
||||
excerpt: h[:raw_excerpt],
|
||||
category: h[:cat_label],
|
||||
category_id: h[:category_id],
|
||||
via: "posts",
|
||||
}
|
||||
end
|
||||
|
|
@ -460,10 +496,14 @@ module DepiedencapAiCitations
|
|||
def self.post_candidates(qvec, vdef)
|
||||
sql = <<~SQL
|
||||
SELECT e.post_id, p.topic_id, p.post_number, t.title, t.slug, p.raw,
|
||||
t.category_id,
|
||||
c.name AS cat_name, pc.name AS parent_cat_name,
|
||||
e.embeddings <=> '#{qvec}' AS dist
|
||||
FROM ai_posts_embeddings e
|
||||
JOIN posts p ON p.id = e.post_id
|
||||
JOIN topics t ON t.id = p.topic_id
|
||||
LEFT JOIN categories c ON c.id = t.category_id
|
||||
LEFT JOIN categories pc ON pc.id = c.parent_category_id
|
||||
WHERE e.model_id = #{vdef.id.to_i} AND e.strategy_id = #{vdef.strategy_id.to_i}
|
||||
AND p.deleted_at IS NULL AND p.post_type = 1
|
||||
AND t.deleted_at IS NULL AND t.archetype = 'regular'
|
||||
|
|
@ -480,12 +520,16 @@ module DepiedencapAiCitations
|
|||
|
||||
ActiveRecord::Base.connection.execute(sql).to_a.map do |row|
|
||||
raw = row["raw"].to_s.gsub(/\s+/, " ").strip[0, 300]
|
||||
parent = row["parent_cat_name"].to_s.presence
|
||||
cat = row["cat_name"].to_s.presence
|
||||
{
|
||||
post_id: row["post_id"].to_i,
|
||||
topic_id: row["topic_id"].to_i,
|
||||
post_number: row["post_number"].to_i,
|
||||
slug: row["slug"],
|
||||
title: row["title"],
|
||||
category_id: row["category_id"].to_i,
|
||||
cat_label: [parent, cat].compact.join(" → "),
|
||||
dist: row["dist"].to_f,
|
||||
raw_excerpt: raw,
|
||||
ce_text: "#{row["title"]}. #{raw}",
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ module DepiedencapAiCitations
|
|||
cta_sources(sources).each do |s|
|
||||
lines << "- [#{s[:title]}](#{canonical(s[:url])}) : #{cta_invitation(s)}"
|
||||
end
|
||||
cat = suggested_category(question)
|
||||
cat = suggested_category(question, sources)
|
||||
lines << "- [#{cat[:label]}](#{cat[:path]}) : #{cat[:cta]}" if cat
|
||||
lines.join("\n")
|
||||
end
|
||||
|
|
@ -285,33 +285,54 @@ module DepiedencapAiCitations
|
|||
end
|
||||
end
|
||||
|
||||
def self.suggested_category(question)
|
||||
# Catégorie suggérée, par priorité :
|
||||
# 1) intention de la question (mots-clés — ex. « entretien » → sous-cat
|
||||
# dédiée, même si les sources citées vivent ailleurs) ;
|
||||
# 2) catégorie dominante des sources citées (la géographie du forum décide
|
||||
# quand la question n'a pas de mot-clé connu — ex. whisky → Comptoir) ;
|
||||
# 3) défaut historique « Le prêt à chausser ».
|
||||
def self.suggested_category(question, sources = nil)
|
||||
q = question.to_s
|
||||
spec =
|
||||
if q.match?(/entretien|cirage|patine|glac|tr[ée]pointe/i)
|
||||
{
|
||||
parent: "Les souliers",
|
||||
child: "Entretien, réparation, glaçage et patine",
|
||||
if q.match?(/entretien|cirage|patine|glac|tr[ée]pointe/i)
|
||||
cat = find_named_category("Les souliers", "Entretien, réparation, glaçage et patine")
|
||||
if cat
|
||||
return {
|
||||
label: "Les souliers → #{cat.name}",
|
||||
path: category_href(cat),
|
||||
cta: "ouvre un sujet avec photos / produits si ton cas n’y est pas.",
|
||||
}
|
||||
else
|
||||
{
|
||||
parent: "Les souliers",
|
||||
child: "Le prêt à chausser",
|
||||
cta: "si aucun de ces fils ne colle à *ton* cas, ouvre un sujet (usage, contraintes, ce que tu cherches).",
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
cat = find_named_category(spec[:parent], spec[:child])
|
||||
if (cat = dominant_source_category(sources))
|
||||
label =
|
||||
cat.parent_category ? "#{cat.parent_category.name} → #{cat.name}" : cat.name
|
||||
return {
|
||||
label: label,
|
||||
path: category_href(cat),
|
||||
cta: "les discussions citées vivent dans cette catégorie — ouvre-y ton sujet si aucune ne colle à *ton* cas.",
|
||||
}
|
||||
end
|
||||
|
||||
cat = find_named_category("Les souliers", "Le prêt à chausser")
|
||||
return nil unless cat
|
||||
|
||||
{
|
||||
label: "#{spec[:parent]} → #{cat.name}",
|
||||
label: "Les souliers → #{cat.name}",
|
||||
path: category_href(cat),
|
||||
cta: spec[:cta],
|
||||
cta: "si aucun de ces fils ne colle à *ton* cas, ouvre un sujet (usage, contraintes, ce que tu cherches).",
|
||||
}
|
||||
end
|
||||
|
||||
# Catégorie la plus fréquente parmi les sources citées. Nil si les sources
|
||||
# n'ont pas de catégorie (canal legacy) ou sont toutes sans catégorie.
|
||||
def self.dominant_source_category(sources)
|
||||
ids = Array(sources).map { |s| s[:category_id] }.compact
|
||||
return nil if ids.empty?
|
||||
|
||||
Category.find_by(id: ids.tally.max_by { |_id, n| n }&.first)
|
||||
end
|
||||
|
||||
def self.find_named_category(parent_name, child_name)
|
||||
parent = Category.find_by(name: parent_name)
|
||||
return nil unless parent
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue