<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Santosh’s Dev Notes]]></title><description><![CDATA[Santosh’s Dev Notes]]></description><link>https://santoshsdevnotes.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 28 Oct 2026 14:43:50 GMT</lastBuildDate><atom:link href="https://santoshsdevnotes.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Context Rot — An Unresolved AI Issue?]]></title><description><![CDATA[Large Language Models (LLMs) are very good at understanding the intent of the user and can give exceptionally good and accurate responses at the start of a…]]></description><link>https://santoshsdevnotes.hashnode.dev/context-rot-an-unresolved-ai-issue</link><guid isPermaLink="true">https://santoshsdevnotes.hashnode.dev/context-rot-an-unresolved-ai-issue</guid><dc:creator><![CDATA[Santosh]]></dc:creator><pubDate>Fri, 23 Oct 2026 18:50:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad3e69223b61315c9b7d2d8/af9709fe-84a9-458d-a883-c2c7d7d8a85f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Large Language Models (LLMs) are very good at understanding the intent of the user and can give exceptionally good and accurate responses at the start of a session. When you are starting out a fresh session, it does not matter whether you are talking about science, literature, mathematics, or planning to find extraterrestrial life in our galaxy — it will give you the response you are looking for.</p>
<p>But as the session moves forward and the input window grows, the responses seem irrelevant or vague, and the results no longer align with your mental image.</p>
<p>Most people will think there is some issue in their prompt or writing style, or that maybe they are not clarifying the idea properly, and they end up confusing the LLM even more as the session decays.</p>
<p>But the issue is not in your idea or writing style. It is in the LLM itself. The information you are giving is not wrong, but the LLM starts to pay less attention with each passing input as the context window grows. This issue is known as <strong>Context Rot</strong>.</p>
<h2>What is Context Rot?</h2>
<p>Context Rot is a flaw in the architectural design of LLMs, and it has been there for the last 5–6 years. It is a phenomenon that degrades the performance of LLMs whenever they try to process a long context.</p>
<p>The problem is that the information is present in the context, but the model is unable to identify it within the context window.</p>
<blockquote>
<p><strong>Context Window</strong> is the working memory of an artificial intelligence or LLM — it is the maximum amount of data that an AI can see or remember during an active session.</p>
</blockquote>
<p>The explanation for this problem is quite simple. The architecture of AI is designed in such a way that it naturally focuses on the beginning and end of the input and neglects the middle part. This means adding more context or information actually reduces the accuracy of the system — the attention of the AI becomes vague, and it starts hallucinating. This is known as <strong>"Lost in the Middle."</strong></p>
<p>Researchers discovered Lost in the Middle using a famous technique called the <strong>"Needle in the Haystack" (NIAH)</strong> test, which was introduced by AI educator and YouTuber Greg Kamradt.</p>
<h2>Needle in the Haystack</h2>
<p>It is a test conducted on LLMs to evaluate the ability of a model to retrieve a specific, isolated piece of information from a large context.</p>
<p>As AI companies expanded and started marketing their AI products with large context windows — such as 1M, 2M, and 10M tokens — researchers needed a benchmark test to validate the claims behind these windows. During the research, they started noticing the Context Rot flaw, and to prove it, they introduced this technique to show how an AI becomes irrelevant even when its context window spans millions of tokens.</p>
<h3>How the test works</h3>
<ul>
<li><strong>The Haystack:</strong> A massive amount of content is loaded into the LLM's context window, such as an essay collection, documentation, or a research paper.</li>
<li><strong>Needle:</strong> A random, out-of-context question is given to the LLM to describe or solve within the given content. This tests not only the model's matching ability but also its thinking capability.</li>
<li><strong>Depth:</strong> The needle is inserted into the haystack at different depths, so the result is an average across many positions — some easy, some difficult.</li>
<li><strong>Length:</strong> The haystack amount starts from 2,000 tokens and goes up to the outer bounds of the model's context window.</li>
<li><strong>Results:</strong> The outcome of NIAH is obtained after a recursive process of testing and evaluating different combinations of depth and context length.</li>
</ul>
<h2>Lost in the Middle</h2>
<p>Even though NIAH is now used as an industry-standard tool, and top AI companies use it to showcase their powerful models, they are still unable to solve the biggest problem in AI: "Lost in the Middle." Models are still not able to manage such large chunks of data.</p>
<p>To understand how the Lost in the Middle problem arises, we need to understand the under-the-hood functioning of an LLM. (To learn about transformer architecture in detail, visit Great Learning.)</p>
<p>Large Language Models use the transformer architecture, which processes information and calculates relationships across an entire block simultaneously — unlike a human, who reads a sentence left to right. The model focuses on and emphasizes the beginning and end of the input, as these parts tend to reveal the user's intent or purpose.</p>
<p>This architecture is the reason models do exceptionally well on early, standalone NIAH tests but fail completely during real-world, complex reasoning conversations, where key details get buried among the instructions.</p>
<p>As there is no solution to this problem yet, you can use these workarounds in the meantime to increase your productivity with AI.</p>
<h2>Techniques to Work Around Context Rot</h2>
<h3>1. Start Fresh Session</h3>
<p>Instead of mentioning every aspect of your goal in one go, divide your idea or work into small, manageable tasks and use a new session each time to complete a single task. This way, the LLM will only try to complete your intent in the most accurate manner and will not get confused by multiple requests stacked on top of each other.</p>
<h3>2. Handoff Approach</h3>
<p>This is a context management technique where you summarize the LLM's output midway through the session and pass that summary down to a new session. It keeps the LLM's decision-making fresh.</p>
<h3>3. RAG (Retrieval-Augmented Generation)</h3>
<p>This is the most common technique used in enterprise settings to handle data at scale. In this technique, data is fetched from an external source or database in chunks using vectorization, and only the relevant chunk is fed into the LLM for that specific input. (If you need an in-depth explanation of RAG, visit GeeksForGeeks.)</p>
<h3>4. RLM (Recursive Language Models)</h3>
<p>This is one of the most recent approaches to context rot, described in the paper <em>"Recursive Language Models"</em> by Alex L. Zhang, Tim Kraska, and Omar Khattab at MIT.</p>
<p>In this work, the researchers explain that context rot can be reduced by storing data as an out-of-context variable inside an isolated Python REPL (Read-Eval-Print Loop) environment. The root LLM never sees the full raw text and only receives small, constant-sized metadata about it; Python execution commands are then used to read, slice, and filter keywords to actively map out the massive variable. The printed output of its own code feeds back into its next logical step.</p>
<p>If the model finds a dense, 10,000-word block of code or text that requires isolated semantic focus, it writes a line of code to call a fresh sub-instance of the LLM. This child model is born with a blank context window, and the root model aggregates the returned values from these sub-instances into the final response.</p>
<h2>FAQs</h2>
<p><strong>Q: Is Context Rot the same thing as hallucination?</strong></p>
<p>Not exactly. Hallucination is when the model makes up information that was never there in the first place. Context Rot is different — the correct information is present in the context window, the model just fails to pay attention to it because it is buried in the middle of a long input. So Context Rot can end up causing hallucination, but the root cause is attention, not invention.</p>
<p><strong>Q: If I use a model with a bigger context window, will Context Rot go away?</strong></p>
<p>No, and this is the part most people get wrong. A bigger context window only increases how much data the model can technically hold, it does not fix how the model pays attention to that data.</p>
<p><strong>Q: How do I know if I am hitting Context Rot in my own conversations?</strong></p>
<p>The clearest sign is when the model starts giving vague, generic, or slightly-off responses after a long session, even though your prompt is clear and you have already given it all the information it needs.</p>
<p><strong>Q: Is there any permanent fix to Context Rot coming?</strong></p>
<p>Right now, no. It is still an open architectural limitation, which is why researchers are actively working on new approaches.</p>
<h2>References</h2>
<ul>
<li>Hong, K., Troynikov, A., and Huber, J. (2025, July 14) — <em>Context Rot: How Increasing Input Tokens Impacts LLM Performance.</em></li>
<li>Trollope, R. (2026, May 18) — <em>Context Is All You Need: Introducing Redis Iris.</em></li>
<li>Zhang, A. L., Kraska, T., and Khattab, O. — <em>Recursive Language Models.</em></li>
</ul>
<p>Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[How to Build an Autonomous Blog Writer with CrewAI and Python]]></title><description><![CDATA[Those who have just started writing, and even experienced writers, face this problem on a day-to-day basis. They sit in front of a laptop screen for a…]]></description><link>https://santoshsdevnotes.hashnode.dev/how-to-build-an-autonomous-blog-writer-with-crewai-and-python</link><guid isPermaLink="true">https://santoshsdevnotes.hashnode.dev/how-to-build-an-autonomous-blog-writer-with-crewai-and-python</guid><category><![CDATA[Python]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[coding]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[large language models]]></category><dc:creator><![CDATA[Santosh]]></dc:creator><pubDate>Sat, 17 Oct 2026 21:48:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad3e69223b61315c9b7d2d8/b1c2b111-79f7-42ee-8af6-f279cb037ea2.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Those who have just started writing, and even experienced writers, face this problem on a day-to-day basis.</p>
<p>They sit in front of a laptop screen for a couple of hours before rage-quitting over</p>
<ul>
<li>what to write,</li>
<li>how to write it,</li>
<li>which research papers are the best,</li>
<li>and how to format the post.</li>
</ul>
<p>The main problem is that writing requires patience, research, and good structure before it can be considered a solid piece of information.</p>
<p>To overcome this, we are going to learn how to automate the task and reduce the effort and hassle of writing.</p>
<p>We will also make the process more interesting, even for experienced writers.</p>
<p><strong>Disclaimer:</strong> Think of AI as an assistant, not a replacement for your effort.</p>
<p>Please don't use these tools to spam blogs created purely for monetary gain.</p>
<h2>What is CrewAI</h2>
<p>CrewAI is an open-source framework and an enterprise platform used to build multi-agent AI systems that orchestrate and collaborate on a complex task, where each agent has a defined role for handling different parts of a job.</p>
<p>You can consider CrewAI as an organisation, agents as the employees of that organisation, and tools as the skill set of those agents.</p>
<h2>Main Components of CrewAI</h2>
<ul>
<li><strong>Crew:</strong> A crew is a team of agents specialised in a single task. It can comprise a single agent or multiple agents, depending on the use case. Just as an organisation can have multiple departments, CrewAI can have multiple crews.</li>
<li><strong>Agents:</strong> Agents are the employees responsible for completing a specialised task, such as research, drafting, editing, etc.</li>
<li><strong>Tools:</strong> Tools are the helping hands of agents and carry out certain tasks, such as web search, API calls, etc.</li>
</ul>
<h2>Prerequisites</h2>
<ul>
<li>AI API keys (paid) or Ollama local setup (free)</li>
<li>A code editor (VS Code, Cursor, or similar)</li>
<li>Concepts of generative AI and prompt engineering</li>
<li>CrewAI basics</li>
<li>Python basics</li>
</ul>
<h2>LLM Setup</h2>
<p>CrewAI requires the configuration of an LLM, so gather the API keys and required details from your LLM provider and write them down in a <code>.env</code> file later, in the format below.</p>
<pre><code class="language-env">AZURE_OPENAI_API_KEY=your-azure-key
AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/
AZURE_OPENAI_API_VERSION=2024-10-21
AZURE_OPENAI_DEPLOYMENT=your-deployment-name
</code></pre>
<p>For my development, I am going to use Ollama, a free, open-source tool for running LLMs locally.</p>
<p>You can use it too, and refer to my blog, How I Built My Own Voice Agent Prototype Using Ollama, ElevenLabs &amp; Whisper?, for setting up Ollama on your local system.</p>
<pre><code class="language-env">OLLAMA_MODEL=llama3.1:8b
OLLAMA_BASE_URL=http://localhost:11434
OTEL_SDK_DISABLED=true
</code></pre>
<h2>Python Setup</h2>
<p>Once your LLM credentials are written down securely, we can proceed with setting up Python on our system.</p>
<p>Python is a programming language used for the development of many kinds of coding projects, and AI/ML is one of them.</p>
<p>You can download it by visiting the official website.</p>
<p>After downloading and setting up Python on your system, create an empty folder and open it inside your code editor.</p>
<p>Make sure you have installed the Python extension in the editor for better and faster development and debugging.</p>
<p>With that, the Python setup is complete.</p>
<p>To set up CrewAI, we need to install the dependencies of CrewAI in our project using the uv package manager.</p>
<p>Open the terminal and install the uv package manager using the command below.</p>
<pre><code class="language-bash">#Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

#Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

#MacOS
curl -LsSf https://astral.sh/uv/install.sh | sh
</code></pre>
<p>Once the package manager has installed, restart your terminal and verify it using the below command.</p>
<p>if the command fails, there might be an issue with the installation process</p>
<pre><code class="language-bash">uv --version
</code></pre>
<p>After installing the package manager, use the following command to set up the boilerplate of your project.</p>
<pre><code class="language-bash">uv init
</code></pre>
<p>After initialising, you need to install a few dependencies, so use the following command to download CrewAI and other libraries.</p>
<pre><code class="language-bash">uv add crewai ddgs requests beautifulsoup4 python-dotenv markdown xhtml2pdf python-docx pyyaml
</code></pre>
<h2>CrewAI Setup</h2>
<p>Once Python and all its libraries are in place, we can start building our project using CrewAI.</p>
<p>Use the following command to set up your blog writing flow.</p>
<pre><code class="language-bash">crewai create
</code></pre>
<p>After running the command, you will be prompted with a form where you need to provide basic information about the project you want to build with CrewAI.</p>
<p>I will cover each approach in a different blog.</p>
<p>For our use case, we are going to create a flow named Blogwriting.</p>
<pre><code class="language-text">What would you like to create?

    crew         A team of AI agents working together
  → flow         A deterministic workflow with full control over agents and crews
    tool         A custom tool for the CrewAI Tool Repository
    skill        An agent skill with instructions and optional assets
    template     A remote project template from the CrewAI gallery


