All tutorials
Track 31·Blueprints

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.

intermediate10 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/31-decomposition-coverage

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.

python
# 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:

python
@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:

text
title = find("Dune")
auth = author(title)
pg = pages(title)
result = format("{title} by {author} has {pages} pages", title=title, author=auth, pages=pg)
text
Dune by Frank Herbert has 412 pages

The 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:

python
@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:

ShapePath
"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:

python
QUERIES = [
    "tell me about Dune",
    "how many pages is 1984?",
    "what can you tell me about Hyperion?",
    "how long is Dune?",
]
bash
uv run main.py

Output:

text
--- 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 pages

The 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_pages and 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.