The Low Code Factory Discuss an Engagement

Home/Case Studies/avocadoctor

IT & product Applied AI Running live — internal pilot

avocadoctor: a support agent that reads your logs on your hardware, and nowhere else.

avocadoctor analyses a remote-desktop support ticket end to end. It reads the evidence bundle attached to the case, investigates the way an engineer would — opening files, checking container states, reading configuration, searching the platform's own source for the error string — and posts a drafted diagnosis back onto the ticket with the evidence behind every claim.

The entire pipeline, the language model included, runs on a single on-premises machine. No ticket text and no log line reaches a cloud API. That is not a configuration setting. It is the only behaviour the code has.

100%
on-premises — enforced at a single egress point that permits no cloud provider
10,000+
error strings indexed across three platforms, tracing a symptom to the code that emits it
30B
mixture-of-experts model, served on one desktop-class machine
01 — Context

Why this exists

Remote-desktop infrastructure produces a particular kind of support ticket. A user cannot connect. A session dies after ninety seconds. Authentication works for one directory group and fails silently for another. Attached to the case is a pile of logs from the gateway, the session daemon, the broker, the hypervisor, the servlet container, and the per-connection RDP, VNC or SSH trace.

The answer is almost always in there. Finding it means knowing which line matters, which configuration key produces it, which layer of the stack actually failed, and whether anyone has seen this before. A first-line engineer spends an hour reconstructing context that a senior engineer would recognise in ninety seconds.

That is precisely the shape of problem a language model is good at. It is also precisely the data an enterprise cannot paste into a hosted chatbot. Support logs are dense with hostnames, usernames, internal address ranges, session identifiers and, often enough to matter, credentials that were never meant to be written down anywhere.

For most organizations that single fact is where the conversation about AI-assisted support ends — not because the technology does not work, but because the first step of every vendor demonstration is an unacceptable data transfer. So we inverted the usual order and started from the constraint rather than adding a privacy story afterwards: the model runs on your hardware, or there is no product.

02 — Business challenge

The failure mode that makes most LLM-on-logs systems unusable

A language model is fluent whether or not it is correct. Ask one how confident it is, and it returns a number that tracks how confident its prose sounds. It is not a probability, and trusting it is how a team ships a system that is wrong in a tone of complete certainty.

In a support queue that is worse than useless. A wrong answer delivered with authority sends an engineer down a three-hour dead end, and it erodes trust in every subsequent answer the system produces. One confidently wrong diagnosis costs more credibility than ten correct ones earn.

The second failure mode is subtler. The interesting faults in a virtual-desktop stack are cross-layer. A user reports "I cannot connect". The gateway logs a TLS error. The real fault is a dead guest agent two layers below, and the TLS error is an unrelated internet scanner hitting the public port. An agent that reads only the gateway's logs will confidently blame the gateway, and it will be able to cite genuine log lines while doing so.

03 — Constraints

What the design had to hold to

  • No data may leave the perimeter. Not to a model provider, not to a telemetry endpoint, not on a demonstration path, not as a fallback when the local model is unavailable.
  • The evidence bundle is untrusted input. It is an archive assembled on a machine we do not control, and it must be treated as hostile until proven otherwise.
  • It must run without a GPU budget or a procurement cycle. A system that requires new hardware approval will not be piloted.
  • It may not act. No service restarts, no configuration changes, no ticket closures, nothing customer-facing.
  • Everything it did must be inspectable. A black box that reads your logs is not an improvement on the problem it was brought in to solve.
04 — Solution

An investigator, three ways of knowing, and a gate the model does not control

A collector runs against the environment and produces an evidence bundle: logs, container states, configuration and health output from every layer of the stack. The bundle lands on a support case. An engineer clicks Analyze. From there the agent opens the bundle safely, redacts it, investigates it, corroborates what it found, scores the result, and either posts a drafted diagnosis or hands the case to a human and says why.

The investigator

Most "AI log analysis" is a single prompt with a log file pasted into it. That fails for two reasons: the bundle is far larger than any sensible context window, and the model has no way to follow a thread. avocadoctor instead gives the model a bounded set of read-only tools and lets it run its own investigation across multiple turns.

ToolWhat it does
list_filesEnumerate the bundle
read_fileRead a bounded window of a file
search_filesSearch across the bundle
get_container_statesWhat was actually running, and what had died
get_configThe effective configuration, and its delta from a known-good baseline
lookup_code_errorTrace an error string back to the source that emits it, across all three platforms