Name of your flow › BlogWriting
</code></pre>
<p>Once your CrewAI foundation is created, you will see a similar folder structure in your code editor.</p>
<pre><code class="language-text">AutonomousBlogWriting/
 .venv/                        
 .gitignore
 .python-version               
  main.py                       
  pyproject.toml                
  README.md
  uv.lock
  blogwriting/                 
      .env                      
      .gitignore
       pyproject.toml            
       README.md
       src/
         blogwriting/
             __init__.py
             main.py           
             crew.py 
             crews/content_crew        
                 config/
                     agents.yaml   
                     tasks.yaml 
                 content_crew.py  
             tools/
                 __init__.py
                 custom_tool.py  
</code></pre>
<p>Our first task is to set up the environment variables, so go to <code>AutonomousBlogWriting/blogwriting/.env</code> and paste your LLM credentials in the format I showed you above.</p>
<h2>Code</h2>
<p>Now that every element is in place, we are going to start combining all those things to build our project.</p>
<p>If you get stuck during this process, I recommend you fork my Git repo to get a better understanding of the code</p>
<p><a href="https://github.com/SantoshGhoghwani/AutonomousBlogWriting">GitHub — Autonomous Writer Git Repository</a></p>
<h2>Config</h2>
<p>To build an organisation, we first need employees who are specialists in their respective domains, so go to <code>AutonomousBlogWriting/blogwriting/src/blogwriting/crews/content_crew/config</code> and start updating the YAML files.</p>
<p><code>agents.yaml</code> — In this file, you need to define all the agents (employees) you want to work with in your organisation.</p>
<p>Each agent has three main components:</p>
<ul>
<li><code>role</code> — The title of your agent</li>
<li><code>goal</code> — The responsibilities of your agent</li>
<li><code>backstory</code> — short summary of how your agent will perform its task</li>
</ul>
<p>We are going to create four agents: researcher, writer, editor, and formatter.</p>
<p>You can always refer to my Git repo for reference, as I have defined all four agents in my YAML file.</p>
<p><code>tasks.yaml</code> — In this file, we are going to define all the respective tasks your agents will perform to generate the expected output.</p>
<p>An agent can have a single task or multiple tasks, depending on the use case.</p>
<p>We are going to create four tasks, one for each agent's goal.</p>
<p>Similar to agents, tasks also have three main components:</p>
<ul>
<li><code>description</code> — A detailed structure of the steps the agent has to take to generate the desired results</li>
<li><code>expected_output</code> — The format in which the agent will generate the result. It can be normal text, a markdown file, code, etc.</li>
<li><code>agent</code> — The name of the agent you want this task to be assigned to.</li>
</ul>
<h2>Tool</h2>
<p>Agents require tools to fetch details from the internet, make API calls, fetch data from databases, and much more.</p>
<p>We need to create a web search tool that lets us find the most relevant information about the topic the user is searching for.</p>
<p>For this, we are going to create a custom tool inside <code>AutonomousBlogWriting/blogwriting/src/blogwriting/tools</code> and name it <code>web_search_tool.py</code>.</p>
<p>It helps us fetch a dictionary of URLs related to the topic and then read the details from those URLs to create a new blog as the output.</p>
<pre><code class="language-python">from typing import Type

