An open API service indexing awesome lists of open source software.

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.

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.

[![Version](https://img.shields.io/badge/version-1.6.12-blue?style=flat-square)](CHANGELOG.md)
[![Python](https://img.shields.io/badge/python-3.11-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org)
[![OpenAI](https://img.shields.io/badge/OpenAI-API-412991?style=flat-square&logo=openai&logoColor=white)](https://platform.openai.com)
[![License](https://img.shields.io/badge/license-MIT-lightgrey?style=flat-square)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/saurabhdave/ios-dev-ai-writer/weekly.yml?style=flat-square&label=2x%2Fweek%20pipeline)](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).