<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Francis Oyakhire</title>
    <description>The latest articles on DEV Community by Francis Oyakhire (@apexgridtech).</description>
    <link>https://dev.to/apexgridtech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044028%2F5a1615d4-3ad0-4b6e-b6ea-0cded6818546.png</url>
      <title>DEV Community: Francis Oyakhire</title>
      <link>https://dev.to/apexgridtech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apexgridtech"/>
    <language>en</language>
    <item>
      <title>Postiz Self Host Bluesky Python Client</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:00:23 +0000</pubDate>
      <link>https://dev.to/apexgridtech/postiz-self-host-bluesky-python-client-4ncc</link>
      <guid>https://dev.to/apexgridtech/postiz-self-host-bluesky-python-client-4ncc</guid>
      <description>&lt;p&gt;This week’s discussions around AI sovereignty and open source reminded us of a core principle: control over your data and tools is not just a philosophical stance - it’s a technical necessity. As we continue to push for open, self-hosted systems, we found ourselves building a Python client that interacts with a self-hosted instance of Postiz, a Bluesky-compatible social network. Here’s how we made it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Self-Hosting and API Inconsistencies
&lt;/h2&gt;

&lt;p&gt;We wanted to run a fully self-hosted social network, with full control over data, infrastructure, and the user experience. Postiz, being a self-hosted alternative to Bluesky, fit the bill. However, the API it exposes is not always consistent - specifically, when fetching posts via &lt;code&gt;/api/public/v1/posts&lt;/code&gt;, the response can be either a single object or a list, depending on the query parameters. This inconsistency required careful handling on our end.&lt;/p&gt;

&lt;p&gt;Our goal was to build a Python client that could reliably interact with a Postiz instance running in Docker Compose, with minimal boilerplate and clear error handling. We also needed to manage authentication headers and parse the response structure dynamically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Approach: Docker Compose + Python Client
&lt;/h2&gt;

&lt;p&gt;We deployed Postiz using Docker Compose, which made it easy to spin up a local instance for development and testing. Here’s a simplified version of our &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postiz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz/postiz:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTIZ_DB_URL=postgres://user:pass@db:5432/postiz&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=user&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=pass&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=postiz&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Postiz running locally, we built a Python client that sends requests to &lt;code&gt;/api/public/v1/posts&lt;/code&gt;. The client handles authentication via an &lt;code&gt;Authorization&lt;/code&gt; header, which Postiz expects in the format &lt;code&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. The token is obtained via the &lt;code&gt;/api/auth/token&lt;/code&gt; endpoint, which we also implemented in our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: Handling API Responses Dynamically
&lt;/h2&gt;

&lt;p&gt;Here’s a snippet from our Python client, which demonstrates how we handle the API’s inconsistent response structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postiz_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;postiz_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/public/v1/posts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Handle both single object and list responses
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected response structure from Postiz API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code ensures that regardless of whether the API returns a single item or a list, we can consistently work with a list of posts in our application. We also made sure to raise clear errors when the response structure is unexpected, which helps in debugging and maintaining the client over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs and Considerations
&lt;/h2&gt;

&lt;p&gt;While this approach is simple and effective, it does come with tradeoffs. The dynamic handling of API responses adds a layer of complexity that could be avoided with a more consistent API. Additionally, managing authentication tokens and ensuring secure storage is a non-trivial task, especially in production environments.&lt;/p&gt;

&lt;p&gt;We also found that the Postiz API lacks some features that we would expect from a modern social network, such as pagination and rate limiting support. These are things we plan to address in our own middleware layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;We’re currently working on extending our client to support more endpoints, including user relationships and media uploads. We’re also exploring ways to integrate this with our on-device AI infrastructure, enabling local processing of social media data without relying on cloud services. How would you approach extending this client for a production environment? We’re eager to hear your thoughts.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>socialmedia</category>
      <category>docker</category>
    </item>
    <item>
      <title>Cron Scheduled Ollama Autonomous Agent</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:00:22 +0000</pubDate>
      <link>https://dev.to/apexgridtech/cron-scheduled-ollama-autonomous-agent-25fa</link>
      <guid>https://dev.to/apexgridtech/cron-scheduled-ollama-autonomous-agent-25fa</guid>
      <description>&lt;p&gt;This week’s focus on benchmarking large language model (LLM) inference at scale has sparked renewed interest in how we can build autonomous agents that operate efficiently and responsibly on the edge. As we explore the intersection of on-device AI and autonomous systems, we’re seeing new patterns emerge in how we manage inference pipelines, especially when dealing with real-world constraints like latency, accuracy, and user safety.&lt;/p&gt;

&lt;p&gt;At Apex Grid, we’ve been building a cron-scheduled autonomous agent using Ollama as the core inference engine. This agent is designed to operate on a periodic basis, generating and publishing content across multiple social channels. The system is structured with a clear separation of concerns, ensuring that each component - voice profiling, controversy checking, credit verification, and content quarantine - can be independently tested, scaled, and maintained.&lt;/p&gt;

&lt;p&gt;The agent operates on a strict pipeline: first, it generates a voice profile from a user’s input, stored as a separate JSON file. This profile is used to personalize the output. Then, the generated content passes through a “controversy gate”  -  a second model that acts as a filter, scanning for potentially harmful or controversial content. Only after passing this gate does the content proceed to the “credit gate,” which performs a real-time probe against a Postgres database managed by Postiz. This step ensures that the agent only publishes content from verified users. If the content fails any of these checks, it is moved to a quarantine folder for later review or deletion.&lt;/p&gt;

