Skip to main content

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.