{"id":129,"date":"2025-09-17T12:53:19","date_gmt":"2025-09-17T06:53:19","guid":{"rendered":"https:\/\/ronniee.net\/?p=129"},"modified":"2025-09-17T12:55:22","modified_gmt":"2025-09-17T06:55:22","slug":"windows-workflow-to-set-up-and-run-the-python-ai-agent-that-generates-posts-with-openai-and-publishes-them-to-wordpress","status":"publish","type":"post","link":"https:\/\/ronniee.net\/?p=129","title":{"rendered":"Windows workflow to set up and run the Python AI agent that generates posts with OpenAI and publishes them to WordPress."},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">1) Project layout (what your folder will look like)<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>wp-ai-agent\/\n\u251c\u2500 ai_agent.py\n\u251c\u2500 requirements.txt\n\u251c\u2500 .env\n\u251c\u2500 .gitignore\n\u2514\u2500 venv\/        (created by python -m venv venv)\n<\/code><\/pre>\n\n\n\n<h1 class=\"wp-block-heading\">2) Files to create<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\"><code>requirements.txt<\/code><\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>openai&gt;=1.0.0\nrequests&gt;=2.31.0\npython-dotenv&gt;=1.0.0\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><code>.gitignore<\/code><\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>venv\/\n.env\n__pycache__\/\n*.pyc\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><code>.env<\/code> (create this and fill in your real values)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>WP_URL=https:\/\/your-site.com\/wp-json\/wp\/v2\/posts\nWP_USER=your_wp_username\nWP_APP_PASSWORD=xxxx xxxx xxxx xxxx\nOPENAI_API_KEY=sk-...\nDEFAULT_STATUS=draft\nMODEL=gpt-4o-mini\n<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Security note:<\/strong> Never commit <code>.env<\/code> to version control.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\"><code>ai_agent.py<\/code><\/h2>\n\n\n\n<p>Copy this file exactly (it uses the modern <code>OpenAI<\/code> client object pattern and robust parsing):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"\"\"\nai_agent.py\nGenerates blog posts with OpenAI and publishes them to WordPress via REST API.\nUsage: python ai_agent.py\n\"\"\"\n\nimport os\nimport time\nimport json\nimport logging\nfrom typing import Dict\nimport requests\nfrom requests.auth import HTTPBasicAuth\nfrom dotenv import load_dotenv\nfrom openai import OpenAI\n\n# --------- Setup logging ----------\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s &#91;%(levelname)s] %(message)s\")\n\n# --------- Load config ----------\nload_dotenv()\nWP_URL = os.getenv(\"WP_URL\")  # example: https:\/\/your-site.com\/wp-json\/wp\/v2\/posts\nWP_USER = os.getenv(\"WP_USER\")\nWP_APP_PASSWORD = os.getenv(\"WP_APP_PASSWORD\")\nOPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\nDEFAULT_STATUS = os.getenv(\"DEFAULT_STATUS\", \"draft\")\nMODEL = os.getenv(\"MODEL\", \"gpt-4o-mini\")\n\nif not (WP_URL and WP_USER and WP_APP_PASSWORD and OPENAI_API_KEY):\n    logging.error(\"Missing required environment variables. Check .env file.\")\n    raise SystemExit(1)\n\n# --------- OpenAI client ---------\nclient = OpenAI(api_key=OPENAI_API_KEY)\n\n# --------- Helpers ---------\ndef generate_post(topic: str) -&gt; Dict&#91;str, str]:\n    \"\"\"\n    Ask OpenAI to produce a JSON object: {\"title\": \"...\", \"content\": \"...\"}\n    Content should be Markdown-ready.\n    \"\"\"\n    system = (\n        \"You are a helpful blogging assistant. \"\n        \"When asked to write a blog post, reply with a JSON object only, with two fields: \"\n        '\"title\" (string) and \"content\" (string, can include markdown). '\n        \"Do not include any extra explanation or text outside the JSON.\"\n    )\n    prompt = f\"Write a detailed, useful blog post about: {topic}\\n\\nMake it informative and reader-friendly.\"\n\n    logging.info(\"Requesting content from OpenAI for topic: %s\", topic)\n    resp = client.chat.completions.create(\n        model=MODEL,\n        messages=&#91;\n            {\"role\": \"system\", \"content\": system},\n            {\"role\": \"user\", \"content\": prompt}\n        ],\n        temperature=0.7,\n        max_tokens=1200\n    )\n\n    raw = resp.choices&#91;0].message.content.strip()\n    logging.debug(\"Raw OpenAI output: %s\", raw&#91;:300])\n\n    # Try to parse JSON out of the response robustly\n    try:\n        # If the model returned JSON directly:\n        data = json.loads(raw)\n    except Exception:\n        # fallback: try to find a JSON substring\n        start = raw.find(\"{\")\n        end = raw.rfind(\"}\")\n        if start != -1 and end != -1 and end &gt; start:\n            try:\n                data = json.loads(raw&#91;start:end+1])\n            except Exception as e:\n                logging.error(\"Failed to parse JSON from model output: %s\", e)\n                data = {\"title\": topic, \"content\": raw}\n        else:\n            logging.warning(\"Model did not return JSON, using raw text as content.\")\n            data = {\"title\": topic, \"content\": raw}\n\n    title = data.get(\"title\", topic)\n    content = data.get(\"content\", data.get(\"body\", data.get(\"text\", \"\")))\n    return {\"title\": title, \"content\": content}\n\ndef publish_post(post: Dict&#91;str, str], status: str = DEFAULT_STATUS) -&gt; Dict:\n    \"\"\"\n    Publish or create a draft post on WordPress via REST API.\n    Returns the WP API JSON on success, raises otherwise.\n    \"\"\"\n    payload = {\n        \"title\": post&#91;\"title\"],\n        \"content\": post&#91;\"content\"],\n        \"status\": status\n    }\n    logging.info(\"Publishing post: %s (status=%s)\", post&#91;\"title\"], status)\n\n    response = requests.post(\n        WP_URL,\n        auth=HTTPBasicAuth(WP_USER, WP_APP_PASSWORD),\n        json=payload,\n        timeout=30\n    )\n\n    if response.status_code in (200, 201):\n        logging.info(\"Published successfully: %s\", response.json().get(\"link\"))\n        return response.json()\n    else:\n        logging.error(\"Failed to publish: %s -&gt; %s\", response.status_code, response.text)\n        response.raise_for_status()\n\n# --------- Main runner ----------\ndef main(topics):\n    for topic in topics:\n        try:\n            post = generate_post(topic)\n            wp_resp = publish_post(post, status=DEFAULT_STATUS)\n            logging.info(\"WP response id=%s link=%s\", wp_resp.get(\"id\"), wp_resp.get(\"link\"))\n            time.sleep(1)  # polite pause between posts\n        except Exception as e:\n            logging.exception(\"Error processing topic '%s': %s\", topic, e)\n\nif __name__ == \"__main__\":\n    # Example topics \u2014 replace or load from file\/db\n    topics = &#91;\n        \"The Future of AI in Web Development\",\n        \"Top 5 Python Tips for Beginners\",\n        \"How WordPress Powers Small Business Websites\"\n    ]\n    main(topics)\n<\/code><\/pre>\n\n\n\n<h1 class=\"wp-block-heading\">3) Windows step-by-step commands<\/h1>\n\n\n\n<p>Open <strong>Command Prompt<\/strong> (easier for venv activation), or use PowerShell with the ExecutionPolicy fix shown below.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A \u2014 Create project and venv (Command Prompt)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>cd G:\\python\nmkdir wp-ai-agent\ncd wp-ai-agent\n\npython -m venv venv\nvenv\\Scripts\\activate.bat\n<\/code><\/pre>\n\n\n\n<p>When activated you\u2019ll see <code>(venv)<\/code> in the prompt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B \u2014 (PowerShell users)<\/h3>\n\n\n\n<p>If you prefer PowerShell and see the execution policy error, run <strong>once<\/strong> in an elevated PowerShell (or in your current session for one-off) before activating:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># temporary for this session (safe)\nSet-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass\n\n# then activate\n.\\venv\\Scripts\\Activate.ps1\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C \u2014 Install dependencies<\/h3>\n\n\n\n<p>With the venv active:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install -r requirements.txt\n<\/code><\/pre>\n\n\n\n<p>If you hit the earlier &#8220;Failed to write executable&#8221; error outside venv, it\u2019s because you weren\u2019t in a venv. The above fixes that.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">D \u2014 Create <code>.env<\/code><\/h3>\n\n\n\n<p>Open Notepad or your editor and paste the <code>.env<\/code> content shown earlier. Save it in the project root.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E \u2014 Run the script<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>python ai_agent.py\n<\/code><\/pre>\n\n\n\n<p>It will generate posts from <code>topics<\/code> list and create drafts (or published posts if you set <code>DEFAULT_STATUS=publish<\/code> but keep default as <code>draft<\/code> while testing).<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">4) Scheduling (Windows Task Scheduler)<\/h1>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open <strong>Task Scheduler<\/strong> \u2192 Create Basic Task.<\/li>\n\n\n\n<li>Trigger: Daily \/ Weekly \/ At startup \u2014 whatever you want.<\/li>\n\n\n\n<li>Action: Start a program\n<ul class=\"wp-block-list\">\n<li>Program\/script: <code>G:\\python\\wp-ai-agent\\venv\\Scripts\\python.exe<\/code><\/li>\n\n\n\n<li>Add arguments: <code>G:\\python\\wp-ai-agent\\ai_agent.py<\/code><\/li>\n\n\n\n<li>Start in: <code>G:\\python\\wp-ai-agent<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>(Optional) Under <strong>General<\/strong>: check &#8220;Run whether user is logged on or not&#8221; and provide credentials (and &#8220;Run with highest privileges&#8221; if needed).<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">5) Troubleshooting quick guide<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Permission \/ pip errors<\/strong>: Always activate venv before installing. If installing globally, use <code>--user<\/code> or run terminal as Administrator (not recommended).<\/li>\n\n\n\n<li><strong>Activate.ps1 blocked<\/strong>: Use <code>activate.bat<\/code> or run <code>Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass<\/code> in PowerShell.<\/li>\n\n\n\n<li><strong>WP REST auth 401\/403<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Make sure your <code>WP_APP_PASSWORD<\/code> is copied exactly and that the user has rights to create posts.<\/li>\n\n\n\n<li>Confirm WP URL is correct: <code>https:\/\/your-site.com\/wp-json\/wp\/v2\/posts<\/code> (no trailing slash issues).<\/li>\n\n\n\n<li>Test with curl: <code>curl -u \"username:application-password\" -X POST \"https:\/\/your-site.com\/wp-json\/wp\/v2\/posts\" -H \"Content-Type: application\/json\" -d \"{\\\"title\\\":\\\"Test\\\",\\\"content\\\":\\\"Hello\\\",\\\"status\\\":\\\"draft\\\"}\"<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>OpenAI errors<\/strong>: check <code>OPENAI_API_KEY<\/code>. If you get rate limit or access errors, check billing &amp; model availability.<\/li>\n\n\n\n<li><strong>Model output not JSON<\/strong>: This script attempts robust parsing. If the model still returns unexpected text, try lowering <code>temperature<\/code> or adjust the system prompt to emphasize strict JSON output.<\/li>\n<\/ul>\n\n\n\n<h1 class=\"wp-block-heading\">6) Next steps \/ enhancements (optional)<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Automatically upload and attach featured images (use WordPress <code>\/wp\/v2\/media<\/code> endpoint) \u2014 can add later.<\/li>\n\n\n\n<li>Store generated posts metadata in a local DB (SQLite) to avoid duplicates.<\/li>\n\n\n\n<li>Use a topics queue (read from CSV or DB) instead of inline <code>topics<\/code> list.<\/li>\n\n\n\n<li>Add retry\/backoff for network errors (e.g., with <code>tenacity<\/code>).<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>1) Project layout (what your folder will look like) 2) Files to create requirements.txt .gitignore .env (create this and fill in your real values) Security note: Never commit .env to version control. ai_agent.py Copy this file exactly (it uses the modern OpenAI client object pattern and robust parsing): 3) Windows step-by-step commands Open Command Prompt [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[14],"tags":[],"class_list":["post-129","post","type-post","status-publish","format-standard","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/ronniee.net\/index.php?rest_route=\/wp\/v2\/posts\/129","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ronniee.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ronniee.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ronniee.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ronniee.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=129"}],"version-history":[{"count":1,"href":"https:\/\/ronniee.net\/index.php?rest_route=\/wp\/v2\/posts\/129\/revisions"}],"predecessor-version":[{"id":130,"href":"https:\/\/ronniee.net\/index.php?rest_route=\/wp\/v2\/posts\/129\/revisions\/130"}],"wp:attachment":[{"href":"https:\/\/ronniee.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=129"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ronniee.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=129"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ronniee.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=129"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}