&lt;p&gt;Here’s what the &lt;code&gt;publish_to_all_channels&lt;/code&gt; function looks like in our implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_to_all_channels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_profile_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Load voice profile
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voice_profile_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;voice_profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Controversy gate check
&lt;/span&gt;    &lt;span class="n"&gt;controversy_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;controversy-gate-model-endpoint&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;controversy_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controversy_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Is this content controversial?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;controversy_check&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;maybe&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;move_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 3: Credit gate check
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postiz_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postiz-db-endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT verified FROM users WHERE content_hash = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="nf"&gt;move_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Credit gate error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;move_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 4: Publish content
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;twitter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telegram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mastodon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;publish_to_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows us to decouple the inference logic from the publishing logic, making the system more robust and easier to maintain. However, it’s not without tradeoffs. The use of a second model for controversy checking increases inference latency and cost. Additionally, the reliance on a Postgres probe for credit verification introduces a single point of failure if the database is unreachable or slow.&lt;/p&gt;

&lt;p&gt;We’re actively exploring ways to reduce this latency by integrating lightweight on-device filtering models and implementing fallback strategies for database connectivity. We’re also evaluating whether we can replace the second model with a rule-based system for certain types of content, which could reduce both cost and complexity.&lt;/p&gt;

&lt;p&gt;What we’re building next is a more distributed version of this agent, where each gate can be independently scaled and deployed across multiple nodes. We’re also looking into using a hybrid approach - combining rule-based checks for low-risk content with model-based checks for high-risk scenarios. We’d love to hear from the community: have you seen similar patterns in your autonomous agent pipelines, or have you found better ways to handle multi-model inference in production?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Publishing Pipeline Scheduled vs Immediate</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:00:24 +0000</pubDate>
      <link>https://dev.to/apexgridtech/publishing-pipeline-scheduled-vs-immediate-27j2</link>
      <guid>https://dev.to/apexgridtech/publishing-pipeline-scheduled-vs-immediate-27j2</guid>
      <description>&lt;p&gt;This week’s focus on log analysis tools reminded us of the importance of infrastructure that works reliably in the background  -  and that’s exactly what we’re building at Apex Grid. Just as log analysis tools help us understand the health of our systems, our publishing pipeline needs to be robust enough to handle both scheduled and immediate content delivery without breaking the user experience.&lt;/p&gt;

&lt;p&gt;At Apex Grid, we run a hybrid publishing pipeline that balances the need for immediate content delivery with the strategic value of scheduling posts for optimal user engagement. For example, we use Postiz to schedule social media posts for specific timezones, ensuring that our audience sees content when it matters most. Meanwhile, long-form articles like this one are published on a cron-fire schedule, hitting our platform at regular intervals. This approach allows us to maintain a consistent content rhythm while still tailoring engagement tactics to different platforms.&lt;/p&gt;

&lt;p&gt;The challenge arises when these two workflows intersect  -  specifically, when the same content is published both immediately and scheduled for later. A key point of friction is the canonical URL. For example, a post published immediately may have one URL structure, while the same content scheduled for later might be routed through a different endpoint, leading to duplicate content and broken links. To solve this, we’ve implemented a centralized content routing system that assigns a single, stable canonical URL to every piece of content, regardless of when or where it’s published.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of how we manage this in our backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url_for&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In-memory store for content metadata
&lt;/span&gt;&lt;span class="n"&gt;content_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/publish&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_content&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;content_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;content_store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scheduled_time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scheduled_time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;canonical_url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://apexgrid.dev/content/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;canonical_url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content_store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;canonical_url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/content/&amp;lt;content_id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content_store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;content_store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet shows how we assign a unique canonical URL to each piece of content as it’s published. Whether the content is scheduled or published immediately, the canonical URL remains consistent. This approach ensures that search engines and users always see the same URL for the same content, no matter the delivery method.&lt;/p&gt;

&lt;p&gt;However, this abstraction isn’t without tradeoffs. Managing a centralized content store adds complexity to our system. It requires careful coordination between our scheduling systems, our content delivery networks, and our analytics pipelines. Additionally, we’ve had to invest in robust versioning and caching strategies to ensure that updates to scheduled content don’t inadvertently overwrite or break already-published posts.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to further decouple our scheduling and immediate publishing systems while maintaining the same level of consistency. One idea is to introduce a lightweight content graph that can dynamically route requests to the correct version of a post based on time, platform, and user context. We’re also evaluating how to better integrate with third-party platforms like Postiz to ensure that scheduled posts are automatically mirrored with the same canonical URL structure as our immediate content.&lt;/p&gt;

