Spaces:
Runtime error
Runtime error
File size: 9,523 Bytes
da4758c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | 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()
|