Spaces:
Paused
Paused
File size: 6,126 Bytes
656439d | 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 | """Agentic multi-hop loop β decomposition, sufficiency judging, and end-to-end
multi-hop retrieval on the offline stack (hash embedder + memory store)."""
from __future__ import annotations
from auralynq.agent.agentic import _decompose, _judge_sufficiency
from auralynq.ingest.models import Chunk, Document, SourceType
from auralynq.llm.fallback import ExtractiveLLM
class _LLM(ExtractiveLLM):
"""ExtractiveLLM (keeps .answer for synthesis) with a scripted .generate."""
name = "scripted"
def __init__(self, script):
super().__init__()
self._script = script
self.prompts = []
def generate(self, prompt, **kw):
self.prompts.append(prompt)
for needle, reply in self._script:
if needle in prompt:
return reply(self) if callable(reply) else reply
return super().generate(prompt, **kw)
# ββ unit: decomposition βββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_decompose_parses_sub_questions():
llm = _LLM([("Break the QUESTION", "Who is X?\nWhat did X create?")])
subs = _decompose(llm, "What did the founder of X create?", 4)
assert subs == ["Who is X?", "What did X create?"]
def test_decompose_falls_back_to_original_on_junk():
llm = _LLM([("Break the QUESTION", "")]) # empty β fallback
assert _decompose(llm, "simple question", 4) == ["simple question"]
llm2 = _LLM([("Break the QUESTION", lambda s: (_ for _ in ()).throw(RuntimeError()))])
assert _decompose(llm2, "q", 4) == ["q"]
# ββ unit: sufficiency βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class _State:
def __init__(self, contexts):
self.contexts = contexts
class _Ctx:
def __init__(self, text):
self.chunk = type("C", (), {"text": text})()
def test_judge_sufficiency():
llm = _LLM([("PASSAGES:", "SUFFICIENT")])
st = _State([_Ctx("some evidence")])
assert _judge_sufficiency(llm, "q", st) is None # sufficient β stop
llm2 = _LLM([("PASSAGES:", "who founded Nokia")])
assert _judge_sufficiency(llm2, "q", st) == "who founded Nokia" # follow-up
# no contexts β nothing to judge
assert _judge_sufficiency(llm2, "q", _State([])) is None
# ββ integration: multi-hop retrieval ββββββββββββββββββββββββββββββββββββββββ
def _seed_two_hop_corpus():
from auralynq.pipeline import index_documents
docs = [
Document(
id="d1",
source="a.txt",
source_type=SourceType.text,
title="a",
content_hash="h1",
chunks=[
Chunk(
id=Chunk.make_id("d1", 0),
doc_id="d1",
ordinal=0,
source="a.txt",
text="Ericsson's main competitor in mobile network equipment is Nokia.",
)
],
),
Document(
id="d2",
source="b.txt",
source_type=SourceType.text,
title="b",
content_hash="h2",
chunks=[
Chunk(
id=Chunk.make_id("d2", 0),
doc_id="d2",
ordinal=0,
source="b.txt",
text="Nokia was founded by Fredrik Idestam in 1865 as a pulp mill company.",
)
],
),
]
index_documents(docs)
def _run_state(monkeypatch, llm, question):
"""Drive the agentic executor and return the final AgentState (so we can
inspect hops + accumulated contexts directly)."""
monkeypatch.setattr("auralynq.agent.runner.get_llm", lambda: llm)
from auralynq.agent.graph import run_agent
from auralynq.agent.runner import _build_deps, _new_state
from auralynq.telemetry.tracing import Trace
deps = _build_deps(Trace(trace_id="t"), None)
state = _new_state(question, None, agentic=True)
return run_agent(state, deps)
def _sources(state):
return {c.chunk.source for c in state.contexts}
def test_multihop_decomposition_retrieves_both_docs(monkeypatch):
_seed_two_hop_corpus()
# decompose into two hops (one per document); then judge sufficiency β stop
llm = _LLM(
[
("Break the QUESTION", "Ericsson main competitor\nwho founded Nokia pulp mill"),
("PASSAGES:", "SUFFICIENT"),
]
)
state = _run_state(
monkeypatch, llm, "What did the founder of Ericsson's main competitor create?"
)
assert state.sub_questions == ["Ericsson main competitor", "who founded Nokia pulp mill"]
assert state.hops >= 2
assert state.answer.strip()
# multi-hop ACCUMULATED evidence from BOTH documents (the whole point)
srcs = _sources(state)
assert any("a.txt" in s for s in srcs) and any("b.txt" in s for s in srcs)
def test_followup_hop_from_sufficiency_judge(monkeypatch):
_seed_two_hop_corpus()
calls = {"n": 0}
def sufficiency(_s):
calls["n"] += 1
return "who founded Nokia pulp mill" if calls["n"] == 1 else "SUFFICIENT"
# single sub-question (echo) + a follow-up produced by the sufficiency judge
llm = _LLM(
[("Break the QUESTION", "Ericsson main competitor Nokia"), ("PASSAGES:", sufficiency)]
)
state = _run_state(monkeypatch, llm, "Ericsson main competitor Nokia")
assert state.hops >= 2 # original hop + one follow-up hop
assert calls["n"] >= 1
# the follow-up hop pulled in the second document's evidence
assert any("b.txt" in s for s in _sources(state))
def test_agentic_strategy_registered():
from auralynq.rag.strategy_registry import get_registry
reg = get_registry()
strat = reg.get("agentic")
assert strat is not None
available, _ = strat.is_available()
assert available is True
|