Every tool is read-only, reads redacted content only, and returns a size-bounded result. The model has a turn budget and a byte budget on its accumulated history, so a long investigation cannot silently outgrow its context window. If it exhausts that budget it returns a no-evidence result that the gate rejects. It never returns a guess.

Three ways of knowing, and it must agree with itself

A code index. The source of every platform in the stack is indexed. When a log line carries an error string, the agent traces it to the code path that emits it and the configuration key that triggers it — more than ten thousand indexed error strings across Guacamole, OpenUDS and Proxmox. The symptom stops being a mystery and becomes a location.

A correlated log timeline. Every plane of the bundle is decoded with its own grammar, timezone-normalised, and merged onto a single timeline. A deterministic correlator then reduces that to a layer verdict: which plane is culpable, and which signals are red herrings. This happens before the model speaks, and it is arithmetic rather than inference.

A case base. Prior resolved cases and public knowledge, held as a hybrid vector and graph memory — pgvector for similarity, Apache AGE for structure. Similarity proposes candidates; the graph narrows them by how they actually relate.

05 — Architecture

The pipeline, and the boundary around it

avocadoctor architecture. The environment (Proxmox, OpenUDS, Guacamole, Windows 11) is read by a collector that produces an evidence bundle, which lands on a Salesforce support case. Clicking Analyze runs the pipeline inside a single on-premises box: safe intake, fail-closed redaction, parse and cross-plane correlation, a multi-turn investigator with six read-only tools, corroboration guards that can veto the model, and a deterministic gate. Cloud LLM providers are denied at the egress point. The result is either a committed diagnosis or a handoff to a human, written back onto the ticket.
Everything from the redaction gate onward runs inside one on-premises boundary. Cloud model providers are denied at the egress point by default, not by configuration.

The egress boundary

Every model call in the system passes through one enforcement point, and it does two things. It fences the endpoint: default-deny, with only loopback and private-range addresses permitted at all, and every cloud provider refused before a byte leaves the process. And it applies a residual-risk backstop, re-scanning everything outbound so that anything resembling a secret which survived the redaction ruleset is withheld from the model, with the withholding recorded in the audit trail.

Critically, in the investigator both checks run on every single turn, not once at the start. A model that opens a new file on turn five is fenced on turn five.

What the agent connects to avocadoctor never connects to the infrastructure it diagnoses. A collector runs against the environment and produces an evidence bundle; the bundle is attached to a support case; the agent reads the case. Its only external integration is the ticketing system. The estate itself may be hosted anywhere — ours runs on GCP — and it makes no difference to the agent, which sees an archive of logs and nothing else. The analysis, including the language model, runs entirely on one on-premises machine.

Redaction that fails closed

The evidence bundle is scanned for archive-level attacks before anything is read — path traversal, symlinks, zip bombs, declared-versus-actual size — and every file is digested. A versioned ruleset then masks secrets, tokens, keys, PEM material, password hashes and connection credentials across every plane. What the model receives is not the log file. It is the redacted projection of it.

INPUT
10:41:22.318 [http-nio-8080-exec-7] WARN  o.a.g.a.LocalAuthenticationProvider -
Authentication attempt from 10.21.4.17 for user "[email protected]" failed.
guacd[7213]: ERROR: Unable to connect to RDP server rdp-host-04.corp.example.com:
certificate validation failed

REDACTED — EXACTLY WHAT THE WORKFLOW RECEIVES
10:41:22.318 [http-nio-8080-exec-7] WARN  o.a.g.a.LocalAuthenticationProvider -
Authentication attempt from <IP_1> for user <USERNAME_1> failed.
guacd[7213]: ERROR: Unable to connect to RDP server <FQDN_1>: certificate validation failed

The diagnostic content survives. The identifiers do not. Operators can run the ruleset against their own text in the console before trusting it with anything.

The redaction tester in the operator console, showing raw input on the left with an IP address, a username and two hostnames, and on the right exactly what the workflow receives, with those identifiers replaced by placeholder tokens. A residual-risk score of 0.00 and a span-by-span breakdown of what was masked are shown.
The redaction tester runs the same engine and the same active ruleset the live pipeline uses, with a residual-risk score and a span-by-span breakdown of what was masked.
06 — The gate

Why the model does not get a vote on its own confidence

This is the core of the system and the part we would defend hardest. The model's self-reported confidence controls nothing. It is recorded for diagnostics and ignored for decisions.

The commit decision is a deterministic score computed from the evidence actually retrieved: how close the nearest prior case really was, how much corroborating evidence was retrieved, and how far independent sources agree. Absent features contribute exactly zero, and there is no model output anywhere in the expression.

