Spaces:
Runtime error
Runtime error
Download tests/test_pipeline.py from MuhammadAzfar/ResearchMind-AI: direct link, hf CLI and curl.
- Browser
- Download file 9.52 kB
-
https://huggingface.co/spaces/MuhammadAzfar/ResearchMind-AI/resolve/main/tests/test_pipeline.py
- Command line
-
hf download hf://spaces/MuhammadAzfar/ResearchMind-AI/tests/test_pipeline.py
-
curl -L -o test_pipeline.py https://huggingface.co/spaces/MuhammadAzfar/ResearchMind-AI/resolve/main/tests/test_pipeline.py
9.52 kB
| import os | |
| import pytest | |
| from src import agents, config, pipeline | |
| from src.documents import load_documents | |
| from src.evidence import EvidenceStore | |
| from tests.conftest import WEB, fake_searcher | |
| def run(fake_llm, embedder, **kwargs): | |
| events = list(pipeline.run_research( | |
| "AI in agriculture", searcher=fake_searcher, embedder=embedder, llm_api=fake_llm, **kwargs | |
| )) | |
| return events, events[-1] | |
| # ------------------------------------------------------------ agents | |
| def test_parse_json_handles_fences_and_prose(): | |
| assert agents.parse_json('```json\n{"a": 1}\n```') == {"a": 1} | |
| assert agents.parse_json('Sure! {"a": 1} hope that helps') == {"a": 1} | |
| assert agents.parse_json("not json") is None | |
| def test_planner_falls_back_to_topic_on_garbage(fake_llm): | |
| assert agents.plan("my topic", 3, lambda *a, **k: "garbage") == ["my topic"] | |
| def test_planner_limits_and_dedupes(): | |
| reply = '{"sub_questions": ["a", "a", "b", "c", "d"]}' | |
| assert agents.plan("t", 3, lambda *a, **k: reply) == ["a", "b", "c"] | |
| def test_clean_citations(): | |
| assert agents.clean_citations("A [1] B [9] C [2, 7] D [2026]", 3) == "A [1] B C [2] D [2026]" | |
| assert agents.clean_citations("x [1, 2]", 5) == "x [1][2]" | |
| def test_extract_claims_keeps_citation_with_its_sentence(): | |
| report = ( | |
| "## Heading\n" | |
| "- Solar panels share land with crops. [1] Drones detect disease early [2].\n" | |
| "This sentence has plenty of words but no citation at all in it.\n" | |
| "> quoted verification note [1]\n" | |
| ) | |
| claims, uncited = agents.extract_claims(report) | |
| assert [c["cites"] for c in claims] == [[1], [2]] | |
| assert claims[0]["text"].startswith("Solar panels share land with crops") | |
| assert uncited == 1 | |
| def test_research_web_dedupes_urls_and_collects_errors(): | |
| from src.search import SearchError | |
| def searcher(q, max_results, search_depth): | |
| if q == "bad": | |
| raise SearchError("boom") | |
| return [{"title": "T", "url": "https://x", "content": "text"}] | |
| sources, errors = agents.research_web(["a", "b", "bad"], {"results_per_question": 2, "search_depth": "basic"}, searcher) | |
| assert len(sources) == 1 and errors == ["boom"] | |
| # ---------------------------------------------------------- evidence | |
| def test_evidence_ranks_relevant_sources_first_and_drops_irrelevant(embedder): | |
| sources = [dict(s, kind="web") for s in WEB["solar"] + WEB["drone"]] | |
| store = EvidenceStore(sources, embedder) | |
| selected, numbered = store.gather(["solar panels on farmland", "drone crop monitoring"], 5) | |
| titles = [s["title"] for s in numbered] | |
| assert titles[0] == "Agrivoltaics explained" | |
| assert "Cooking pasta" not in titles | |
| assert len(selected) <= config.MAX_EVIDENCE_CHUNKS | |
| assert all(sum(1 for c in selected if c["source_idx"] == i) <= config.MAX_CHUNKS_PER_SOURCE for i in range(3)) | |
| def test_support_for_returns_passages_from_cited_source_only(embedder): | |
| sources = [dict(s, kind="web") for s in WEB["solar"] + WEB["drone"]] | |
| store = EvidenceStore(sources, embedder) | |
| store.gather(["solar panels on farmland", "drone crop monitoring"], 5) | |
| passages = store.support_for("drones detect plant disease", [2], k=2) | |
| assert passages and all("Drones" in p for p in passages) | |
| assert store.support_for("anything", [99]) == [] | |
| # ----------------------------------------------------------- pipeline | |
| def test_full_agent_loop_catches_and_fixes_unsupported_claim(fake_llm, embedder): | |
| events, last = run(fake_llm, embedder) | |
| assert last["done"] and not last["error"] | |
| result = last["result"] | |
| # Checker flagged the planted claim, Reviser removed it, re-check is clean | |
| m = result["metrics"] | |
| assert m["first_pass"]["unsupported"] == 1 | |
| assert m["final"]["unsupported"] == 0 and m["final"]["support_rate"] == 1.0 | |
| assert m["revised"] is True | |
| assert "cheese" not in result["report"] | |
| # Invented citation [9] removed, sources + verification note appended | |
| assert "[9]" not in result["report"] | |
| assert "## Sources" in result["report"] and "https://a.example/agri" in result["report"] | |
| assert "Verification" in result["report"] | |
| # Trace shows every agent | |
| trace = last["trace"] | |
| for agent in ("Planner", "Researcher", "Retriever", "Writer", "Checker", "Reviser"): | |
| assert agent in trace | |
| assert "Translator" not in trace | |
| def test_draft_streams_progressively(fake_llm, embedder): | |
| events, _ = run(fake_llm, embedder) | |
| drafts = [e["report"] for e in events if e["report"] and e["report"].startswith("*Draft")] | |
| assert len(drafts) >= 3 | |
| def test_urdu_runs_translator_after_verification(fake_llm, embedder): | |
| _, last = run(fake_llm, embedder, language="Urdu") | |
| result = last["result"] | |
| assert result["report"].startswith("[UR]") | |
| assert "cheese" not in result["report_english"] | |
| assert "Translator" in last["trace"] | |
| def test_documents_only_mode(fake_llm, embedder, tmp_path): | |
| doc = tmp_path / "notes.txt" | |
| doc.write_text("Greenhouse farming with solar panels on farmland increases crop yield per hectare. " * 10) | |
| _, last = run(fake_llm, embedder, use_web=False, document_paths=[str(doc)]) | |
| assert last["done"] | |
| sources = last["result"]["sources"] | |
| assert sources and all(s["kind"] == "document" for s in sources) | |
| assert "📄" in last["result"]["report"] | |
| def test_no_web_and_no_documents_is_a_clear_error(fake_llm, embedder): | |
| _, last = run(fake_llm, embedder, use_web=False) | |
| assert last["error"] and "nothing to research" in last["report"] | |
| def test_empty_topic_is_rejected(fake_llm, embedder): | |
| events = list(pipeline.run_research(" ", searcher=fake_searcher, embedder=embedder, llm_api=fake_llm)) | |
| assert events[-1]["error"] | |
| def test_search_failure_without_documents_surfaces_message(fake_llm, embedder): | |
| from src.search import SearchError | |
| def broken(q, max_results=4, search_depth="basic"): | |
| raise SearchError("`TAVILY_API_KEY` is missing") | |
| events = list(pipeline.run_research("topic here", searcher=broken, embedder=embedder, llm_api=fake_llm)) | |
| assert events[-1]["error"] and "TAVILY_API_KEY" in events[-1]["report"] | |
| def test_writer_failure_is_reported_not_raised(embedder, fake_llm): | |
| import types | |
| from src.llm import LLMError | |
| def bad_stream(*a, **k): | |
| raise LLMError("All Groq models failed.") | |
| yield | |
| broken = types.SimpleNamespace(complete=fake_llm.complete, stream=bad_stream) | |
| events = list(pipeline.run_research("topic here", searcher=fake_searcher, embedder=embedder, llm_api=broken)) | |
| assert events[-1]["error"] and "All Groq models failed" in events[-1]["report"] | |
| def test_checker_outage_still_delivers_report(embedder, fake_llm): | |
| import types | |
| from src.llm import LLMError | |
| def complete(messages, role="fast", temperature=0.0, json_mode=False): | |
| if "Planner" in messages[0]["content"]: | |
| return fake_llm.complete(messages, role, temperature, json_mode) | |
| raise LLMError("rate limited") | |
| llm_api = types.SimpleNamespace(complete=complete, stream=fake_llm.stream) | |
| _, last = None, list(pipeline.run_research("topic here", searcher=fake_searcher, embedder=embedder, llm_api=llm_api))[-1] | |
| assert last["done"] and "unavailable" in last["result"]["report"] | |
| def test_reviser_guard_rejects_stub_rewrites(embedder): | |
| report = "word " * 200 | |
| claims = [{"id": 1, "text": "x [1]", "cites": [1]}] | |
| results = {1: {"verdict": "unsupported", "reason": "r"}} | |
| class Store: | |
| def support_for(self, *a, **k): | |
| return ["evidence"] | |
| assert agents.revise(report, claims, results, Store(), lambda *a, **k: "ok") == report | |
| # ---------------------------------------------------------- documents | |
| def test_load_documents_pdf_txt_and_warnings(tmp_path): | |
| from reportlab.lib.pagesizes import LETTER | |
| from reportlab.platypus import Paragraph, SimpleDocTemplate | |
| from reportlab.lib.styles import getSampleStyleSheet | |
| pdf = tmp_path / "paper.pdf" | |
| SimpleDocTemplate(str(pdf), pagesize=LETTER).build( | |
| [Paragraph("Agrivoltaics lets farmers grow crops under solar panels and earn extra income. " * 5, getSampleStyleSheet()["BodyText"])] | |
| ) | |
| txt = tmp_path / "empty.txt" | |
| txt.write_text("hi") | |
| odd = tmp_path / "data.xyz" | |
| odd.write_text("whatever " * 30) | |
| docs, warnings = load_documents([str(pdf), str(txt), str(odd)]) | |
| assert [d["title"] for d in docs] == ["paper.pdf"] | |
| assert "Agrivoltaics" in docs[0]["content"] and docs[0]["kind"] == "document" | |
| assert len(warnings) == 2 | |
| # ---------------------------------------------------------- evaluation | |
| def test_evaluate_aggregates_and_writes_reports(fake_llm, embedder, tmp_path): | |
| import functools | |
| import evaluate | |
| runner = functools.partial(pipeline.run_research, searcher=fake_searcher, embedder=embedder, llm_api=fake_llm) | |
| rows = [evaluate.evaluate_topic("AI in agriculture", "Quick", runner)] | |
| rows.append(evaluate.evaluate_topic("x", "Quick", runner)) # too short -> failure row | |
| assert rows[0]["ok"] and rows[0]["support_first"] < rows[0]["support_final"] == 1.0 | |
| assert not rows[1]["ok"] | |
| summary = evaluate.aggregate(rows) | |
| assert summary["completed"] == 1 and summary["revised_count"] == 1 | |
| evaluate.write_outputs(rows, summary, str(tmp_path), "Quick") | |
| text = (tmp_path / "RESULTS.md").read_text(encoding="utf-8") | |
| assert "first draft" in text and "100%" in text and (tmp_path / "results.json").exists() | |