<?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>Data Substrate Versus Vector Db Rag</title>
      <dc:creator>Francis Oyakhire</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:00:15 +0000</pubDate>
      <link>https://dev.to/apexgridtech/data-substrate-versus-vector-db-rag-2g3n</link>
      <guid>https://dev.to/apexgridtech/data-substrate-versus-vector-db-rag-2g3n</guid>
      <description>&lt;p&gt;This week's news about improved OCR capabilities for document images brought to mind a more fundamental question: how do we build systems that can &lt;em&gt;reason&lt;/em&gt; about the data they process? When dealing with AI applications in regulated environments like Nigerian microfinance, the answer hinges on the distinction between a "data substrate" and vector databases used in RAG systems.&lt;/p&gt;

&lt;p&gt;We're building a regtech tool for Nigerian microfinance banks that needs to analyze loan applications, detect fraud, and ensure compliance with evolving financial regulations. The data we work with is not just text  -  it's structured, versioned, and often comes with legal and audit implications. A vector database like Weaviate or Pinecone is useful for retrieving similar documents or answering questions based on unstructured content. But when we need to &lt;em&gt;reason&lt;/em&gt; about the data  -  like verifying that a particular clause in a loan agreement was present in version 1.2 but not 1.3  -  that's where a data substrate shines.&lt;/p&gt;

&lt;p&gt;A data substrate is a versioned, citable, queryable layer that sits beneath the AI application. It's not a replacement for vector DBs, but a complement. For example, in our regtech tool, we use a data substrate to store and version all loan agreements, client data, and regulatory documents. Each version is immutable and can be queried for specific clauses, dates, or jurisdictions. This is crucial when auditors ask, "Was this term in the contract when the loan was approved?"&lt;/p&gt;

&lt;p&gt;Here's how we model our data substrate using a PostgreSQL-based schema:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;document_versions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;version_number&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each document is stored as a JSONB blob, and we track its version number, creation timestamp, and metadata like the jurisdiction or regulation it relates to. This allows us to perform queries like:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_versions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'loan_agreement'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'jurisdiction'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Nigeria'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'regulation'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CBN 2023'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&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 gives us full traceability  -  a key requirement in regulated environments. A vector DB would struggle to provide this level of versioning and auditability without significant overhead.&lt;/p&gt;

&lt;p&gt;But there are tradeoffs. A data substrate is not optimized for similarity searches or natural language queries. If we need to find all loan agreements that mention "collateral" in their content, we'd have to query the JSONB content field, which is less efficient than using a vector DB's similarity search. So, we use both: the data substrate for versioning and audit, and a vector DB for content-based retrieval.&lt;/p&gt;

&lt;p&gt;What's next? We're exploring hybrid architectures that combine the best of both worlds  -  using the data substrate for versioned facts and the vector DB for semantic similarity. We're also experimenting with integrating this with on-device AI models, so that microfinance officers in remote parts of Nigeria can run compliance checks without relying on cloud infrastructure. How would you approach this problem if you were building for a similar regulated environment?&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>Sun, 25 Oct 2026 15:00:15 +0000</pubDate>
      <link>https://dev.to/apexgridtech/gpu-preflight-for-cron-agents-313f</link>
      <guid>https://dev.to/apexgridtech/gpu-preflight-for-cron-agents-313f</guid>
      <description>&lt;p&gt;This week, the open-source community buzzed with updates on model serving infrastructure, including the latest PRs for vLLM-Omni support. As teams race to deploy more efficient and scalable inference pipelines, one often-overlooked piece of the puzzle remains: ensuring that the hardware is ready before the job even starts. At Apex Grid, we’ve spent the last few months refining a two-stage GPU preflight check that has become a critical part of our cron agent infrastructure.&lt;/p&gt;

&lt;p&gt;When deploying cron jobs that depend on GPU resources, it’s not enough to simply wrap the job in a try/except block. That approach risks silently failing when the underlying issue is not the job itself, but the state of the GPU. For example, another job might be monopolizing the card, or the kernel might be in a state where it’s not responding to requests. Our solution involves a two-stage preflight check that ensures both the GPU and the serving stack are in a healthy state before we even attempt to run the job.&lt;/p&gt;