score = w_sim · similarity  +  w_ev · evidence  +  w_agr · agreement

On top of it sits the grounding rule: no evidence, no commit. An answer with no resolvable citations scores zero, regardless of its content and regardless of its tone. And citations must actually resolve — a claim's cited text is the real stored log line retrieved from the artifact store, not the model's paraphrase of it. A citation that does not resolve is dropped, and dropping enough of them collapses the score.

The confidence gate in the operator console. Three features are shown as bars: evidence 0.80, agreement 1.00, similarity 0.76, producing a decision score of 0.82 against a commit threshold of 0.55. Below is an audited what-if panel for tuning the threshold.
Three features, one score, one threshold, and no model output anywhere in the computation. The same arithmetic is written onto the ticket alongside the diagnosis, so an engineer can see why the system was willing to commit rather than only what it concluded.

Three independent vetoes

Scoring is necessary but not sufficient. A wrong answer can still be well-evidenced if the model cites genuine lines in support of a false conclusion — a failure we hit repeatedly in development. So three deterministic guards can veto the model outright.

  • Component corroboration. The answer blames a component the evidence contradicts. Killed.
  • Layer corroboration. The answer blames a layer the deterministic correlator has already ruled out with high confidence. Killed.
  • Red-herring corroboration. The answer's mechanism is a signal the parser has already flagged as noise. Killed.

When a guard fires, the findings are withheld, the score collapses to zero, and the agent retries. If it still cannot ground an answer, it hands off to a human and says so plainly, in writing, on the ticket.

The central design trade avocadoctor is built to prefer deferring honestly over being confidently wrong. In a support queue a wrong answer delivered with authority is worse than no answer at all. We optimised against that failure mode deliberately, and we accept its cost: the agent defers more often than a less careful system would.
07 — What it looks like in practice

A committed diagnosis, and a refusal

When the evidence is there, the agent commits and the diagnosis lands on the ticket in a fixed structure: what it thinks is wrong, the reasoning, a suggested fix explicitly marked not applied, and the arithmetic that authorised it.

A committed diagnosis posted as an agent-only comment on a Salesforce support case. It contains four sections: what we think is wrong, the analysis, a suggested resolution marked 'reviewed suggestion — not applied', and a confidence line reading similarity 0.74, evidence 0.80, agreement 1.00, score 0.81, against a commit threshold of 0.55.
A committed diagnosis as the engineer sees it. Note the third paragraph of the analysis: the agent reports that it could not read the configuration file because redaction had masked it, and says so rather than inventing its contents. The fail-closed boundary surfaces in the output, honestly, as a limit on the claim.

The refusal matters more

A live case on our own stack. Users could not connect to a Windows 11 desktop, and the logs were dominated by TLS handshake failures at the edge. The agent made nineteen tool calls across two attempts: it checked what was running, checked the configuration for drift, and then walked down the stack — gateway, broker, hypervisor, and finally into the guest itself.

The investigation timeline showing nineteen tool calls across two attempts. Turn 1 lists files, gets container states and reads configuration. Turn 2 reads the guacd log, the OpenUDS trace and operations logs, the Guacamole properties file, then searches for SSL_do_handshake. After a retry it reads the nginx error log, searches for connection-refused errors, and reads the OpenUDS auth log, the Proxmox VM status and a guest probe file. Each row shows arguments, bytes returned and latency.
This is not a summary of what the agent did. It is the record of it: every tool call, by turn and attempt, with arguments, bytes returned and latency.

It concluded:

The SSL handshake failures in nginx are caused by an external internet scan targeting the public HTTPS port, not a configuration or product issue. The root cause is a Windows guest VM that is running but has a non-responsive QEMU guest agent, preventing the UDS broker from assigning a valid IP address. The UDS actor reports ip:unknown, and the upstream connection is never established.

Three things are worth drawing out. It looked past the loudest signal: the TLS errors were the most visible thing in the bundle by an order of magnitude, and they were noise. It crossed layers: the symptom appeared at the edge and the fault was in the guest, three layers down. And it rejected a plausible memory — the case base offered a prior Guacamole handshake bug at 0.72 similarity, a superficially excellent match and exactly the kind of thing a retrieval-augmented system pastes back with confidence. The corroboration guard rejected it because the evidence in this bundle contradicted it.

And then the part that matters most. Having produced a coherent, specific, cross-layer answer, the agent withheld it and handed the case to a human, because it could not resolve its citations back to stored evidence. The gate does not care how sure the model sounds. It posted its ranked options to the ticket, each marked as a reviewed suggestion not applied, told the engineer plainly why it would not commit, and got out of the way.

