# Show City Briefing Agent

Daily pipeline that discovers Max Amini shows exactly **7 days out**, gathers
city intelligence and local news, fact-checks with GPT-4o, generates 20
crowd-work ideas, ranks them to a top-10 with Claude, and publishes a
mobile-first HTML briefing locally at `http://localhost:8085/brief/<slug>`.

**Phase 12 scope:** briefing generation + publishing only. **Email / Brevo is
deferred to a later phase.** See `AGENT.md` for the scope guardrail.

## Pipeline

```
n8n (09:00 America/Los_Angeles, daily)
  └─► POST /api/v1/brief/run ─► http://city-briefing-agent:8000
        └─► FastAPI orchestrator (Python)

        Hub DB API (planner.maxify.it clean mirror, SccClient)
          ↓
        Shows whose show_date == today + 7 days
          ↓
        For each show (asyncio.gather, isolated):
          check_existing_brief → previous_show_lookup →
          historical_sales_lookup → voice_profile_load →
          youtube_transcripts (Max Amini's recent uploads + transcripts) →
          perplexity_city → perplexity_news (also extracts competitor_warnings) →
          openai_fact_check (GPT-4o removes unverifiable / high-risk claims) →
          openai_generation (20 ideas, grounded against recent transcripts) →
          claude_ranking (top 10) → html_generation →
           local_web_deployment → url_verification → og_verification →
          mark_complete (status = 'published')
```

A `published` brief means: data generated, HTML created, HTML served by the
local `briefs-web` container, the local URL returned HTTP 200, and OG metadata
verified (title, description, image, url present; OG image itself publicly
accessible).

## Architecture

```
show-city-briefing-agent/
├── app/
│   ├── main.py                  FastAPI app, lifespan opens DB pool + migrate()
│   ├── config.py                Pydantic Settings, secrets mandatory, no defaults
│   ├── logging.py               SecretRedactingFormatter (regex-based)
│   ├── db.py                    psycopg AsyncConnectionPool + checksum migrations
│   ├── models.py                Pydantic: Show, PastShow, VoiceProfile, ...
│   ├── repositories.py          BriefRepo + RunRepo over city_briefs / city_briefs_runs
│   ├── api/routes_brief.py      POST /api/v1/brief/run  |  GET /api/v1/brief/runs/{id}
│   ├── pipeline/
│   │   ├── orchestrator.py      OrchestrateBriefsService (the whole flowchart)
│   │   ├── stages.py            one async function per flowchart box
│   │   └── retry.py             async retry w/ exponential backoff, stage-aware
│   ├── providers/
│   │   ├── hub.py               SccClient fork (read-only Planner mirror)
│   │   ├── perplexity.py        sonar-pro, semaphore-bounded, extracts competitor_warnings here
│   │   ├── openai_client.py     GPT-4o fact-check AND 20-idea generation
│   │   ├── anthropic_client.py  Claude ranking
│   │   └── youtube.py           Data API v3 list + youtube-transcript-api transcripts
│   ├── voice/loader.py          reads voice_profiles/<comedian>.json
│   ├── html/
│   │   ├── generator.py         Jinja2 render + inline-fallback
│   │   ├── deploy.py            writes <slug>.html to bind-mounted BRIEF_OUTPUT_DIR
│   │   └── verify.py            HTTP 200 + OG metadata + OG image accessible
│   └── templates/brief.html.j2 mobile-first HTML w/ the canonical OG tags
├── voice_profiles/max-amini.json   baseline comedian voice profile
├── migrations/001_initial.sql      city_briefs + city_briefs_runs tables
├── integrations/n8n/               version-controlled workflow JSON + README
├── imports/                        bind-mounted working copy for "Import from File"
├── docker/Dockerfile              python:3.12-slim base
├── docker/nginx-local.conf         local static brief server
└── docker-compose.yml              agent + briefs-db + briefs-web
```

## Secrets