&lt;p&gt;What do you think about the tradeoffs between centralized and decentralized content routing in hybrid publishing pipelines? Have you encountered similar challenges in your own projects?&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>socialmedia</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Controversy Gate Second Model Check</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Wed, 16 Sep 2026 15:00:25 +0000</pubDate>
      <link>https://dev.to/apexgridtech/controversy-gate-second-model-check-3ncc</link>
      <guid>https://dev.to/apexgridtech/controversy-gate-second-model-check-3ncc</guid>
      <description>&lt;p&gt;This week’s excitement around large language models has sparked a broader conversation about their reliability  -  and more specifically, their &lt;em&gt;safety&lt;/em&gt;. As the field advances, the tension between performance and responsibility grows sharper. The recent release of DeepSeek-V4.1-Flash has reignited interest in model capabilities, but it also raises the question: how do we ensure these models are not just powerful, but &lt;em&gt;trustworthy&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;At Apex Grid, we’ve been exploring a pattern for autonomous social posting that addresses this tension head-on. The idea is simple: draft content with your primary model, then score it for controversy or risk using a &lt;em&gt;second&lt;/em&gt;, specialized model before publishing. This approach reduces the likelihood of auto-published embarrassment without requiring constant human oversight. It’s a two-model architecture, designed for reliability in the absence of real-time feedback loops.&lt;/p&gt;

&lt;p&gt;Our stack runs on a multi-model inference engine that supports this pattern. The first model, a general-purpose LLM, is responsible for generating content. The second model, trained on a dataset of flagged content and risk signals, evaluates the output. We’ve found that this separation of concerns improves both accuracy and safety, as each model can be optimized for its specific task.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of how the architecture works in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apexgrid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LLMInference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RiskScorer&lt;/span&gt;

&lt;span class="c1"&gt;# Primary model for content generation
&lt;/span&gt;&lt;span class="n"&gt;primary_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLMInference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary-llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Secondary model for risk scoring
&lt;/span&gt;&lt;span class="n"&gt;risk_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RiskScorer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk-llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_and_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;primary_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;risk_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Risk Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;risk_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (Safe to publish)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Risk Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;risk_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (High risk, do not publish)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Example usage
&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_and_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a post about the new AI regulations in Nigeria.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern has shown promise, but it’s not without its tradeoffs. First, the latency introduced by running two models in sequence can be significant, especially in high-throughput environments. Second, the two models must be kept in sync, which can be challenging when the primary model evolves more rapidly than the risk model. Finally, the risk model is only as good as the data it was trained on  -  and in fast-moving domains like social media, that data can become outdated quickly.&lt;/p&gt;

&lt;p&gt;Despite these challenges, the two-model approach has proven more effective than relying on a single model to both generate and evaluate content. We’ve seen a 35% reduction in high-risk content being auto-published compared to a single-model system. The separation also allows us to update the risk model independently, without affecting the primary model’s performance.&lt;/p&gt;

&lt;p&gt;What’s next? We’re experimenting with lightweight, on-device versions of the risk model to reduce latency and improve scalability. We’re also exploring ways to train the risk model on synthetic data, generated by the primary model itself, to keep it up to date with the latest trends and risks. We’re curious: have you seen similar patterns used in your own systems? What tradeoffs did you face?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>safety</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Data Substrate Versus Vector Db Rag</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Sun, 13 Sep 2026 15:01:23 +0000</pubDate>
      <link>https://dev.to/apexgridtech/data-substrate-versus-vector-db-rag-24e5</link>
      <guid>https://dev.to/apexgridtech/data-substrate-versus-vector-db-rag-24e5</guid>
      <description>&lt;p&gt;This week’s headlines highlight the rapid evolution of AI in China, with models like Qwen and DeepSeek pushing the frontier of capability. But as these models grow more sophisticated, the conversation around how data is stored, queried, and used as a foundation for AI systems becomes more critical than ever. At Apex Grid, we’re building a regtech tool for Nigerian microfinance banks, and in doing so, we’ve had to grapple with the difference between a data substrate and vector databases in the context of RAG.&lt;/p&gt;

&lt;p&gt;In the world of AI-powered applications, the distinction between a data substrate and a vector database is often blurred. A data substrate is a versioned, citable, and queryable layer of structured and unstructured data that serves as a reference for AI systems. It’s about precision, traceability, and governance. On the other hand, a vector database excels at similarity search and is the go-to tool for embedding-based retrieval in RAG systems. Each has its place, but the choice depends on the problem at hand.&lt;/p&gt;

&lt;p&gt;For our regtech tool, we needed to ensure that AI-generated insights could be traced back to specific data sources  -  a non-negotiable requirement in a financial compliance context. When a bank’s AI system flags a suspicious transaction, it’s not enough to say “the model thinks this is risky.” We need to know which regulation it’s referencing, which historical pattern it’s matching, and how the data was processed. That’s where a data substrate shines.&lt;/p&gt;

&lt;p&gt;To illustrate, consider a scenario where our system processes loan applications. A vector database might quickly find similar applications based on embeddings of text and numerical features. But if we later need to audit why a particular application was flagged, the vector database lacks the structure and lineage to answer that. A data substrate, however, can link the AI’s decision to specific, versioned data entries  -  making the system both compliant and explainable.&lt;/p&gt;

&lt;p&gt;Here’s a simplified example of how we structure our data substrate using a SQL-like query to retrieve the source of a decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;decision_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;regulation_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;source_data_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;embedding_hash&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; 
    &lt;span class="n"&gt;ai_decisions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; 
    &lt;span class="n"&gt;decision_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-07-12-001'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query returns the regulation version used, the source data that influenced the decision, and a hash of the embedding that was used in the RAG pipeline. This level of traceability is critical in regulated environments and is a feature that vector databases alone cannot provide.&lt;/p&gt;

