boost / docs / langchain integration

Interop · shipped in PR #466

The skill catalog, behind LangChain's retriever contract.

boost's tapped catalog is 10,000+ retrievable procedures with frontmatter, provenance and a lock file. boost-langchain makes it addressable from a Python agent runtime: a retriever over the same measured engine boost search runs on, a document loader for one SKILL.md, and a LangGraph node that pulls the right procedure into a run mid-flight. No API key, ever — and nothing tapped means [], not an exception.

3Seams, all thin on purpose
10k+Catalog items retrievable
4Metrics floored behind the engine
0API keys required
The fit

Two audiences — one of them should leave now

If you are a LangChain developer who just wants skills in your editor — Claude Code, Cursor, Windsurf, Gemini CLI — you do not need this package. Install the boost CLI and run boost install <skill>: no langchain dependency at all.

This package exists for the other direction: putting the catalog inside a LangChain application, so a chain can search it, stuff it into context, or route on it at runtime. That scoping decision shaped the whole API — it is a retriever, not an installer. Installing is the CLI's job, with its lock file and agent symlinks; duplicating that path here would have created a second, redundant way to install and nothing else. What a Python agent runtime actually lacks is reach: the same procedure otherwise gets re-written as a prompt string by hand.

Everything that decides what comes back — ranking, fusion, dedupe, the degrade ladder — stays in boost_cli.core, where the repo's eval gate measures it. This package only translates the result into LangChain's types.

Architecture

Three thin seams over one measured engine

Queries flow left to right; Documents flow back. The seams own translation; the engine owns retrieval; the gate below the engine is why wrapping it beats re-embedding the catalog.

Your LangChain app chain · | · Runnable LangGraph StateGraph prompt assembly boost_langchain BoostRetriever skill_context_node SkillMarkdownLoader translation only — no ranking boost engine BM25 full-content ⊕ dense (optional) rag.rrf_fuse · RRF_K=60 boost_cli.core — same as `boost search` Tapped catalog ~/.boost/repos skills · rules workflows query k · kind index hits Documents Tier-1 eval gate · required floors recall@k · hit@1 · MRR · nDCG@k golden set, every merge — see the Evaluation page measures this engine Degrades → hybrid RRF (BM25 + dense) → dense vectors → BM25 full-content → [] when nothing is tapped metadata["engine"] names which rung answered — the first thing to check when quality surprises an empty shelf is an answer, not an error: the chain keeps running without boost context

The seams never rank. Everything that decides what comes back lives in boost_cli.core, under the gate.

  • Reuse, don't re-embed. The retriever wraps rag.retrieve_any — the engine boost search runs on — rather than re-embedding the catalog into a vector store. Retrieval quality is the reason: boost's Tier-1 gate floors recall@k, hit@1, MRR and nDCG@k over a golden query set on every merge, so this retriever ships with numbers rather than claims. A fresh embedding pipeline here would start from zero evidence.
  • Degrade to [], never raise. The always-on engine is a pure-stdlib BM25 full-content index that builds itself on first use (rag.ensure() makes a fresh tap behave like boost search does). With boost's [rag] extra the dense engine joins by reciprocal rank fusion; a Voyage or OpenAI key upgrades embedding quality but a local model ships with the extra. Nothing tapped returns [] — a chain keeps running without boost context rather than crashing on it. One honest caveat: the BM25 tier tokenizes [a-z0-9], so non-Latin queries (CJK, emoji) retrieve nothing from it — the dense tier is what serves them.
  • One catalog, three kinds, one filter. boost indexes skill (SKILL.md), rule (.mdc, .cursorrules, …) and workflow (slash commands / subagents). kind narrows retrieval to one of them — a router choosing among rules should not wade through skills — and None searches all three.
Paste this

Twenty lines, standard chain shape

BoostRetriever is a langchain-core BaseRetriever, so it is a Runnable and drops into the usual retrieval-chain shape unchanged.

from boost_langchain import BoostRetriever

retriever = BoostRetriever(k=4)
docs = retriever.invoke("set up code review for a python repo")
for d in docs:
    print(d.metadata["name"], d.metadata["kind"], d.metadata["tap"])

# It is a standard Runnable retriever, so the usual chain shape works:
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt          # your ChatPromptTemplate
    | model           # your chat model
    | StrOutputParser()
)
chain.invoke("how should this repo handle commit messages?")

# kind narrows retrieval to one of boost's three item kinds:
BoostRetriever(kind="rule")       # only rules (.mdc, .cursorrules, ...)
BoostRetriever(kind="workflow")   # only slash commands / subagents
Validated at construction, not at query time. k and kind are pydantic-checked fields: a typo'd kind would otherwise return [] for every query — indistinguishable from an empty catalog — and a negative k would silently drop the last hit via slice semantics. Both fail loudly the moment you build the retriever instead.

Each Document's page_content is the item's indexed text — its name and one-line description followed by the full body, i.e. exactly the surface BM25 scored, so what you retrieve is what matched. Pass full_content=False for just the description (what a router choosing among items wants, rather than a chain stuffing context). Metadata carries:

keywhat it holds
name / kindthe item's name and which of the three kinds it is
tap / source / versionprovenance: registry, in-repo path, frontmatter version
enginewhich engine actually answered — "BM25 full-content", "dense vectors", or "hybrid RRF (BM25 + dense)"

For one specific skill — no retrieval, just loading — SkillMarkdownLoader yields a SKILL.md as a single Document: body as content, frontmatter as metadata. One Document per skill, not one per chunk: a skill file is already the author's chosen unit of instruction (run any LangChain splitter over it if you want chunks). Parsing reuses boost's own frontmatter parser, so the file reads exactly the way boost's catalog reads it.

from boost_langchain import SkillMarkdownLoader

# From a path (a SKILL.md, or a directory containing one):
doc = SkillMarkdownLoader("path/to/skill").load()[0]

# From boost's canonical store, for a skill you already installed:
doc = SkillMarkdownLoader.from_installed("brainstorming").load()[0]

doc.page_content   # the markdown body, frontmatter stripped
doc.metadata       # frontmatter keys (name, version, tags, ...) + source

Two details that earn their keep: the real file path always wins over any frontmatter key that happens to be named source — provenance must state where the bytes came from, and a self-reported value is the one thing that cannot. And from_installed declines a name installed as a rule or workflow by kind rather than with a bare missing-file error: those kinds materialize into agent context files and keep no store copy to load — retrieve them with BoostRetriever(kind="rule") instead.

Sequence · a LangGraph run

Pull the procedure mid-run, not up front

The alternative this exists to avoid is stuffing every procedure into the system prompt: that spends context on procedures the run never needs and stops scaling the moment the catalog does. skill_context_node retrieves against the current state instead, so each turn carries only the procedures that turn asked for.

user turn HumanMessage skills node skill_context_node(k=3) BoostRetriever → boost engine agent node your model START → state["messages"] — node reads the last human turn retriever.invoke(query) · k=3, full bodies Documents + name · tap · path · version · engine {"messages": [SystemMessage]} — each procedure prefixed ### name (kind — tap:path @ version) model answers with the procedure in context ✓ no human turn · nothing tapped · no match → {} no state update — the graph runs on without boost never blocked on retrieval

The no-op path is a contract, not an accident: a graph wired through this node must keep working on a machine with an empty catalog.

from boost_langchain import skill_context_node
from langgraph.graph import StateGraph, MessagesState, START

builder = StateGraph(MessagesState)
builder.add_node("skills", skill_context_node(k=3))   # or kind="workflow", or a configured BoostRetriever
builder.add_node("agent", call_model)                 # your model node
builder.add_edge(START, "skills")
builder.add_edge("skills", "agent")
  • It retrieves on the last human message. Earlier human turns are already answered context; retrieving on them would re-inject procedures for questions the graph has moved past.
  • Provenance rides along. Injected text steers the model, so when a run goes wrong the first question is which skill said that and where it came from. Every injected procedure is prefixed with its name, tap, in-repo path and version — a poisoned or stale skill is traceable from the transcript alone, without re-running retrieval.
  • k defaults to 3 here, not the retriever's 8. These are full procedure bodies landing in a live conversation: three is a context budget, eight is a search page. Pass a configured BoostRetriever to control retrieval wholesale.
  • The node imports only langchain_core. A graph node is just a callable from state to a partial state update, so langgraph itself stays where the pyproject puts it — an optional extra.
Observe

Tracing you don't write, ground truth you already have

Because BoostRetriever is a langchain-core BaseRetriever, LangSmith instrumentation traces its calls automatically — enable tracing the standard way (LANGSMITH_TRACING=true + a key) and every retrieval shows up with its query, its documents, and the engine metadata naming which of boost's engines answered.

Offline, boost's required gate already floors recall@k / hit@1 / MRR / nDCG@k over a golden query set on every merge (the Evaluation page is that story). To run online evals against the same ground truth, publish the golden set as a LangSmith dataset with the publish_golden_dataset.py script (under integrations/langchain/scripts/) — key-gated, opt-in, run from a boost checkout. Re-running replaces examples wholesale, so the dataset always mirrors tests/eval/golden.jsonl rather than accreting. The point is one set of judgments in both places: an online regression and an offline regression mean the same thing.

None of this touches the required gate, which stays offline, deterministic and key-free — a required check that depends on a SaaS account is a required check that fails when someone else's billing lapses.

The honest dependency paragraph. boost's own [eval] extra pins the langchain 0.3 stack, because ragas — through 0.4.3 — still imports a ChatVertexAI path that langchain-community 0.4 removed, so import ragas crashes beside langchain 1.x. What dissolves the conflict is distribution isolation, not an unpin: boost-langchain is a separately versioned package, so the langchain 1.x stack lives in its own environment and its own CI leg while [eval] keeps its 0.3-stack pins in its own. The two majors never co-install — pip refuses the combination loudly — and each surface tests against the stack it really runs on. The unpin becomes a small follow-up the day ragas ships a release after 0.4.3, which already carries the fix on its main branch.

Install

One pip install, two optional extras

pip · boost-langchain
# the package — pulls in boost-skill-cli + langchain-core
$ pip install boost-langchain

# give boost something to search (once)
$ boost tap anthropics/skills

# optional: the LangGraph runtime for skill_context_node graphs
$ pip install 'boost-langchain[langgraph]'

# optional: publish the golden set as a LangSmith dataset
$ pip install 'boost-langchain[langsmith]'
$ python integrations/langchain/scripts/publish_golden_dataset.py
  published 91 examples to LangSmith dataset 'boost-golden-retrieval'

The dataset publish is illustrative output; it exits with a one-line hint rather than a traceback when the key or package is missing.

More depth: the package README covers the same ground as a reference, and the roadmap card holds the full reasoning — including the packaging decision and the corrected ragas story.