# frozen_string_literal: true RSpec.describe DiscourseAi::Agents::Tools::Researcher do before { SearchIndexer.enable } after { SearchIndexer.disable } fab!(:llm_model) let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) } let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") } let(:progress_blk) { Proc.new {} } fab!(:admin) fab!(:user) fab!(:category) { Fabricate(:category, name: "research-category") } fab!(:tag_research) { Fabricate(:tag, name: "research") } fab!(:tag_data) { Fabricate(:tag, name: "data") } fab!(:topic_with_tags) { Fabricate(:topic, category: category, tags: [tag_research, tag_data]) } fab!(:post) { Fabricate(:post, topic: topic_with_tags) } fab!(:another_post) { Fabricate(:post) } before { SiteSetting.ai_bot_enabled = true } describe "#invoke" do it "can correctly filter to a topic id" do researcher = described_class.new( { dry_run: true, filter: "topic:#{topic_with_tags.id}", goals: "analyze topic content" }, bot_user: bot_user, llm: llm, context: DiscourseAi::Agents::BotContext.new(user: user, post: post), ) results = researcher.invoke(&progress_blk) expect(results[:number_of_posts]).to eq(1) end it "returns filter information and result count" do researcher = described_class.new( { filter: "tag:research after:2023", goals: "analyze post patterns", dry_run: true }, bot_user: bot_user, llm: llm, context: DiscourseAi::Agents::BotContext.new(user: user, post: post), ) results = researcher.invoke(&progress_blk) expect(results[:filter]).to eq("tag:research after:2023") expect(results[:goals]).to eq("analyze post patterns") expect(results[:dry_run]).to eq(true) expect(results[:number_of_posts]).to be > 0 expect(researcher.filter).to eq("tag:research after:2023") expect(researcher.result_count).to be > 0 end it "handles empty filters" do researcher = described_class.new({ goals: "analyze all content" }, bot_user: bot_user, llm: llm) results = researcher.invoke(&progress_blk) expect(results[:error]).to eq("No filter provided") end it "accepts max_results option" do researcher = described_class.new( { filter: "category:research-category" }, agent_options: { "max_results" => "50", }, bot_user: bot_user, llm: llm, ) expect(researcher.options[:max_results]).to eq(50) end it "returns error for invalid filter fragments" do researcher = described_class.new( { filter: "invalidfilter tag:research", goals: "analyze content" }, bot_user: bot_user, llm: llm, context: DiscourseAi::Agents::BotContext.new(user: user, post: post), ) results = researcher.invoke(&progress_blk) expect(results[:error]).to include("Invalid filter fragment") end it "returns correct results for non-dry-run with filtered posts" do # Stage 2 topics, each with 2 posts topics = Array.new(2) { Fabricate(:topic, category: category, tags: [tag_research]) } topics.flat_map do |topic| [ Fabricate(:post, topic: topic, raw: "Relevant content 1", user: user), Fabricate(:post, topic: topic, raw: "Relevant content 2", user: admin), ] end # Filter to posts by user in research-category researcher = described_class.new( { filter: "category:research-category @#{user.username}", goals: "find relevant content", dry_run: false, }, bot_user: bot_user, llm: llm, context: DiscourseAi::Agents::BotContext.new(user: user, post: post), ) responses = 10.times.map { |i| ["Found: Relevant content #{i + 1}"] } results = nil last_progress = nil progress_blk = Proc.new { |response| last_progress = response } DiscourseAi::Completions::Llm.with_prepared_responses(responses) do researcher.llm = llm_model.to_llm results = researcher.invoke(&progress_blk) end expect(last_progress).to include("find relevant content") expect(last_progress).to include("category:research-category") expect(results[:dry_run]).to eq(false) expect(results[:goals]).to eq("find relevant content") expect(results[:filter]).to eq("category:research-category @#{user.username}") expect(results[:results].first).to include("Found: Relevant content 1") end end end