import requests
from bs4 import BeautifulSoup
from crewai.tools import BaseTool
from ddgs import DDGS
from pydantic import BaseModel, Field


class WebSearchInput(BaseModel):
    query: str = Field(..., description="The search query, e.g. 'latest trends in RAG'")
    max_results: int = Field(5, description="Number of results to return (1-10)")


class WebSearchTool(BaseTool):
    name: str = "web_search"
    description: str = (
        "Search the web for up-to-date information. Returns title, URL and snippet "
        "for each result. Use it to research topics and collect sources."
    )
    args_schema: Type[BaseModel] = WebSearchInput

    def _run(self, query: str, max_results: int = 5) -&gt; str:
        max_results = max(1, min(int(max_results), 10))
        try:
            results = DDGS().text(query, max_results=max_results)
        except Exception as e:
            return f"Search failed: {e}. Try rephrasing the query or retry shortly."
        if not results:
            return "No results found. Try a broader or differently worded query."
        return "\n\n".join(
            f"[{i}] {r.get('title', '').strip()}\n"
            f"URL: {r.get('href', '')}\n"
            f"Snippet: {r.get('body', '').strip()}"
            for i, r in enumerate(results, 1)
        )


class PageReaderInput(BaseModel):
    url: str = Field(..., description="Full URL of the page to read (include https://)")
    max_chars: int = Field(4000, description="Maximum characters of page text to return")


