The analyze node

The analyze node is the brain of the agent. Every time we arrive at a node, we hand the LLM the current title, summary, and the list of children, then ask: how relevant is this node, and which child should we descend into? The answer comes back as structured JSON.

retriever.py
python
prompt = f"""You are navigating a research paper tree to answer a query.

Query: "{state['query']}"

Current node:
  id      : {node.id}
  title   : {node.title}
  summary : {getattr(node, 'summary', '')[:300]}
  pages   : {node.page_start}-{node.page_end}

Children: {json.dumps(children_info, indent=2) if children_info else "None (leaf node)"}

Decide:
1. confidence      : 0-1, how likely does this node contain the answer?
2. should_descend  : true only if a specific child is more relevant than this node
3. target_child_id : the id of the best child to visit (null if should_descend is false)
4. reasoning       : one sentence explaining your decision

Respond ONLY as valid JSON, no markdown fences."""

The prompt hands the LLM exactly what it needs: the query, the current node summary, and the child menu. The response schema is declared inline so the model produces parseable output.

retriever.py
python
raw, elapsed = _call_llm(client, model, prompt, "navigate", call_num)
raw = _strip_fences(raw)

try:
    decision = json.loads(raw)
except json.JSONDecodeError:
    logger.warning("[!] JSON parse failed, using fallback decision")
    decision = {
        "confidence": 0.5,
        "should_descend": bool(node.children),
        "target_child_id": node.children[0].id if node.children else None,
        "reasoning": "Fallback: could not parse LLM response",
    }

conf     = float(decision.get("confidence", 0.5))
descend  = bool(decision.get("should_descend", False))
child_id = decision.get("target_child_id")

Two safety nets. _strip_fences removes accidental markdown backticks before json.loads. If parsing still fails, we fall back to a neutral decision so the agent never crashes on a bad response.

Prompt-based JSON works with any OpenAI-compatible endpoint, which matters because the workshop defaults to OpenRouter so you can try cheap models. Structured outputs are better when you have the feature available, but a well-written prompt plus a fallback parser gets you very close at a fraction of the lock-in.

Quiz: Quiz

Loading practice…