Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ env/
.ruff_cache/
.pytest_cache/

# Node (email-playground): keep the source for the build stage, drop the heavy/generated bits.
node_modules/
.react-email/
email-playground/out/
# Built fresh by the Docker template-builder stage, so never ship a local copy.
/src/templates/*.html

# OS / editor
.DS_Store
.idea/
Expand Down
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SUBJECT_TTL=604800

# Email delivery (Resend). Only needed when sending: python main.py GOTO you@example.com
RESEND_API_KEY=
EMAIL_FROM=Mediapulse <onboarding@resend.dev>
EMAIL_FROM=MediaPulse <onboarding@resend.dev>

# Subscriber + ticker database (Postgres/Supabase, read-only). Used by the campaign job and to
# enrich the analyst with each ticker's listing profile (sector, business, HQ). Skipped if unset.
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ jobs:
with:
python-version: "3.11"

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Build email templates
working-directory: email-playground
run: |
npm ci
npm run build:templates

- name: Install dependencies
run: pip install -r requirements.txt -r requirements-dev.txt

Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ env/
.vscode/

.subject_cache.json

# Generated email templates (built from email-playground, fresh each deploy/CI run)
/src/templates/*.html

# Node (email-playground)
node_modules/
.react-email/
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ All application code lives under `src/`. Packages keep their top-level names (`a
- `src/agents/orchestrator.py` — the pipeline: analyst → 5 parallel beat desks (researcher → writer → editor) → managing-editor gap roundtable → masthead → reviewer → deterministic clean/assemble/dedupe. Most non-agent logic (citation gating, URL/article validation, dedupe, subject-name canonicalization, prose humanizing) lives here.
- `src/agents/` — one module per agent (`analyst`, `researcher`, `writer`, `editor`, `managing_editor`, `reviewer`), plus `beats.py` (beat desks), `campaign.py` (top-level run over subscriptions), and `tools/` (Serper search, web fetch).
- `src/agents/skills/` — `SKILL.md` files that control agent behavior (`subject-profile`, `section-research`, `newsletter-format`). Prefer editing these over code when changing how agents research or write.
- `src/utils/` — `db.py` (subscriptions/tickers from the Mediapulse Postgres), `memory.py`, `client.py`, `guardrails.py`, `sections.py`, `mailer.py`, `email_template.py`, `ticker.py`.
- `src/utils/` — `db.py` (subscriptions/tickers from the MediaPulse Postgres), `memory.py`, `client.py`, `guardrails.py`, `sections.py`, `mailer.py`, `email_template.py`, `ticker.py`. `email_template.py` no longer hand-builds HTML: it parses the newsletter markdown and fills the tokenized template at `src/templates/newsletter.html`.
- `email-playground/` — a standalone React Email (TypeScript) project that is the visual source-of-truth for MediaPulse email templates (the newsletter is the first one). `npm run build:templates` renders each template to a tokenized `src/templates/<name>.html` that the Python side consumes (`email_template.py` for the newsletter). Those HTML files are gitignored build artifacts, regenerated fresh in CI and the Docker build. Restyle emails there, not in Python. See `email-playground/README.md`.
- `tests/` — pytest suite covering the deterministic logic (see Testing below).

## Setup
Expand Down Expand Up @@ -56,6 +57,12 @@ pytest # whole suite
conda run -n agentic-mediapulse pytest # if pytest is not on PATH
```

The email-template tests read `src/templates/newsletter.html`, which is gitignored and generated by the playground. Build it once before running the suite locally (CI does this in the `tests` job):

```
cd email-playground && npm install && npm run build:templates
```

Config is in `pyproject.toml` under `[tool.pytest.ini_options]`: `pythonpath = ["src"]`, `testpaths = ["tests"]`, `asyncio_mode = "auto"` (async tests need no decorator). CI runs `pytest` in a separate `tests` job after installing both requirement files.

The suite covers the deterministic logic, not the LLM agents: orchestrator text/section helpers, the email template, Serper tools, the DB profile shaping, guardrail middleware, memory/ticker context providers, the mailer, the client model resolution, and the campaign delivery flow. Conventions for new tests:
Expand All @@ -78,5 +85,5 @@ The suite covers the deterministic logic, not the LLM agents: orchestrator text/
## External services

- `SERPER_API_KEY` — web search.
- `MEDIAPULSE_DATABASE_URL` — Postgres for subscriptions and ticker data; schema is defined in the upstream [Mediapulse](https://github.com/hyperjumptech/mediapulse) repo.
- `MEDIAPULSE_DATABASE_URL` — Postgres for subscriptions and ticker data; schema is defined in the upstream [MediaPulse](https://github.com/hyperjumptech/mediapulse) repo.
- `SECRET_KEY` — API auth.
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# syntax=docker/dockerfile:1

FROM node:20-slim AS template-builder
WORKDIR /build/email-playground
COPY email-playground/package.json email-playground/package-lock.json ./
RUN npm ci
COPY email-playground/ ./
RUN npm run build:templates

FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
Expand All @@ -9,6 +18,7 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
COPY --from=template-builder /build/src/templates/ src/templates/

RUN useradd --no-create-home --shell /bin/false app \
&& chown -R app:app /app
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# agentic-mediapulse

A simpler, agentic version of [Mediapulse](https://github.com/hyperjumptech/mediapulse). Give it a subject — a stock ticker, a company name, or an industry theme — and a newsroom of focused agents researches, writes, and edits a locale-aware briefing across five editorial sections, with every claim traced to a real source.
A simpler, agentic version of [MediaPulse](https://github.com/hyperjumptech/mediapulse). Give it a subject — a stock ticker, a company name, or an industry theme — and a newsroom of focused agents researches, writes, and edits a locale-aware briefing across five editorial sections, with every claim traced to a real source.

## Setup

Expand Down Expand Up @@ -34,7 +34,7 @@ A simpler, agentic version of [Mediapulse](https://github.com/hyperjumptech/medi
| `REDIS_URL` | Subject-memory store — default `redis://localhost:6379/0` | Yes |
| `MEDIAPULSE_DATABASE_URL` | Postgres connection string (read-only) | Yes |
| `RESEND_API_KEY` | Resend key for email delivery | Yes |
| `EMAIL_FROM` | Sender address, e.g. `Mediapulse <hello@example.com>` | Yes |
| `EMAIL_FROM` | Sender address, e.g. `MediaPulse <hello@example.com>` | Yes |
| `SECRET_KEY` | Required on every API request (`X-API-Key` header) | Yes |
| `ANALYST_MODEL` | Model override for the analyst — falls back to `OPENAI_MODEL` | No |
| `RESEARCHER_MODEL` | Model override for the researcher — falls back to `OPENAI_MODEL` | No |
Expand Down Expand Up @@ -120,4 +120,4 @@ To change how an agent writes or researches, edit its skill file in `src/agents/

## Database

Subscriptions and ticker data are read from the [Mediapulse](https://github.com/hyperjumptech/mediapulse) database. Set `MEDIAPULSE_DATABASE_URL` to your Mediapulse Postgres connection string — the schema is defined in that repo.
Subscriptions and ticker data are read from the [MediaPulse](https://github.com/hyperjumptech/mediapulse) database. Set `MEDIAPULSE_DATABASE_URL` to your MediaPulse Postgres connection string — the schema is defined in that repo.
3 changes: 3 additions & 0 deletions email-playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.react-email/
out/
65 changes: 65 additions & 0 deletions email-playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# email-playground

The home for MediaPulse's [React Email](https://react.email/components) templates and their live
preview. Each template lives as a React component under `emails/`. A template can also export a
tokenized HTML build that the Python app fills at send time. The newsletter is the first such
template; others can be added the same way.

## Setup

```
npm install
```

## Design and preview

```
npm run dev
```

Opens the React Email preview server at http://localhost:3000. Every component in `emails/` shows
up, rendered from its `PreviewProps`. Edits hot-reload. Styling is Tailwind via react-email's
`<Tailwind>` component, inlined into the HTML at render time.

## Build the tokenized templates

```
npm run build:templates
```

For every `emails/*.tsx` that exports both a default component and a `templateProps` object, this
renders the component in template mode and writes `../src/templates/<name>.html`: a tokenized HTML
file with `{{placeholders}}` and `<!--#region-->…<!--/region-->` blocks that the Python side fills
per message. The output is a gitignored build artifact, regenerated in CI and the Docker build.
**The Python tests and the running app require these files**, so run it once after `npm install`.

## Adding a template that the Python app can fill

1. Create `emails/<name>.tsx` with a default-exported component and a `PreviewProps` for the dev
server.
2. Add a `templateMode` prop. When set, wrap variable, repeatable, and optional regions in
`<Region name="...">` (which emits `[[#name]]`/`[[/name]]` markers) and leave content as
`{{token}}` placeholders.
3. Export `templateProps`: the props that drive template-mode rendering.
4. Run `npm run build:templates`, then fill `src/templates/<name>.html` from Python.

Templates without `templateProps` are preview-only and are skipped by the build.

## Newsletter token reference

| Token / region | Filled by Python with |
| --------------------------------------- | ---------------------------------------------- |
| `{{title}}` | title (the markdown `#` heading) |
| `<!--#standfirst-->` `{{summary}}` | the standfirst paragraph (omitted if empty) |
| `<!--#sections-->` | placeholder for all rendered sections |
| `<!--#section-->` `{{section_name}}` | a standard section, repeated per section |
| `<!--#quickhits-->` | the highlighted Quick Hits panel variant |
| `<!--#sectionsep-->` | separator, kept between sections, dropped first|
| `<!--#item-->` `{{item_summary}}` | repeated once per item in a section |
| `<!--#itemsep-->` | separator, kept between items, dropped on first|
| `<!--#readlink-->` `{{item_url}}` / `{{item_title}}` | the "Read:" citation (omitted if no URL) |
| `{{footer_note}}` | the subscription footer line |
| `<!--#unsubscribe-->` `{{unsubscribe_url}}` / `{{symbol}}` | unsubscribe link (omitted if no URL) |

The section names come from [`src/utils/sections.py`](../src/utils/sections.py); the one matching
`QUICK_HITS` renders in the highlighted panel.
Loading
Loading