class PageReaderTool(BaseTool):
    name: str = "read_webpage"
    description: str = (
        "Fetch a web page and return its main text. Use on a promising URL from "
        "web_search when the snippet isn't enough."
    )
    args_schema: Type[BaseModel] = PageReaderInput

    def _run(self, url: str, max_chars: int = 4000) -&gt; str:
        try:
            resp = requests.get(
                url, timeout=15,
                headers={"User-Agent": "Mozilla/5.0 (compatible; BlogFlowBot/1.0)"},
            )
            resp.raise_for_status()
        except Exception as e:
            return f"Could not fetch {url}: {e}"
        soup = BeautifulSoup(resp.text, "html.parser")
        for tag in soup(["script", "style", "nav", "footer", "header", "aside", "form"]):
            tag.decompose()
        text = " ".join(soup.get_text(separator=" ").split())
        return text[:max_chars] + ("... [truncated]" if len(text) &gt; max_chars else "")
</code></pre>
<h2>LLM and Crew</h2>
<p>Everything around the project is set up.</p>
<p>Now we have to build our crew and flow, then merge them into <code>main.py</code>.</p>
<p>Start your Ollama server and create a file named <code>config.py</code> inside <code>AutonomousBlogWriting/blogwriting/src/blogwriting</code>.</p>
<pre><code class="language-python">import os

from crewai import LLM
from dotenv import load_dotenv

load_dotenv()


def get_llm(temperature: float = 0.4) -&gt; LLM:
    return LLM(
        model=f"ollama/{os.getenv('OLLAMA_MODEL', 'llama3.1:8b')}",
        base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
        temperature=temperature,
    )
</code></pre>
<p>We are going to call our LLM from this file and set it in our <code>content_crew.py</code> file, where we are going to define our agents and tasks in CrewAI and call them based on the agent role and task description.</p>
<p>Below is our <code>content_crew.py</code> file. You need to update it in <code>AutonomousBlogWriting/blogwriting/src/blogwriting/crews/content_crew/content_crew.py</code>.</p>
<pre><code class="language-python">from pathlib import Path

