Skip to content

Repository files navigation

🧠 Serotonin Script

PythonFastAPITaskiqCoverageLintLicense

AI-driven medical content engine using RAG (LlamaIndex + Qdrant), FastAPI, and Taskiq for automated multi-platform publishing with physician style preservation.


🎯 Overview

Serotonin Script is an autonomous system for generating and distributing medically-accurate content across social platforms. It leverages RAG (Retrieval-Augmented Generation) to ensure medical precision while preserving the unique authorial voice of healthcare professionals.

The system covers the full content lifecycle: from a single /draft Slack command → RAG-powered generation → physician approval → multi-platform publishing → post-publish vectorization for continuous style improvement.

Key Capabilities

  • Style Preservation — Vector-based retrieval of physician's writing patterns via hybrid search (dense + BM25)
  • Medical Accuracy — Fact-checking against PubMed API and clinical guidelines (Chain-of-Verification)
  • Multi-Platform Publishing — Automated distribution to Telegram, X (Twitter), Threads via n8n workflows
  • Async-First Architecture — High-performance task processing via Taskiq + Redis (chosen over Celery: ~50-80 MB memory footprint vs ~150-200 MB, 1-2s startup vs 7-10s)
  • Slack-Native UX — Draft approval workflow with interactive Block Kit UI
  • RAG Feedback Loop — Published posts automatically vectorized back into Qdrant for continuous style learning
  • Production Observability — Prometheus metrics, Grafana dashboards (backend, LLM costs, Taskiq queue), Loki log aggregation

🛠 Tech Stack

LayerTechnologyPurpose
API FrameworkFastAPIAsync-native REST API
Task QueueTaskiq 0.11+ + RedisBackground job processing, async-native
AI EngineClaude 3.5 Sonnet / GPT-4oContent generation with LLM router + fallback
Vector StoreQdrantSemantic search for style matching and knowledge retrieval
RAG FrameworkLlamaIndexRetrieval-augmented generation pipeline
SearchHybrid (dense + BM25)Qdrant hybrid mode for improved retrieval precision
External DataPubMed API + BeautifulSoupMedical fact verification
Orchestrationn8n (self-hosted)Workflow automation, scheduling, social delivery
DatabasePostgreSQL + AlembicRelational data with async sessions (asyncpg)
MonitoringPrometheus + Grafana + Loki + PromtailMetrics, dashboards, log aggregation
Reverse ProxyNginxHTTPS termination

Why Taskiq over Celery?

AspectCeleryTaskiq
ArchitectureSync-firstAsync-native (shared event loop with FastAPI)
Dependency InjectionManual wiringTaskiqDepends — identical to FastAPI
Memory per worker~150-200 MB~50-80 MB
Cold start7-10 seconds1-2 seconds
Type hintsPartialFull (Pydantic-native)
TestingComplex mockingDirect async function calls

See ADR: Taskiq over Celery for the full decision record.


📁 Project Structure

