Verbalization#
EQL queries are Python objects — they can be inspected, composed, stored, and now: read aloud.
Verbalization turns any EQL expression into a plain-English sentence. This is useful for:
Debugging — instantly understand what a complex query actually asks.
Explainability — surface query intent in logs, UIs, or reports.
Testing — assert on what a query means (its intent and structure), not just what it returns. Two queries can return the same rows on a fixture yet express different intent (e.g.
and_vsor_); a verbalization assertion pins the intent regardless of the data.
A few grammar words, in plain English
You don’t need any grammar background to read this page. A handful of terms come up now and then — here is what each means in plain words, with the technical name in brackets in case you’re curious:
the “X of Y” form — “the battery of a Robot” (the possessive / genitive).
a “that …” description — extra words hung on a noun to describe it, as in “the Robot that is assigned to a Mission” (a relative clause).
the subject — the thing a sentence is about, or the thing doing the action (a Robot in “a Robot is operational”).
active vs. passive — the doer first (“the Person who owns a Book”) versus the thing it is done to first (“a Book is owned by a Person”).
“is” vs. “are” — the verb switches to match one thing versus several (“its salary is …” vs. “their salaries are …”).
a “by” relation — a field like
owned_by, where the other thing does the action (its agent).
The Quick API#
The simplest way to verbalize any EQL expression is verbalize_expression. With no extra
arguments it returns plain text. Pass a renderer to control color and layout.
from krrood.entity_query_language.factories import variable, entity, an
from krrood.entity_query_language.verbalization.pipeline import verbalize_expression
from krrood.entity_query_language.verbalization.example_domain import Robot
robots = [Robot("R2D2", 95, True), Robot("C3PO", 20, False), Robot("BB8", 80, True)]
r = variable(Robot, domain=robots)
query = an(entity(r).where(r.battery > 50))
print(verbalize_expression(query))
Find a Robot whose battery is greater than 50
The output reads like a natural sentence describing exactly what the query selects.
More Conditions, Still Readable#
Adding more .where() conditions does not break the sentence — EQL connects them naturally.
query = an(entity(r).where(r.battery > 50, r.name != "BB8"))
print(verbalize_expression(query))
Find a Robot whose battery is greater than 50, and whose name is not 'BB8'
Cross-Variable Conditions#
Verbalization handles cross-variable comparisons too — the sentence describes the relationship between variables.
from krrood.entity_query_language.verbalization.example_domain import Mission
missions = [Mission(robots[0], 1), Mission(robots[1], 3)]
m = variable(Mission, domain=missions)
query = an(entity(r).where(m.assigned_to == r, m.priority > 2))
print(verbalize_expression(query))
Find a Robot that is assigned to a Mission with priority greater than 2
Logical Operators#
Conditions combined with and_, or_, and not_ are verbalized with natural connectives.
from krrood.entity_query_language.factories import variable, and_, or_, not_
x = variable(int, [1, 5, 12])
print(verbalize_expression(and_(x > 1, x < 10, x != 5)))
print(verbalize_expression(or_(x > 10, x < 0)))
print(verbalize_expression(not_(x > 5)))
an Integer is between 1 and 10 and not 5
an Integer is greater than 10, or the Integer is less than 0
an Integer is not greater than 5
Notice the or_ form joins the options with a plain “… or …” — an “or” that allows either one or
both (an inclusive or). It deliberately does not start with “either”, which would suggest only
one of them can hold. Chained and_ conditions are separated by commas with a final “and”.
Boolean and Indexed Attributes#
A bool attribute is read as a short “… is …” statement — “the first task of a Worker is
completed” — instead of the “X of Y” form (the possessive, e.g. “the battery of a Robot”) used
for other attributes. Grammar calls the “is
from krrood.entity_query_language.factories import variable, not_
from krrood.entity_query_language.verbalization.example_domain import Task, Worker
w = variable(Worker, domain=None)
print(verbalize_expression(w.tasks[0].completed))
print(verbalize_expression(not_(w.tasks[0].completed)))
the first task of a Worker is completed
the first task of a Worker is not completed
A numeric index like [0] becomes a position word — “first”, “second” (an ordinal) — and merges
into the singular noun, so it reads “the first task” rather than “the first of the tasks”. The final
yes/no field then reads “is completed” / “is not completed”.
Comparing a yes/no attribute to a yes/no value builds the answer into the verb itself, rather than tacking on “is True”:
t = variable(Task, domain=None)
print(verbalize_expression(t.completed == True))
print(verbalize_expression(t.completed == False))
print(verbalize_expression(t.completed == variable(bool, [True, False]))) # left open
a Task is completed
a Task is not completed
a Task is either completed or not
== True reads “is completed”, == False (and != True) reads “is not completed”, and a
boolean variable whose domain holds both values reads “is either completed or not”.
Relational Attributes#
Some attributes are named like a little action — a verb in its “-ed” form plus a small linking word
(a past participle plus a preposition), like assigned_to, owned_by, or written_by. Navigating
through one of these reads as a “that …” / “which …” description of the related thing (a relative
clause), instead of the plain “the assigned_to of …” (the “X of Y” / possessive form). The
linking word moves in front of “which” (“to which”, “by which”):
m = variable(Mission, domain=None)
print(verbalize_expression(m.assigned_to))
print(verbalize_expression(m.assigned_to.operational))
the Robot to which a Mission is assigned
the Robot to which a Mission is assigned is operational
The main noun (“the Robot”) is the attribute’s declared type. A “by” relation (owned_by,
written_by) — where the other thing does the action (its agent) — is read with the doer first (the
active voice): “the Person who owns a Book”, “the Author who writes a Document”, rather
than the thing-it-happens-to first (the passive) “the Person by which a Book is owned”. Every other
relation keeps the “… which … is color_in) is not treated as a relation.
When the robot is the thing the sentence is about (its subject) — as with a yes/no attribute, “the Robot to which it is assigned is operational” — the attributes that follow read “its battery … its power” consistently (once “its” points at the robot, it keeps pointing there):
query = an(entity(m).where(
m.assigned_to.operational, m.assigned_to.battery > 5, m.assigned_to.power > 1,
))
print(verbalize_expression(query))
Find a Mission such that the Robot to which it is assigned is operational, its battery is greater than 5, and its power is greater than 1
But when the sentence is about an attribute — “the battery of the Robot … is greater than 5” — the battery, not the robot, is what it is about, so a following “its power” would sound like the battery’s power. To stay clear, the owner is spelled out instead:
query = an(entity(m).where(m.assigned_to.battery > 5, m.assigned_to.power > 10))
print(verbalize_expression(query))
Find a Mission such that the battery of the Robot to which it is assigned is greater than 5, and the power of the Robot is greater than 10
Absence Conditions (== None)#
A comparison to None is read as an absence, not as a value. The exact wording adapts to the
attribute:
a plain noun attribute reads “
has no (“a Pose has no orientation”);” an action-style attribute (the “-ed”-verb-plus-linking-word kind above, like
assigned_to,owned_by,shipped_to) reads as “has not been — the related type is filled in automatically from the attribute’s declared type;any ” a bare variable (nothing to name) reads “
does not exist” .
m = variable(Mission, domain=None)
print(verbalize_expression(m.assigned_to == None))
print(verbalize_expression(variable(Mission, domain=None) == None))
a Mission has not been assigned to any Robot
a Mission does not exist
Whether an attribute counts as action-style is decided by the shape of the word — the part before the
linking word must be a real “-ed” verb form (a past participle) — so assigned_to (a verb) becomes
the “has not been assigned to” form, while a noun that merely ends in a small word (e.g. color_in)
stays “has no color_in”. Inside a query this absence is said as its own sentence (“such that the
Mission has not been assigned to any Robot”) — it never joins the “whose …” group, because that would
force the sentence to swap who-does-what, which doesn’t fit there.
Domain-Constrained Values#
When a single-value variable (an int/float/str/bool or an enum) is given a small fixed set of
allowed values, the verbalizer lists them as “one of …” where the value would go.
r = variable(Robot, domain=robots)
print(verbalize_expression(r.battery == variable(int, [10, 50, 90])))
print(verbalize_expression(r.battery == variable(int, [10, 50])))
the battery of a Robot is one of 10, 50, or 90
the battery of a Robot is one of 10 or 50
Three or more values use a comma before “or” (the serial comma: “one of 10, 50, or 90”); a pair drops it (“one of 10 or 50”). A variable that stands for a whole object (not a single value) is never listed out — it stays “a Robot”.
Concrete Object Values#
Comparing against a concrete domain object means its identity, so the verbalizer says
“a specific repr. When the class has
an obvious identifying field it is appended:
m = variable(Mission, domain=None)
print(verbalize_expression(m.assigned_to == robots[0])) # robots[0] is Robot("R2D2", …)
the Robot to which a Mission is assigned is a specific Robot with name 'R2D2'
The identifying field(s) are the dataclass fields marked with GrammarMetadata.is_identifying_field
(via field(metadata=FieldMetadata(other_metadata=[GrammarMetadata(is_identifying_field=True)]).as_dict())),
otherwise the first present of name / id / label / key / uuid (Robot has name, so this
reads “a specific Robot with name ‘R2D2’”). With none of those, it falls back to a bare “a
specific Robot”.
Factoring Repeated Comparisons#
Two comparisons that check the same fields on two parallel parts of an object — here a period’s begin and end — fold into one natural sentence, “the beginning and end of the period have the same month and year”, instead of repeating each.
A field whose attribute name reads awkwardly can register a display_name in its
GrammarMetadata; the verbalizer then uses that word wherever the field appears (here begin
surfaces as “beginning”).
from dataclasses import dataclass, field
from krrood.entity_query_language.verbalization.grammar_metadata import GrammarMetadata
from krrood.patterns.field_metadata import FieldMetadata
@dataclass
class Date:
month: int
year: int
@dataclass
class Period:
begin: Date = field(
metadata=FieldMetadata(
other_metadata=[GrammarMetadata(display_name="beginning")]
).as_dict()
)
end: Date = None
p = variable(Period, domain=None)
query = an(entity(p).where(p.begin.month == p.end.month, p.begin.year == p.end.year))
print(verbalize_expression(query))
Find a Period such that the beginning and end of the Period have the same month and year
Underspecified Constructions (an)#
An underspecified an(...) construction asks the system to make something rather than find it —
“Generate a … given that …”. Several single-value settings on one object are combined into one
“… respectively” line (up to three; longer or phrase-length values are said separately, and a None
becomes a “has no” line):
from krrood.entity_query_language.factories import an
@dataclass
class Point:
x: float
y: float
z: float
print(verbalize_expression(an(Point)(x=1, y=2, z=3)))
Generate a Point given that the x, y, and z of the Point are 1, 2, and 3 respectively
Aggregations#
Aggregation functions (count, sum, average, max, min) are wrapped with
the definite article and a descriptive phrase when verbalized. Here we need a domain with
a numeric field:
import datetime
from krrood.entity_query_language.factories import variable
import krrood.entity_query_language.factories as eql
from krrood.entity_query_language.verbalization.example_domain import (
AmountDetails,
BankTransaction,
)
t = variable(BankTransaction, domain=None)
print(verbalize_expression(eql.count(t)))
print(verbalize_expression(eql.sum(t.amount_details.amount)))
print(verbalize_expression(eql.average(t.amount_details.amount)))
print(verbalize_expression(eql.max(t.amount_details.amount)))
print(verbalize_expression(eql.min(t.amount_details.amount)))
the number of BankTransactions
the sum of the amount of the amount_details of a BankTransaction
the average of the amount of the amount_details of a BankTransaction
the maximum of the amount of the amount_details of a BankTransaction
the minimum of the amount of the amount_details of a BankTransaction
All aggregations start with “the” (the definite article): “the number of”, “the sum of”, …. The attribute after it uses the “of the …” (possessive) form.
When a WHERE condition filters the very attribute being aggregated, that attribute is named in full once (in the aggregate) and the condition refers back to it as a bare “the battery”:
query = an(entity(eql.average(m.assigned_to.battery)).where(m.assigned_to.battery > 5))
print(verbalize_expression(query))
Find the average of the battery of the Robot to which a Mission is assigned such that the battery is greater than 5
Reports (presenting results, not searching)#
Some queries present results rather than search for a match — a calculation, or an ordered listing. These open with “Report” rather than “Find”. Report-ness and conditions are orthogonal: a report may still be filtered.
A set_of that computes an aggregate is a calculation, so it opens with “Report” and drops the
code-like parentheses:
from krrood.entity_query_language.factories import a, set_of
from krrood.entity_query_language.verbalization.example_domain import Employee
employee = variable(Employee, domain=None)
print(verbalize_expression(a(set_of(eql.sum(employee.salary)))))
Report the sum of salaries of Employees
When it is grouped, the grouping is stated first as a “For each
query = a(
set_of(employee.department, eql.sum(employee.salary)).grouped_by(employee.department)
)
print(verbalize_expression(query))
For each department, report the sum of salaries of Employees
An ordered query is also a report — ordering shows all the matching results in sequence, a listing rather than a hunt for one. So the subject is plural, and a filter doesn’t change that (it just shortens the list). When there is more than one, the wording matches it: the verb becomes “are” (“whose salaries are …”) and a per-item value spreads to each (“their salaries”):
print(verbalize_expression(an(entity(employee).ordered_by(employee.salary))))
print(verbalize_expression(
an(entity(employee).where(employee.salary > 5).ordered_by(employee.salary))
))
Report Employees ordered by their salaries from lowest to highest
Report Employees whose salaries are greater than 5, ordered by their salaries from lowest to highest
The same matching applies wherever the subject is plural — including a limit that ranks several
(“the top three Employees … whose salaries are greater than 1000”).
A limit is the exception: it ranks and counts (“Find the top three …”).
A grouped query’s having filter is attached to the group name as a “whose
total = eql.sum(employee.salary)
print(verbalize_expression(
a(set_of(employee.department, total).grouped_by(employee.department).having(total > 30000))
))
For each department whose sum of salaries of Employees is greater than 30000, report the sum of salaries of Employees
A plain (non-aggregating, unordered) set_of stays a search and also drops the parentheses.
Several attributes of the same owner fold into one shared “X of Y” phrase rather than repeating the
owner:
print(verbalize_expression(a(set_of(employee.department, employee.name))))
Find the department and name of an Employee
Date Range Folding#
When a lower-bound and an upper-bound comparison on the same datetime attribute appear together, the verbalizer folds them into a single “between … and …” phrase.
bt = variable(BankTransaction, domain=None)
lo = datetime.datetime(2026, 5, 15)
hi = datetime.datetime(2026, 5, 30)
query = an(entity(bt).where(bt.booking_date >= lo, bt.booking_date <= hi))
print(verbalize_expression(query))
Find a BankTransaction whose booking_date is between May 15, 2026 and May 30, 2026
The output uses “is between … and …” with the datetime values formatted in a human-readable form. The same folding happens when the comparisons appear on an aggregation sub-query’s WHERE clause (see the next section).
Nested Sub-Queries and Aggregation Scoping#
Aggregation sub-queries nest naturally. A scoped aggregation — “the sum of amounts
among BankTransactions whose booking_date is between …” — is produced when an aggregate
appears inside an entity() wrapper with its own WHERE conditions.
bt = variable(BankTransaction, domain=None)
bt_sum = variable(BankTransaction, domain=None)
start = datetime.datetime(2026, 5, 15)
end = datetime.datetime(2026, 5, 30)
sum_val = an(entity(eql.sum(bt_sum.amount_details.amount)).where(
bt_sum.booking_date >= start, bt_sum.booking_date <= end,
))
query = an(entity(bt).where(bt.amount_details.amount == sum_val))
print(verbalize_expression(query))
Find a BankTransaction such that the amount of its amount_details is equal to the sum of amounts among BankTransactions whose booking_date is between May 15, 2026 and May 30, 2026
The word “its” stands in for a repeated “of the BankTransaction” on the outer condition. The scoped aggregation automatically uses “among” and the WHERE conditions inside the sub-query continue to work (date-range folding, pronouns, etc.).
A maximum-value variant produces a similarly compact form:
bt_max = variable(BankTransaction, domain=None)
max_val = an(entity(eql.max(bt_max.amount_details.amount)))
query = an(entity(bt).where(bt.amount_details.amount == max_val))
print(verbalize_expression(query))
Find a BankTransaction with the maximum amount
And a scoped aggregation can stand alone as the main query — no outer entity needed:
cutoff = datetime.datetime(2024, 5, 17)
scoped_sum = an(entity(eql.sum(bt.amount_details.amount)).where(bt.booking_date < cutoff))
print(verbalize_expression(scoped_sum))
Find the sum of the amount of the amount_details of a BankTransaction whose booking_date is before May 17, 2024
Same-Type Variable Disambiguation#
When two variables of the same type appear in a query, the verbalizer distinguishes them by appending a numeric index — “Employee 1”, “Employee 2”.
from krrood.entity_query_language.verbalization.example_domain import Employee
emp1 = variable(Employee, domain=None)
emp2 = variable(Employee, domain=None)
query = an(entity(emp1).where(emp1.salary > emp2.salary))
print(verbalize_expression(query))
Find Employee 1 whose salary is greater than the salary of Employee 2
The same mechanism also handles disambiguation when an aggregate and an entity share a type:
emp = variable(Employee, domain=None)
query_agg = an(entity(eql.average(emp.salary)).where(emp.starting_salary > 20000))
print(verbalize_expression(query_agg))
Find the average of salaries of Employees whose starting_salary is greater than 20000
It also counts the related entity a relational attribute introduces, so two missions assigned to two different robots read “Robot 1” / “Robot 2” rather than two indistinguishable *”the Robot”*s — and a repeat of either reduces to its numbered label:
from dataclasses import dataclass
@dataclass
class Pair:
primary: Mission
secondary: Mission
pair = variable(Pair, domain=None)
query = an(entity(pair).where(
pair.primary.assigned_to.battery > 5,
pair.primary.assigned_to.power > 1,
pair.secondary.assigned_to.battery > 3,
))
print(verbalize_expression(query))
Find a Pair such that the battery of Robot 1, to which its primary is assigned, is greater than 5, the power of Robot 1 is greater than 1, and the battery of Robot 2, to which its secondary is assigned, is greater than 3
Custom Predicates#
A custom predicate can control its verbalization by implementing
_verbalization_template_. The template is a string with {field_name} placeholders
corresponding to the predicate’s dataclass fields.
from krrood.entity_query_language.verbalization.example_domain import (
Location,
IsReachable,
)
loc = variable(Location, domain=None)
print(verbalize_expression(IsReachable(loc)))
a Location is reachable
Predicates with multiple fields receive their arguments in positional order:
from krrood.entity_query_language.verbalization.example_domain import (
Department,
StaffMember,
WorksIn,
)
dept = variable(Department, domain=None)
emp = variable(StaffMember, domain=None)
print(verbalize_expression(WorksIn(emp, dept)))
a StaffMember works in a Department
When a predicate does not define _verbalization_template_, the verbalizer falls
back to a generic description.
Grouped Queries (set_of + grouped_by + having)#
Queries built with set_of, grouped_by, and having are verbalized as a structured
sentence with the selection, GROUP BY, and HAVING clauses clearly separated.
from krrood.entity_query_language.factories import a, set_of
emp = variable(Employee, domain=None)
avg_salary = eql.average(emp.salary)
query = a(
set_of(emp.department, avg_salary)
.grouped_by(emp.department)
.having(avg_salary > 30000)
)
print(verbalize_expression(query))
For each department whose average of salaries of Employees is greater than 30000, report the average of salaries of Employees
The HAVING clause is attached to the group name as “For each department whose
Colored Terminal Output#
For richer output in a terminal, use VerbalizationPipeline with a renderer (verbalize_expression
itself only returns plain text). Each part of the sentence is color-coded by what kind of thing it is
(its role).
from krrood.entity_query_language.verbalization.pipeline import VerbalizationPipeline
from krrood.entity_query_language.verbalization.rendering.formatter import ANSIFormatter
from krrood.entity_query_language.verbalization.rendering.renderer import ParagraphRenderer
print(VerbalizationPipeline(ParagraphRenderer(ANSIFormatter())).verbalize(query))
For each department whose average of salaries of Employees is greater than 30000, report the average of salaries of Employees
Color legend:
Color |
Role |
Example |
|---|---|---|
Cornflower blue |
Variable type |
|
Teal |
Attribute |
|
Orange |
Operator |
|
Green |
Logical connective |
|
Gray |
Literal value |
|
Yellow |
Keyword / rule structure |
|
HTML Output for Notebooks#
Build a VerbalizationPipeline with an HTMLFormatter renderer to produce <span> tags for direct use in Jupyter or any HTML context.
from IPython.display import HTML
from krrood.entity_query_language.verbalization.pipeline import VerbalizationPipeline
from krrood.entity_query_language.verbalization.rendering.formatter import HTMLFormatter
from krrood.entity_query_language.verbalization.rendering.renderer import ParagraphRenderer
HTML(VerbalizationPipeline(ParagraphRenderer(HTMLFormatter())).verbalize(query))
Hierarchical HTML#
Use HierarchicalRenderer to get an indented bullet structure — great for rule trees.
from krrood.entity_query_language.verbalization.rendering.renderer import HierarchicalRenderer
HTML(VerbalizationPipeline(HierarchicalRenderer(HTMLFormatter())).verbalize(query))
Verbalizing Rule Trees#
Verbalization really shines on rule trees. The if/then structure is rendered clearly.
from krrood.entity_query_language.factories import (
variable, entity, an, deduced_variable, inference
)
from krrood.entity_query_language.verbalization.example_domain import (
Bird,
LoveBirds,
BirdView,
StrongLoveBird,
)
birds = [Bird('tweety'), Bird('snappy'), Bird('sleepy')]
bird = birds[0]
love_birds = [
LoveBirds(birds[0], birds[1], True),
LoveBirds(birds[1], birds[2], False),
]
love_birds = variable(LoveBirds, domain=love_birds)
bird_view = deduced_variable(BirdView)
rule_query = an(entity(inference(StrongLoveBird)(bird=love_birds.bird_1)).where(love_birds.strong_love))
HTML(VerbalizationPipeline(HierarchicalRenderer(HTMLFormatter())).verbalize(rule_query))
- a LoveBirds has strong_love
then
there's a StrongLoveBird
- whose bird is its bird_1
The hierarchical renderer shows the If/then structure with each condition and conclusion on its own line, indented under the relevant part.
Deep Nesting in Hierarchical Mode#
The hierarchical view really shines on rules with deeply nested attribute chains — the bullet structure makes the relationship between conditions visually clear. Here is a drawer-detection rule with a multi-hop path:
from krrood.entity_query_language.factories import variable, entity, an, inference
from krrood.entity_query_language.verbalization.example_domain import (
Handle,
Container,
FixedConnection,
PrismaticConnection,
Drawer,
)
fc = variable(FixedConnection, domain=None)
pc = variable(PrismaticConnection, domain=None)
h = variable(Handle, domain=None)
drawer_rule = an(entity(inference(Drawer)(
container=fc.parent,
handle=fc.child,
)).where(
fc.parent == pc.child,
fc.child == h,
))
HTML(VerbalizationPipeline(HierarchicalRenderer(HTMLFormatter())).verbalize(drawer_rule))
there's a FixedConnection
- whose parent is the child of a PrismaticConnection
- whose child is a Handle
then
there's a Drawer
- whose container is the parent of the FixedConnection
- whose handle is the child of the FixedConnection
And a cabinet rule that aggregates over multiple drawers — notice the “if” part (the condition) uses “there are” and the “then” part uses the plural “are” to match the several drawers:
from krrood.entity_query_language.verbalization.example_domain import Cabinet
pc = variable(PrismaticConnection, domain=None)
dr = variable(Drawer, domain=None)
cabinet_rule = an(entity(inference(Cabinet)(
container=pc.parent,
drawers=dr,
)).where(pc.child == dr.container))
HTML(VerbalizationPipeline(HierarchicalRenderer(HTMLFormatter())).verbalize(cabinet_rule))
there's a PrismaticConnection
- whose child is the container of a Drawer
then
there's a Cabinet
- whose container is the parent of the PrismaticConnection
- whose drawers are Drawers
Verbalization as an Explanation Tool#
Because EQL tracks how inferences were made, you can verbalize the query that produced any inferred result — not just hand-written queries.
from krrood.entity_query_language.explanation.explanation import explain_inference
inferred_views = list(rule_query.evaluate())
inferred_object = inferred_views[0]
explanation = explain_inference(inferred_object)
print(verbalize_expression(explanation.query_root))
If a LoveBirds has strong_love, then there's a StrongLoveBird whose bird is its bird_1
This outputs the exact query that matched and produced inferred_object, described in English.
It is directly useful for displaying why a robot perceives something as a Drawer, a Door, etc.
Hyperlinks to Source Code#
Pass link_resolver=AutoAPIResolver(...) when constructing the renderer and class/attribute
names become clickable links — opening the corresponding Sphinx AutoAPI documentation page.
For links rendered inside this documentation site, use AutoAPIResolver.for_in_site_docs(): it
emits links relative to the current page, so they resolve to the sibling autoapi/ tree in any
build or host — a local _build/html, a Pages preview, or the published site — with no hard-coded
URL. (For a live preview from an IDE instead, AutoAPIResolver.for_package("krrood") targets the
IDE’s localhost server against a local sphinx-build doc doc/_build/html.)
The demo below uses Robot and Mission from
krrood.entity_query_language.verbalization.example_domain — real classes that Sphinx AutoAPI
documents like any other module under src, so their hyperlinks resolve to the generated AutoAPI
pages in any built docs tree (no hand-maintained mock API needed).
from krrood.entity_query_language.verbalization.example_domain import Robot, Mission
from krrood.entity_query_language.factories import variable, entity, an
from krrood.entity_query_language.verbalization.pipeline import VerbalizationPipeline
from krrood.entity_query_language.verbalization.rendering.formatter import HTMLFormatter
from krrood.entity_query_language.verbalization.rendering.renderer import HierarchicalRenderer
from krrood.entity_query_language.verbalization.rendering.source_link_resolver import AutoAPIResolver
vd_robots = [Robot("R2D2", 95, True), Robot("C3PO", 20, False)]
vd_missions = [Mission(vd_robots[0], 3)]
r = variable(Robot, domain=vd_robots)
m = variable(Mission, domain=vd_missions)
linked_query = an(entity(r).where(m.assigned_to == r, m.priority > 2))
# `for_in_site_docs()` emits links relative to this page (which lives at eql/user/), so they
# resolve to the sibling AutoAPI tree in any build — a local _build/html, a Pages preview, or the
# published site — without hard-coding a host.
resolver = AutoAPIResolver.for_in_site_docs()
HTML(VerbalizationPipeline(HierarchicalRenderer(HTMLFormatter(), resolver)).verbalize(linked_query))
API Reference#
verbalize_expression()— the simplest entry point; returns plain text (verbalize_expression(expr))VerbalizationPipeline— colour / layout / hyperlinks via a rendererVerbalizationPipeline.plain().verbalize(expression)— plain prose (whatverbalize_expressionuses)VerbalizationPipeline.ansi().verbalize(expression)— ANSI-coloured proseVerbalizationPipeline.html(hierarchical=True).verbalize(expression)— HTML indented bulletsVerbalizationPipeline.html(hierarchical=True, link_resolver=resolver).verbalize(expression)— with source hyperlinksVerbalizationPipeline(renderer).verbalize(expression)— from a renderer you build directly
ParagraphRenderer— flat prose layoutHierarchicalRenderer— indented bullet list layoutPlainFormatter— no colour markupANSIFormatter— terminal escape codesHTMLFormatter—<span>tags for notebooks