import yaml
from crewai import Agent, Crew, Process, Task

from config import get_llm

CONFIG_DIR = Path(__file__).parent / "config"
AGENTS = yaml.safe_load((CONFIG_DIR / "agents.yaml").read_text(encoding="utf-8"))
TASKS = yaml.safe_load((CONFIG_DIR / "tasks.yaml").read_text(encoding="utf-8"))


def run_step(task_key: str, inputs: dict, tools=None, temperature=0.4, max_iter=None) -&gt; str:
    task_cfg = dict(TASKS[task_key])
    agent_key = task_cfg.pop("agent")          # "agent: researcher" -&gt; look up in agents.yaml

    extra = {"max_iter": max_iter} if max_iter else {}
    agent = Agent(
        config=AGENTS[agent_key],
        tools=tools or [],
        llm=get_llm(temperature),
        allow_delegation=False,
        verbose=True,
        **extra,
    )
    task = Task(config=task_cfg, agent=agent)
    crew = Crew(agents=[agent], tasks=[task], process=Process.sequential, verbose=True)
    return str(crew.kickoff(inputs=inputs).raw).strip()   # fills {topic}, {draft}, etc.
</code></pre>
<h2>Exporters</h2>
<p><code>exporters.py</code> is a file that we are going to use to store the LLM response in our desired format, such as Markdown, DOCX, PDF, etc.</p>
<p>Create a new file named <code>exporters.py</code> in <code>AutonomousBlogWriting/blogwriting/src/blogwriting</code>.</p>
<p>For now, this file supports a limited number of formats that the LLM response can be converted to.</p>
<p>If you need more, you can update this file according to your needs.</p>
<pre><code class="language-python">import re
from pathlib import Path
from typing import Callable, Dict, Tuple

import markdown as md_lib

Meta = Dict[str, str]

CSS = """
@page { size: A4; margin: 2cm; }
body { font-family: Helvetica, Arial, sans-serif; font-size: 11pt; line-height: 1.5; color: #222; }
h1 { font-size: 22pt; margin-bottom: 4pt; }
h2 { font-size: 15pt; margin-top: 18pt; }
h3 { font-size: 12pt; }
a { color: #1a5fb4; }
code, pre { font-family: Courier; font-size: 9.5pt; background-color: #f2f2f2; }
blockquote { margin-left: 12pt; color: #555; }
table { border-collapse: collapse; }
td, th { border: 1px solid #bbb; padding: 4pt; }
"""


def split_front_matter(text: str) -&gt; Tuple[Meta, str]:
    m = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", text.strip(), re.S)
    if not m:
        return {}, text.strip()
    meta: Meta = {}
    for line in m.group(1).splitlines():
        if ":" in line:
            k, v = line.split(":", 1)
            meta[k.strip().lower()] = v.strip().strip("'\"")
    return meta, m.group(2).strip()


def slugify(text: str) -&gt; str:
    return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")[:60] or "blog-post"


def _html_doc(meta: Meta, body: str) -&gt; str:
    html_body = md_lib.markdown(body, extensions=["extra", "sane_lists", "tables", "fenced_code"])
    title = meta.get("title", "Blog Post")
    return (
        f"&lt;html&gt;&lt;head&gt;&lt;meta charset='utf-8'&gt;&lt;title&gt;{title}&lt;/title&gt;"
        f"&lt;style&gt;{CSS}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;{html_body}&lt;/body&gt;&lt;/html&gt;"
    )


def _to_md(meta: Meta, body: str, path: Path, raw: str) -&gt; None:
    path.write_text(raw.strip() + "\n", encoding="utf-8")


def _to_html(meta: Meta, body: str, path: Path, raw: str) -&gt; None:
    path.write_text(_html_doc(meta, body), encoding="utf-8")


def _to_pdf(meta: Meta, body: str, path: Path, raw: str) -&gt; None:
    from xhtml2pdf import pisa

    with open(path, "wb") as f:
        status = pisa.CreatePDF(src=_html_doc(meta, body), dest=f, encoding="utf-8")
    if status.err:
        raise RuntimeError("PDF generation failed")