&lt;p&gt;That said, vector databases are not obsolete. In our system, we use a vector database for the initial retrieval phase of the RAG pipeline. It’s fast, efficient, and well-suited for finding similar documents or embeddings. But once the relevant data is retrieved, it’s moved into the data substrate for versioning and governance. This hybrid approach ensures we get the best of both worlds  -  speed and relevance from the vector database, and traceability and auditability from the data substrate.&lt;/p&gt;

&lt;p&gt;Tradeoffs are inevitable. Maintaining a data substrate adds complexity and overhead compared to a pure vector DB approach. It requires more storage, more careful schema design, and more rigorous versioning. But for applications in regulated sectors, the tradeoff is worth it. In Nigeria’s microfinance space, where compliance is a top priority, the cost of a misclassified transaction or an untraceable AI decision is far greater than the cost of maintaining a data substrate.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to integrate the data substrate with more dynamic AI models, enabling real-time auditing and feedback loops. We’re also looking into how to make the data substrate more accessible to non-technical users  -  after all, the value of traceable data is only realized when it can be understood and acted upon by the people who need it most. What do you think is the next big challenge in making data substrates both powerful and user-friendly?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>architecture</category>
      <category>regtech</category>
    </item>
    <item>
      <title>GPU Preflight For Cron Agents</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:00:40 +0000</pubDate>
      <link>https://dev.to/apexgridtech/gpu-preflight-for-cron-agents-3na6</link>
      <guid>https://dev.to/apexgridtech/gpu-preflight-for-cron-agents-3na6</guid>
      <description>&lt;p&gt;This week’s news about self-hosted AI coding agents has sparked renewed interest in the reliability of infrastructure that runs these agents. While the focus has been on the agent’s capabilities, we’ve found that the underlying infrastructure needs just as much attention - particularly when it comes to GPU-dependent cron jobs. At Apex Grid Technologies, we’ve been working on a robust preflight system for GPU-dependent cron agents that ensures they only run when the hardware is truly available.&lt;/p&gt;

&lt;p&gt;Our cron agents are responsible for a variety of tasks, from model training to inference pipelines. All of them rely on GPU resources, and we’ve seen firsthand how brittle a simple try/except block can be in this context. A GPU might appear available at the start of a job, only for another task to consume it mid-execution, leading to silent failures or timeouts that are hard to debug. That’s why we’ve built a two-stage preflight system that checks both the health of the GPU and its availability.&lt;/p&gt;

&lt;p&gt;The first stage of our preflight is a lightweight health check: we send a request to &lt;code&gt;/api/tags&lt;/code&gt; and ensure it responds within 3 seconds. This check confirms that the GPU is not only present but also accessible by the system. The second stage is a warmup check, where we send a trivial request to &lt;code&gt;/api/generate&lt;/code&gt; and measure the response time. This helps us detect if the GPU is currently being monopolized by another job. If either of these checks fails, the cron job skips cleanly, avoiding the pitfalls of firing and swallowing an error.&lt;/p&gt;

&lt;p&gt;Here’s how we implement this in our code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gpu_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# First stage: health check
&lt;/span&gt;        &lt;span class="n"&gt;health_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/api/tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;health_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="c1"&gt;# Second stage: warmup check
&lt;/span&gt;        &lt;span class="n"&gt;warmup_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;warmup_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper function, &lt;code&gt;gpu_ready()&lt;/code&gt;, is used before every GPU-dependent cron job. It ensures that the job only runs when the GPU is both healthy and available, preventing resource contention and reducing the risk of silent failures.&lt;/p&gt;

&lt;p&gt;Of course, this approach has its tradeoffs. The warmup check adds some latency to the overall process, which might be problematic for jobs that need to run as quickly as possible. Additionally, the &lt;code&gt;/api/generate&lt;/code&gt; endpoint used in the warmup must be designed to be lightweight - anything too heavy could skew the results or add unnecessary load. We’ve also had to handle edge cases, like when the GPU is in a state of transition (e.g., during a reboot or driver update), where the health check might pass but the warmup fails.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to make this preflight even more intelligent. One idea is to use metrics from the GPU itself - such as utilization or temperature - rather than relying solely on HTTP endpoints. We’re also considering introducing a caching layer that remembers past failures and adjusts the preflight strategy accordingly. What do you think? Would you prefer a more hardware-centric approach, or do you see value in keeping the preflight logic at the application layer?&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>ai</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Ollama v1 OpenAI Compat Drops Think Toggle</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Mon, 07 Sep 2026 15:00:59 +0000</pubDate>
      <link>https://dev.to/apexgridtech/ollama-v1-openai-compat-drops-think-toggle-5a24</link>
      <guid>https://dev.to/apexgridtech/ollama-v1-openai-compat-drops-think-toggle-5a24</guid>
      <description>&lt;p&gt;This week's headlines buzzed with the promise of running large language models at unprecedented speeds, but the technical details beneath the surface often get lost in the hype. As engineers, we care more about the nuts and bolts  -  like when a seemingly minor API change can break a pipeline in unexpected ways. That’s exactly what happened to us when working with Ollama’s OpenAI-compatible endpoint and the Qwen3-family models.&lt;/p&gt;

