Skip to main content

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 existsgenerate_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 existsquality_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.