Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 10: Automatic Alerts

It was 8 PM. Dr. Portbridge was still at the clinic, manually cross-referencing vaccination records with appointment dates to find overdue animals. “There has to be a way to automate this,” she muttered. She needed the system to reason, to look at data and draw conclusions.


The problem

So far, our ontology describes things but doesn’t infer anything. If an animal hasn’t been vaccinated in over a year, a human has to notice. If an appointment is marked as an emergency but assigned to an intern, nobody catches it. We want the system to derive new facts from existing ones.

Rules

A rule defines an if-then inference: if certain patterns hold in the data, then new facts follow.


rule flag_unvaccinated:
  match:
    ?animal a Animal
    ?animal vaccinations 0
  then:
    ?animal a UnvaccinatedAnimal

Note: This is a simplified example. Real vaccine-overdue logic would compare the givenOn date and validFor duration from Chapter 9, date arithmetic. The important thing here is the pattern: match conditions, then assert conclusions.

Let’s unpack the syntax.

Variables

Variables start with ?. They bind to values during pattern matching:

  • ?animal: binds to any Animal instance

Patterns

Each line in the match: block is a pattern:

PatternMeaning
?animal a Animal?animal is an instance of Animal
?animal vaccinations 0?animal has an attribute vaccinations whose value is 0

Patterns are combined with implicit AND. All must hold simultaneously.

Assertions

Each line in the then: block is an assertion, a new fact to create:

AssertionMeaning
?animal a UnvaccinatedAnimalClassify ?animal as an UnvaccinatedAnimal

A more practical example

Let’s flag emergency appointments that are assigned to interns:


concept UnsafeAssignment

rule flag_intern_emergency:
  match:
    ?appt a Appointment
    ?appt urgency Emergency
    ?appt animal [ treatedBy [ a Intern ] ]
  then:
    ?appt a UnsafeAssignment

This says: “If an appointment is an Emergency, and the treating vet is an Intern, flag it as an UnsafeAssignment.”

Inferring new relationships

Rules don’t just classify, they can create relationships:


rule assign_primary_vet:
  match:
    ?animal a Animal
    ?animal owner [ preferredVet ?vet ]
  then:
    ?animal treatedBy ?vet

“If an animal’s owner has a preferred vet, assign that vet to the animal.”

(This requires adding preferredVet to Owner, we’ll do that below.)

Weight-based alerts

Here’s a rule using numeric comparison:


concept OverweightAnimal

rule flag_overweight_dog:
  match:
    ?dog a Dog
    ?dog weight [ > 40.0 ]
  then:
    ?dog a OverweightAnimal

Worked example

Suppose the clinic records two dogs:


fact rex a Dog
  name "Rex"
  species Dog
  weight 45.0
  neutered true

fact buddy a Dog
  name "Buddy"
  species Dog
  weight 22.0
  neutered true

When the rules run, the reasoner walks every Dog, checks the weight constraint, and derives one new fact:

rex   a OverweightAnimal      # 45.0 > 40.0  → flagged
# buddy is left untouched     # 22.0 > 40.0  → does not match

The derived rex a OverweightAnimal triple is added to the graph alongside the data you wrote. Nothing in the original facts changes. A rule only ever adds facts.

Classifying by an exact value

Comparisons aren’t the only test. A pattern can match an attribute against a literal value directly (a boolean, an enum member, a string, a date, or a date-time):


concept NeedsNeutering

rule flag_unneutered_dog:
  match:
    ?dog a Dog
    ?dog neutered false
  then:
    ?dog a NeedsNeutering

“Any dog whose neutered flag is false is tagged NeedsNeutering.”

Where rules run

These rules aren’t just documentation. When a Dolfin package is deployed in Agrafe, its rules are compiled and handed to the reasoner (retox). Every time data is written, the reasoner forward-chains the rules to a fixpoint, and the derived triples (rex a OverweightAnimal, …) become queryable through the deployment’s SPARQL API right next to the data you wrote.

Not available yet: aggregators. Rules match and compare individual values. They cannot yet count, sum, or average over a collection (e.g. “an animal with zero vaccinations” or “an owner with more than 5 animals” are aggregations and are not supported in rules today). Stick to attribute matches, comparisons, nested patterns, and relationships, which run end-to-end now.

The story so far


# package.dlf
package <http://happypaws.com/clinic>:
  dolfin_version "1"
  version "0.1.0"
  author "Dr. Helen Portbridge"
  description "The Happy Paws veterinary clinic data model"

# clinic.dlf
concept Species:
  one of:
    Dog
    Cat
    Bird
    Rabbit
    Reptile
    Other

concept Urgency:
  one of:
    Routine
    Urgent
    Emergency

concept AppointmentStatus:
  one of:
    Scheduled
    InProgress
    Completed
    Cancelled

concept Owner:
  has firstName: one string
  has lastName: one string
  has phoneNumbers: at least 1 string
  has email: optional string
  has address: optional string
  has preferredVet: optional Veterinarian

concept Veterinarian:
  has name: one string
  has licenseNumber: one string
  has specialization: optional string

concept Surgeon:
  sub Veterinarian
  has surgeryCount: one int
  has certifiedProcedures: at least 1 string

concept Dentist:
  sub Veterinarian
  has dentalCertification: one string

concept Intern:
  sub Veterinarian
  has university: one string
  has year: one int

concept Vaccination:
  has vaccineName: one string
  has dateAdministered: one string
  has batchNumber: optional string

concept Animal:
  has name: one string
  has species: one Species
  has age: optional int
  has weight: optional float
  has owner: optional Owner
  has vaccinations: Vaccination
  has allergies: string

concept Dog:
  sub Animal
  has breed: optional string
  has neutered: one boolean

concept Cat:
  sub Animal
  has indoor: one boolean

concept Bird:
  sub Animal
  has wingspan: optional float
  has canFly: one boolean

concept Appointment:
  has animal: one Animal
  has scheduledFor: one date_time
  has reason: one string
  has urgency: one Urgency
  has status: one AppointmentStatus
  has diagnosis: optional string
  has treatments: string
  has notes: optional string

property treatedBy: Animal -> Veterinarian

# Derived concepts (created by rules)
concept UnvaccinatedAnimal
concept UnsafeAssignment
concept OverweightAnimal

# Rules
rule flag_unvaccinated:
  match:
    ?animal a Animal
    ?animal vaccinations 0
  then:
    ?animal a UnvaccinatedAnimal

rule flag_intern_emergency:
  match:
    ?appt a Appointment
    ?appt urgency Emergency
    ?appt animal [ treatedBy [ a Intern ] ]
  then:
    ?appt a UnsafeAssignment

rule flag_overweight_dog:
  match:
    ?dog a Dog
    ?dog weight [ > 40.0 ]
  then:
    ?dog a OverweightAnimal

rule assign_primary_vet:
  match:
    ?animal a Animal
    ?animal owner [ preferredVet ?vet ]
  then:
    ?animal treatedBy ?vet

Try it

Write a rule that classifies a Cat as a SeniorCat if its age is greater than or equal to 10:


concept SeniorCat

rule flag_senior_cat:
  match:
    # your patterns here
  then:
    # your assertion here

The alerts were a revelation. The system caught an intern assigned to an emergency before it became a problem. It flagged three overweight dogs whose owners hadn’t noticed the gradual change. But when Dr. Portbridge looked closely at that last rule, weight [ > 40.0 ], a doubt crept in. Forty what? A number with no unit was a number she couldn’t trust, and one threshold couldn’t possibly fit a parakeet and a mastiff alike. Before she added any more rules, the weights themselves needed to mean something.