serotonin_script/
├── backend/
│ ├── api/
│ │ ├── middleware/ # auth (Slack sig), error_handler, logging, rate_limit (Redis sliding-window)
│ │ └── routes/ # drafts, feedback (Slack interactions), health
│ ├── config/ # settings (Pydantic), system_prompts, lexicon (Slack UI text)
│ ├── integrations/
│ │ ├── external/ # pubmed_client (NCBI E-utils), web_scraper (BeautifulSoup)
│ │ └── llm/ # anthropic_client, openai_client, router (fallback logic)
│ ├── models/ # db_models (SQLAlchemy 2.0), schemas (Pydantic v2), enums
│ ├── rag/
│ │ ├── indexing/ # document_loader (MD/PDF/TXT), chunking (SentenceSplitter), embedder
│ │ ├── pipelines/ # hybrid_search (dense + BM25)
│ │ └── retrieval/ # style_retriever, knowledge_retriever, base protocol
│ ├── repositories/ # draft_repository, feedback_repository, post_repository
│ ├── services/ # content_generator, draft_service, fact_checker, style_matcher, publisher_service
│ ├── utils/ # structured logging (Structlog)
│ ├── workers/
│ │ ├── middlewares/ # LoggingMiddleware, RetryMiddleware (exp. backoff), PrometheusMiddleware
│ │ ├── tasks/ # generate_draft, publish_post, ingest_guideline, scheduled_post, vectorize_post
│ │ ├── broker.py # Taskiq Redis broker (ListQueueBroker + RedisAsyncResultBackend, TTL 1h)
│ │ ├── callbacks.py # Slack Block Kit notifications on task complete/failure
│ │ └── dependencies.py # TaskiqDepends: StyleMatcher, FactChecker, LLMRouter, ContentGenerator, PublisherService
│ └── tests/
│ ├── unit/ # 20 test modules — services, RAG, workers, API, middleware
│ └── integration/ # test_draft_service.py (full service stack)
├── knowledge_base/
│ ├── doctor_style/ # Physician's articles & posts (.md) + metadata.json
│ └── medical_guidelines/ # Clinical protocol PDFs
├── slack_app/
│ ├── blocks/ # draft_card.json, approval_modal.json, status_message.json
│ ├── handlers/ # slash_commands.py (/draft), interactions.py, events.py
│ └── utils/block_builder.py # Dynamic Block Kit UI constructor
├── orchestration/
│ ├── n8n/ # Workflow definitions + credentials guide
│ └── monitoring/ # n8n health check (circuit breaker)
├── database/
│ ├── migrations/ # Alembic versions (initial schema + platform/scheduled_at)
│ └── seeds/initial_data.sql
├── infra/
│ ├── docker/ # Dockerfile.backend, Dockerfile.worker, Dockerfile.base
│ ├── monitoring/ # Prometheus, Grafana dashboards (backend/llm_costs/taskiq), Loki, Promtail
│ └── nginx/nginx.conf
├── scripts/
│ ├── index_knowledge_base.py # Bulk ingestion into Qdrant
│ ├── test_pipeline.py # E2E pipeline test
│ └── deploy.sh / migrate.sh / setup.sh
├── docs/
│ ├── architecture.md
│ ├── api_spec.yaml # OpenAPI 3.0
│ ├── deployment.md
│ ├── runbook.md
│ ├── taskiq_guide.md
│ └── adr/ # 001-vector-store, 002-llm-selection, 003-taskiq-over-celery
└── docker-compose.yml

🚀 Quick Start

Prerequisites

  • Docker & Docker Compose
  • Python 3.13 (for local development)
  • Slack workspace with /draft slash command configured
  • API keys: Anthropic, OpenAI
  • n8n credentials: Telegram Bot Token, X (Twitter) OAuth2, Threads Access Token (configured inside n8n, not in .env)

Installation

# Clone repository
git clone https://github.com/PyDevDeep/serotonin-script.git
cd serotonin-script
# Configure environment
cp .env.example .env
# Edit .env with your API keys and credentials# Start all services (API + worker + Redis + Qdrant + PostgreSQL + n8n + monitoring)
docker-compose up --build

Service URLs

ServiceURL
APIhttp://localhost:8000
API Docs (Swagger)http://localhost:8000/docs
n8n Workflowshttp://localhost:5678
Grafanahttp://localhost:3000

📖 Usage

1. Index Knowledge Base

# Ingest physician's writing samples + medical guidelines into Qdrant
python scripts/index_knowledge_base.py

Loads documents from knowledge_base/doctor_style/ and knowledge_base/medical_guidelines/ — chunks, embeds, and stores vectors in two separate Qdrant collections.

2. Generate Draft via Slack

/draft anxiety management tips
/draft depression coping strategies telegram

Full workflow:

Slack /draft
└─► n8n Webhook
└─► POST /api/v1/draft ← returns task_id immediately (< 500ms)
└─► Taskiq generate_draft task
├── StyleMatcher — retrieves top-5 physician posts (Qdrant)
├── FactChecker — PubMed API + web scraping + Chain-of-Verification
└── ContentGenerator (Claude 3.5 Sonnet → GPT-4o fallback)
└─► Slack callback → Block Kit draft card

3. Approve & Publish

From the Slack draft card:

  • Publish to Telegram / X / Threads — triggers publish_post Taskiq task → publisher_service.py dispatches a webhook to n8n → n8n executes the platform-specific workflow (Telegram Bot API / Twitter API v2 / Threads API)
  • Edit — opens Slack modal with full text editor + platform/schedule selector
  • Regenerate — re-queues generate_draft with same topic

Publishing architecture note:publisher_service.py is a thin dispatcher — it sends a structured webhook payload to n8n and tracks publication status in PostgreSQL. The actual social platform API calls (auth, formatting, retry logic) live entirely in n8n workflows under orchestration/n8n/workflows/. To modify platform-specific publishing behavior, edit the n8n workflow — not the Python service.

4. RAG Feedback Loop

After publishing, vectorize_post task automatically embeds the final approved text back into Qdrant (doctor_style collection) — the system continuously learns the physician's evolving style.


🔧 Development

Run Tests

# Full test suite with coverage
make test# Unit tests only
make test-unit
# Integration tests (requires running containers)
make test-integration

Local Backend

# Install dependencies
poetry install
# Run API server
poetry run uvicorn backend.api.main:app --reload
# Run Taskiq worker (2 processes, max 10 concurrent async tasks)
poetry run taskiq worker backend.workers.broker:broker --workers 2 --max-async-tasks 10

Database Migrations

alembic revision --autogenerate -m "description"
alembic upgrade head

✅ Test Coverage

Overall: 98% (4627 statements, 103 missed)

ModuleCoverage
services/content_generator.py100%
services/draft_service.py100%
services/fact_checker.py100%
services/style_matcher.py100%
api/middleware/auth.py100%
api/middleware/error_handler.py100%
integrations/external/pubmed_client.py100%
integrations/llm/router.py100%
rag/pipelines/hybrid_search.py100%
rag/retrieval/knowledge_retriever.py100%
rag/retrieval/style_retriever.py100%
workers/tasks/generate_draft.py100%
workers/tasks/publish_post.py100%
workers/callbacks.py100%
api/routes/feedback.py96%
api/middleware/rate_limit.py91%
services/publisher_service.py91%
api/routes/drafts.py40%
integrations/external/web_scraper.py38%

api/routes/drafts.py (40%) and web_scraper.py (38%) are the remaining gaps — route integration tests and scraper HTTP mocking are the next testing targets.


📊 Monitoring

Three pre-built Grafana dashboards:

DashboardURLTracks
Backend Metricshttp://localhost:3000/d/backend_metricsRequest rate, latency (p95), error rate
LLM Costshttp://localhost:3000/d/llm_costsToken usage, API calls, cost per platform
Taskiq Metricshttp://localhost:3000/d/taskiq_metricsQueue depth, task duration, failure rate

Prometheus alert rules configured for:

  • Task failure rate > 5%/hour
  • Queue depth > 100 tasks
  • Task duration p95 > 60s
  • LLM error rate > 10% in 5 minutes

📚 Documentation

DocumentDescription
ArchitectureSystem design and component interactions
API SpecOpenAPI 3.0 specification
Taskiq GuideAsync worker patterns and configuration
DeploymentProduction deployment guide
RunbookOperational procedures and troubleshooting
ADR: Vector StoreQdrant selection rationale
ADR: LLM SelectionClaude + GPT-4o fallback design
ADR: Taskiq vs CeleryTask queue decision record

⚙️ CI/CD

