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_ vs or_); 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 form predicative.

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 wording. When the owner is the thing the whole query is about, it is referred back to with “it” / “its” the second time (rather than repeating its name): “the battery of the Robot to which it is assigned, “the Person who owns it. A plain noun attribute is untouched and keeps the “X of Y” form (“the name of the department of an Employee”); a noun that merely ends in a preposition (e.g. 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 any — the related type is filled in automatically from the attribute’s declared type;

  • 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 rather than printing the object’s (possibly huge) 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 frame (the natural reading of GROUP BY). The key is named once — as the bare group label — and is not restated as a column:

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 is …” condition (the total read as a plain possession of the group), so it is clear which group the condition filters:

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 is greater than 30000” — putting the filter on the group it restricts, rather than a trailing “having …” that would look like it describes the whole list of results. The GROUP BY part names only the grouping key, without repeating the full set of selected columns.

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

Robot, Mission

Teal

Attribute

battery, assigned_to

Orange

Operator

is greater than, is not

Green

Logical connective

and, or, such that

Gray

Literal value

50, "BB8"

Yellow

Keyword / rule structure

If, then, whose

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))
For each department whose average of salaries of Employees is greater than 30000, report the average of salaries of Employees

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))
For each department whose average of salaries of Employees is greater than 30000, report the average of salaries of Employees

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))
If
  - 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))
If
  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))
If
  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.

API Reference#

  • verbalize_expression() — the simplest entry point; returns plain text (verbalize_expression(expr))

  • VerbalizationPipeline — colour / layout / hyperlinks via a renderer

    • VerbalizationPipeline.plain().verbalize(expression) — plain prose (what verbalize_expression uses)

    • VerbalizationPipeline.ansi().verbalize(expression) — ANSI-coloured prose

    • VerbalizationPipeline.html(hierarchical=True).verbalize(expression) — HTML indented bullets

    • VerbalizationPipeline.html(hierarchical=True, link_resolver=resolver).verbalize(expression) — with source hyperlinks

    • VerbalizationPipeline(renderer).verbalize(expression) — from a renderer you build directly

  • ParagraphRenderer — flat prose layout

  • HierarchicalRenderer — indented bullet list layout

  • PlainFormatter — no colour markup

  • ANSIFormatter — terminal escape codes

  • HTMLFormatter<span> tags for notebooks