RAG · tool calling
A working prototype of an LLM tool-calling agent that answers natural-language questions about a Drupal site's content and configuration, backed by a crawler that builds a searchable SQLite knowledge graph.
Anyone who works with a large Drupal site ends up asking the same questions over and over: what pages exist about a topic, where a specific component is used, what the current menu or theme configuration is. Answering that means knowing exactly where to look in the Drupal admin. I built Drupal Copilot to answer those questions in plain language instead: a crawler turns a Drupal site's JSON:API into a queryable knowledge graph, and a tool-calling agent sits on top of it so a question gets answered by looking something up, not by guessing.

I built the crawler to read the JSON:API root's own links and classify every endpoint it finds by naming pattern instead of hardcoding the content types I happened to be building against. It's the one decision in this codebase that would let the same crawler point at a different Drupal site's content model without a code change.
The agent gets nine tools (search, get entity, find related entities, aggregate, page structure, site settings, and a couple of graph-inspection tools) and a 10-step cap. The first step forces a tool call, so the model has to look something up before it answers; later steps let it decide when it has enough. If it hits the cap, I show an explicit message instead of letting it fail silently.
Sources are capped at 3 per tool call by a structural score, then re-ranked at the end of the turn by whether the entity's title or path actually shows up as a substring in the generated answer, capped again to 8 for display. It's a cheap heuristic, not semantic matching. I chose it over adding an embeddings step because it's good enough for grounding citations without a second model call.
Crawling and relationship-resolving go through a provider interface with a single Drupal implementation today. The crawler and graph code depend on that interface, not the Drupal-specific class directly, so a second CMS would slot in at that seam instead of needing a rewrite.
A full crawl defers the FTS index sync until the end instead of updating it after every single entity write, then rebuilds it once. Syncing on every write was the obvious way to build it. It just doesn't scale to a full site crawl.
The SQLite driver I'm using is synchronous under the hood and my ORM wraps it asynchronously, so concurrent writes during a crawl could race. I added a write queue that serializes them instead of trying to make SQLite handle concurrency it isn't built for.
The Knowledge Builder's API and the chat app itself have no authentication. The only real boundary is CORS locked to localhost, which is a fine boundary for a single-operator local tool and not one for a multi-tenant product. If this needs to serve more than one person, that's the first thing I'd add, not an afterthought.
The embed script drops a chat widget into any page that includes it, iframed against the app's own embed route. There's no sandbox attribute, no postMessage origin check, and no allowlist on that route. Any site can embed it today. I know exactly what's missing here. I haven't built it yet.
Test coverage is real and concentrated in the Knowledge Builder's pure logic: relationship building, pagination, discovery, normalization, the query engine. The agent loop and the LLM service, the most stateful and complex part of the system, have no tests yet.
The crawler retries against Drupal with backoff and a configurable rate-limit delay, and it specifically detects the case where an expired session returns an HTML login page instead of JSON, surfacing that as a clear hint instead of a raw parse error. None of that protection exists on the app's own API surface yet.
Drupal Copilot is Phase 1 of a two-phase plan. It can answer questions about content, structure, and site configuration, and I've written down exactly what it can't do yet rather than leaving that implicit. It's a local, single-operator tool right now, built in a focused implementation push rather than iterated over a long commit history, so I treat it as a working prototype that proves the architecture end to end, not a system that's carried real production usage yet.