unlob Docs
Browse documentation

LlamaIndex

A retriever with no index to build, or the MCP tool spec.

As a retriever

The natural fit. Hits are already passages, so there is nothing to chunk, embed or store — the retriever is a function call.

import os, httpx
from llama_index.core.retrievers import BaseRetriever
from llama_index.core.schema import NodeWithScore, TextNode

class UnlobRetriever(BaseRetriever):
    def __init__(self, top_k: int = 8, **kwargs):
        self.top_k = top_k
        self._client = httpx.Client(
            base_url="https://api.unlob.com",
            headers={"x-api-key": os.environ["UNLOB_API_KEY"]},
            timeout=20,
        )
        super().__init__(**kwargs)

    def _retrieve(self, query_bundle) -> list[NodeWithScore]:
        r = self._client.get("/search", params={
            "q": query_bundle.query_str,
            "limit": self.top_k,
            # One row per near-duplicate cluster, so the context window does not
            # fill with the same syndicated article.
            "collapse": "story",
        })
        r.raise_for_status()
        body = r.json()
        if body.get("partial"):
            # Incomplete, not empty. Worth surfacing rather than silently
            # returning three nodes as though that were the whole corpus.
            print("unlob: partial results — part of the corpus was unreachable")
        return [
            NodeWithScore(
                node=TextNode(
                    text=h["snippet"],
                    id_=h["id"],
                    metadata={k: h.get(k) for k in
                              ("url", "host", "title", "independent_sources")},
                ),
                score=h["score"],
            )
            for h in body["results"]
        ]
from llama_index.core.query_engine import RetrieverQueryEngine
engine = RetrieverQueryEngine.from_args(UnlobRetriever(top_k=8))
print(engine.query("How does tokio schedule tasks?"))

score is comparable within a response, not between responses — do not threshold on an absolute value. Use min_quality or min_host_rank if you want a floor that means the same thing every time.

As MCP tools

llama-index-tools-mcp converts the server into a tool spec, which gives an agent all eleven rather than search alone:

from llama_index.tools.mcp import BasicMCPClient, McpToolSpec

mcp_client = BasicMCPClient(
    "https://api.unlob.com/mcp",
    headers={"x-api-key": os.environ["UNLOB_API_KEY"]},
)
tools = McpToolSpec(client=mcp_client).to_tool_list()

That is worth it when the agent should be able to corroborate a claim or brief itself on an entity, not only search.

Skipping the pipeline

If the question is “what should I know about X”, assemble_context does the whole retrieval loop server-side and returns a packed, deduplicated, trust-ranked reading set — one request instead of retrieve-rerank-synthesise.

pack = client.get("/assemble_context",
                  params={"q": question, "budget": 4000}).json()

Next