Spaces:
Running
Running
eliezer avihail
UI: send-on-Enter + live reasoning trace; docs: answer-cache decision (#106)
a170f20 unverified | 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") | |