Decomposition coverage: routing by question shape
A decomposition is a few-shot example. The planner matches on question shape, not on the specific values in the intent string. Two examples cover two shapes; queries outside both fall back to docstrings.
Before you start
The one new thing: the planner matches a decomposition on question shape, not on the specific values in the intent string. Write an example using "Foundation" and it guides queries about Dune, Hyperion, and any other book. Write two examples with structurally different paths and the planner routes each query to the right one.
The agent#
Four primitives over a small book catalog: find a title, look up its author, look up its page count, and format a response from a template.
# catalog.py
from difflib import get_close_matches
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import decomposition, primitive
CATALOG = {
"Foundation": {"author": "Isaac Asimov", "pages": 244},
"Neuromancer": {"author": "William Gibson", "pages": 271},
"Dune": {"author": "Frank Herbert", "pages": 412},
"1984": {"author": "George Orwell", "pages": 328},
"Hyperion": {"author": "Dan Simmons", "pages": 482},
}
class BookCatalog(PlanExecute):
@primitive(read_only=True)
def find(self, query: str) -> str:
"""Find a book by title and return its canonical title."""
hits = get_close_matches(query, CATALOG.keys(), n=1, cutoff=0.4)
return hits[0] if hits else query
@primitive(read_only=True)
def author(self, title: str) -> str:
"""Return the author of a book given its canonical title."""
return str(CATALOG[title]["author"])
@primitive(read_only=True)
def pages(self, title: str) -> int:
"""Return the page count of a book given its canonical title."""
return int(CATALOG[title]["pages"])
@primitive(read_only=True)
def format(self, template: str, title: str = "", author: str = "", pages: int = 0) -> str:
"""Compose a response sentence by filling title, author, and pages into a template."""
return template.format(title=title, author=author, pages=pages)Stage 1: no decompositions#
With only docstrings to go on, the planner has no example path. It will
produce a valid plan most of the time, but the shape varies: it might skip
author, use a different template, or call primitives in a different order
each run.
Stage 2: one decomposition#
Add one example for the "tell me about" shape:
@decomposition(intent="tell me about Foundation")
def _example_about(self) -> str:
title = self.find("Foundation")
auth = self.author(title)
pg = self.pages(title)
return self.format("{title} by {author} has {pages} pages",
title=title, author=auth, pages=pg)Now ask "tell me about Dune" -- a book that never appeared in the intent string:
title = find("Dune")
auth = author(title)
pg = pages(title)
result = format("{title} by {author} has {pages} pages", title=title, author=auth, pages=pg)Dune by Frank Herbert has 412 pagesThe planner matched the question shape, not "Foundation". But ask "how many pages is 1984?" and there is no example for that shape, so the plan is unpredictable again.
Stage 3: two decompositions#
Add a second example with a different path: no author call, shorter template:
@decomposition(intent="how many pages is Neuromancer?")
def _example_pages(self) -> str:
title = self.find("Neuromancer")
pg = self.pages(title)
return self.format("{title} has {pages} pages", title=title, pages=pg)Now there are two shapes:
| Shape | Path |
|---|---|
| "tell me about X" | find -> author -> pages -> format with author |
| "how many pages is X?" | find -> pages -> format without author |
Run four queries, none of which name Foundation or Neuromancer:
QUERIES = [
"tell me about Dune",
"how many pages is 1984?",
"what can you tell me about Hyperion?",
"how long is Dune?",
]uv run main.pyOutput:
--- intent ---
tell me about Dune
--- plan ---
title = find("Dune")
auth = author(title)
pg = pages(title)
result = format("{title} by {author} has {pages} pages", title=title, author=auth, pages=pg)
--- result ---
Dune by Frank Herbert has 412 pages
--- intent ---
how many pages is 1984?
--- plan ---
title = find("1984")
pg = pages(title)
result = format("{title} has {pages} pages", title=title, pages=pg)
--- result ---
1984 has 328 pagesThe first and third queries match the "about" shape. The second and fourth match the "pages" shape. All four produce consistent plans.
What to notice#
- The intent string is a routing signal, not a filter. "tell me about Foundation" does not mean the decomposition only applies to Foundation. It gives the planner an example of what "tell me about" questions look like.
- Gaps show up as inconsistency. If you remove
_example_pagesand rerun the "how many pages" queries, the plan shape becomes unpredictable. That inconsistency is the signal to add another decomposition. - Two examples cover the whole catalog. Five books, two shapes, zero per-book examples. The planner generalizes from the shape, not the values.