https://github.com/saurabhdave/ios-dev-ai-writer
AI agent that auto-generates weekly Medium-style iOS articles & linkedin post from real trend signals.
https://github.com/saurabhdave/ios-dev-ai-writer
agent ai ai-agents apple content-generation content-generation-ai github-actions ios openai python swift
Last synced: 4 months ago
JSON representation
AI agent that auto-generates weekly Medium-style iOS articles & linkedin post from real trend signals.
- Host: GitHub
- URL: https://github.com/saurabhdave/ios-dev-ai-writer
- Owner: saurabhdave
- License: mit
- Created: 2026-03-06T19:47:04.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-03-27T05:21:28.000Z (4 months ago)
- Last Synced: 2026-03-27T17:06:48.079Z (4 months ago)
- Topics: agent, ai, ai-agents, apple, content-generation, content-generation-ai, github-actions, ios, openai, python, swift
- Language: Python
- Homepage: https://saurabhdave.github.io/ios-ai-articles/
- Size: 699 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# ios-dev-ai-writer
> Automated Apple-platform engineering articles, Swift code examples, LinkedIn posts, and a developer newsletter — generated by a multi-stage LLM pipeline and published on a schedule.
[](CHANGELOG.md)
[](https://www.python.org)
[](https://platform.openai.com)
[](LICENSE)
[](https://github.com/saurabhdave/ios-dev-ai-writer/actions)
---
## What It Does
Twice a week, a GitHub Actions job scans iOS/Swift trend sources, picks a topic, and runs it through a multi-stage LLM pipeline that produces a full Medium-style article — complete with a Swift code example, a LinkedIn post, and a developer newsletter issue. Everything is committed and published automatically.
**Published output lives at [saurabhdave/ios-ai-articles](https://github.com/saurabhdave/ios-ai-articles).**
---
## Pipeline
```
Trend Scanner (8 sources)
│
▼
Topic Agent ──► dedup check (embedding similarity + theme cluster guard)
│
▼
Outline Agent
│
▼
Article Agent (~900–1,200 words, 5-section Medium structure)
│
▼
Editor Pass ──► Voice Pass ──► Factual Grounding ──► Layout Repair loop
│
├──► Code Agent (Swift 6.2.4, snippet validation + repair loop)
├──► LinkedIn Agent (senior-voice post, claim guardrails)
├──► Newsletter Agent (6-section SwiftTribune-style, Markdown + HTML)
└──► Review Agent (quality scores → repair trigger if below threshold)
│
▼
Auto-commit to ios-ai-articles
```
---
## Architecture
```mermaid
flowchart TD
A[GitHub Actions · Mon & Thu 10:00 UTC] --> B[main.py]
B --> C[weekly_pipeline.py]
C --> S[trend_scanner.py]
S --> S1[HackerNews]
S --> S2[Reddit r/iOSProgramming]
S --> S3[Apple Docs & News]
S --> S4[WWDC Feed]
S --> S5[Viral iOS / Social]
S --> S6[Custom JSON sources]
S --> S7[Web Search — top iOS topics]
C --> D[topic_agent]
C --> E[outline_agent]
C --> F[article_agent]
C --> G[editor_agent · polish]
G --> VP[editor_agent · voice pass]
VP --> FG[editor_agent · factual grounding]
FG --> LR[editor_agent · layout repair]
C --> H[code_agent]
C --> L[linkedin_agent]
C --> NL[newsletter_agent]
C --> R[review_agent]
D & E & F & G & VP & FG & LR & H & L & NL & R --> API[OpenAI API]
LR --> J[outputs/articles/]
C --> K[outputs/trends/]
C --> M[outputs/linkedin/]
C --> N[outputs/codegen/]
NL --> NW[outputs/newsletter/ · .md + .html]
R --> O[outputs/quality_history.json]
J & M & N & NW --> P[saurabhdave/ios-ai-articles]
```
---
## Features
### Content Quality
| Feature | Detail |
|---|---|
| **Editor pass** | Polish for clarity, tone, and Medium readability |
| **Voice pass** | Strips AI writing patterns — "Choose X/Z" constructs, hedge phrases, passive recommendations, vague claims |
| **Factual grounding** | Conservative rewrite pass to reduce hallucinated claims |
| **Layout repair loop** | Iteratively scores article against a 14-point Medium rubric and repairs until score ≥ threshold |
| **Deterministic repair** | Post-process fixes malformed backticks and strips `Operational note:` template artifacts before publication |
| **Self-review** | LLM scores each article on overall quality, technical depth, and actionability |
| **Review-triggered repair** | Re-runs editor pass when review score falls below threshold |
### Topic Discovery
| Feature | Detail |
|---|---|
| **8 trend sources** | HackerNews, Reddit, Apple Docs, WWDC, viral/social, platforms, custom JSON, web search |
| **Embedding dedup** | `text-embedding-3-small` cosine similarity catches near-duplicate topics with low lexical overlap |
| **Theme cluster guard** | Hard limit on same-cluster articles (Swift concurrency, UIKit migration, SwiftUI profiling) per rolling window |
| **Apple-platform only** | Topics filtered to iOS/Swift/SwiftUI/Xcode — AI-first subjects explicitly excluded |
### Code Generation
| Feature | Detail |
|---|---|
| **Swift 6 first** | `@Observable` preferred; targets Swift 6.2.4, compiler mode 6 by default |
| **Validation modes** | `snippet` (syntax + placeholder check), `compile` (strict typecheck), `none` |
| **Repair loop** | Up to N attempts to fix failing snippets before falling back to `omit` or `error` |
| **Codegen metadata** | Per-run JSON with path (`direct`/`repaired`/`omitted`) and repair attempt count |
### Output & Automation
| Feature | Detail |
|---|---|
| **LinkedIn post** | Senior-voice post with code snippet (auto/always/never) and claim guardrails |
| **Newsletter** | 6-section SwiftTribune issue — Markdown + email-safe inline-styled HTML, auto-incrementing issue number |
| **Quality history** | `outputs/quality_history.json` accumulates layout scores, review scores, and repair counts across all runs |
| **Structured logging** | JSON log lines with agent name, token usage, step timing, and status — readable in GitHub Actions |
| **URL safety** | Unverified URLs stripped from article body before publication |
| **Reference quality check** | Homepage-level reference URLs (e.g. `developer.apple.com/documentation/swiftui`) flagged with a `WARNING` log event; prompt enforces specific page links, SE proposal citations, and WWDC session citations |
| **Author context injection** | Articles grounded in your real production experience via `scanners/author_context.json` — failure modes, device-specific observations, and migration gotchas injected per topic family |
---
## Project Structure
```
ios-dev-ai-writer/
├── agents/
│ ├── topic_agent.py # topic selection + dedup
│ ├── outline_agent.py
│ ├── article_agent.py
│ ├── editor_agent.py # polish → voice → factual grounding → layout repair
│ ├── code_agent.py # Swift codegen + validation loop
│ ├── linkedin_agent.py
│ ├── newsletter_agent.py
│ └── review_agent.py
├── scanners/
│ ├── trend_scanner.py # 8-source trend aggregation → TrendSignal
│ ├── custom_trends.json
│ └── author_context.json # your real-world experience bullets per topic family
├── workflows/
│ └── weekly_pipeline.py # main orchestrator
├── prompts/ # all LLM prompt templates
├── utils/
│ ├── article_repair.py # deterministic post-processing for article cleanup
│ ├── observability.py # structured JSON logging
│ └── openai_logging.py # OpenAI client + token tracking
├── tests/
│ └── test_openai_config.py # focused compatibility tests for OpenAI config helpers
├── outputs/ # gitignored — published to ios-ai-articles
├── .github/workflows/
│ ├── weekly.yml # scheduled pipeline
│ └── release.yml # GitHub Release on v* tag
├── config.py # all env-var configuration
└── main.py
```
---
## Setup
**Requirements:** Python 3.11, an OpenAI API key.
```bash
# 1. Clone and install
git clone https://github.com/saurabhdave/ios-dev-ai-writer.git
cd ios-dev-ai-writer
pip install -e .
# 2. Configure
cp .env.example .env
# Set OPENAI_API_KEY at minimum
# 3. Run
python main.py
```
---
## Personalising the Output
The pipeline can ground articles in your real iOS engineering experience via `scanners/author_context.json`. When the generated topic matches a known family (e.g. concurrency, SwiftUI rendering), those bullets are injected into the article prompt so the output references your specific failure modes and production observations — not a generic senior-engineer persona.
### How to fill it in
Open `scanners/author_context.json` and add 2–3 first-person bullet points per topic family you have experience with:
```json
{
"concurrency": [
"Migrated a URLSession-heavy networking layer to async/await — cancellation semantics were the main gotcha",
"Hit data races in a shared cache when moving to Swift 6 strict concurrency"
],
"swiftui_rendering": [
"Profiled a feed view re-rendering 3x per scroll tick — root cause was @Observable held by multiple ancestor views"
],
"architecture": [],
"testing": [],
"performance": [],
"migration": []
}
```
**Guidelines:**
- Write in first person and be specific — device names, API names, failure conditions
- Include at least one failure mode per family ("This broke when…", "We hit this under…")
- Leave a family as `[]` if you have no direct experience — the prompt falls back to a generic senior-engineer voice
- The `_instructions` key is ignored by the pipeline; it's a reminder for you
**Topic families and their keywords:**
| Family | Matched when topic contains |
|---|---|
| `concurrency` | async, await, actor, task, concurrency, combine |
| `swiftui_rendering` | render, swiftui, instruments, profil, body, view |
| `architecture` | architecture, pattern, design, injection, dependency |
| `testing` | test, xctest, mock, stub, tdd |
| `performance` | performance, memory, cpu, battery, optimis |
| `migration` | migrat, urlsession, uikit, legacy, deprecated |
---
## Configuration
All settings are driven by environment variables. Set them in `.env` or export directly.
### Core
| Variable | Default | Description |
|---|---|---|
| `OPENAI_API_KEY` | — | **Required** |
| `OPENAI_MODEL` | `gpt-5-mini` | Model for all pipeline stages |
| `OPENAI_TEMPERATURE` | `0.7` | Global temperature cap |
| `OPENAI_REASONING_EFFORT` | model-aware | `none`\|`minimal`\|`low`\|`medium`\|`high`\|`xhigh` — used for GPT-5 and o-series models. `gpt-5.1` defaults to `none`; `temperature` is only sent for `gpt-5.1` when reasoning is `none`. |
### Content Quality
| Variable | Default | Description |
|---|---|---|
| `EDITOR_PASS_ENABLED` | `true` | Polish for clarity, tone, readability |
| `VOICE_PASS_ENABLED` | `true` | Remove AI writing fingerprints from prose |
| `FACT_GROUNDING_ENABLED` | `true` | Conservative factual rewrite pass |
| `FACT_GROUNDING_MAX_PASSES` | `1` | Max factual grounding iterations |
| `MEDIUM_LAYOUT_REINFORCEMENT_ENABLED` | `true` | Iterative layout repair loop |
| `MEDIUM_LAYOUT_MAX_REPAIR_PASSES` | `2` | Max layout repair iterations |
| `MEDIUM_LAYOUT_MIN_SCORE` | `8` | Minimum passing layout score (out of 14) |
| `SELF_REVIEW_ENABLED` | `true` | LLM self-review scoring pass |
| `REVIEW_REPAIR_ENABLED` | `true` | Re-run editor pass if review score is low |
| `REVIEW_REPAIR_MIN_SCORE` | `7` | Score threshold that triggers repair |
### Trend Discovery
| Variable | Default | Description |
|---|---|---|
| `TREND_DISCOVERY_ENABLED` | `true` | Enable multi-source trend scanning |
| `TREND_SOURCES` | *(all 8)* | Comma-separated: `hackernews,reddit,apple,wwdc,viral,social,platforms,custom,websearch` |
| `TREND_MAX_ITEMS_PER_SOURCE` | `10` | Items fetched per source |
| `TREND_HTTP_TIMEOUT_SECONDS` | `12` | HTTP timeout for trend fetches |
| `REDDIT_USER_AGENT` | `ios-dev-ai-writer/1.0 (weekly trend scanner)` | User-agent string for Reddit RSS requests |
| `CUSTOM_TRENDS_FILE` | `scanners/custom_trends.json` | Path to custom trends JSON |
| `TOPIC_INTERESTS` | *(19 topics — see config.py)* | Comma-separated list of preferred topic areas fed to the topic agent |
| `TOPIC_SIMILARITY_THRESHOLD` | `0.72` | Cosine similarity threshold above which a candidate topic is rejected as a semantic near-duplicate |
### Code Generation
| Variable | Default | Description |
|---|---|---|
| `SWIFT_LANGUAGE_VERSION` | `6.2.4` | Target Swift release |
| `SWIFT_COMPILER_LANGUAGE_MODE` | `6` | Maps to `swiftc -swift-version` |
| `CODEGEN_VALIDATION_MODE` | `snippet` | `snippet`\|`compile`\|`none` |
| `CODEGEN_FAILURE_MODE` | `omit` | `omit` (publish without code) \| `error` (fail pipeline) |
### Output
| Variable | Default | Description |
|---|---|---|
| `LINKEDIN_POST_ENABLED` | `true` | Generate LinkedIn post |
| `LINKEDIN_CODE_SNIPPET_MODE` | `auto` | `auto`\|`always`\|`never` |
| `NEWSLETTER_ENABLED` | `true` | Generate newsletter issue |
| `NEWSLETTER_NAME` | `iOS Dev Weekly` | Newsletter display name |
| `OUTPUT_QUALITY_HISTORY_PATH` | `outputs/quality_history.json` | Append-only quality record |
| `CROSS_REPO_DEDUP_ENABLED` | `true` | Fetch published titles from the output repo via GitHub API to guard against state drift |
| `PUBLISHED_REPO_API_URL` | *(ios-ai-articles articles API)* | GitHub Contents API URL used for cross-repo dedup |
---
## Output Artifacts
| Artifact | Location |
|---|---|
| Article (Markdown) | `outputs/articles/YYYY-MM-DD-{slug}.md` |
| Trend snapshot | `outputs/trends/{timestamp}-trend-signals.json` |
| LinkedIn post | `outputs/linkedin/YYYY-MM-DD-{slug}-linkedin.md` |
| Code metadata | `outputs/codegen/YYYY-MM-DD-{slug}-codegen.json` |
| Newsletter (Markdown) | `outputs/newsletter/YYYY-MM-DD-issue-N.md` |
| Newsletter (HTML) | `outputs/newsletter/YYYY-MM-DD-issue-N.html` |
| Quality history | `outputs/quality_history.json` |
| Run summary (CI only) | `outputs/run_summary.json` — ephemeral, not committed or published |
Outputs are gitignored locally and auto-published to [saurabhdave/ios-ai-articles](https://github.com/saurabhdave/ios-ai-articles) on every CI run. `run_summary.json` is the exception — it is consumed by the "Write run summary" CI step to populate the GitHub Actions step summary and is never committed.
---
## GitHub Actions
**Schedule:** Monday and Thursday at 10:00 UTC (`0 10 * * 1,4`).
**Secrets required:**
| Secret | Purpose |
|---|---|
| `OPENAI_API_KEY` | LLM API access |
| `DEPLOY_TOKEN` | GitHub PAT with `contents: write` on `saurabhdave/ios-ai-articles` |
**Pipeline steps:** checkout → install deps → `python main.py` → write run summary to GitHub Actions step summary → commit quality history and newsletter counter back to this repo → publish outputs to content repo (skipped if state commit fails).
A GitHub Release is created automatically when a `v*` tag is pushed (see `.github/workflows/release.yml`).
---
## Versioning
Follows [Semantic Versioning](https://semver.org). Current version: **1.6.12** — see [CHANGELOG.md](CHANGELOG.md).
To cut a release:
```bash
# 1. Update VERSION, CHANGELOG.md, and pyproject.toml
# 2. Commit and tag
git tag vX.Y.Z
git push origin vX.Y.Z
# GitHub Actions creates the release automatically
```
---
## License
MIT — see [LICENSE](LICENSE).