torchdocs-agent / tests /agent /test_loop.py
eliezer avihail
UI: send-on-Enter + live reasoning trace; docs: answer-cache decision (#106)
a170f20 unverified
Raw
History Blame Contribute Delete
6.38 kB
from agent.loop import _humanize, _parse_action, answer_agentic
from agent.schemas import Answer
def test_parse_action_extracts_json_from_noise():
assert _parse_action('sure! {"action":"search_docs","query":"sgd"} ')["query"] == "sgd"
assert _parse_action("no json here")["action"] == "answer"
assert _parse_action('{"action":"bogus"}')["action"] == "answer"
def test_humanize_renders_each_step_and_falls_back():
# a search step → query + the last segment of each heading path, capped at 4
line = "search_docs('sgd momentum') → ['torch.optim > SGD', 'SGD > Per-parameter options']"
out = _humanize(line)
assert "🔍" in out and "sgd momentum" in out
assert "SGD" in out and "Per-parameter options" in out
assert "torch.optim >" not in out # only the leaf heading is shown
assert _humanize("read_page(http://x) → full page added").startswith("📖")
assert _humanize("ask_source → 2 referral links").startswith("🔗")
# an unrecognised shape degrades to the raw line, never crashes
assert "weird step" in _humanize("weird step")
def test_loop_streams_a_progress_trace(monkeypatch):
scripted = ScriptedAgent(
['{"action":"search_docs","query":"dataloader"}', '{"action":"answer"}'], None
)
monkeypatch.setattr("agent.llm._raw_completion", scripted.plan)
monkeypatch.setattr(
"agent.tools.search_docs",
lambda q, library=None, kind=None, k=8: {"query": q, "sections": [], "titles": ["H"]},
)
monkeypatch.setattr(
"agent.grounded.answer_from_sections",
lambda q, s, referrals=None, provider=None, client=None: Answer(answer_md="ok"),
)
seen: list[str] = []
answer_agentic("how do I load data?", progress=seen.append)
# seed search + planner search are both surfaced, and the final writing step
assert sum(1 for line in seen if line.startswith("🔍")) == 2
assert seen[-1].startswith("✍️")
class ScriptedAgent:
"""Drives the loop: a queue of planner actions, then a final Answer JSON."""
def __init__(self, plan_actions, final_answer):
self._plans = list(plan_actions)
self._final = final_answer
def plan(self, prompt, system, provider=None, client=None):
return self._plans.pop(0)
def final(self, *a, **k):
return self._final
def test_loop_decomposes_and_answers(monkeypatch):
# planner asks for two searches, then answers
scripted = ScriptedAgent(
['{"action":"search_docs","query":"cnn image classification"}',
'{"action":"search_docs","query":"dataloader images"}',
'{"action":"answer"}'],
None,
)
calls = {"search": 0}
def fake_search(query, library=None, kind=None, k=8):
calls["search"] += 1
return {"query": query, "sections": [{"url": f"u{calls['search']}", "anchor": "",
"heading_path": "H", "content": "text"}], "titles": ["H"]}
monkeypatch.setattr("agent.llm._raw_completion", scripted.plan)
monkeypatch.setattr("agent.tools.search_docs", fake_search)
captured = {}
def fake_answer(question, sections, referrals=None, provider=None, client=None):
captured["sections"] = sections
captured["referrals"] = referrals
return Answer(answer_md="done")
monkeypatch.setattr("agent.grounded.answer_from_sections", fake_answer)
result = answer_agentic("how do I build a CNN for images?")
assert result.answer_md == "done"
# 1 seed search + 2 planner-driven searches, all distinct results accumulated
assert calls["search"] == 3
assert len(captured["sections"]) == 3
def test_loop_source_question_adds_referrals(monkeypatch):
scripted = ScriptedAgent(
['{"action":"ask_source","question":"conv2d internals"}', '{"action":"answer"}'],
None,
)
monkeypatch.setattr("agent.llm._raw_completion", scripted.plan)
# seed search runs first — stub it so the test stays offline
monkeypatch.setattr(
"agent.tools.search_docs",
lambda q, library=None, kind=None, k=8: {"query": q, "sections": [], "titles": []},
)
captured = {}
def fake_answer(question, sections, referrals=None, provider=None, client=None):
captured["referrals"] = referrals
return Answer(answer_md="see source")
monkeypatch.setattr("agent.grounded.answer_from_sections", fake_answer)
answer_agentic("how is conv2d implemented?")
assert captured["referrals"] # ask_source contributed referral links
assert any("deepwiki" in r.url for r in captured["referrals"])
def test_loop_stops_at_budget(monkeypatch):
# planner always wants to search; budget must cap it
scripted = ScriptedAgent(
['{"action":"search_docs","query":"x"}'] * 20, None
)
monkeypatch.setattr("agent.llm._raw_completion", scripted.plan)
monkeypatch.setattr(
"agent.tools.search_docs",
lambda q, library=None, kind=None, k=8: {"query": q, "sections": [], "titles": []},
)
captured = {}
monkeypatch.setattr(
"agent.grounded.answer_from_sections",
lambda q, s, referrals=None, provider=None, client=None: captured.setdefault("hit", True)
or Answer(answer_md="x"),
)
answer_agentic("q") # must terminate (MAX_STEPS / budget), not loop forever
assert captured["hit"]
def test_planner_kind_reaches_search_docs(monkeypatch):
# the planner decides the content space; its kind must reach the tool
scripted = ScriptedAgent(
['{"action":"search_docs","query":"cross entropy loss","kind":"api"}',
'{"action":"answer"}'],
None,
)
seen = []
def fake_search(query, library=None, kind=None, k=8):
seen.append((query, kind))
return {"query": query, "sections": [], "titles": []}
monkeypatch.setattr("agent.llm._raw_completion", scripted.plan)
monkeypatch.setattr("agent.tools.search_docs", fake_search)
monkeypatch.setattr(
"agent.grounded.answer_from_sections",
lambda q, s, referrals=None, provider=None, client=None: Answer(answer_md="ok"),
)
answer_agentic("what loss functions exist for classification?")
# seed search first (no kind), then the planner's api-scoped search
assert seen[0] == ("what loss functions exist for classification?", None)
assert seen[1] == ("cross entropy loss", "api")