All credentials live in the local FastAPI `.env` (git-ignored). The
`.env.example` is the only env file tracked. Mandatory secrets:

| Env var | Purpose |
|---|---|
| `BRIEF_SERVICE_API_KEYS` | Bearer tokens accepted by `POST /api/v1/brief/run` (one is reused by n8n's `City Briefing API Key` headerAuth credential) |
| `DATABASE_URL` | `city_briefs_ops` Postgres |
| `SCC_API_KEY` | Read access to `planner.maxify.it` clean Hub mirror |
| `PERPLEXITY_API_KEY` | Perplexity Sonar-Pro (city intel + local news) |
| `OPENAI_API_KEY` | GPT-4o fact-check AND 20-idea generation |
| `ANTHROPIC_API_KEY` | Claude Sonnet 4.6 ranking to Top 10 |
| `YOUTUBE_API_KEY` | YouTube Data API v3 (list recent uploads from Max Amini's channel) |

## Local dev

```bash
cp .env.example .env        # then fill in real secrets
docker compose up -d --build
curl http://localhost:8002/health            # expect {"status":"ok","version":"12.0.0"}
curl -I http://localhost:8085/brief/og-cover.jpg
```

Manual trigger (without n8n):

```bash
curl -X POST http://localhost:8002/api/v1/brief/run \
  -H "Authorization: Bearer $BRIEF_SERVICE_API_KEYS" \
  -H "Content-Type: application/json" \
  -d '{"trigger":"manual"}'
```

Polling a run:

```bash
curl http://localhost:8002/api/v1/brief/runs/$BRIEF_RUN_ID \
  -H "Authorization: Bearer $BRIEF_SERVICE_API_KEYS"
```

The local n8n editor is at `http://localhost:5678`; the workflow is already
imported, credentialed, and active in that instance.

## Acceptance criteria — Phase 12

- [x] Architecture defined; scaffold implemented (this repo)
- [x] Local n8n runs daily at 09:00 Los Angeles time
- [ ] Shows exactly 7 days out are discovered
- [ ] Multiple shows can be processed independently
- [ ] Duplicate briefs are prevented (Python gate + `UNIQUE(show_id)`)
- [ ] Previous show is correctly identified (most recent past show in same city)
- [ ] Historical ticket sales are retrieved
- [ ] Existing voice profile is loaded
- [ ] YouTube recent uploads + transcripts are extracted (grounds idea-gen)
- [ ] City intelligence is retrieved from Perplexity
- [ ] Local news since the previous show is retrieved
- [ ] GPT-4o removes unverifiable / high-risk claims (fact-check stage)
- [ ] GPT-4o generates 20 city-specific ideas (avoiding Max's recent bits)
- [ ] Claude filters / ranks the ideas to Top 10
- [ ] Competitor coverage can be flagged (by the Perplexity news call)
- [ ] HTML brief is generated
- [ ] HTML is mobile-first
- [x] HTML is served by the local `briefs-web` container
- [x] Local public URL works at `http://localhost:8085/brief/<slug>`
- [x] URL returns HTTP 200 (local smoke test)
- [x] Open Graph metadata exists (local smoke test)
- [x] OG image is publicly accessible (local smoke test)
- [x] Brief can be opened without authentication locally
- [ ] Database records the generation and publication status
- [ ] Failed stages are logged
- [ ] Retryable errors are retried (3× exponential backoff)
- [ ] One failed show does not stop other shows (asyncio.gather isolation)
- [x] No API secrets are exposed (Pydantic Settings + SecretRedactingFormatter)

## Deferred production work

VPS deployment, `just2done.com` DNS/TLS, and production n8n are deferred while
the system runs locally. The current source of truth is the local n8n instance,
the local Postgres database, and `http://localhost:8085/brief/<slug>`.

## Explicitly NOT in this phase

Email, Brevo, transactional templates, sender config, delivery status, retry
logic, credentials, n8n email nodes, email failure handling. Parked for a
future Phase 13 once the briefing-generation + publishing pipeline is stable.