&lt;p&gt;Our preflight logic hinges on two simple but effective endpoints: one that checks for basic health (&lt;code&gt;/api/tags&lt;/code&gt;) and another that performs a lightweight inference (&lt;code&gt;/api/generate&lt;/code&gt;). The first check ensures that the model server is responsive and can return a list of available tags  -  a quick smoke test. The second check runs a minimal inference task, which helps us detect if the GPU is actually available and not being blocked by another job. We set a warmup timeout for this second check, typically between 3 and 10 seconds, depending on the model size and workload.&lt;/p&gt;

&lt;p&gt;Here’s how our &lt;code&gt;gpu_ready()&lt;/code&gt; helper looks 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;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;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warmup_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# First check: basic health
&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;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="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;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/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;tags_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;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;# Second check: warmup inference
&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;generate_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="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;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/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;Hello, world!&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;warmup_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;generate_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;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 approach has several advantages. It ensures that the GPU is not just available but also in a state where it can handle the job without stalling or timing out. It also allows us to cleanly skip the job if the preflight check fails, rather than letting it fail later and potentially leaving the system in an inconsistent state. This has been particularly useful in environments where multiple cron jobs share the same GPU resource pool.&lt;/p&gt;

&lt;p&gt;Of course, there are tradeoffs. The preflight check adds a small amount of latency to the overall job scheduling process. Additionally, it requires that the model server exposes both the &lt;code&gt;/api/tags&lt;/code&gt; and &lt;code&gt;/api/generate&lt;/code&gt; endpoints, which may not be the case for all serving stacks. We’ve mitigated this by making the preflight logic configurable  -  allowing teams to skip the second stage if needed, or to use a different lightweight inference task.&lt;/p&gt;

&lt;p&gt;Looking ahead, we’re exploring ways to make this preflight logic more adaptive. For example, we’re experimenting with using a lightweight model for the warmup inference that’s specifically tuned for this task, reducing the time it takes to complete the second stage. We’re also looking into integrating this logic more deeply with Kubernetes and other orchestration tools, to make GPU readiness checks part of the broader system health monitoring pipeline. What do you think  -  would you prefer a preflight check that’s even more lightweight, or one that gives more detailed diagnostics when it fails?&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>Thu, 22 Oct 2026 15:05:38 +0000</pubDate>
      <link>https://dev.to/apexgridtech/ollama-v1-openai-compat-drops-think-toggle-27ne</link>
      <guid>https://dev.to/apexgridtech/ollama-v1-openai-compat-drops-think-toggle-27ne</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;We recently ran into an issue with the Ollama &lt;code&gt;/v1&lt;/code&gt; endpoint, which is designed to be compatible with OpenAI's API. Our models, particularly those from the Qwen3 family, were returning empty content. This was not the behavior we expected, given the models' capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Noticed
&lt;/h2&gt;

&lt;p&gt;We started by examining the requests and responses to identify what was going wrong. We noticed that the &lt;code&gt;/v1&lt;/code&gt; endpoint was silently dropping the &lt;code&gt;think: false&lt;/code&gt; toggle for the Qwen3-family models. This toggle is crucial as it controls whether the model spends its entire &lt;code&gt;num_predict&lt;/code&gt; budget on internal reasoning or not.&lt;/p&gt;

&lt;p&gt;Here's a snippet of the payload we were sending:&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;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Translate 'hello' to French."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="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;We expected the model to translate "hello" to French without extensive internal reasoning. However, the model was spending its entire &lt;code&gt;num_predict&lt;/code&gt; budget on reasoning and returning empty content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Approach
&lt;/h2&gt;

&lt;p&gt;To fix this issue, we switched to the native &lt;code&gt;/api/chat&lt;/code&gt; endpoint provided by Ollama. This endpoint allows us to have more control over the model's behavior and ensures that the &lt;code&gt;think&lt;/code&gt; toggle is respected.&lt;/p&gt;