&lt;p&gt;We were running a service that relied on Ollama’s &lt;code&gt;/v1&lt;/code&gt; endpoint to interface with Qwen3 models, expecting the &lt;code&gt;think: false&lt;/code&gt; toggle to short-circuit the model’s internal reasoning and return a prompt-based response. Instead, we noticed that the models were consistently exhausting their &lt;code&gt;num_predict&lt;/code&gt; budget and returning empty content. This was a silent failure  -  no errors, no logs, just empty strings where we expected output.&lt;/p&gt;

&lt;p&gt;After some digging, we realized the issue was not with the model itself, but with the endpoint. The &lt;code&gt;/v1&lt;/code&gt; OpenAI-compatible endpoint silently ignored the &lt;code&gt;think: false&lt;/code&gt; parameter for Qwen3-family models. This meant the model would process the entire prompt, perform internal reasoning, and then  -  with no tokens left to predict  -  return nothing. It was a classic case of a misaligned expectation between the API and the model's behavior.&lt;/p&gt;

&lt;p&gt;To fix this, we pivoted to Ollama’s native &lt;code&gt;/api/chat&lt;/code&gt; endpoint, which respects the &lt;code&gt;think: false&lt;/code&gt; toggle as intended. Here’s a comparison of the two payloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OpenAI-compat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(fails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Qwen&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is the capital of France?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"num_predict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Native&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/chat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(works&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;expected)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is the capital of France?"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"num_predict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference lies in how the &lt;code&gt;think&lt;/code&gt; parameter is interpreted and where it's placed in the payload. The &lt;code&gt;/v1&lt;/code&gt; endpoint appears to ignore &lt;code&gt;think: false&lt;/code&gt; for certain models, while the native endpoint correctly honors it. We noticed the issue by monitoring the &lt;code&gt;num_predict&lt;/code&gt; usage and correlating it with the output length. When the &lt;code&gt;num_predict&lt;/code&gt; budget was consumed but no output was returned, it became clear that something was wrong in the reasoning phase.&lt;/p&gt;

&lt;p&gt;This experience highlights the importance of testing across endpoints and understanding the nuances of model-specific behavior. While the &lt;code&gt;/v1&lt;/code&gt; endpoint is convenient for compatibility with OpenAI tools, it’s not a one-size-fits-all solution  -  especially for models like Qwen3 that have unique interaction patterns.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to unify our internal tooling to handle these endpoint differences more gracefully. We’re also considering contributing to the Ollama project to clarify the behavior of the &lt;code&gt;/v1&lt;/code&gt; endpoint for specific models. What would you do if you encountered a similar silent failure in your AI pipeline?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>debugging</category>
      <category>python</category>
    </item>
    <item>
      <title>Postiz Self Host Bluesky Python Client</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:00:18 +0000</pubDate>
      <link>https://dev.to/apexgridtech/postiz-self-host-bluesky-python-client-3pm9</link>
      <guid>https://dev.to/apexgridtech/postiz-self-host-bluesky-python-client-3pm9</guid>
      <description>&lt;p&gt;This week, the AI space saw a surge in open-source enthusiasm, with new tools and libraries capturing developer attention. While the headlines focus on stars and strategic partnerships, we’re diving into the nitty-gritty of how we’ve built a self-hosted Postiz instance using Docker Compose and connected it with a Python client that communicates via the &lt;code&gt;/api/public/v1/posts&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;At Apex Grid Technologies, we’ve been evaluating lightweight, self-hosted social platforms as part of our broader exploration into on-device AI integration. Postiz stood out for its minimalism and open API, which made it a natural fit for our experimentation. We chose to self-host it using Docker Compose to ensure we had full control over the environment and could easily scale or modify it as needed.&lt;/p&gt;

&lt;p&gt;Setting up Postiz with Docker Compose was straightforward. We used the official Docker image and configured it with our desired settings, such as database connections and authentication options. Here’s a simplified version of our &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postiz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz/postiz:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTIZ_DB_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://user:password@db:5432/postiz"&lt;/span&gt;
      &lt;span class="na"&gt;POSTIZ_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supersecretkey"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the Postiz instance was up and running, we built a Python client to interact with its API. We focused on the &lt;code&gt;/api/public/v1/posts&lt;/code&gt; endpoint, which allows for creating and retrieving posts. The client is built using &lt;code&gt;requests&lt;/code&gt;, and we handle both the JSON payload and the response structure carefully.&lt;/p&gt;

&lt;p&gt;Here’s how we construct the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &amp;lt;your-jwt-token&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is a test post from the Python client.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visibility&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/api/public/v1/posts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received a list of posts:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received a single post:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve noticed that the &lt;code&gt;/api/public/v1/posts&lt;/code&gt; endpoint can return either a single object or a list depending on the context  -  for example, when creating a new post, it returns the created object, but when fetching all posts, it returns a list. This is a subtle but important detail for any client consuming the API. We handle this by checking the type of the response using &lt;code&gt;isinstance()&lt;/code&gt; and processing accordingly.&lt;/p&gt;

&lt;p&gt;One tradeoff we’ve made is the lack of advanced features like rate limiting or more granular permissions, which are not exposed through the public API. However, for our use case, this is acceptable, and we can always extend the client or the Postiz setup if needed.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to integrate this setup with our on-device AI models. The idea is to have a local, self-hosted social layer that can interact with AI-generated content in real time, without relying on external cloud services. We’re also looking into ways to make the Python client more robust, including support for async requests and better error handling.&lt;/p&gt;

&lt;p&gt;What are your thoughts on self-hosted social platforms and their potential for AI integration?&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>socialmedia</category>
      <category>docker</category>
    </item>
    <item>
      <title>Cron Scheduled Ollama Autonomous Agent</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Tue, 01 Sep 2026 15:00:22 +0000</pubDate>
      <link>https://dev.to/apexgridtech/cron-scheduled-ollama-autonomous-agent-30ph</link>
      <guid>https://dev.to/apexgridtech/cron-scheduled-ollama-autonomous-agent-30ph</guid>
      <description>&lt;p&gt;This week’s news about new constraints on AI agents within networked systems reminded us of a critical design decision we made early in our work on autonomous social agents. While the headlines focus on policy and governance, the real challenge lies in the infrastructure that enables these agents to operate safely and autonomously. Here's how we built one such agent using Ollama, cron scheduling, and a layered gate system to ensure responsible behavior.&lt;/p&gt;

&lt;p&gt;We're building a social agent that autonomously generates content for multiple channels. The agent runs on a Linux server, with a cron job triggering it every hour. The core of the agent is an Ollama model that generates text, but before any content is published, it must pass through a series of gates to ensure it's safe, relevant, and aligned with our values.&lt;/p&gt;

&lt;p&gt;Our stack runs on a combination of Ollama for the LLM, Postgres for data storage, and Postiz for message queuing. We chose Postiz over other systems because of its lightweight design and compatibility with our existing infrastructure. The agent’s workflow is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Voice Profile Check&lt;/strong&gt;: Each user has their own voice profile stored as a JSON file. The agent loads this profile to determine the tone, style, and personality of the generated content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controversy Gate&lt;/strong&gt;: A second Ollama model acts as a classifier, scanning the generated text for any potentially controversial or harmful content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit Gate&lt;/strong&gt;: A Postgres query checks the Postiz DB to ensure the agent has sufficient credits or permissions to publish the content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarantine Folder&lt;/strong&gt;: Any content that fails the gates is moved to a quarantine folder for review before being discarded or reprocessed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the function signature for &lt;code&gt;publish_to_all_channels&lt;/code&gt;, which encapsulates the entire process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publish_to_all_channels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice_profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;check_voice_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voice_profile&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Voice profile check failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;controversy_gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Controversy gate failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;credit_gate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Credit gate failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CHANNELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;send_to_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generated_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to publish content: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;move_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these gates has its own tradeoffs. The voice profile check ensures consistency, but it adds overhead in terms of file I/O and memory usage. The controversy gate is computationally expensive, as it involves running a second model. The credit gate introduces a dependency on Postgres and Postiz, which can be a bottleneck under high load.&lt;/p&gt;

&lt;p&gt;We're actively working on optimizing the controversy gate by experimenting with smaller, more efficient models that can run locally without sacrificing accuracy. We're also exploring ways to batch process content to reduce the number of model inferences required.&lt;/p&gt;

&lt;p&gt;What we're building next is a more distributed version of this agent that can run on multiple nodes, with each node handling a subset of the channels. This will help scale the system and reduce the load on any single component. We're also considering integrating real-time feedback from users to dynamically adjust the voice profile and gate thresholds.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Ninety-one days: why we built a food price tracker, and why we need you to fill it</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Mon, 31 Aug 2026 15:00:02 +0000</pubDate>
      <link>https://dev.to/apexgridtech/ninety-one-days-why-we-built-a-food-price-tracker-and-why-we-need-you-to-fill-it-3gpn</link>
      <guid>https://dev.to/apexgridtech/ninety-one-days-why-we-built-a-food-price-tracker-and-why-we-need-you-to-fill-it-3gpn</guid>
      <description>&lt;p&gt;The most recent official figure for what food costs in Nigeria describes &lt;strong&gt;May 2026&lt;/strong&gt;. Today is 30 August. That is &lt;strong&gt;ninety-one days&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not an accusation. The National Bureau of Statistics publishes the Selected Food Price Watch monthly, it is careful work with a real sample frame, and it is current through May — we checked the catalogue this week and pulled the file. Every statistical office in the world publishes with a lag, because doing it properly takes time.&lt;/p&gt;

&lt;p&gt;But a lag is still a lag. A woman deciding what to put in the pot this week cannot use a number about May. Neither can a trader pricing stock, a lender sizing a loan, or an economist trying to see inflation while it is happening rather than after it has finished.&lt;/p&gt;

&lt;p&gt;So we built the fast half: &lt;strong&gt;&lt;a href="https://foodtracker.apexgridapps.com" rel="noopener noreferrer"&gt;foodtracker.apexgridapps.com&lt;/a&gt;&lt;/strong&gt;. Crowd-reported retail food prices, by state, in the units people actually buy in. It updates the moment someone reports a price.&lt;/p&gt;

&lt;p&gt;It does not replace the official series and it is not trying to. NBS remains the statutory record, and where the two disagree, NBS is the one that counts. What we can add is speed — and, over time, something more interesting: &lt;strong&gt;the gap between the two is itself a measurement&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this came from
&lt;/h2&gt;

&lt;p&gt;The idea did not start with us. It came out of a set of questions from &lt;strong&gt;Queen Esther Oye&lt;/strong&gt; , one of the advisors who reviews our work.&lt;/p&gt;

&lt;p&gt;She asked three things that all turned out to need the same missing ingredient: a model of how food inflation would move in the fourth quarter; whether diaspora remittances act as informal adaptation grants when food prices spike and state safety nets fail; and a measure of purchasing power adjusted for localised food shocks rather than a national average.&lt;/p&gt;

&lt;p&gt;Every one of those needs food prices &lt;strong&gt;by state&lt;/strong&gt; , close to now. We went to look at what we held.&lt;/p&gt;

&lt;p&gt;We held 229 rows. One month. Nine of thirty-seven states.&lt;/p&gt;

&lt;p&gt;That is the honest inventory. We had built engines that needed state-level food prices and we did not have state-level food prices. You cannot model a localised food shock from nine states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ninety-one days is not a rounding error in food
&lt;/h2&gt;

&lt;p&gt;With most numbers, a quarter’s lag is an inconvenience. With food it can invert the answer.&lt;/p&gt;

&lt;p&gt;Nigerian food prices are seasonal, and the season that matters most is the one we are in. Grain stocks run down between planting and harvest, and prices climb through the lean months before falling back when the new crop arrives. A figure describing May, read in late August, is not simply an old number — it is a number from the &lt;em&gt;other side&lt;/em&gt; of that curve, and nothing in it tells you which way things have moved since.&lt;/p&gt;

&lt;p&gt;So the honest position is not that a May figure is a bit out of date. It is that &lt;strong&gt;a May figure cannot answer an August question at all&lt;/strong&gt; , and quoting it as though it can is how a stale number does real damage. It is not a price any more; it is a claim about the past wearing today’s clothes.&lt;/p&gt;

&lt;p&gt;That is the gap this fills. Not better than the official series — sooner than it, and openly rougher.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are asking
&lt;/h2&gt;

&lt;p&gt;The tracker is live and nearly empty. That is the awkward stage of every crowd project, and there is only one way through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Report one price.&lt;/strong&gt; Open the site, tap the item you bought, confirm the price if it looks right or type what you actually paid. It takes about fifteen seconds. No account, no email, no phone number — we do not collect your name or your IP address, and there is nothing to sign up for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then send it to somebody in another state.&lt;/strong&gt; This is the part that matters more than the reporting. A food price tracker with ten states is a Lagos newsletter. Nigeria has thirty-seven, and the prices that move first are rarely the ones in the cities that already get written about.&lt;/p&gt;

&lt;p&gt;We are tracking the basket people actually buy: rice, beans, gari, yam, tomatoes, pepper, onions, palm oil, bread, eggs, with millet, sorghum, maize and the rest one tap away. Prices are reported in market units — per mudu, per paint, per tuber — because asking a shopper for a price per kilogram gets you a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules we set ourselves
&lt;/h2&gt;

&lt;p&gt;A crowd tracker is only worth having if it is honest about being one.&lt;/p&gt;

&lt;p&gt;Every figure carries &lt;strong&gt;how many people reported it, the spread between the highest and lowest, and how old the newest report is&lt;/strong&gt;. A median of one report is labelled &lt;em&gt;“ single report — not a price”&lt;/em&gt;, because that is what it is. There is no way to get the bare number out of our API without that context attached — we built it that way deliberately, since the bare number is what ends up quoted.&lt;/p&gt;

&lt;p&gt;And we do not convert a mudu into a kilogram. A mudu of gari and a mudu of beans do not weigh the same, and the vessel varies by region and by trader. Publishing a kilogram price we had not measured would be inventing precision. So the conversion factor sits in our data as &lt;code&gt;null&lt;/code&gt;, marked &lt;code&gt;UNCALIBRATED&lt;/code&gt;, until somebody weighs it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters who counts
&lt;/h2&gt;

&lt;p&gt;There is a version of this country’s economic story that is told entirely from outside it — assembled from satellite passes, wire copy, and quarterly reports written three months after the fact. It is not usually wrong. It is just late, and thin, and shaped by whoever had the budget to collect it.&lt;/p&gt;

&lt;p&gt;The alternative is not complaining about that. It is counting.&lt;/p&gt;

&lt;p&gt;A woman in Gombe reporting what she paid for a mudu of rice this morning is doing statistics. Ten thousand of those, across thirty-seven states, is a national series that belongs to the people who generated it — published under a licence that keeps it free for researchers, journalists and NGOs, with the code open so anyone can check our arithmetic or run their own copy.&lt;/p&gt;

&lt;p&gt;We are ninety-one days behind on what food costs. We do not have to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://foodtracker.apexgridapps.com" rel="noopener noreferrer"&gt;foodtracker.apexgridapps.com&lt;/a&gt;&lt;/strong&gt; — report one price, then share it with someone in a state we are missing.&lt;/p&gt;

</description>
      <category>africa</category>
    </item>
    <item>
      <title>Publishing Pipeline Scheduled vs Immediate</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:00:14 +0000</pubDate>
      <link>https://dev.to/apexgridtech/publishing-pipeline-scheduled-vs-immediate-2eho</link>
      <guid>https://dev.to/apexgridtech/publishing-pipeline-scheduled-vs-immediate-2eho</guid>
      <description>&lt;p&gt;This week, AWS announced a new version of AWS Glue that promises to make data integration faster and more cost-effective. While that’s impressive, we’re more interested in the underlying challenge: how to reliably move data across systems, especially when timing and consistency matter. At Apex Grid, we’ve wrestled with a similar problem in our publishing pipeline  -  specifically, how to balance the need for scheduled content with the immediacy of real-time updates.&lt;/p&gt;

&lt;p&gt;We run a hybrid publishing system. Some content is scheduled  -  think social media posts that go out at optimal times for different timezones  -  while other content, like long-form articles, is published immediately, often triggered by a cron job or an event. This duality introduces a subtle but critical problem: managing canonical URLs across different publishing channels and timelines.&lt;/p&gt;

&lt;p&gt;In a perfect world, every piece of content would have a single, unchanging URL. But when you publish the same article both immediately and later as part of a scheduled post, you end up with multiple URLs pointing to the same content. This breaks the web’s expectations around link consistency and SEO, and it complicates analytics when you’re tracking user engagement across platforms.&lt;/p&gt;

&lt;p&gt;Our approach to solving this has been to introduce a canonical URL system that dynamically resolves to the correct version of the content based on context. Here’s how it works in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_canonical_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# context can be 'social', 'web', or 'email'
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;social&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# return scheduled version
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://apexgrid.com/social/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;article_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;web&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# return the original, long-form version
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://apexgrid.com/articles/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;article_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# fallback or other contexts
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://apexgrid.com/articles/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;article_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function isn’t magic. It relies on a backend mapping system that tracks all versions of an article and their associated contexts. Every time a new version is published  -  whether scheduled or immediate  -  we update this mapping. When a user clicks on a link, the system resolves the canonical URL in real time, ensuring they land on the correct version of the content.&lt;/p&gt;

&lt;p&gt;But this isn’t without tradeoffs. Managing multiple URLs increases complexity in our CMS and affects caching strategies. We’ve had to invest in a robust URL resolver that can handle edge cases, like when a scheduled post is published early or when an immediate post is later rescheduled. It also requires careful coordination with our analytics team to ensure that user behavior is tracked consistently across all versions.&lt;/p&gt;

&lt;p&gt;Despite these challenges, the hybrid model has proven valuable. It allows us to maintain the immediacy of our long-form content while still leveraging the power of scheduling for social media. It’s a balance that’s not easy to achieve, but it’s one we’re confident in.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re working on a more intelligent version of this system that can automatically detect when a scheduled post is no longer needed  -  for example, if an article is updated and the scheduled version is now obsolete. We’re also exploring ways to unify our content models further, so that the distinction between scheduled and immediate publishing becomes less of a technical burden and more of a strategic choice.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>socialmedia</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Controversy Gate Second Model Check</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:00:15 +0000</pubDate>
      <link>https://dev.to/apexgridtech/controversy-gate-second-model-check-43k0</link>
      <guid>https://dev.to/apexgridtech/controversy-gate-second-model-check-43k0</guid>
      <description>&lt;p&gt;This week’s release of a Go-based agent framework brought renewed attention to the balance between automation and oversight in AI systems. While the framework promises improved user experience, it also raises questions about the reliability of AI in high-stakes scenarios - questions that are especially relevant when it comes to autonomous systems making decisions on our behalf.&lt;/p&gt;

&lt;p&gt;We’ve been working on a system that allows for autonomous social posting, but with a key twist: the content is drafted by one model, then scored for potential controversy or risk by a &lt;em&gt;second&lt;/em&gt; model before being published. This two-model architecture helps reduce the chance of auto-published embarrassment without requiring constant human intervention. The idea is simple but powerful: trust the model to generate ideas, but let a different model act as a gatekeeper, filtering out content that might be harmful, controversial, or just plain unwise.&lt;/p&gt;

&lt;p&gt;Here's a high-level look at how the system works in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSequenceClassification&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="c1"&gt;# Primary model: generates content
&lt;/span&gt;&lt;span class="n"&gt;primary_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSequenceClassification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;primary_tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Secondary model: evaluates risk
&lt;/span&gt;&lt;span class="n"&gt;secondary_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSequenceClassification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secondary-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;secondary_tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secondary-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;primary_tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;primary_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;primary_tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;skip_special_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_risk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;secondary_tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;secondary_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;risk_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# assuming class 1 is "controversial"
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;risk_score&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_if_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_risk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Publishing:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# publish_to_platform(draft)
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Risk score too high:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern has its merits: the primary model can be optimized for creativity, while the secondary model can be fine-tuned for sensitivity and risk detection. It also allows for a degree of separation between the model that creates and the model that evaluates, which can help mitigate the risk of bias or overconfidence in a single model’s judgment.&lt;/p&gt;

&lt;p&gt;However, the two-model approach isn’t foolproof. There are scenarios where the primary model may generate content that is clearly controversial, but the secondary model fails to flag it. This can happen if the secondary model is not trained on a sufficiently diverse dataset or if the two models have overlapping biases. In some cases, the secondary model may even flag non-controversial content as risky if its training data is skewed toward certain types of language or topics.&lt;/p&gt;

&lt;p&gt;We're actively exploring ways to improve the robustness of this architecture. One idea is to use a third model for cross-validation, or to introduce a lightweight human-in-the-loop system for high-risk content. Another is to fine-tune the secondary model using a synthetic dataset that includes a wide range of edge cases and ambiguous content scenarios.&lt;/p&gt;

&lt;p&gt;What do you think? Are there other patterns or techniques you’ve seen that help reduce the risk of auto-published embarrassment without sacrificing autonomy?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>safety</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
