Introspection
describe() dumps the active rule set and the fact schemas as plain data:
TrafficAdvice.describe()
# {
# "facts": [{"name": "Light", "fields": {...}}],
# "rules": [
# {"name": "green", "predicate": {"type": "Eq", "field": "color", "value": "green"},
# "priority": 3, "is_default": False},
# {"name": "yellow_safe", ...},
# {"name": "stop", ...},
# {"name": "fallback","predicate": null, "priority": 0, "is_default": True},
# ],
# }
This is the canonical way to:
- Render a rule set in an external tool or UI
- Diff two versions of a rule set
- Persist the rule set to a store
- Feed it back into an LLM so the model knows exactly which cases are already handled
Combined with Predicate.from_dict, predicates round-trip cleanly, so you can
load a previously stored rule set and re-evaluate it.
Feeding the rule set to an LLM
When the engine's @Default calls an LLM, passing type(self).describe() in
the system prompt tells the model precisely which cases are already covered by
rules - preventing it from reasoning about inputs that will never reach it:
import json
@Default
async def llm_fallback(self, ticket: Ticket) -> Team:
rules_schema = json.dumps(type(self).describe(), indent=2)
agent = Agent(
"anthropic:claude-haiku-4-5",
result_type=Team,
system_prompt=(
"You are a support ticket classifier. "
"These deterministic rules already handle common cases - "
"you only receive tickets that matched none of them.\n\n"
f"Existing rules:\n{rules_schema}"
),
)
result = await agent.run(f"Subject: {ticket.subject}\n\n{ticket.body}")
return result.output
Without this, the LLM and rules engine are two separate systems with no shared understanding. With it, they form one coherent pipeline.