> ## Documentation Index
> Fetch the complete documentation index at: https://gladlabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Content router service

# Content Router Service

**File:** `src/cofounder_agent/services/content_router_service.py`
**Tested by:** `src/cofounder_agent/tests/unit/services/test_content_router_service.py` (and any integration test that exercises a full pipeline run)
**Last reviewed:** 2026-05-23

## What it does

The content router is the single entry point for "given a topic, run the
whole content pipeline." As of 2026-05-16 (Lane C Stage 4) it is a **thin
TemplateRunner dispatcher**: it builds the shared pipeline context
(`image_service`, `settings`, `style_tracker`, `site_config`,
`models_by_phase`, experiment assignment) and hands it to
`TemplateRunner.run(template_slug, context)` keyed on
`pipeline_tasks.template_slug`. The LangGraph template (registered in
`services/pipeline_templates/__init__.py`) owns the node ordering —
the router no longer threads 12 stages directly. A NULL `template_slug`
fails loud per `feedback_no_silent_defaults`. The legacy chunked
StageRunner path was deleted in the same Stage 4 cut along with
`plugins/stage_runner.py` itself.

The router also owns two cross-cutting concerns the stages can't see:
the GPU mode switch (Ollama → image-gen → Ollama) around the featured image
stage, and the writer-fallback canary that compares
`pipeline_writer_model` against the model the draft actually came back
with so a silent 72B → 27B downgrade is logged loudly instead of going
unnoticed.

## Public API

* `process_content_generation_task(topic, style, tone, target_length, ...) -> dict[str, Any]` —
  runs the full pipeline. Required: `database_service` (for
  `pipeline_tasks` persistence). Optional: `task_id`,
  `generate_featured_image`, `models_by_phase`, `quality_preference`,
  `category`, `target_audience`, `tags`. Returns the shared `result`
  dict — see the `result["status"]` field for `pending`, `published`,
  `awaiting_approval`, `rejected`, or `failed`.

The function is the only public surface — there are no classes or
helper exports. Other modules call this via
`services/flows/content_generation.py` (the Prefect-orchestrated
content pipeline that owns dispatch as of 2026-05-16 Stage 4) or
`/api/tasks/generate` (HTTP entry).

## Configuration

The router itself reads only one DB-configured setting directly:

* `pipeline_writer_model` (default: writer chain decides) — compared
  against the model that produced the draft to detect silent fallback.
* `pipeline_dry_run_mode` (default `false`) — when `true`, the writer
  chain short-circuits with `AllModelsFailedError`. The router demotes
  the resulting halt to a `dry_run_halt` audit event at `severity=info`
  instead of the usual `error` so it doesn't drown real failures.

Every other tunable lives on the individual stages — see
`docs/architecture/multi-agent-pipeline.md` and
`docs/architecture/anti-hallucination.md` for the per-stage settings.

## Dependencies

* **Reads from:**
  * `services.container.get_service("settings")` (DI seam, falls back to
    `None` outside lifespan)
  * `services.image_style_rotation.ImageStyleTracker`
  * `services.image_service.get_image_service`
  * `services.gpu_scheduler.gpu` for the image-gen/Ollama mode switch
  * `site_config` (from `AppContainer` or DI) for the writer-fallback +
    dry-run checks
  * `plugins.registry.get_core_samples()` for the stage list
* **Writes to:**
  * `pipeline_tasks` (status, error\_message, task\_metadata) via
    `database_service.update_task`
  * `audit_log` (multiple event\_types: `task_started`,
    `generation_complete`, `qa_passed`/`qa_failed`,
    `pipeline_complete`, `dry_run_halt`, `error`, `writer_fallback`)
  * `webhook_events` indirectly via webhook delivery
    (`emit_webhook_event("task.failed", ...)` on failure). Earlier
    docs called this `pipeline_events`; the actual writer was always
    `webhook_events`. The unrelated `pipeline_events` table was
    dropped 2026-05-04 (poindexter#366).
* **External APIs:** none directly — stages own the LLM/HTTP calls.

## Failure modes

* **Stage halt before content exists** — `generate_content` returns
  `continue_workflow=False`. Router raises `RuntimeError` with the
  stage's `detail`; nothing further runs and the task is marked
  `failed`. Diagnose via the `error` audit event payload (full
  traceback in logs).
* **Stage halt after content exists** — `quality_evaluation` returns
  halt. Same pattern. The partially-generated content, image,
  and SEO metadata are preserved in `task_metadata` so an operator can
  still review what was produced.
* **Cross-model QA rejection** — on the graph\_def path `qa.aggregate`
  (the qa.\* rail block that replaced the deleted `cross_model_qa` stage,
  \#355) sets `status="rejected"`, persists the reject via
  `modules/content/atoms/_qa_persist.py`, and halts the graph; not raised as an
  error.
* **Silent writer fallback** — primary model timed out or 500'd, writer
  chain succeeded with the next model. Detected by comparing
  `pipeline_writer_model` to `result["model_used"]` after
  `generate_content`. Fires `writer_fallback` audit event at
  `severity=warning`. Visible on the /pipeline Grafana dashboard.
* **Dry-run halt** — when `pipeline_dry_run_mode=true`, the writer
  chain raises `AllModelsFailedError`. The router demotes the audit
  severity from `error` to `info` so the 24h error count isn't
  poisoned by intentional dry-run noise.

## Common ops

* **Re-run a failed task:** call
  `process_content_generation_task(...)` with the same `task_id`. The
  function tolerates re-entry; downstream stages handle idempotency
  (e.g. publish\_service has a slug-suffix guard).
* **Toggle dry-run:** `poindexter settings set pipeline_dry_run_mode true`
  to short-circuit the writer chain without consuming GPU/cloud time.
* **Check writer-fallback events:**
  `SELECT * FROM audit_log WHERE event_type = 'writer_fallback' ORDER BY created_at DESC LIMIT 20;`
* **Disable featured-image stage for one task:** pass
  `generate_featured_image=False`. The stage runs but short-circuits.
* **Inspect what halted:** the failure path stores `error_stage` and
  `error_message` in `pipeline_tasks.task_metadata`.

## See also

* `docs/architecture/multi-agent-pipeline.md` — stage-by-stage
  breakdown with the prompts and gates each stage owns.
* `docs/architecture/anti-hallucination.md` — how the validator + QA
  stages slot into the run order.
* `docs/architecture/services/multi_model_qa.md` — the `MultiModelQA`
  rail library that the `qa.*` atoms delegate to.
* `feedback_writer_model_canary` (operator design note)
  — operator playbook for diagnosing pipeline-wide approval-rate drops
  via the writer-fallback event.
