<?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>Cron Scheduled Ollama Autonomous Agent</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:00:29 +0000</pubDate>
      <link>https://dev.to/apexgridtech/cron-scheduled-ollama-autonomous-agent-3338</link>
      <guid>https://dev.to/apexgridtech/cron-scheduled-ollama-autonomous-agent-3338</guid>
      <description>&lt;p&gt;This week, the AI infrastructure space saw some eye-opening benchmarks, with modular systems showing dramatic performance gains over alternatives. Amid all the noise, we’ve been quietly iterating on a specific use case that sits at the intersection of autonomous agents and on-device AI: a cron-scheduled Ollama-based social agent that runs on our infrastructure and interacts with users across multiple channels.&lt;/p&gt;

&lt;p&gt;We’re building a system where an autonomous agent generates content at regular intervals, processes it through multiple gates, and only publishes it if it passes all checks. The core of this system is Ollama, which we use for its lightweight footprint and ability to run large language models locally. To make this work, we’ve designed a modular architecture that separates concerns: voice profiles are stored as separate files, controversy is checked by a second model, and credit checks are made by querying a Postgres database. Rejected content is quarantined for later review.&lt;/p&gt;

&lt;p&gt;Here’s how the publish pipeline looks in code. We’ve abstracted the main function as &lt;code&gt;publish_to_all_channels&lt;/code&gt;, which is called by a cron job every 12 hours. The function is responsible for generating content, running it through the gates, and publishing if all checks pass:&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;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&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="c1"&gt;# Step 1: Generate content using Ollama
&lt;/span&gt;    &lt;span class="n"&gt;client&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a social media post about AI ethics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;generated_content&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="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;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Run through voice profile check
&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;generated_content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;log_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_content&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_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="c1"&gt;# Step 3: Run through controversy gate using a second model
&lt;/span&gt;    &lt;span class="n"&gt;controversy_client&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;model&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-checker&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_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controversy_client&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="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;generated_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_result&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;controversial&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;high_risk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;log_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_content&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="c1"&gt;# Step 4: Run through credit gate by querying Postiz DB
&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTIZ_DB_NAME&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTIZ_DB_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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTIZ_DB_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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTIZ_DB_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTIZ_DB_PORT&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="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 * FROM credits WHERE content = %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="n"&gt;generated_content&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;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="nf"&gt;log_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_content&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="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;Database 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;log_to_quarantine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database_error&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="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 5: Publish the content to all channels
&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;generated_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach lets us keep the system modular and scalable. Each gate is a self-contained function that can be swapped out or updated independently. For example, we could replace the controversy-checker model with a different one if needed, or even switch to a rule-based system for certain use cases. The quarantine system is equally important  -  it allows us to review content that failed any gate, and potentially reprocess it later if conditions change.&lt;/p&gt;

&lt;p&gt;There are tradeoffs, of course. By using cron jobs, we’re introducing a fixed interval between runs, which may not be ideal for real-time use cases. Also, the current setup assumes a single content generation per run, which might not scale well for high-frequency publishing. We’re actively exploring ways to decouple the scheduling from the processing pipeline, and are looking into event-driven architectures that could allow for more dynamic content generation and publishing.&lt;/p&gt;

&lt;p&gt;We’re also working on improving the performance of the controversy gate  -  the second model check is currently a bit slow, and we’re experimenting with model quantization and caching to reduce latency. What do you think is the right balance between accuracy and speed in this kind of gate? We’d love to hear your thoughts in the comments.&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, 10 Oct 2026 15:00:28 +0000</pubDate>
      <link>https://dev.to/apexgridtech/publishing-pipeline-scheduled-vs-immediate-2o2n</link>
      <guid>https://dev.to/apexgridtech/publishing-pipeline-scheduled-vs-immediate-2o2n</guid>
      <description>&lt;p&gt;This week, the AI world turned its gaze toward Beam, a new model that's stirring conversations about the next wave of generative AI. But while the world is talking about models, we're talking about the systems that bring them to life  -  specifically, how we balance the immediacy of content delivery with the strategic power of scheduling.&lt;/p&gt;