An agent that would have committed there is an agent that will eventually commit something wrong with exactly the same fluency.

08 — Operations

Everything it did is inspectable

Orchestration is Temporal, so every analysis is a durable workflow. A crash mid-investigation resumes rather than restarts, and the full event history of every run is replayable after the fact.

The operator console run board, listing three analysis runs with their case identifier, state, category, decision score and retry count. One run is handed off with a score of 0.00; two are closed with scores of 0.82 and 0.83. A left-hand navigation shows monitor, analyze, govern and learn sections including a redaction tester, knowledge base, audit trail and write-back.
The run board: every analysis, its state, its category, its score and its retries. Committed, handed off, or quarantined — and the arithmetic behind each verdict.
The LLM call inspector, showing two model calls made during a run. Each records the model (qwen3-30b-a3b-instruct-2507), the local endpoint, latency, input and output token counts, retry count, and the redacted prompt that was sent.
The LLM inspector: every call the system made to the model, the redacted prompt that was sent, the token counts, and any endpoint error. The endpoint is a private address on the local machine, which is the only address the egress point permits.

Alongside these sit the investigation timeline, the redaction report and tester, the evidence trail that resolves each claim back to the line that produced it, the gate view with its read-only what-if panel, a hash-chained audit trail of every decision and every external write, and OpenTelemetry spans across the investigation, each turn and each tool call.

09 — Deployment

Where it runs

The whole pipeline — retrieval, embeddings, reasoning and the operator console — fits on a single desktop-class machine. That is a deliberate target rather than a limitation. It means the answer to "where does our support data go?" is "the box in the corner of your server room", and it means a pilot needs no GPU budget and no procurement cycle.

HardwareAMD Ryzen AI Max+ 395, 128 GB unified memory
ReasoningQwen3-30B-A3B, a 30B mixture-of-experts model, served locally over an OpenAI-compatible API
EmbeddingsBGE-M3, served locally
DataPostgreSQL with pgvector and Apache AGE
OrchestrationTemporal — durable, replayable workflows
ConsoleReact
Live stackProxmox 8.4.19 · OpenUDS 4.0.0 · Apache Guacamole 1.5.2 · Windows 11 guests · nginx
TicketingSalesforce, behind an adapter — the platform is not built around one vendor
10 — Status & boundaries

What it does not do

avocadoctor runs today against our own virtual-desktop estate, diagnosing real tickets from a real support queue with Salesforce as the ticketing front end. It is a pilot on our own infrastructure rather than a system with paying customers behind it, and we will say otherwise the day that changes.

  • It never remediates. No service restarts, no configuration changes, no ticket closures. It diagnoses and drafts; a person acts.
  • It never publishes anything customer-facing. The write-back is an internal, agent-only comment plus structured fields for the support team.
  • It defers when it cannot prove an answer — by design, and readily. That is the trade described above, and we would make it again.
  • It is not built around one ticketing vendor. Salesforce is an adapter, and so is the first-party portal. Vendor differences live at the boundary, never in the reasoning core.
11 — What this demonstrates

Lessons

The engineering that distinguishes a usable AI system from a demonstration has very little to do with the model. It is the boundary drawn around it. Every consequential decision in avocadoctor — the single egress point, the fail-closed redaction, the deterministic score, the three vetoes, the requirement that a citation resolve to a stored line — is a decision about what the model is permitted to influence, and none of them would be improved by a better model.

The second lesson is that the refusal is the product. It is straightforward to build an agent that always answers. What is difficult, and what makes such a system safe to put in front of an engineer, is building one that can recognise when it has not proved its case and say so. That capability cannot be added later, because it has to be structural: the model must not be able to talk its way past it.

The third is that constraints stated first produce better architectures than constraints retrofitted. Had we built the agent and then attempted to add a privacy story, we would have arrived at a perimeter with exceptions in it. Starting from "the model runs on your hardware or there is no product" forced every downstream decision — which model, how much context, how to make a modest local model behave — and the result is a system whose central claim is structural rather than promised.

The reasoning core, the redaction boundary, the deterministic gate and the audit trail are platform-independent. Supporting another stack means writing adapters — a log grammar, a code index, a case corpus — rather than building a new product.

Is your data the reason you have not tried AI yet?

If the blocker is that your logs, tickets or documents cannot leave your network, that is the constraint we design around rather than the one we ask you to relax. We are looking for a first pilot with a support organization in exactly that position.

Discuss an Engagement Applied AI