I Built a Self-Healing AI Layer for Odoo — Here's What Actually Broke
Enterprise ERP platforms like Odoo get customized constantly — every business adds its own fields (x_studio_client_tax_id, x_vendor_code_v2, whatever they need). The problem: hardcoded integrations break the instant a database schema diverges from what they expect. A missing field, an invalid selection value, a type mismatch — and the whole transaction crashes. So I built a middleware layer that doesn't assume a fixed schema. It introspects Odoo's live schema at runtime, semantically maps messy incoming data to the right fields using vector search, and when Odoo rejects a write, it reads the actual error, asks a local LLM to propose a fix, and retries — autonomously, with hard limits. This post isn't a tutorial. It's what actually happened when I built it, including the parts that didn't work the way I expected — because those turned out to be the most useful parts. The Architecture, Briefly Next.js Dashboard FastAPI Backend Odoo 17 (Docker) | +--> Qdrant (local embeddings, field search) +--> Ollama (local LLM, payload repair) Schema introspection via Odoo's fields_get() over XML-RPC — including custom fields, no hardcoded field lists anywhere Semantic field matching — every Odoo field gets embedded locally (sentence-transformers, zero API cost) and indexed in Qdrant. Incoming messy keys get embedded the same way and matched by cosine similarity A real LangGraph state machine for the self-healing loop — not a for loop with a try/except wearing a costume. Nodes for schema fetch, create attempt, and repair; a conditional edge that routes to repair-and-retry on failure, capped at 3 attempts FastAPI + WebSocket streaming the whole process live to a Next.js dashboard, so you can watch the agent fail, reason about the error, and retry in real time I deliberately ran this entirely on local models — sentence-transformers for embeddings, Ollama (Qwen2.5 3B) for the repair agent. Zero API cost, zero external dependency. That choice has real consequences, which I'll get to. The Eval Numbers I built a small labeled eval harness (6 cases, 5 deliberately broken across different Odoo validation rules, 1 clean control) rather than eyeballing whether it "seemed to work": 83.3% overall self-healing success rate 33.3% first-try success rate 1.6 average attempts to success That first-try number matters more than it looks like it should. It's the real cost of choosing a 3B local model over a hosted API — and honestly, seeing it in black and white was more useful than pretending the system "just worked." What Actually Broke (the interesting part) 1. The model hallucinated from outdated Odoo knowledge I fed the local LLM an invalid type selection field, along with the live schema explicitly listing valid values. It proposed "Customer" and "Supplier". Those were valid Odoo field values — in Odoo 8, which the model apparently remembers from pretraining. It ignored the live schema I explicitly gave it in the prompt and pattern-matched from general "knowledge" of Odoo instead. That's a genuinely instructive failure: smaller instruction-tuned models will sometimes trust their priors over your context, even when the context is right there. 2. "notes" matched the wrong field, and it was the right call Querying "notes" against the schema, the system matched Odoo's built-in comment field (0.4969 similarity) more strongly than the custom field it was actually meant to represent, x_studio_delivery_notes (0.4707). Neither score cleared my confidence threshold (0.75) for auto-apply. Both got flagged for human review instead of the system guessing. That's the entire point of confidence gating — a single generic word like "notes" should produce ambiguous results, and the system correctly refused to pretend otherwise. 3. The self-healing loop can't invent a foreign key A payload with an invalid country_id — pointing to a record that doesn't exist — failed all 3 repair attempts. Dropping or emptying an invalid selection value is a viable fix an LLM can reason its way to. Inventing a valid foreign-key ID isn't — the model has no way to know what a real one would be. This is a legitimate architectural boundary, not a bug I need to apologize for. 4. PDF invoices broke the pipeline until I routed them through the same guardrail as everything else Early version: extract text from a PDF invoice, pull out "Label: Value" pairs, send straight into the self-healing loop. Result: the LLM guessed blindly at field names (vendor_name → partner_name, itself also wrong) across all 3 attempts. Fix: route PDF-extracted labels through the same semantic confidence gate as any other input, instead of assuming they're already clean field keys. Now the system correctly says "I'm not confident about any of these, here's what I found, a human should look" — which is a worse UX in the moment but the actually correct behavior for a system that might be writing to a real database. 5. Odoo sometimes just... accepts garbage I sent "is_company": "yes" (a string) expecting Odoo to reject it since the field wants a boolean. It didn't — Odoo's ORM silently coerced it. Not every "invalid" value I threw at the system actually reached the self-healing loop; some got absorbed upstream by Odoo itself. This reshaped how I designed later test cases — targeting Selection and many2one fields specifically, since Odoo validates those strictly, unlike loose type coercion elsewhere. Why This Matters More Than "It Worked" Any of these failures could have been quietly avoided by picking easier test cases and writing a README that only shows the happy path. I think that's the wrong instinct. A system that never fails in its own documentation either wasn't tested hard enough, or isn't being honest about its limits — and "I know exactly where and why this breaks" is a more useful thing to be able to say than "it works." The confidence-gating layer specifically exists because of failures like #2 and #4 — it's not decoration, it's the actual answer to "how do you know this won't silently write bad data." What I'd Do With More Time Swap in a hosted structured-output model (GPT-4o-mini or Claude) as a configurable option and directly compare eval numbers against the local model — right now the local-vs-hosted trade-off is described, not measured Expand the eval set well past 6 cases A resolution mechanism for many2one fields (search-by-name instead of failing outright) — would directly fix failure case #3 Full code, architecture doc, and eval report: github.com/Nida-shafiq/odoo-shadow-schema-ai Happy to talk through any part of this in the comments especially curious if anyone's hit similar local-model-hallucinates-from-pretraining issues in a different domain.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to