def _to_txt(meta: Meta, body: str, path: Path, raw: str) -&gt; None:
    text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", body)
    text = re.sub(r"^#{1,6}\s*", "", text, flags=re.M)
    text = re.sub(r"(\*\*|__|`)", "", text)
    path.write_text(text.strip() + "\n", encoding="utf-8")


_INLINE = re.compile(r"(\*\*.+?\*\*|`.+?`|\*.+?\*)")


def _add_runs(par, text: str) -&gt; None:
    text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text)
    for part in _INLINE.split(text):
        if not part:
            continue
        if part.startswith("**") and part.endswith("**") and len(part) &gt; 4:
            par.add_run(part[2:-2]).bold = True
        elif part.startswith("`") and part.endswith("`") and len(part) &gt; 2:
            par.add_run(part[1:-1]).font.name = "Courier New"
        elif part.startswith("*") and part.endswith("*") and len(part) &gt; 2:
            par.add_run(part[1:-1]).italic = True
        else:
            par.add_run(part)


def _to_docx(meta: Meta, body: str, path: Path, raw: str) -&gt; None:
    from docx import Document

    doc = Document()
    buf: list = []

    def flush():
        if buf:
            _add_runs(doc.add_paragraph(), " ".join(buf))
            buf.clear()

    in_code = False
    for line in body.splitlines():
        if line.strip().startswith("```"):
            flush()
            in_code = not in_code
            continue
        if in_code:
            par = doc.add_paragraph()
            par.add_run(line).font.name = "Courier New"
            continue
        s = line.strip()
        if not s:
            flush()
        elif (h := re.match(r"^(#{1,6})\s+(.*)", s)):
            flush()
            doc.add_heading(h.group(2), level=min(len(h.group(1)) - 1, 4))
        elif re.match(r"^[-*+]\s+", s):
            flush()
            _add_runs(doc.add_paragraph(style="List Bullet"), re.sub(r"^[-*+]\s+", "", s))
        elif re.match(r"^\d+[.)]\s+", s):
            flush()
            _add_runs(doc.add_paragraph(style="List Number"), re.sub(r"^\d+[.)]\s+", "", s))
        elif s.startswith("&gt;"):
            flush()
            _add_runs(doc.add_paragraph(style="Intense Quote"), s.lstrip("&gt; "))
        elif re.match(r"^(-{3,}|\*{3,})$", s):
            flush()
        else:
            buf.append(s)
    flush()
    doc.save(path)


EXPORTERS: Dict[str, Tuple[str, Callable]] = {
    "markdown": (".md", _to_md),
    "md": (".md", _to_md),
    "html": (".html", _to_html),
    "pdf": (".pdf", _to_pdf),
    "docx": (".docx", _to_docx),
    "txt": (".txt", _to_txt),
}

SUPPORTED_FORMATS = sorted(set(EXPORTERS) - {"md"})


def export(markdown_text: str, fmt: str, out_dir: str, fallback_title: str = "blog post") -&gt; Path:
    fmt = fmt.lower().strip()
    if fmt not in EXPORTERS:
        raise ValueError(f"Unsupported format '{fmt}'. Choose from: {', '.join(SUPPORTED_FORMATS)}")
    ext, writer = EXPORTERS[fmt]
    meta, body = split_front_matter(markdown_text)
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)
    path = out / f"{slugify(meta.get('title', fallback_title))}{ext}"
    writer(meta, body, path, markdown_text)
    return path
</code></pre>
<h2>Flow and Main</h2>
<p>This is the easiest and last part of our project.</p>
<p>To bring your code live, you just have to create a sequential flow, because there is no separate routing, and every time a query is run, we need to go through a defined number of steps, calling our agents one by one without any loops or conditions.</p>
<p>In <code>AutonomousBlogWriting/blogwriting/src/blogwriting</code>, create a new file named <code>flow.py</code> and paste the code below for the sequential flow of your crew</p>
<pre><code class="language-python">import re
from pathlib import Path

from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel

from crews.content_crew.content_crew import run_step
from exporters import export
from tools.web_search_tool import PageReaderTool, WebSearchTool