&lt;p&gt;At Apex Grid, we're building a publishing pipeline that bridges the gap between immediate content delivery and scheduled publishing. Our platform, Postiz, handles both: scheduling social posts to hit user timezones precisely, and publishing long-form content on a cron-fire. The challenge? Ensuring consistency across channels, especially when dealing with canonical URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Consistency Across Channels
&lt;/h3&gt;

&lt;p&gt;When content is published immediately, it's straightforward  -  the URL is fixed, and it's shared across all platforms. But when content is scheduled, the URL might not be available until the post is published. This creates a disconnect between the scheduled post (which may be shared on social media with a placeholder URL) and the final published article (which has its own canonical URL). &lt;/p&gt;

&lt;p&gt;The result? A fragmented user experience, broken links, and a lack of trust in the content's integrity. For a technical audience, this is more than a UX issue  -  it's a data integrity problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our Approach: Hybrid Publishing with Smart Routing
&lt;/h3&gt;

&lt;p&gt;Our solution is a hybrid publishing model that leverages both scheduled and immediate publishing. For social media, we schedule posts based on user timezones using a cron-based system that respects local time zones. For long-form content, we publish immediately using a webhook-driven architecture. &lt;/p&gt;

&lt;p&gt;The key lies in our internal routing system, which maps scheduled posts to their canonical URLs once the content is published. This system uses a lightweight event-driven architecture, where each scheduled post is associated with a content ID that's resolved at publish time.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of our routing logic in Python:&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;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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytz&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_canonical_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Fetch the canonical URL from the database based on the post ID
&lt;/span&gt;    &lt;span class="n"&gt;canonical_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&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 canonical_url FROM posts WHERE id = ?&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="n"&gt;post_id&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;canonical_url&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;canonical_url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone_str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scheduled_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&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;minute&lt;/span&gt;&lt;span class="o"&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;second&lt;/span&gt;&lt;span class="o"&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;microsecond&lt;/span&gt;&lt;span class="o"&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;scheduled_time_utc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scheduled_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Schedule the post to be published at scheduled_time_utc
&lt;/span&gt;    &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;publish_post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;run_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scheduled_time_utc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&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_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;canonical_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_canonical_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_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;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Update the scheduled post with the canonical URL
&lt;/span&gt;        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE scheduled_posts SET canonical_url = ? WHERE post_id = ?&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="n"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;# Trigger the webhook to publish the long-form content
&lt;/span&gt;        &lt;span class="n"&gt;webhook_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&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://api.postiz.com/webhooks/publish/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post_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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Handle error: no canonical URL found
&lt;/span&gt;        &lt;span class="n"&gt;logger&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;Could not find canonical URL for post ID &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post_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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tradeoffs: Flexibility vs Complexity
&lt;/h3&gt;

&lt;p&gt;This hybrid approach gives us the flexibility to schedule posts for optimal engagement while ensuring that all content is published with a consistent canonical URL. However, it adds complexity to our system  -  we now have to manage two different publishing workflows and ensure that the mapping between scheduled posts and canonical URLs is always accurate.&lt;/p&gt;

&lt;p&gt;We also have to be careful with time zones and cron scheduling, which can be a pain point for any system that needs to respect local time. Our current solution uses the &lt;code&gt;pytz&lt;/code&gt; library to handle time zone conversions, but we're always on the lookout for more robust, future-proof alternatives.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;We're currently working on a new feature that will allow users to preview scheduled posts with their canonical URLs before they're published. This will help reduce errors and improve the user experience for our content creators. We’re also exploring ways to unify the scheduling and publishing workflows into a single, more intuitive interface.&lt;/p&gt;

&lt;p&gt;What do you think about the tradeoffs between scheduling and immediate publishing in content platforms? Have you faced similar challenges in your 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, 07 Oct 2026 15:00:27 +0000</pubDate>
      <link>https://dev.to/apexgridtech/controversy-gate-second-model-check-5aal</link>
      <guid>https://dev.to/apexgridtech/controversy-gate-second-model-check-5aal</guid>
      <description>&lt;p&gt;This week, Cloudflare introduced Clef, a new open-source decision model and fine-tuning platform that shifts the focus from just generating content to evaluating it. As the conversation around AI-generated content and its societal impact grows, we’re seeing a shift toward systems that don’t just create, but also &lt;em&gt;evaluate&lt;/em&gt; what they create. At Apex Grid, we've been exploring a similar pattern in our work on autonomous social posting, where the risk of embarrassment or controversy is high.&lt;/p&gt;