&lt;p&gt;Here's the payload we used with the &lt;code&gt;/api/chat&lt;/code&gt; 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;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Translate 'hello' to French."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"think"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="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;By switching to the &lt;code&gt;/api/chat&lt;/code&gt; endpoint, we were able to control the model's behavior more effectively and get the expected output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Switching to the &lt;code&gt;/api/chat&lt;/code&gt; endpoint has its own set of tradeoffs. While it gives us more control over the model's behavior, it also requires us to refactor our code to handle the new endpoint. Additionally, the &lt;code&gt;/api/chat&lt;/code&gt; endpoint may have different rate limits and pricing structures compared to the &lt;code&gt;/v1&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;However, the benefits of having more control over the model's behavior and getting the expected output outweigh these tradeoffs for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Doing Next
&lt;/h2&gt;

&lt;p&gt;We are currently exploring other endpoints and features provided by Ollama to see if they offer any additional benefits for our use case. We are also working on improving our monitoring and alerting systems to catch such issues earlier.&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>Mon, 19 Oct 2026 15:00:14 +0000</pubDate>
      <link>https://dev.to/apexgridtech/postiz-self-host-bluesky-python-client-32gn</link>
      <guid>https://dev.to/apexgridtech/postiz-self-host-bluesky-python-client-32gn</guid>
      <description>&lt;p&gt;This week, the conversation around Docker and auto-updating containers has sparked a lot of debate, especially around the tradeoffs between convenience and control. As engineers who've spent time wrestling with container orchestration, we’ve found that sometimes the simplest setups are the most reliable  -  and that includes self-hosting.&lt;/p&gt;

&lt;p&gt;We’re running a self-hosted instance of Postiz, a self-hostable Bluesky client, using Docker Compose. The goal is to have a lightweight, zero-ops setup that can be managed directly from a Python script. This approach avoids the complexity of Kubernetes or managed services while still giving us full control over the deployment and API interactions.&lt;/p&gt;

&lt;p&gt;Our Docker Compose setup is minimal, pulling the latest Postiz image from GitHub Packages and exposing the necessary ports. Here's a simplified version of our &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postiz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/postiz/postiz:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTIZ_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://user:pass@postgres: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;postgres&lt;/span&gt;
  &lt;span class="na"&gt;postgres&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:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pass&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postiz&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python client communicates with the Postiz instance via the &lt;code&gt;/api/public/v1/posts&lt;/code&gt; endpoint. This endpoint supports both creating a new post and fetching a list of posts, depending on the HTTP method used. The JSON payload for a new post includes the text, visibility, and optionally a reply to another post. Here's the shape of the request body:&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This is a new post from the Python client."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"visibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply"&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;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"at://example.com/0123456789abcdef0123456789abcdef"&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 &lt;code&gt;Authorization&lt;/code&gt; header is required and must be a valid Bearer token, typically obtained via the &lt;code&gt;/auth/token&lt;/code&gt; endpoint. This token is scoped to the user and provides access to the &lt;code&gt;/api/public/v1/posts&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;One of the quirks we've encountered is that the &lt;code&gt;/api/public/v1/posts&lt;/code&gt; endpoint can return either a single post object or a list of posts, depending on the context of the request. This behavior is not always well-documented, so we've implemented a helper function in our Python client to normalize the response:&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;url&lt;/span&gt;&lt;span class="p"&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="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;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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&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;list&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="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function ensures that regardless of whether the API returns a single object or a list, our code can handle it consistently.&lt;/p&gt;

&lt;p&gt;We’re currently working on extending the Python client to support more of Postiz’s API endpoints, including user management and moderation tools. We’re also exploring ways to integrate this setup with a local development environment that mirrors production as closely as possible.&lt;/p&gt;

&lt;p&gt;What would you prioritize next if you were building a self-hosted Bluesky client?&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, 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>
  </channel>
</rss>
