Skip to content
Kavyasri Jadala
All work
Open sourceAgentic AI · SHACL · Local LLMs · Evaluation

Self-Repairing Safety Extraction

Adverse-drug-event narratives must become structured records. A model will write one that reads perfectly and contains an invented date.

My role. Sole author

valid records, 3B model + loop
0% → 85%valid records, 3B model + loop
accuracy cost of repair (14B)
85.5% → 83.5%accuracy cost of repair (14B)
LLM calls in the verifier
0LLM calls in the verifier
  • Python
  • RDF
  • SHACL
  • SPARQL
  • Local LLMs
  • Benchmark harness

01Problem

When a patient has a bad reaction to a drug, someone writes it down in prose — a doctor on a hotline, a pharmacist filing a report, a nurse typing into a form. Before it can be used it must become a structured record: which drug, which event, how severe, what date, what outcome. That conversion is done by hand, by trained people, at volume, against reporting clocks measured in days.

02Context

A language model looks well suited to the reading. The obstacle is trust: a model will produce a record that reads perfectly and contains an invented date, a severity that contradicts the narrative, or a field it quietly dropped. "We asked another model and it said the first one did fine" is not reproducible, shares the first model's blind spots, and is not a criterion you can show a regulator.

03Data

  • 20 synthetic adverse-event narratives with ground-truth records (seed 20260720), from a generator that can produce 200.
  • An ontology defined in LinkML, from which the SHACL shapes are generated.
  • Everything runs locally on CPU — safety narratives should not leave the building.

04Architecture

Validator, not an opinion
  1. Narrative
  2. LLM extractionstructured record
  3. RDF conversion
  4. SHACL + SPARQL validationdeterministic · no model calls
  5. Violations → repair instructionsloop: ≤ 4 rounds or no progress
  6. Best conformant graph

05What I built

The whole repository: ontology and shapes, the extraction and repair loop, the corpus generator, the four-arm benchmark and the write-up. A generated shape — closed, with enumerated severity and outcome, an ID pattern and exactly-one cardinality:

ontoloop/ontology/generated_shapes.ttlsource ↗
pv:AdverseEvent a sh:NodeShape ;
  rdfs:comment "An untoward medical occurrence. Must name exactly one suspect drug, which is what makes the report actionable for signal detection." ;
  sh:closed true ;
  sh:ignoredProperties ( rdf:type ) ;
  sh:property [ sh:in ( "mild" "moderate" "severe" "life_threatening" "fatal" ) ;
          sh:maxCount 1 ;
          sh:minCount 1 ;
          sh:order 2 ;
          sh:path pv:severity ],
      [ sh:datatype xsd:string ;
          sh:description "Stable identifier, prefixed by record type." ;
          sh:maxCount 1 ;
          sh:nodeKind sh:Literal ;
          sh:order 0 ;
          sh:path pv:id ;
          sh:pattern "^(CASE|PT|DRUG|AE)-[0-9]{6}$" ],
      [ sh:in ( "recovered" "recovering" "not_recovered" "fatal" "unknown" ) ;
          sh:maxCount 1 ;
          sh:minCount 1 ;
          sh:order 3 ;
          sh:path pv:outcome ],

06Modeling / AI approach

The stop condition is cardinality, datatypes, enumerations, patterns — and SPARQL rules for constraints a schema cannot express on its own, like the fact that a drug cannot cause an event that began before the first dose:

ontoloop/ontology/rules.ttlsource ↗
pv:OnsetNotBeforeDrugStart
  a sh:SPARQLConstraint ;
  rdfs:label "onset before drug start" ;
  sh:message "Event onset {?onset} precedes suspect drug start {?start}; a drug cannot cause an event that began before it was administered." ;
  sh:prefixes pv: ;
  sh:select """
      SELECT $this ?onset ?start
      WHERE {
          $this pv:onset_date ?onset ;
                pv:suspect_drug ?drug .
          ?drug pv:start_date ?start .
          FILTER (?onset < ?start)
      }
  """ .

Violations are rewritten as imperative repair instructions and fed back. Same input, same violations, every run — that reproducibility is what makes the loop measurable.

07Evaluation

Four arms, two metrics that can disagree — structural conformance and field accuracy:

| Arm | Conformance | Field accuracy | p50 latency | Output tokens | |---|---|---|---|---| | llama3.2 3B | 0% | 71.0% | 9.2 s | 3,300 | | phi4 14B | 65% | 85.5% | 27.2 s | 3,693 | | 3B + loop | 85% | 78.5% | 19.0 s | 7,971 | | 14B + loop | 80% | 83.5% | 31.6 s | 5,826 |

A 3B model that could not produce a single valid record on its own produces 17 of 20 once a verifier tells it what is wrong — beating a model nearly five times its size in less wall-clock time.

And the loop makes factual accuracy worse. Case by case, repair improved accuracy on zero of twenty cases and damaged three.

08Production considerations

  • Pick the arm by the requirement. If downstream needs valid records, run the small model with the loop. If it needs correct ones, run the large model without it.
  • Conformance is not correctness. Asked to fill a missing start date it cannot find, a model supplies a plausible one — and a plausible date satisfies every shape.
  • Cost is visible. The loop roughly doubles output tokens for the small model; that is the price of validity.

09Results

  • 0% → 85% conformant records from a 3B model with the loop.
  • 85.5% → 83.5% field accuracy when the loop is added to the 14B model.
  • A reproducible, open-source harness with CI.

10Tradeoffs / lessons

SHACL rejects severity: "very bad" because the ontology enumerates the legal values. It cannot reject severity: "mild" on a narrative that says requiring hospital admission, because that is a claim about the world, not about the schema.

Constraint-guided repair buys structural validity and pays for it in correctness. Know which of the two you are measuring.