&lt;p&gt;The problem we're solving is straightforward: when an autonomous system generates content for public consumption  -  think social media posts, customer service replies, or even internal communications  -  it must be evaluated for potential risk before publication. The naive approach is to use a single model to both generate and evaluate content. But that’s a bad idea. The same model that’s incentivized to be creative might not be the best judge of its own output. Worse, it might be biased toward its own style, missing real-world nuance.&lt;/p&gt;

&lt;p&gt;Our approach is to split the work between two models: one for &lt;em&gt;drafting&lt;/em&gt; content, and another for &lt;em&gt;scoring&lt;/em&gt; it for controversy or risk. The first model, let’s call it the &lt;em&gt;generator&lt;/em&gt;, is optimized for fluency, creativity, and relevance. The second, the &lt;em&gt;evaluator&lt;/em&gt;, is optimized for detecting offensive, controversial, or high-risk content. The evaluator is trained on a diverse dataset of annotated examples, and it returns a score that the system can use to decide whether to publish, edit, or reject the content.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of what our system looks like in 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;from&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;evaluator&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Evaluator&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;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generator-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;evaluator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Evaluator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evaluator-v1&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&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;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluator&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# threshold for acceptable risk
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;draft&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content flagged for review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This two-model architecture has a number of benefits. First, it creates a clear separation of concerns  -  the generator doesn’t need to worry about being a judge, and the evaluator doesn’t need to be a creative writer. Second, it allows us to fine-tune each model independently, with different data and objectives. And third, it opens up the possibility of using specialized models for evaluation, such as those trained on legal, cultural, or ethical datasets.&lt;/p&gt;

&lt;p&gt;But this approach is not without its tradeoffs. One major challenge is ensuring that the evaluator model isn’t too strict or too lenient. If it’s too strict, it might prevent legitimate content from being published. If it’s too lenient, it might allow harmful content to slip through. Balancing this requires careful calibration, and it’s an active area of research for us.&lt;/p&gt;

&lt;p&gt;Another challenge is the computational overhead. Running two models in sequence increases latency and resource usage, which can be a concern in real-time systems. We’ve experimented with various optimizations, like model quantization and caching, to reduce this impact without sacrificing accuracy.&lt;/p&gt;

&lt;p&gt;What’s next? We’re exploring ways to integrate real-time feedback from users into the evaluator model, so it can learn from the actual consequences of its decisions. We’re also looking into hybrid models that can both generate and evaluate content, but with internal mechanisms that prevent self-bias. And finally, we’re considering how to make this pattern more general  -  not just for social posting, but for any system where autonomous agents need to assess the risk of their own output.&lt;/p&gt;

&lt;p&gt;How might you apply this two-model pattern in your own work? Are there use cases where a separate evaluator model is essential, and where it’s not worth the cost?&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, 04 Oct 2026 15:00:21 +0000</pubDate>
      <link>https://dev.to/apexgridtech/data-substrate-versus-vector-db-rag-3312</link>
      <guid>https://dev.to/apexgridtech/data-substrate-versus-vector-db-rag-3312</guid>
      <description>&lt;p&gt;This week, news emerged about open-source tools that aim to reduce reliance on Nvidia's CUDA ecosystem, highlighting a broader shift in how AI infrastructure is being built. While the focus was on compute and communication libraries, we found ourselves thinking about the layers beneath  -  the data substrate that powers AI applications and the tradeoffs between vector databases and data substrates in real-world use cases.&lt;/p&gt;

&lt;p&gt;At Apex Grid, we're building a regtech tool for Nigerian microfinance banks, where compliance and traceability are not just features  -  they're requirements. One of the core challenges we faced was ensuring that every AI inference could be traced back to a specific version of training data, with citable sources and audit logs. This is where a data substrate  -  a versioned, citable, and queryable reference layer for AI  -  becomes essential.&lt;/p&gt;

&lt;p&gt;Vector databases and RAG (Retrieval-Augmented Generation) systems are powerful for specific use cases, like semantic search or chatbot memory. But they're not designed for the kind of traceability and version control that regulatory environments demand. For instance, a vector DB might store embeddings of documents, but it doesn't track which version of a document was used in a particular inference. RAG-over-files might pull data from a corpus, but it lacks the fine-grained metadata required for compliance audits.&lt;/p&gt;

