Plugin Discourse RAG (grain post, rerank cross-encoder, citations whitelistées, gate déterministe, réécriture de requête multi-tour) + documentation du chantier (design, éval, sécurité, pipeline). Export sanitisé depuis le monorepo privé via scripts/export_rag_public.py (aucun endpoint interne, IP, compte démo ou chemin d'infrastructure). WIP : agent en test sur groupe restreint, pas encore public sur le forum.
77 lines
2.7 KiB
Ruby
77 lines
2.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Éval FULL (génération) — tourne DANS le conteneur (`rails runner`).
|
|
# Entrée : JSON [{id, question}] (ARGV[0]). Sortie : JSONL sur stdout.
|
|
#
|
|
# Pour chaque question, émet les deux versions de l'evidence :
|
|
# - prod_block / prod_sources : instructions_block + for_question du
|
|
# plugin de prod (retrieval hybrid grain topic, tel qu'envoyé au LLM)
|
|
# - posts : top-25 grain post natif (pour construire côté hôte le bloc
|
|
# « nouveau pipeline » : rerank → dédup topic → top-6)
|
|
#
|
|
# La première ligne émise est {"type":"persona","system_prompt":...}.
|
|
|
|
require "json"
|
|
|
|
questions = JSON.parse(File.read(ARGV[0]))
|
|
|
|
persona = AiAgent.find_by(name: "Guide Depiedencap")
|
|
puts JSON.generate({ type: "persona", system_prompt: persona&.system_prompt })
|
|
$stdout.flush
|
|
|
|
questions.each do |q|
|
|
out = { id: q["id"], question: q["question"] }
|
|
%w[category expected_behavior expected_topic_ids
|
|
inject_evidence inject_title].each do |k|
|
|
out[k] = q[k] if q.key?(k)
|
|
end
|
|
|
|
# --- pipeline prod (grain topic, tel quel) ---------------------------------
|
|
begin
|
|
out["prod_block"] =
|
|
DepiedencapAiCitations::Retrieval.instructions_block(q["question"])
|
|
out["prod_sources"] =
|
|
DepiedencapAiCitations::Retrieval.for_question(q["question"]).map do |s|
|
|
{ title: s[:title], url: s[:url], via: s[:via] }
|
|
end
|
|
rescue StandardError => e
|
|
out["prod_error"] = "#{e.class}: #{e.message}"
|
|
end
|
|
|
|
# --- candidats grain post (nouveau pipeline : rerank côté hôte) ------------
|
|
out["posts"] =
|
|
begin
|
|
qvec = DiscourseAi::Embeddings::Vector.instance.vector_from(q["question"])
|
|
sql = <<~SQL
|
|
SELECT e.post_id, p.topic_id, p.post_number, t.title, t.slug,
|
|
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
|
|
WHERE e.model_id = 1 AND e.strategy_id = 1
|
|
AND p.deleted_at IS NULL
|
|
ORDER BY e.embeddings <=> '#{qvec}'
|
|
LIMIT 25
|
|
SQL
|
|
rows = ActiveRecord::Base.connection.execute(sql).to_a
|
|
post_ids = rows.map { |r| r["post_id"].to_i }
|
|
raws = Post.where(id: post_ids).pluck(:id, :raw).to_h
|
|
rows.map do |row|
|
|
raw = raws[row["post_id"].to_i].to_s.gsub(/\s+/, " ").strip[0, 300]
|
|
{
|
|
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"],
|
|
dist: row["dist"].to_f.round(4),
|
|
text: "#{row["title"]}. #{raw}",
|
|
}
|
|
end
|
|
rescue StandardError => e
|
|
[{ error: "#{e.class}: #{e.message}" }]
|
|
end
|
|
|
|
puts JSON.generate(out)
|
|
$stdout.flush
|
|
end
|