class BlogState(BaseModel):
    topic: str = ""
    audience: str = "general readers"
    tone: str = "conversational and informative"
    word_count: int = 900
    output_format: str = "markdown"      # markdown | html | pdf | docx | txt
    output_dir: str = "output"
    research_notes: str = ""
    draft: str = ""
    edited_post: str = ""
    final_markdown: str = ""
    output_path: str = ""


def _clean_markdown(text: str) -&gt; str:
    m = re.match(r"^```(?:markdown|md)?\s*\n(.*)\n```\s*$", text.strip(), re.S)
    return (m.group(1) if m else text).strip()


class BlogFlow(Flow[BlogState]):
    def _save_step(self, name: str, text: str) -&gt; None:
        d = Path(self.state.output_dir) / "intermediate"
        d.mkdir(parents=True, exist_ok=True)
        (d / f"{name}.md").write_text(text, encoding="utf-8")

    @start()
    def research(self):
        s = self.state
        s.research_notes = run_step(
            "research_task", s.model_dump(),
            tools=[WebSearchTool(), PageReaderTool()], temperature=0.2, max_iter=10,
        )
        self._save_step("1_research", s.research_notes)

    @listen(research)
    def drafting(self):
        s = self.state
        s.draft = run_step("writing_task", s.model_dump(), temperature=0.7)
        self._save_step("2_draft", s.draft)

    @listen(drafting)
    def editing(self):
        s = self.state
        s.edited_post = run_step("editing_task", s.model_dump(), temperature=0.3)
        self._save_step("3_edited", s.edited_post)

    @listen(editing)
    def formatting(self):
        s = self.state
        raw = run_step("formatting_task", s.model_dump(), temperature=0.1)
        s.final_markdown = _clean_markdown(raw)
        path = export(s.final_markdown, s.output_format, s.output_dir, fallback_title=s.topic)
        s.output_path = str(path)
        print(f"Final output written to: {path}")
        return s.output_path
</code></pre>
<p>In the above file, we have created some variables that contain predefined values, and topic is the only variable that is not optional.</p>
<p>To change these variables at runtime, we are going to add arguments in our main file, such as word count, tone, etc.</p>
<p>Below is the <code>main.py</code> file, which lets you run your crew.</p>
<pre><code class="language-python">import argparse
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent / "src"))

from flow import BlogFlow  # noqa: E402
from exporters import SUPPORTED_FORMATS  # noqa: E402



def main() -&gt; None:
    p = argparse.ArgumentParser(description="Autonomous blog-writing Flow (CrewAI + Ollama)")
    p.add_argument("--topic", required=True, help="Blog topic")
    p.add_argument("--audience", default="general readers")
    p.add_argument("--tone", default="conversational and informative")
    p.add_argument("--words", type=int, default=900, help="Target word count")
    p.add_argument("--format", default="markdown", choices=SUPPORTED_FORMATS + ["md"],
                   help="Output file format")
    p.add_argument("--output-dir", default="output")
    p.add_argument("--plot", action="store_true", help="Save a flow diagram (flow.html) and exit")
    args = p.parse_args()

    flow = BlogFlow()
    if args.plot:
        flow.plot("blog_flow")
        return

    result = flow.kickoff(inputs={
        "topic": args.topic,
        "audience": args.audience,
        "tone": args.tone,
        "word_count": args.words,
        "output_format": args.format,
        "output_dir": args.output_dir,
    })
    print(f"\nDone -&gt; {result}")


if __name__ == "__main__":
    main()
</code></pre>
<p>All our files are in place. All we have to do is open our terminal and run <code>main.py</code> to generate amazing ideas.</p>
<pre><code class="language-bash">python main.py --topic "C++ vs Rust: which is faster?" --format pdf
</code></pre>
<p>With this, we have come to the end of our blog. If you are stuck somewhere, have any opinions, or want me to cover a specific topic, the comments section is open.</p>
<p>Feel free to drop your thoughts.</p>
<p>Thank you for reading!</p>
<hr />
<p>For more such content, visit my <a href="https://medium.com/@santoshghoghwani2524">Medium blog</a>.</p>
]]></content:encoded></item></channel></rss>