&lt;p&gt;To illustrate, here's a simplified example of how we model data in our regtech tool using a data substrate approach. We maintain a versioned dataset of loan applications, each with a unique identifier, timestamp, and provenance:&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;class&lt;/span&gt; &lt;span class="nc"&gt;LoanApplication&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;application_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&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="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provenance&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;application_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;application_id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;
        &lt;span class="n"&gt;self&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;data&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provenance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provenance&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_provenance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each time a new version of a loan application is submitted, it's stored as a new entry in the dataset. When an AI model is trained or queried, it references the specific version and provenance of the data used. This ensures that any regulatory inquiry can be traced back to the exact data and version used in the inference.&lt;/p&gt;

&lt;p&gt;Vector databases, on the other hand, are better suited for use cases where the primary concern is speed and similarity search. For instance, a customer support AI that needs to quickly find similar past interactions would benefit from a vector DB. But in our case, the need for traceability and auditability made a data substrate the right choice.&lt;/p&gt;

&lt;p&gt;That said, there are tradeoffs. A data substrate requires more storage and careful design to manage versioning and provenance. It also introduces complexity in querying and indexing, especially when dealing with large datasets. Vector DBs are more performant for certain types of queries but lack the metadata richness of a data substrate.&lt;/p&gt;

&lt;p&gt;We're now working on a hybrid approach: using a data substrate for core compliance and audit use cases, while leveraging vector DBs for parts of the system that require high-speed similarity search, like fraud detection. This allows us to maintain the traceability we need without sacrificing performance.&lt;/p&gt;

&lt;p&gt;What's next? We're exploring how to integrate this hybrid model with on-device AI, ensuring that even when data is processed locally, it remains citable and traceable. We're also looking into how to make the data substrate more interoperable with existing AI frameworks  -  a challenge that's only going to become more pressing as the industry moves away from monolithic systems toward modular, auditable components.&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, 01 Oct 2026 15:00:22 +0000</pubDate>
      <link>https://dev.to/apexgridtech/gpu-preflight-for-cron-agents-33gg</link>
      <guid>https://dev.to/apexgridtech/gpu-preflight-for-cron-agents-33gg</guid>
      <description>&lt;p&gt;This week, the AI landscape saw a major leap with the release of a full-duplex voice model capable of real-time decision-making. While impressive, such models often rely on robust infrastructure to ensure consistent performance. At Apex Grid Technologies, we’ve been working on ensuring that every GPU-dependent cron job runs reliably, even in the face of unpredictable resource contention.&lt;/p&gt;

&lt;p&gt;We build and run a fleet of cron agents that interface with GPU-accelerated workloads. These agents are responsible for training models, running inference, and managing data pipelines. But in a shared environment, where multiple jobs might be competing for the same GPU, a naive try/except block around a GPU operation is not enough. It can lead to silent failures, partial execution, and wasted compute hours.&lt;/p&gt;

&lt;p&gt;To solve this, we’ve implemented a two-stage preflight check before any GPU-dependent cron job is allowed to proceed. The first stage checks whether the GPU is accessible and responsive. We send a lightweight request to &lt;code&gt;/api/tags&lt;/code&gt;, which should return within 3 seconds. This helps us detect if the GPU is even online or if the service is unreachable. The second stage is a warmup check: we send a trivial inference request to &lt;code&gt;/api/generate&lt;/code&gt;, which should return within a short timeout window (e.g., 10 seconds). This helps us detect if the GPU is being monopolized by another job, which would cause our warmup request to hang indefinitely.&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Stage 1: Check if the GPU is accessible
&lt;/span&gt;        &lt;span class="n"&gt;tags_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="n"&gt;tags_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="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;exceptions&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;span class="c1"&gt;# Stage 2: Check if the GPU can handle a warmup request
&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;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;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: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;json&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;prompt&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;warmup&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;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="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="n"&gt;warmup_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="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;exceptions&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;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 function returns &lt;code&gt;False&lt;/code&gt; if either of the checks fails. In such cases, the cron job cleanly skips execution and logs the failure. This avoids the common pitfall of trying to run a GPU job and then failing silently, which can be difficult to debug later.&lt;/p&gt;

