In one sentence
An author uploads a document and asks for a course. The platform reads the document, decides what the lessons should be, writes the content of each one with an LLM, generates the pictures, and leaves a draft the author can edit — a process of minutes, driven entirely by events.
Every step is a separate worker, and every worker talks only to authoring-service-v2. Nothing in the chain talks to anything else in the chain, which is why a failure at any stage leaves the run visible and restartable rather than lost.
The chain
sequenceDiagram autonumber participant A as Author participant AV2 as authoring-v2 participant AM as asset-manager participant EW as extraction-worker participant CW as content-worker participant IW as image-worker A->>AV2: upload-document tool AV2->>AM: create upload URL A->>AM: PUT file to S3 AM-->>AV2: authoring.asset.upload.completed A->>AV2: request-module-from-doc AV2-->>EW: authoring.extract.run.requested EW-->>AV2: authoring.extract.gates.ready EW-->>AV2: authoring.extract.tree.ready AV2-->>CW: authoring.content.generation.requested.v3 CW-->>AV2: authoring.content.generation.ready AV2-->>IW: authoring.image.batch.requested IW->>AM: upload generated images IW-->>AV2: authoring.image.batch.ready
Step by step
- Upload. The author picks a file. authoring-v2 asks asset-manager for a presigned URL and the browser uploads straight to S3 — the file never passes through the API. On confirm, asset-manager publishes
authoring.asset.upload.completed(asset-manager-service/app/api/v1/assets.py:52), which authoring-v2 consumes (authoring-service-v2/app/consumers/register.py:65). - Start the run. The
request-module-from-doctool registers a run, debits credits, and kicks a durable workflow. Credits are debited up front and refunded if the run fails — see credits & billing. - Extract. authoring-v2 publishes
authoring.extract.run.requested(app/services/extraction_orchestration.py:190). extraction-worker parses the document — large PDFs go out to the MinerU parser on Modal via RAGFlow — and answers twice:authoring.extract.gates.readywith what it understood, thenauthoring.extract.tree.readywith the proposed lesson structure (extraction-worker/app/consumers/run_consumer.py:145, structure_consumer.py:115). Progress arrives continuously onauthoring.extract.progress, which is what the author watches in the UI. - Index for retrieval. In parallel the source document is indexed into RAGFlow by rag-context-worker, so the content step can quote the actual document rather than inventing text.
- Write the content. For each lesson, authoring-v2 publishes
authoring.content.generation.requested.v3— one message per lesson, carrying every concept plus retrieved evidence. content-worker generates the screens and blocks and writes them directly into authoring-v2's database, then answersauthoring.content.generation.ready(authoring-content-worker/app/workers/content_worker.py:588). It owns no database of its own; that shared-table coupling is the strongest seam in the pipeline. - Illustrate. authoring-v2 publishes
authoring.image.batch.requested(app/services/tool_dispatcher.py:326). image-worker generates images with Gemini — the batch path rides the Batch API's cheaper 24-hour tier — uploads them to asset-manager, and answersauthoring.image.batch.ready. Runs never gate on images: late pictures delay the artwork, not the module. - Draft ready. The author now has an editable draft and can change anything with the authoring tools, then publish it.
The research variant
When there is no source document, the author gives a topic instead. authoring-v2 publishes authoring.research.run.requested (app/tools/handlers/orchestrator_handlers.py:224); research-worker searches the web through OpenRouter and returns candidate topics. The author confirms a selection — a deliberate human gate — and a deeper pass builds the tree. From tree.ready onward the flow rejoins the chain above at step 5.
Where it breaks today
| Issue | Detail |
|---|---|
| The non-DBOS fallback never indexes the document | The fallback path at doc_to_module.py:187 kicks index-document onto authoring.tool-call.requested, a subject the code itself records as having no subscriber (authoring-service-v2/app/tools/scopes.py:8-9). rag-context-worker is not listening, so on that path retrieval has nothing to retrieve. See the authoring-v2 verdicts. |
| Cancelling an upload does not cancel the parse | rag-context-worker listens on authoring.rag.indexing.cancel.requested but nothing publishes it. The MinerU parse keeps running and stays billable. Same for document deletion, which never reaches RAGFlow. See rag-context verdicts. |
| Re-applying the AUTHORING stream definition breaks images | image-worker widens the stream's subjects at boot; the checked-in JSON does not include that widening (ARCHITECTURE.md §3.3). Applying the file as-is silently stops image delivery. |