Building through graphs
UNDER the HOOD
For judges and engineers: how NODUS actually works, what it was tested on, and where to read the code.
The graph
FalkorDB holds everything: the made-up company (plans, systems, alert inboxes, people, past incidents), the lessons with the sentences they quote, and the agent's memory of every warning and answer. Remove FalkorDB and nothing can be matched.
Lessons as data
Each lesson is 3 to 6 conditions of four kinds: something is present, something is absent, a property has a value, or there is exactly a certain number of something. Every condition quotes its source word for word, checked by a script; a person approves the lesson.
A compiler, not an AI
A small compiler turns each condition into one parameterised Cypher query from a hand-written template. Names are checked against the graph model, paths are type-checked, values travel as parameters. No AI writes a query that runs.
Anchor-first matching
Every query starts from the plan. When one condition finds something (an alert inbox, a procedure), its id is passed to the next, so “the inbox” means the same inbox throughout. With two candidates, the one giving the most matches wins.
A published rule
All conditions true: full shape. All but the lesson's last step: one step away. Otherwise a note or nothing. It's a rule anyone can read, not a judgement.
The agent and AG-UI
Saving a plan runs the loop: notice, compare, count, warn, remember. The same run streams as standard AG-UI events, so every graph query appears as a tool call with its arguments and result.
Two conditions, as they run
These are produced by the compiler for the demo plan. Relationships are always named when counted: we found that FalkorDB counts two parallel unnamed relationships as one.
Read the compiler ↗// "This plan does not follow the staged procedure" (an absence) MATCH (s:Plan) WHERE s.id = $start OPTIONAL MATCH (s)-[r1:FOLLOWS]->(t) WHERE t.id = $target RETURN count(r1) = 0 AS holds, [s.id] AS ids // "Nobody active is watching that inbox" MATCH (s) WHERE s.id = $start OPTIONAL MATCH (s)<-[r1:WATCHES]-(t:Person) WHERE t.active = $tp0 RETURN count(r1) = 0 AS holds, [s.id] AS ids
Synthetic test, run once
| Test split | NODUS | Keyword search |
|---|---|---|
| Same structure, different words: right lesson raised | 18 of 18 | 18 of 18 only at threshold 0; 1 of 18 above it |
| Same words, different structure: false alarms | 0 of 18 | 18 of 18 |
| Unrelated plans: false alarms | 0 of 10 | 10 of 10 |
| One condition missing: the right one named | 20 of 20 | not applicable |
The test plans are generated from the same definitions as the lessons, so this shows the queries implement the lessons and that wording is neither needed nor enough. It is not evidence of accuracy on real company data. Full details are in eval/RESULTS.md.
Code ↗
Python engine, FastAPI service, this Next.js site, 129 tests.
Graph model ↗
Every node and relationship, and the condition each one exists for.
Build diary ↗
Every step, decision, problem and fix, written as it happened.