&lt;p&gt;There are tradeoffs to this approach. The preflight adds some latency to the cron job’s execution, as it requires two HTTP requests before any real work is done. However, this latency is minimal compared to the cost of running a job that will fail due to resource contention. Additionally, the preflight checks are not foolproof. For example, if the GPU is in a state of partial failure (e.g., one kernel is stalled but others are healthy), the preflight might not detect it. But in practice, this is rare, and the preflight is sufficient for most use cases.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to make these preflight checks even more robust. One idea is to use GPU metrics from the host system (e.g., via NVIDIA’s &lt;code&gt;nvidia-smi&lt;/code&gt;) to get a more direct view of the GPU’s state. We’re also considering integrating with Kubernetes and other orchestration systems to allow for more intelligent scheduling of GPU jobs. What do you think? Have you encountered similar issues with GPU contention in your own workflows?&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, 28 Sep 2026 15:00:22 +0000</pubDate>
      <link>https://dev.to/apexgridtech/ollama-v1-openai-compat-drops-think-toggle-5ek5</link>
      <guid>https://dev.to/apexgridtech/ollama-v1-openai-compat-drops-think-toggle-5ek5</guid>
      <description>&lt;p&gt;This week, the open-source community buzzed with excitement around LiveKit’s new WebRTC stack and cloud offerings. As developers and engineers, we’re always on the lookout for tools that simplify real-time communication and AI integration. But even with these advancements, we often run into subtle but impactful issues in the stack that require deep debugging. One such incident recently occurred in our work with Ollama and the Qwen3-family models.&lt;/p&gt;

&lt;p&gt;We use Ollama’s /v1 endpoint to interface with multiple LLMs, including the Qwen3 series, in a production setting. Recently, we noticed that for certain models, the response content was consistently empty, despite the model being correctly invoked. This wasn’t a matter of timeouts or errors - it was a complete absence of output. After extensive logging and profiling, we traced the issue to a seemingly innocuous parameter: &lt;code&gt;think: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ollama’s /v1 endpoint is designed to be OpenAI-compatible, which makes it easy to integrate with existing tooling and workflows. However, we discovered that for the Qwen3-family models, the &lt;code&gt;think: false&lt;/code&gt; toggle was being silently ignored. This meant that even when we explicitly set &lt;code&gt;think: false&lt;/code&gt;, the model would still consume its entire &lt;code&gt;num_predict&lt;/code&gt; budget on internal reasoning, leaving no tokens for the actual response. The result? Empty content, no errors, and a lot of confusion.&lt;/p&gt;

&lt;p&gt;To confirm this, we compared the payloads sent to the /v1 endpoint versus the native /api/chat endpoint. Here’s a simplified version of what we sent to the /v1 endpoint:&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="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;"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;"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;100&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="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;And here’s what we sent to the native endpoint:&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="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;"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;"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;100&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 difference? The absence of the &lt;code&gt;think: false&lt;/code&gt; toggle in the native endpoint. Surprisingly, the native endpoint didn’t require it to produce a response. This suggests that the &lt;code&gt;/v1&lt;/code&gt; endpoint’s compatibility layer was either misinterpreting or ignoring the &lt;code&gt;think&lt;/code&gt; parameter for this specific model family.&lt;/p&gt;

&lt;p&gt;The tradeoff here is clear: while the /v1 endpoint offers a familiar interface for those coming from the OpenAI ecosystem, it’s not always fully compatible with all models. In our case, it introduced a silent failure mode that was difficult to diagnose without deep logging and testing.&lt;/p&gt;

&lt;p&gt;We’ve since moved our Qwen3-family integrations to the native /api/chat endpoint, where the behavior is predictable and consistent. However, this incident highlights a broader challenge: as we work with increasingly diverse model ecosystems, compatibility layers can introduce subtle, hard-to-detect issues.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to build more robust, model-agnostic interfaces that can handle these edge cases without requiring users to switch endpoints. We’re also considering whether to contribute a patch or a workaround back to the Ollama community to improve the /v1 endpoint’s compatibility with Qwen3 and similar models. What do you think - should compatibility layers be more transparent about their limitations, or is it better to rely on native APIs for critical workflows?&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, 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>
  </channel>
</rss>