Four GitHub Actions workflows form a fully automated pipeline triggered on push to main:

WorkflowTriggerWhat it does
lint.ymlpush / PR → mainRuff linter, Ruff formatter check, Pyright type checker
test.ymlpush / PR → mainpoetry installcp .env.example .envpytest
build.ymlpush → mainBuilds and pushes 3 Docker images to GHCR (backend, worker, scheduler) tagged latest + commit SHA
deploy.ymlon build.yml successSSH into VPS → git pull origin mainbash scripts/deploy.sh

Pipeline flow on every merge to main:

push → main
├─► lint.yml (parallel)
├─► test.yml (parallel)
└─► build.yml → pushes ghcr.io/<owner>/serotonin_script-{backend,worker,scheduler}
└─► deploy.yml → SSH → git pull → deploy.sh

deploy.yml runs only if build.yml concluded with success (if: github.event.workflow_run.conclusion == 'success'). Required GitHub Secrets: SERVER_HOST, SERVER_USER, SERVER_SSH_KEY.


🏭 Production Deployment

See docs/deployment.md for the full guide. Quick reference:

Docker Compose (VPS)

The production stack uses two Compose files layered together: docker-compose.yml (infrastructure services) and infra/docker-compose.prod.yml (application services).

# One-command deployment
bash scripts/deploy.sh

deploy.sh executes in order:

  1. Tears down existing application containers (preserves named volumes)
  2. Builds new images from infra/docker/Dockerfile.base (multi-stage, non-root user seratonin)
  3. Starts postgres + redis and waits for health checks
  4. Runs Alembic migrations via scripts/migrate.sh
  5. Brings up all services

Services in Production

ServiceImagePortNotes
backendDockerfile.base80012 Uvicorn workers, metrics disabled
workerDockerfile.base9000Taskiq worker, Prometheus metrics on :9000
schedulerDockerfile.base9001Taskiq scheduler for cron tasks
postgrespostgres:15-alpineinternalExternal named volume docker_postgres_data
redisredis:7.2-alpineinternalAOF persistence, external volume docker_redis_data
qdrantqdrant/qdrant:latestinternalExternal volume docker_qdrant_data
n8nn8nio/n8n:latest5678External volume docker_n8n_data
prometheusprom/prometheus9090Scrapes backend :8001/metrics and worker :9000
grafanagrafana/grafana3000Dashboards: backend, LLM costs, Taskiq
loki + promtailGrafana stack3100Log aggregation from Docker socket

Docker Image

Dockerfile.base uses a two-stage build:

Stage 1 (builder): python:3.13-slim
└─ Poetry 2.0.1 exports requirements.txt (prod deps only)
Stage 2 (runtime): python:3.13-slim
└─ Non-root user: seratonin:seratonin
└─ Model cache dirs: /app/cache/huggingface, /app/cache/fastembed
└─ Shared by: backend, worker, scheduler (different CMD per service)

Persistent Volumes

All data volumes are declared as external: true with fixed names — they survive docker-compose down and must be pre-created on the host:

docker volume create docker_postgres_data
docker volume create docker_redis_data
docker volume create docker_qdrant_data
docker volume create docker_n8n_data

🤝 Contributing

  1. Fork the repository
  2. Create feature branch (git checkout -b feature/amazing-feature)
  3. Commit changes (git commit -m 'Add amazing feature')
  4. Push to branch (git push origin feature/amazing-feature)
  5. Open Pull Request

See CONTRIBUTING.md for detailed guidelines.


📄 License

This project is licensed under the MIT License — see LICENSE for details.


🙏 Acknowledgments

  • LlamaIndex for the RAG framework
  • Taskiq for modern async-native task processing
  • Qdrant for vector search with hybrid mode

Created byPyDevDeep

About

AI-driven medical content engine using RAG (LlamaIndex/Qdrant), FastAPI, and Taskiq for automated multi-platform publishing.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages