dist/codex/nlweb-protocol/skills/nlweb-tools-framework/SKILL.md
Design and implement NLWeb tools — the per-Schema.org-type handlers that turn a query into a specialized response (search, item_details, compare_items, ensemble, recipe_substitution, accompaniment, conversation_search, etc.). Covers `tools.xml`, the ToolSelector router, builtin handlers in `methods/`, writing a custom tool with a `<returnStruc>` contract, and disabling tool selection for raw retrieval. Use when extending NLWeb beyond the default query → results flow.
npx skillsauth add orcaqubits/agentic-commerce-claude-plugins nlweb-tools-frameworkInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Fetch live docs:
AskAgent/python/core/router.py::ToolSelector for how routing actually picks a tool.AskAgent/python/methods/: generate_answer.py, item_details.py, compare_items.py, ensemble_tool.py, recipe_substitution.py, accompaniment.py.<returnStruc> JSON contract that handlers must satisfy.Confusingly, "tool" means two different things in NLWeb depending on context:
methods/ that the ToolSelector routes a query to (e.g., compare_items.py). This is the meaning used in this skill./mcp (ask, list_sites, who). See the nlweb-mcp-server skill for that meaning.When NLWeb's docs say "tools framework," they mean (1).
For every /ask request:
ToolSelector (core/router.py) inspects the decontextualized query + detected Schema.org type.site_types.xml / tools.xml for the candidate tools for that type.<returnStruc> JSON output schema) asking "which tool fits?"methods/<tool>.py is invoked.| Handler | Purpose |
|---------|---------|
| generate_answer.py | RAG synthesis — used for mode=generate |
| item_details.py | Deep-dive on a single result |
| compare_items.py | Side-by-side comparison of 2+ results |
| ensemble_tool.py | Multi-tool composition (e.g., "find a recipe and pair a wine") |
| recipe_substitution.py | Suggest ingredient swaps in a Recipe |
| accompaniment.py | "Goes with" suggestions (wine for food, sides for entrée) |
| multi_site_query.py | Query that spans multiple sites |
| conversation_search.py | Search within prior conversation context |
| statistics_handler.py | Aggregations over indexed data |
There are also demo-specific handlers like cricketLens.py / cricket_query.py showing how to build a deeply specialized domain tool.
<returnStruc> ContractEvery LLM call NLWeb makes is paired with a <returnStruc> block in prompts.xml defining the exact JSON shape expected back. Example for tool selection:
<returnStruc>
{
"selected_tool": "compare_items",
"confidence": 0.92,
"reasoning": "User explicitly asked to compare two products"
}
</returnStruc>
This is mixed-mode programming in action — the LLM output is parsed as JSON and drives Python control flow. Handlers themselves use <returnStruc> for their own LLM calls (rank results, generate summary, extract key fields).
site_types.xml maps Schema.org @type values to allowed tools, with inheritance:
<site_type name="Recipe" extends="CreativeWork">
<tool>search</tool>
<tool>item_details</tool>
<tool>recipe_substitution</tool>
<tool>accompaniment</tool>
</site_type>
Tools inherit from parent types; specific overrides take precedence. The default site_type catches everything not enumerated.
For debugging or raw retrieval, set in config_nlweb.yaml:
tool_selection_enabled: false
This bypasses the router entirely — every query goes through plain retrieval + ranking. Useful for:
Don't confuse these:
mode (request param) = list / summarize / generate — controls the output styleA mode=generate query may be routed through compare_items, recipe_substitution, or generate_answer depending on what the router picks.
Add a new handler in methods/<your_tool>.py:
# Sketch — verify base class signature in current methods/*.py files
class YourToolHandler:
name = "your_tool"
description = "Handles queries of pattern X for type Y"
async def handle(self, query, site, schema_type, context, stream):
# 1. Retrieve relevant items
items = await context.retriever.search(query, site=site)
# 2. Rank
ranked = await context.ranker.rank(items, query)
# 3. Run any tool-specific LLM call(s)
# 4. Stream results back
await stream.send({"results": ranked[:5]})
Register the tool:
tools.xml (or config_tools.yaml if that's where the registry lives in current code).site_type entries in site_types.xml.<promptString> entry in prompts.xml if your tool needs an LLM call with a <returnStruc>.Build a custom tool if:
Use a built-in if:
generate_answer.py handles it.compare_items / item_details.<returnStruc>reasoning, confidence) — helps debugging and lets you log model decisions.# Force the router to pick your tool:
curl 'http://localhost:8000/ask?query=test&site=X&streaming=false&forced_tool=your_tool'
(Verify forced_tool param name in current code — may be a different name or only available in mode: development.)
If multiple tools could fit a query, ToolSelector picks one. To bias selection:
site_types.xml to put your tool earlier in the list for relevant types<returnStruc> confidence threshold in prompts.xml<promptString> description is too vague; the router can't tell when to use it.<returnStruc> is too complex or the model tier is too low; bump to high for that call.site_types.xml extends attribute typo'd or the parent type not defined.Always cross-reference methods/ and site_types.xml in the live repo — both move fast.
development
Build with Spree's headless Next.js storefront — the official `spree/storefront` repo (Next.js 16 App Router with Server Actions and Turbopack, React 19 Server Components, Tailwind CSS 4, TypeScript 5, `@spree/sdk`, Sentry), server-only auth (httpOnly JWT cookies + publishable key), MeiliSearch faceted catalog, one-page checkout with Apple/Google Pay/Klarna/Affirm/SEPA, multi-region market routing, GA4 + JSON-LD SEO, and Vercel/Docker deployment. Use when forking or customizing the storefront, or evaluating headless adoption.
tools
Build Spree extensions as Rails engines — gem scaffolding, `bin/rails g spree:extension`, mounting routes/migrations/assets, the modern `prepend` decorator pattern (`*_decorator.rb` with `self.prepended(base)`), generators (`spree:model_decorator`, `spree:controller_decorator`), the four customization surfaces in preference order (Events > Webhooks > Dependencies > Decorators), Spree::Dependencies for swapping service objects, gem release/versioning, and the deprecated Deface engine. Use when building a reusable Spree extension or adding non-trivial customization to an app.
development
Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication, `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching, lifecycle events (`{model}.created/.updated/.deleted` via `publishes_lifecycle_events`), the canonical event catalog (order.*, payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job orchestration. Use when wiring event-driven business logic, building webhook consumers, or replacing ActiveSupport callback chains.
tools
Cross-cutting Spree development patterns — the customization preference hierarchy (Events > Webhooks > Dependencies > Decorators), `Spree::Dependencies` service-object swapping, the `_decorator.rb` + `prepend` + `self.prepended` idiom, idempotent subscribers and webhook receivers, multi-store scoping discipline, prefixed IDs, calculator polymorphism (shipping/promotion/tax share the base), service-object composition with `dry-monads` or simple results, why to avoid `class_eval` reopening and Deface, and Spree-on-Rails idioms (Hotwire/Turbo Stimulus, ActiveStorage, Action Cable, Sidekiq). Use when designing the architecture of a Spree extension or solving cross-cutting concerns.