Build1 publisher3 min readPublished
A self-healing Odoo layer scored 83.3 percent, and the 16.7 percent is the useful part
A developer wired runtime schema introspection, vector field matching and a local LLM repair loop into Odoo 17, then published the failure list. The failures define the boundary.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- Enterprise ERP platforms like Odoo are customized constantly, with businesses adding their own fields such as x_studio_client_tax_id or x_vendor_code_v2; hardcoded integrations break the instant a database schema diverges from what they expect, via a missing field, invalid selection value or type mismatch.
- The middleware performs schema introspection via Odoo's fields_get() over XML-RPC, including custom fields, with no hardcoded field lists anywhere.
- Every Odoo field is embedded locally using sentence-transformers and indexed in Qdrant; incoming messy keys are embedded the same way and matched by cosine similarity.
- The self-healing loop is a LangGraph state machine with nodes for schema fetch, create attempt and repair, and a conditional edge routing to repair-and-retry on failure, capped at 3 attempts.
- The stack is a Next.js dashboard connected by WebSocket to a FastAPI backend, which talks to Odoo 17 in Docker over XML-RPC, plus Qdrant for local embeddings and field search and Ollama for local LLM payload repair.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer published a build log for a middleware layer that reads an Odoo instance's live schema at runtime, semantically maps messy incoming keys to real fields, and when Odoo rejects a write, hands the actual error to a local LLM, takes a proposed fix and retries, capped at three attempts [s1c2][s1c3][s1c4]. It matters because the enumerated failures, not the headline success rate, are what tell you where such a layer can be trusted in production.
The premise is sound and familiar to anyone who has integrated against a customized ERP: businesses add their own fields, things like x_studio_client_tax_id or x_vendor_code_v2, and any integration with a hardcoded field list breaks the moment the schema diverges [s1c1]. The build avoids that by calling Odoo's fields_get() over XML-RPC rather than shipping a field map [s1c2]. Every field is embedded locally with sentence-transformers, indexed in Qdrant, and matched by cosine similarity against embedded incoming keys [s1c3]. The repair loop is a LangGraph state machine with distinct nodes for schema fetch, create attempt and repair [s1c4]. The whole thing runs on local models, with Ollama serving Qwen2.5 3B as the repair agent and no hosted API in the path [s1c6].
The eval is six labeled cases: five deliberately broken against different Odoo validation rules, one clean control [s1c7]. Reported results are 83.3 percent overall self-healing success, 33.3 percent first-try success, and 1.6 average attempts to success [s1c8][s1c9][s1c10]. At that sample size, 83.3 percent means five of six cases landed [1] and 33.3 percent means two [2]. Since one of the six was a clean control that needed no repair at all, at most one of the five broken payloads went in correctly on the first pass [2]. The author's own reading is that the first-try number is the real cost of a 3B local model versus a hosted one [s1c11].
Three failures are worth carrying into your own design. First, given an invalid selection value and the live schema in the prompt listing the valid options, the model proposed "Customer" and "Supplier", values that were valid in Odoo 8 [s1c12]; the author attributes this to smaller instruction-tuned models trusting pretraining priors over supplied context [s1c13]. Second, the query "notes" matched Odoo's built-in comment field at 0.4969 similarity, ahead of the custom x_studio_delivery_notes at 0.4707 [s1c14]. Neither cleared the 0.75 auto-apply threshold, so both were flagged for human review [s1c15]. The gap between the two candidates was 0.0262, and the winner sat 0.2531 below the threshold [4], which is what an honest ambiguous result looks like rather than a near miss. Third, an invalid country_id pointing at a nonexistent record failed all three attempts [s1c16]. Dropping or emptying a bad selection value is reasoning a model can do; fabricating a valid foreign key is not [s1c16].
The PDF path is the least resolved. An early version extracted text, pulled "Label: Value" pairs and fed them straight into the healing loop, and the model guessed blindly at field names until the author routed extraction through the same confidence guardrail as everything else [s1c17].
Watch the denominator. Six cases and one 3B model is a shape, not a benchmark, and the load-bearing constraints, the 0.75 gate, the three-attempt cap and the fact that referential fixes are out of scope, are the parts that would need to hold at volume [s1c7][s1c15][s1c4][s1c16].