← all conversations

Blog post generator workflow

2025-10-077 turns36,604 charsgpt-51 fork(s)
agentic-workflowblog-post-generationlocal-ollama

Summary

The user wants to build a Python-based agentic workflow using Ollama to automatically generate .md blog post files locally.

Messages

⑂ thread forked here
I want to create an agent workflow which will take https://github.com/kliewerdaniel/rez02.git as a starting point and create an agentic workflow which will generate .md files which will be blog posts for the site, so what I want is for it to create everything for the blog post and have it run on a manual trigger at first but later I want to use cron or something to automate
Perfect — that’s a great use case for OpenAI’s **Agent Builder workflow JSON** setup: a **blog post generator agent** that runs on demand (manual trigger) and later supports **scheduled (cron) automation**. Let’s first break down the design so we can generate the JSON for the OpenAI Agent Builder. --- ## 🧠 Step 1: Define the Goal **Goal:** Automate blog post generation for the Hugo/Jekyll site in [`rez02`](https://github.com/kliewerdaniel/rez02.git). **Action:** Generate Markdown (`.md`) blog posts in `/content/posts/` (or `/blog/` depending on rez02’s structure). **Inputs:** - Topic or keyword (manual or later automated prompt) - Writing style or persona (optional, e.g. “KonradFreeman style”) - Target category or tags **Outputs:** - `.md` file with proper frontmatter and content - Optional image links or references --- ## ⚙️ Step 2: Define the Workflow Phases We’ll create an **agentic workflow** that does the following steps: 1. **Clone & Setup (Initialization)** - Pull or verify the local clone of `rez02` repo. - Ensure there’s a `/content/posts` directory. - Prepare metadata templates. 2. **Topic Generation (Optional Step)** - Generate a trending topic list or select a topic manually. - Choose one or more topics for post creation. 3. **Draft Generation** - Generate Markdown content (with frontmatter) for the chosen topic. - Use stylistic/persona guidance if provided. 4. **File Creation** - Save `.md` file into `/content/posts/`. - Format filename using `slugify(title)` and `date`. 5. **(Optional) Git Automation** - Commit and push changes to GitHub for deployment (Netlify trigger). - Controlled by a flag: `"push_to_repo": true/false`. 6. **Manual Trigger Now → Scheduled Trigger Later** - Start with manual execution. - Later, schedule with cron or GitHub Actions to auto-run weekly or daily. --- ## 🧩 Step 3: Define the Agent Components ### Agent Name: `rez02_blog_agent` ### Description: Generates Markdown blog posts for the rez02 Hugo/Jekyll site and saves them to the content directory. ### Tools (if you’re using OpenAI Agent Builder): - `code_inter
gpt-5
That’s a cool starting point — the **rez02** repo is a Next.js / MDX-based blog + portfolio site (uses `content/blog/` for markdown/MD files) citeturn0view0 Here’s a proposed plan + architecture for building an *agentic workflow* that (initially on manual trigger, later via schedule) will: - generate a new blog post as a `.md` or `.mdx` file - commit / push it (or at least stage it) into the repo’s `content/blog/` - optionally trigger a build / deployment - manage metadata, slug, frontmatter, consistency, etc. I’ll break it into phases, tools, and a sample pipeline. --- ## 1. High-level design Here’s how I imagine the flow: 1. **Trigger** (manual or scheduled) 2. **Prompt / Input** (maybe some high-level topic, title, or theme) 3. **Agent (LLM + prompt template + logic)** - generate the blog text (with structure, headings, body) - generate frontmatter (title, date, tags, excerpt, slug) - generate any images if needed (or image URLs) - produce the `.md` content 4. **Filesystem / Git** - write the `.md` into `content/blog/` with correct filename (slug + date) - stage / commit / push (or open a PR) 5. **Build / Deploy** (if desired) - e.g. via Netlify, Vercel, or via CI/CD 6. **Monitoring / Logging / fallback / human review** Over time, you can schedule this via cron or GitHub Actions or external scheduler. The “agentic” part means the agent is smart and can decide e.g. “choose a topic from my topic list”, “choose 3 tags”, etc. --- ## 2. Choice of orchestration & tools You have many options here; depends on your familiarity. A few approaches: | Tool / Platform | Pros | Cons / Considerations | |---|---|---| | **GitHub Actions** | easy to integrate with your repo, you can have a manual workflow dispatch + scheduled cron jobs, and steps to run your agent + commit | you’ll have to make your agent code action-friendly | | **Local script + cron** | more control, you can run a Python / Node script locally or on a small VM | need a host that’s always up; managing secrets (GitHub token) etc | | **Serverless / cloud function** | you can host an “agent endpoint” that is triggered, does the work, updates repo | might need to manage cold start, logs, costs | | **Agent orchestration frameworks** (LangChain, AutoGPT variants, etc) | higher-level control, agents, memory, chaining | possibly more complexity for integration with Git, commit workflows, reliability | Because your repo is already on GitHub and supports builds via Netlify (or deploy pipeline), GitHub Actions is a very natural choice. You can start with a manual dispatch workflow, then add a `schedule` trigger. --- ## 3. Detailed sample architecture & implementation sketch Here’s a step-by-step sample of how you might build this (say, using Node.js + OpenAI as the LLM backend). You can adapt to Python or whichever you prefer. ### a. Create an "agent" module / script A file like `scripts/generate-post.js` (or `.ts`) in your repo: ```ts import fs from 'fs/promises'; import path from 'path'; import { OpenAI } from "openai"; // or whichever client import slugify from "slugify"; // config / constants const BLOG_DIR = path.join(process.cwd(), "content", "blog"); const MODEL = "gpt-4"; // or whichever const DEFAULT_TAGS = ["tech", "programming", "webdev"]; // helper: sample prompt templates function buildPrompt(topic: string | null) { return ` You are a blog post generator. Write a blog post in Markdown, with frontmatter (YAML) at the top. Include title, date (YYYY-MM-DD), tags (list), description/excerpt, slug. If a topic is given, use it; else choose a topic relevant to web dev / AI / programming. The body should have 3–5 sections with headings, and around 600–1,200 words. Use clean markdown (no HTML). Also return recommended images (just URLs or image names) if relevant. `; } // run the agent async function run(topic?: string) { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY!, }); const prompt = buildPrompt(topic ?? null); const resp = await openai.chat.completions.create({ model: MODEL, messages: [ { role: "system", content: "You are a helpful assistant who writes blog posts." }, { role: "user", content: prompt } ], }); const content = resp.choices[0].message.content; // You must **parse** the output: split frontmatter vs body, extract slug, date, etc. // naive splitting: const match = content.match(/(?s)^---\n([\s\S]+?)\n---\n([\s\S]+)/); if (!match) { throw new Error("Could not parse output for frontmatter."); } const frontmatter = match[1]; const body = match[2]; // parse frontmatter lines for slug, date let slug: string | undefined = undefined; let date: string | undefined = undefined; for (const line of frontmatter.split("\n")) { if (line.startsWith("slug:")) slug = line.replace("slug:", "").trim(); if (line.startsWith("date:")) date = line.replace("date:", "").trim().replace(/['"]/g, ""); } if (!slug) { // fallback: slugify the title or topic + timestamp // (you might also parse out `title:` etc.) throw new Error("No slug in frontmatter"); } if (!date) { date = new Date().toISOString().slice(0, 10); } // filename const filename = `${date}-${slug}.md`; const filepath = path.join(BLOG_DIR, filename); const fileContents = `---\n${frontmatter}\n---\n\n${body}`; await fs.writeFile(filepath, fileContents, "utf-8"); console.log("Wrote file:", filepath); // Optionally: commit + push (or return the filepath) return { filepath, filename }; } // If invoked directly if (require.main === module) { const topic = process.argv[2]; run(topic).then(() => { console.log("Done"); }).catch(err => { console.error("Error:", err); process.exit(1); }); } ``` You’ll want to harden this: better parsing (YAML frontmatter parser), error handling, slug collisions, tag generation, image downloads or hosting, etc. ### b. Git commit / push logic Inside your script (after writing), you could: - Use a Git library (e.g. `simple-git` in Node) to `git add` the new file, `git commit -m "chore: add post <slug>"`, and `git push origin main` (or open a PR branch). - Or leave that to the CI workflow (e.g. let GitHub Actions pick up the new file via another step). - You need a GitHub token with write permissions. ### c. GitHub Actions workflow In your repo, create `.github/workflows/generate-blog.yml`: ```yaml name: Generate Blog Post on: workflow_dispatch: inputs: topic: description: "Topic or prompt for the blog post" required: false type: string schedule: - cron: '0 3 * * *' # every day at 03:00 UTC, or whatever you like jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: "20" - name: Install dependencies run: npm ci - name: Run generate script env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | node scripts/generate-post.js "${{ github.event.inputs.topic || '' }}" - name: Commit & Push changes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add content/blog git commit -m "chore: automated blog post generation" git push env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # optionally protect against "no changes" errors continue-on-error: true ``` - The `workflow_dispatch` allows you to run it manually from GitHub UI. - The `schedule` allows periodic automated runs later. - You’ll need to add `OPENAI_API_KEY` and `GITHUB_TOKEN` to your repo’s secrets. - Be careful: you may want to guard against spam / accidental runs, or require review. ### d. Deployment / build trigger - Because your site build uses the repo content (MD files), pushing the new file should trigger your usual build / deploy (Netlify, Vercel, etc). - If you have a CI/CD pipeline already in place, it will rebuild with the new post. - Optionally, you can add extra steps (e.g. run `npm run build` to check compilation). ### e. Human-in-the-loop / review Initially, you may not want fully automatic posts. You might prefer: - The script just **writes a draft** to a folder like `content/blog/drafts/` and opens a PR for review. - Or have a “preview” mode: the agent outputs the markdown for inspection, then you manually merge / move. - Or a step that sends you a summary + link to review before commit. You can build that logic in — e.g. skip the commit step, open a GitHub PR via API, etc. --- ## 4. Key challenges & enhancements - **Prompt reliability / parsing**: The agent’s output must be reliably parseable (frontmatter vs body). Using templates + guardrails helps. - **Slug collisions / duplicates**: Ensure uniqueness, maybe check existing slugs. - **Tag / topic selection logic**: You can maintain a list of themes / topics and have the agent pick one or accept input. - **Images / media**: If your blog posts reference images, you’ll need logic to generate or fetch images, store them (e.g. in `public/` or remote storage), and reference them. - **Error handling / rollback**: If generation fails or build fails, you might want to revert commit or alert you. - **Cost & rate limits**: If using an LLM, be aware of token costs and limits. - **Quality / fact checking**: Especially if fully automated, the content may have factual mistakes — human review is useful. - **Chron jobs & scheduling**: You may want more flexible schedules (e.g. weekly, monthly) or rate limits (e.g. max 2 posts/week). - **Metadata / SEO / summaries**: You may want to generate additional fields (e.g. meta description, canonical URL, reading time). - **Internationalization / multiple languages**: If you want to post in multiple languages, that’s an extra layer. --- ## 5. Example end-to-end 1. You go to GitHub → Actions → *Generate Blog Post*, enter “Latest trends in LLM agent frameworks” as topic → run. 2. Action checks out, installs deps, runs your script with that topic. 3. Script calls OpenAI, generates frontmatter + body, writes `content/blog/2025-10-07-latest-trends-in-llm-agent-frameworks.md`. 4. Action commits & pushes the file. 5. The site’s build pipeline detects change, rebuilds, deploys. 6. Blog post appears under `/blog/latest-trends-in-llm-agent-frameworks`. Later, that same workflow runs automatically at 3:00 UTC daily (or whatever cron schedule), generating new posts without manual trigger. --- ## 6. Next steps / what you should do now - Decide your **agent backend** (OpenAI, Anthropic, local LLM, etc). - Build (or adapt) the generation script (Node / Python) to produce well-formatted markdown + frontmatter. - Add parsing & error resilience. - Add Git commit logic (or make it return the files for manual commit initially). - Configure GitHub Action (manual dispatch + scheduled). - Test manually, check output, maybe review before enabling schedule. - Monitor for content quality, adjust prompt, improve with feedback. If you like, I can help you scaffold a full working repository (scripts + GitHub Actions) tailored to your **rez02** setup. Do you want me to generate that for you (i.e. a starter commit)?
gpt-5
I want to do it all locally, I use ollama and then just whichever model like gpt-oss the smaller parameter one I have installed locally already, let us start with creating the agentic workflow which will generate the content first so we can nail down that part first and then worry about the rest of it later. So let us start with a CLIne prompt to start from the repo rez02.git first and start from there to create this completely local agentic workflow
Cool — doing it all locally first is a good idea. Let’s design a **local, CLI-driven “agent” workflow** that: - Clones or bootstraps from the `rez02` repo (or you run it inside an existing checkout) - Invokes an LLM locally (via Ollama + your installed model) - Generates a `.md` or `.mdx` file (with frontmatter + body) under `content/blog/` (or wherever `rez02` expects) - (At first) leaves it uncommitted / for you to review - Has a CLI command interface (e.g. `generate-post`, maybe with a prompt/topic argument) - Is modular so you can later hook up git push, scheduling, etc Let me sketch the design + a starter template for that local agent. We’ll assume: - You have Ollama installed and a local model (say `gpt-oss` or some model name) accessible via a CLI or API - The `rez02` project is on your machine (you `cd rez02`) - The blog content directory is `content/blog/` (or adapt to whatever `rez02` uses) --- ## 1. Project structure & dependencies In your `rez02` repo (or alongside), you’ll add a small CLI/agent tool. Something like: ``` rez02/ content/ blog/ … existing posts … agent/ index.js prompts.js utils/ parseFrontmatter.js slugify.js package.json … ``` In `package.json`, you can add a script like: ```json { "name": "rez02", "scripts": { "generate-post": "node agent/index.js" }, "dependencies": { "gray-matter": "^4.0.3", "slugify": "^1.6.6", // any other utility deps } } ``` So you can run: ```bash cd rez02 npm install npm run generate-post -- "Optional topic prompt" ``` --- ## 2. Interfacing with Ollama / your local LLM You need a function that, given a prompt (string), sends it to your local model (via Ollama) and returns its output. Depending on how Ollama is set up, it might provide a CLI command like: ```bash ollama run gpt-oss --prompt "…" ``` Or maybe an HTTP API endpoint. Let’s assume the CLI style for now. In `agent/llm.js`: ```js import { spawnSync } from "child_process"; export function queryLLM(prompt) { const model = process.env.AGENT_MODEL || "gpt-oss"; // run: ollama run [model] --prompt "<prompt>" const result = spawnSync("ollama", ["run", model, "--prompt", prompt], { encoding: "utf-8", }); if (result.error) { throw result.error; } if (result.status !== 0) { console.error("LLM error:", result.stderr); throw new Error("LLM process failed"); } return result.stdout; } ``` You can adapt this if Ollama uses a different CLI or API. --- ## 3. Prompt template & structure In `agent/prompts.js`: ```js export function buildPrompt(topic) { return ` You are a blog post writing agent. Generate a markdown file (or MDX) with YAML frontmatter and body. Frontmatter keys: title, date (YYYY-MM-DD), tags (list), excerpt/summary, slug. Date should default to today’s date. Slug should be URL-safe lowercase with hyphens. If “topic” is given, write about that; otherwise, choose a topic in web dev, AI, programming. Body: include 3–5 sections with headings, 600–1,200 words. Write in clear markdown (no HTML). Output exactly: \`\`\` --- <YAML frontmatter> --- <Markdown body> \`\`\` Do not include extra commentary outside the markdown block. `; } ``` You might iterate on this prompt to get more consistent formatting. --- ## 4. Frontmatter parsing, slug handling, and file writing In `agent/index.js`: ```js #!/usr/bin/env node import fs from "fs/promises"; import path from "path"; import matter from "gray-matter"; import slugify from "slugify"; import { queryLLM } from "./llm.js"; import { buildPrompt } from "./prompts.js"; async function run(topic) { const prompt = buildPrompt(topic); const output = queryLLM(prompt); // Expect the LLM returns something like: // --- // title: "My Title" // date: 2025-10-07 // tags: // - web // - AI // excerpt: "…" // slug: my-title // --- // // # Heading 1 // … // parse with gray-matter — but since the LLM may include the ``` fences, strip them let trimmed = output.trim(); // remove surrounding triple-backticks if present if (trimmed.startsWith("```")) { trimmed = trimmed.replace(/^```+/, "").replace(/```+$/, "").trim(); } const parsed = matter(trimmed); const { data: frontmatter, content: body } = parsed; let slug = frontmatter.slug; if (!slug) { // fallback: slugify title or topic if (frontmatter.title) { slug = slugify(frontmatter.title, { lower: true, strict: true }); } else { slug = slugify(topic || "post", { lower: true, strict: true }); } } let date = frontmatter.date; if (!date) { const d = new Date(); date = d.toISOString().slice(0, 10); } const filename = `${date}-${slug}.md`; const blogDir = path.join(process.cwd(), "content", "blog"); const filePath = path.join(blogDir, filename); const fileData = `---\n${matter.stringify("", frontmatter).trim()}\n---\n\n${body}`; await fs.writeFile(filePath, fileData, "utf-8"); console.log("Wrote:", filePath); console.log("Frontmatter:", frontmatter); } async function main() { const topic = process.argv[2]; try { await run(topic); } catch (err) { console.error("Error:", err); process.exit(1); } } main(); ``` You may need to tweak how `matter.stringify` is used (or manually reconstruct frontmatter as YAML), but this gives the scaffold. --- ## 5. Usage & testing 1. `cd rez02` 2. `npm run generate-post` → LLM chooses a topic and writes a new file 3. `npm run generate-post "Topic about XYZ"` → you specify the topic 4. Inspect `content/blog/` to see the file, review it manually Run it multiple times, iterate on prompt to improve consistency (especially frontmatter keys, formatting, avoiding extra commentary). --- ## 6. Next iterations & safety - You may want to **validate** that frontmatter has required fields (title, slug, date), else retry / error. - You may want to **avoid overwriting** existing files: check if `filePath` exists, and choose a different slug or bail. - You might want to generate in a **drafts** folder first (e.g. `content/blog/drafts/`) instead of publishing immediately. - You may want to add a **preview mode**: print the generated markdown to stdout instead of writing, for review. - You could add **logging**, retries, and error handling around the LLM call. - Later, you can add a commit/push wrapper locally (e.g. a `gitCommitAndPush` function using `child_process` or a Git library). --- If you like, I can generate a **fully working, ready-to-run local agent scaffold** (in Node or Python) tailored to `rez02`, with example prompt templates and instructions. Do you want me to produce that scaffold for you now?
gpt-5
take everything we have talked about and outline the workflow in complete detail I want to build it with python and ollama and it just output the .md files
Cool — let’s map out a **complete, detailed workflow / blueprint** for building a *local*, Python + Ollama-based agentic system whose job is to generate `.md` blog posts inside your `rez02` site repository. Once this is stable, you can layer on scheduling, git automation, etc. Here’s how I’d design it, step by step, including components, error handling, interfaces, and iterative enhancements. I’ll first give a high-level overview, then drill into each component and interactions, then a sample skeleton structure in Python, and finally caveats & next steps. --- ## High-Level Workflow Overview Here’s the end goal in terms of what you want the system to do: 1. You run a CLI command locally (e.g. `python agent/generate_post.py "Optional Topic Prompt"`) 2. That command runs the agent logic, which: - Builds a prompt (with templates, system + user instructions) - Invokes the local LLM via Ollama - Receives the response - Parses it into *frontmatter* + *body* - Validates the parsed output (ensure required fields, etc) - Chooses or computes a slug + date, resolves collisions - Writes a `.md` file in the correct directory (`content/blog/`) - Reports back success / path / metadata 3. You inspect the generated file, tweak if desired 4. (Later) you can add steps to commit / push / schedule / pipeline Between runs, you might want to maintain state (e.g. avoid duplicate topics, track which topics used, etc). So the **components** are: - CLI entrypoint - Prompt / template management - LLM invocation via Ollama (Python) - Parsing & frontmatter handling - Slug / filename / collision logic - File I/O (writing `.md`) - Validation & error handling - Optional future extensions (git, scheduling, reviews, memory, etc) --- ## Key Dependencies & Setup - **Ollama Python library**: There is an official Python client for Ollama that you can integrate directly. citeturn0search0turn0search11 - Or, if you prefer, you can shell out to Ollama CLI (e.g. `subprocess`) — but using the Python library gives cleaner error handling. - A YAML / frontmatter parsing library like `PyYAML` or `ruamel.yaml`, or a “gray-matter” style library for Python (many exist) - A slugification utility (e.g. `slugify`) - Standard Python modules (os, pathlib, datetime, etc) You’ll also need to ensure your Ollama model is installed / accessible locally (e.g. via `ollama pull <model>`). The Python library expects Ollama to be installed. citeturn0search0turn0search19 --- ## Detailed Workflow & Component Breakdown Below is a step-by-step logical flow, and for each step I’ll note edge cases / validations. ### 1. CLI Entrypoint & Argument Parsing - You have a Python script, say `agent/generate_post.py`. - It accepts (optionally) a topic string (or other parameters, e.g. tags, tone) as a CLI argument or via flags. - E.g. usage: ``` python agent/generate_post.py # auto topic python agent/generate_post.py --topic "Recent trends in AI agents" python agent/generate_post.py -t "X" ``` - Optionally you might support a “dry-run / preview” mode (print to stdout, don’t write) or “overwrite” flag, etc. - The script sets up configuration (model name, blog directory, etc) from constants or a config file. ### 2. Prompt & Template Construction - Based on whether a topic was supplied, select or generate a topic. - Build a prompt that instructs the model to output exactly a valid Markdown document with YAML frontmatter, e.g.: ``` You are a blog post generator. Output the entire blog post in Markdown format, with YAML frontmatter first (between `---`), then the body. Required frontmatter keys: title, date (YYYY-MM-DD), tags (list), excerpt, slug. You may include additional optional fields. Date should default to today’s date if not given. Slug should be lowercase, hyphenated, URL safe. Use 3–5 sections with headings, ~600–1,200 words. Do not include commentary outside the markdown. If a “topic” is given, write about that; otherwise pick a reasonable topic. ``` - You can refine the prompt further as you test to reduce parsing errors or undesired output. ### 3. Invoke LLM via Ollama (Python) - Use the `ollama` Python client to send a chat or completion request, e.g.: ```python from ollama import chat response = chat(model="your_model_name", messages=[{'role': 'user', 'content': prompt}]) content = response['message']['content'] ``` - Alternatively, use `ollama.generate` if you have a simpler “prompt → completion” interface. citeturn0search0 - Consider using streaming (if supported) so you can show progress or partial output. - Handle errors: timeouts, model not loaded, invalid responses, etc. ### 4. Parse & Normalize Output - After you receive the raw output string, you should **trim / strip** wrapper fences (e.g. if the model encloses with ```), either by regex or heuristics. - Use a frontmatter parser to split out the YAML metadata vs body. Many Python libraries parse frontmatter (e.g. `python-frontmatter`, `ruamel.yaml` + custom split, etc). - Extract from the metadata fields: at minimum `title`, `date`, `tags`, `excerpt`, `slug`. - If optional fields are missing, fill them (e.g. if `slug` missing, slugify `title`; if `date` missing, use today). - Validate: - `title` is nonempty - `slug` is URL-safe (only lowercase letters, digits, hyphens) - `date` parseable as `YYYY-MM-DD` - `tags` is a list (or convert comma string) - The body has some minimal length - If parsing fails or validations fail, you may want to: - Retry (resend prompt with corrections) - Abort with error and dump the raw output for manual inspection ### 5. Filename / Slug / Collision Logic - Compute the filename: typically `<date>-<slug>.md` - Use the `content/blog/` directory (or whatever your `rez02` expects) - If a file with that name already exists, you need collision resolution: - Append a suffix like `-1`, `-2`, etc - Or re-slugify with extra context - Or skip writing and error - Optionally maintain a record of used slugs / topics (in a JSON or state file) to avoid duplicates over time. ### 6. Write the Markdown File - Construct the final file content: ```text --- <YAML frontmatter> --- <body markdown text> ``` - If your frontmatter library doesn’t produce exactly the format you want, you can manually serialize (e.g. using `ruamel.yaml`) with desired order. - Write the file to disk in UTF-8. - Optionally set file permissions, or ensure correct EOL. - Optionally output a success message like: ``` ✅ Wrote new blog post: content/blog/2025-10-07-my-topic.md Title: "My Topic" Slug: my-topic ``` ### 7. Exit / Return Metadata - The CLI script should exit with status 0 on success, nonzero on failure. - Optionally, return (print) JSON with metadata (path, title, slug, tags) so that later tools (e.g. git automation) can consume it. --- ## Sample Python Project Skeleton (Directories & Files) Here’s a rough proposed structure: ``` rez02/ content/ blog/ (existing .md files) agent/ __init__.py generate_post.py prompt_templates.py llm_client.py parser.py utils.py config.py pyproject.toml or setup.py requirements.txt … (rest of the rez02 site) ``` ### Key files & their roles (sketches) #### agent/config.py ```python from pathlib import Path # Name of your local Ollama model MODEL_NAME = "gpt-oss" # or whatever you have # Directory in which to write blog posts (relative to project root) BLOG_DIR = Path(__file__).parent.parent / "content" / "blog" # Minimal body length threshold (in characters or words) MIN_BODY_LENGTH = 200 # Slugify options SLUGIFY_OPTIONS = {"lower": True, "separator": "-", "regex_pattern": r"[^\w\-]"} # e.g. slugify settings # (Optional) state file to record used topics or slugs STATE_FILE = Path(__file__).parent.parent / "agent_state.json" ``` #### agent/llm_client.py ```python from ollama import chat from agent.config import MODEL_NAME def ask_llm(prompt: str) -> str: resp = chat(model=MODEL_NAME, messages=[{"role": "user", "content": prompt}]) return resp["message"]["content"] ``` You might want to support streaming or retries. #### agent/prompt_templates.py ```python def build_post_prompt(topic: str | None) -> str: base = """ You are a blog post generation agent. Generate a markdown blog post with YAML frontmatter first (between `---`), then the body. Required frontmatter keys: title, date (YYYY-MM-DD), tags, excerpt, slug. Date should default to today if omitted. Slug should be lowercase, hyphenated, URL-safe. If a topic is provided, write about that; otherwise pick a topic in web dev / AI / programming. Make ~3–5 sections with headings. Target 600–1200 words. Use clear, well-structured markdown. Do not include commentary outside the markdown. Do not wrap in extra quotes or explanations. """ if topic: return base + f"\nTopic: {topic}\n" else: return base + "\nNo explicit topic was given; choose one.\n" ``` #### agent/parser.py ```python import re import frontmatter # using python-frontmatter from datetime import datetime from slugify import slugify from agent.config import SLUGIFY_OPTIONS, MIN_BODY_LENGTH def clean_output(raw: str) -> str: s = raw.strip() # Remove triple backticks fences at start/end if s.startswith("```"): s = re.sub(r"^```[\s\S]*?\n", "", s) if s.endswith("```"): s = re.sub(r"\n```$", "", s) return s.strip() def parse_post(markdown: str) -> dict: """ Returns dict with keys: - metadata: dict of frontmatter - body: str Raises on parse / validation error. """ cleaned = clean_output(markdown) post = frontmatter.loads(cleaned) meta = post.metadata body = post.content.strip() # Validate title title = meta.get("title") if not title or not isinstance(title, str): raise ValueError("Missing or invalid title in metadata") # Slug slug = meta.get("slug") if not slug: slug = slugify(title, **SLUGIFY_OPTIONS) # Date date = meta.get("date") if not date: date = datetime.utcnow().strftime("%Y-%m-%d") # Tags tags = meta.get("tags", []) if isinstance(tags, str): tags = [t.strip() for t in tags.split(",")] # Excerpt excerpt = meta.get("excerpt", "") # Validation of body length if len(body) < MIN_BODY_LENGTH: raise ValueError(f"Body too short: {len(body)} chars") return { "title": title, "slug": slug, "date": date, "tags": tags, "excerpt": excerpt, "body": body, } ``` #### agent/utils.py ```python from pathlib import Path from agent.config import BLOG_DIR def ensure_blog_dir(): BLOG_DIR.mkdir(parents=True, exist_ok=True) def compute_filepath(date: str, slug: str) -> Path: filename = f"{date}-{slug}.md" return BLOG_DIR / filename def resolve_collision(filepath: Path) -> Path: if not filepath.exists(): return filepath # if exists, try suffixes base = filepath.with_suffix("") # remove .md for i in range(1, 100): candidate = Path(f"{base}-{i}.md") if not candidate.exists(): return candidate raise FileExistsError("Too many collisions for file naming") ``` #### agent/generate_post.py (the main script) ```python #!/usr/bin/env python3 import sys from agent.llm_client import ask_llm from agent.prompt_templates import build_post_prompt from agent.parser import parse_post from agent.utils import ensure_blog_dir, compute_filepath, resolve_collision from agent.config import BLOG_DIR def main(): topic = None if len(sys.argv) > 1: topic = " ".join(sys.argv[1:]) prompt = build_post_prompt(topic) raw = ask_llm(prompt) try: parsed = parse_post(raw) except Exception as e: print("❌ Error parsing generated post:", e) print("Raw output:\n", raw) sys.exit(1) ensure_blog_dir() filepath = compute_filepath(parsed["date"], parsed["slug"]) filepath = resolve_collision(filepath) # Construct full content # We'll build YAML frontmatter string manually import yaml meta = { "title": parsed["title"], "date": parsed["date"], "tags": parsed["tags"], "excerpt": parsed["excerpt"], "slug": parsed["slug"], } fm = yaml.safe_dump(meta, sort_keys=False).strip() full = f"---\n{fm}\n---\n\n{parsed['body']}\n" with open(filepath, "w", encoding="utf-8") as f: f.write(full) print("✅ Wrote new post:", filepath) print("Title:", parsed["title"]) print("Slug:", parsed["slug"]) print("Tags:", parsed["tags"]) if __name__ == "__main__": main() ``` You’ll want to add error handling, logging, maybe retries, etc. --- ## Execution Example - Install dependencies: `pip install ollama python-frontmatter PyYAML slugify` - Pull / install your local model e.g. `ollama pull <model>` - From project root: ```bash python agent/generate_post.py # or python agent/generate_post.py "How agentic workflows work" ``` - The script will generate (or fail) with printed output - You check `content/blog/` — there should be a `.md` file with the generated content If it fails parsing, you'll see the raw LLM output to debug or adjust the prompt. --- ## Edge Cases, Improvements & Future Layers Here’s what you should watch out for and possible improvements: - **Prompt failures / hallucinations**: The model might not adhere strictly to the format, place YAML incorrectly, etc. You may need to refine the prompt, add guardrails, or post-process more robustly. - **Ambiguous metadata**: Some generated frontmatter might omit or mis-format tags, slug, excerpt — your parser must be resilient or fallback. - **Slug collisions / duplicates**: Over time, you might hit duplicates — maintain a state file or efficient check. - **Topic planning / memory**: You might want the agent to pick topics it hasn’t used, or maintain a queue of topics. - **Quality control / human in the loop**: For now, manual review is wise. Later you could add a “draft mode” or open in editor. - **Error retries**: On parse errors, try a second prompt with corrections (e.g. “You didn’t output valid YAML, try again”) - **Config / templating**: Have configuration to control style, tone, reading length, etc. - **Testing harness**: Run multiple generations, sampling, check frontmatter validity programmatically. - **Versioning / backups**: Keep previous drafts. - **Integration later**: Once this is stable, you can integrate with git commit, push, scheduling (cron or `cron` + script), or even build triggers. --- If you like, I can build for you a **ready-to-run prototype** (Python + Ollama) based on this design, already wired up to the `rez02` repository. Would you like me to deliver that codebase for you so you can test immediately?
gpt-5