Skip to content

Drafting with AI

There are two drafting paths in the codebase. They are grounded in different things, over different transports, under different model task names, and only one of them is reachable from the application.

The path the application uses

Endpoint Does Gate
POST /articles/ai-draft Write a new draft on a topic moderator
POST /articles/{id}/ai-refine Rewrite an existing article moderator

Both queue the same Celery task, generate_article_draft. Neither ever runs inside the request — drafting is always background work, watched from /jobs.

Model task name: article. Configure it in Settings → AI Models.

Grounding is opt-in, per request

Flag Reaches How
use_library The indexed clinical library An MCP server at the clinical_mcp_url site setting, tool clinical_semantic_search
use_pubmed Published literature NCBI E-utilities directly. No third party. An API key is optional and only raises the rate limit.

Both are off unless the corresponding site flag is on — clinical_library_enabled and pubmed_enabled both default to false, so a site that has not pointed itself at a library simply does not offer it.

References found by grounding are merged into whatever the article already cites, not substituted for them.

What "refine" rewrites

ai-refine is not a separate function. It calls the same task with article_id set, the article's own title as the topic, and free-text instructions.

The task then rewrites the whole article — title, slug, summary, content and every section — from the model's reply. Two things protect you:

  1. Each section's variant and id are preserved, so existing [[id#section]] cross-references pointing into the article do not break.
  2. The article is snapshotted first. This was added because the one operation that rewrites an entire article left nothing to go back to.

It never publishes. The article stays whatever status it was.

The batch path

services/article_writer.py is not reachable from any route. It is driven only by python -m scripts.generate_articles, a resumable batch script that runs synchronously with no Celery.

It differs in two ways that matter if you are reading the code:

  • It reads the clinical library directly from Milvus over HTTP (CLINICAL_MILVUS_URI, CLINICAL_MILVUS_COLLECTION) rather than through the MCP server.
  • It uses the extraction model task, not article.

It never uses PubMed. It always writes status="draft" and stamps generated_by as clinical-library:{model}. References are built only from the metadata of the passages actually retrieved — never invented by the model.

Two implementations of one idea

Clinical-library grounding exists twice, over two transports, for these two callers. If you change how retrieval works, check both.

What a generated draft is

  • status = "draft" — invisible to learners.
  • generated_by set — which is exactly what puts it in the generated but unread queue.

Nothing publishes itself. A person reads it, or it stays in the queue.

The hand-written standard a finished article is held to — corpus passages, mechanism first, textbook tone, tables, tidy references — is in Writing articles.