<?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: Redouane Achouri</title>
    <description>The latest articles on DEV Community by Redouane Achouri (@redouane_cc).</description>
    <link>https://dev.to/redouane_cc</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%2F4013900%2F1ea204ec-f5c5-4342-b834-fed688a0a56f.png</url>
      <title>DEV Community: Redouane Achouri</title>
      <link>https://dev.to/redouane_cc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/redouane_cc"/>
    <language>en</language>
    <item>
      <title>How to avoid duplicate email sends with Hail MCP</title>
      <dc:creator>Redouane Achouri</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:42:39 +0000</pubDate>
      <link>https://dev.to/redouane_cc/how-to-avoid-duplicate-email-sends-with-hail-mcp-3ap7</link>
      <guid>https://dev.to/redouane_cc/how-to-avoid-duplicate-email-sends-with-hail-mcp-3ap7</guid>
      <description>&lt;p&gt;An AI agent sends an email, then the service accepts it, but the connection drops before the agent gets an answer.&lt;/p&gt;

&lt;p&gt;The agent tries again. Does your lead get the same email twice?&lt;/p&gt;

&lt;p&gt;This is an easy problem to miss when connecting AI agents to tools. You can reduce the risk by saving a retry key before sending, then tracking each message outside the chat.&lt;/p&gt;

&lt;p&gt;We’ll use &lt;a href="https://hail.so/" rel="noopener noreferrer"&gt;Hail MCP&lt;/a&gt; as the example. It gives MCP clients tools for email, texts, and calls. The same ideas apply to other tools that perform actions.&lt;/p&gt;

&lt;p&gt;Disclosure: I am one of the founders of Hail.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout does not mean the email failed
&lt;/h2&gt;

&lt;p&gt;When a request times out, you know the client stopped waiting. You do not yet know what happened at the other end.&lt;/p&gt;

&lt;p&gt;The email might have been accepted before the connection dropped. Even cancelling the request may arrive too late to stop it.&lt;/p&gt;

&lt;p&gt;That makes “just try again” risky.&lt;/p&gt;

&lt;p&gt;Instead, your workflow needs to recognize a repeated attempt at the same send.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save one key for each planned message
&lt;/h2&gt;

&lt;p&gt;An idempotency key tells a service that a request is another attempt at the same action.&lt;/p&gt;

&lt;p&gt;Think of it as a receipt number. When you ask about the same purchase twice, you want the original receipt.&lt;/p&gt;

&lt;p&gt;Hail’s &lt;code&gt;send_email&lt;/code&gt; tool accepts an &lt;code&gt;idempotency_key&lt;/code&gt;. If you leave it out, the tool creates a fresh UUID for that invocation.&lt;/p&gt;

&lt;p&gt;For a workflow that may retry, create and save your own key before the first attempt.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;campaign_042:lead_018:email:intro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep that key for retries of this specific message. Give a separate key to a later follow-up.&lt;/p&gt;

&lt;p&gt;Avoid generating a new key whenever a request fails. A new key represents a new action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pass the key with the approved message
&lt;/h2&gt;

&lt;p&gt;Here is an example of arguments for Hail’s &lt;code&gt;send_email&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;Use an inbox you control for testing. Replace the sender with a verified address in your account.&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;"to"&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="s2"&gt;"your-test-inbox@example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"from_"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verified-sender@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The details you requested"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body_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;"Here are the details we agreed to send."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_consent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marketing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"consent_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_REAL_PERMISSION_RECORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"campaign_042:lead_018:email:intro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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;"campaign_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"campaign_042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lead_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lead_018"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"intro"&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;Set &lt;code&gt;recipient_consent&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; only when you can make that statement honestly. Hail requires this confirmation. It does not verify permission for you.&lt;/p&gt;

&lt;p&gt;Marketing messages also require a real &lt;code&gt;consent_source&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For outbound campaigns, write the sales copy yourself. Let the agent find leads and run approved actions. Save the finished message so a retry uses the same content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a record outside the chat
&lt;/h2&gt;

&lt;p&gt;A retry key helps with repeated requests. Your workflow also needs a lasting record of what it intended to send.&lt;/p&gt;

&lt;p&gt;Save these fields in your database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The campaign and lead IDs.&lt;/li&gt;
&lt;li&gt;The exact sender, recipient, subject, and body.&lt;/li&gt;
&lt;li&gt;The retry key.&lt;/li&gt;
&lt;li&gt;The current state.&lt;/li&gt;
&lt;li&gt;The Hail email ID, when available.&lt;/li&gt;
&lt;li&gt;The last attempt time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use states that distinguish “accepted” from “unknown.”&lt;/p&gt;

&lt;p&gt;If the connection drops without a result, mark the operation as unknown. Do not quietly return it to the unsent list.&lt;/p&gt;

&lt;p&gt;Otherwise, another worker or a new chat may send it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the result before retrying
&lt;/h2&gt;

&lt;p&gt;If you saved a Hail email ID, use &lt;code&gt;get_email&lt;/code&gt; to check that message.&lt;/p&gt;

&lt;p&gt;If you have no ID, keep the original key and unchanged request. Follow the service’s retry behavior and retention limits.&lt;/p&gt;

&lt;p&gt;Hail can replay a saved response for the same key. That response can include a saved failure. Repeating the request does not always start another attempt.&lt;/p&gt;

&lt;p&gt;A request may also still be processing. Wait and investigate before creating a replacement operation.&lt;/p&gt;

&lt;p&gt;If the result remains unclear, pause for review. Creating a fresh key just to clear an error can cause another send.&lt;/p&gt;

&lt;p&gt;Also, keep acceptance separate from delivery. A service accepting your request does not mean the email reached the inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resume batches one message at a time
&lt;/h2&gt;

&lt;p&gt;Suppose a batch contains 100 leads. The first 40 messages succeed, then the connection fails.&lt;/p&gt;

&lt;p&gt;Restarting all 100 messages could repeat the first 40.&lt;/p&gt;

&lt;p&gt;Give every planned send its own record and key. Resume from those records, checking unknown results before sending anything else.&lt;/p&gt;

&lt;p&gt;If several workers process the batch, claim each operation with an atomic database update. This prevents two workers from picking up the same ready message.&lt;/p&gt;

&lt;p&gt;Keep the retry key too. A worker can crash after acceptance, before saving the result locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the lost-response case first
&lt;/h2&gt;

&lt;p&gt;Before using a real lead list, simulate this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The service accepts the email.&lt;/li&gt;
&lt;li&gt;The connection drops before the result reaches your worker.&lt;/li&gt;
&lt;li&gt;The worker retries with the original key and message.&lt;/li&gt;
&lt;li&gt;The service returns the saved result.&lt;/li&gt;
&lt;li&gt;Only one email has been accepted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also test a changed message, two workers claiming the same operation, and an opt-out received after approval.&lt;/p&gt;

&lt;p&gt;These checks matter more than a successful first send. They show whether your workflow can recover when the answer disappears halfway back.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
    </item>
    <item>
      <title>How to build production RAG AI agents: document ETL, chunking, hybrid search, reranking, LangGraph orchestration. 13 lessons that cut costs 15-20x.</title>
      <dc:creator>Redouane Achouri</dc:creator>
      <pubDate>Tue, 04 Aug 2026 10:43:38 +0000</pubDate>
      <link>https://dev.to/redouane_cc/how-to-build-production-rag-ai-agents-document-etl-chunking-hybrid-search-reranking-langgraph-2ae6</link>
      <guid>https://dev.to/redouane_cc/how-to-build-production-rag-ai-agents-document-etl-chunking-hybrid-search-reranking-langgraph-2ae6</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i" class="crayons-story__hidden-navigation-link"&gt;13 Things I Learned Building AI Agents for Technical Field Service&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/redouane_cc" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013900%2F1ea204ec-f5c5-4342-b834-fed688a0a56f.png" alt="redouane_cc profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/redouane_cc" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Redouane Achouri
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Redouane Achouri
                
              
              &lt;div id="story-author-preview-content-4305319" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/redouane_cc" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013900%2F1ea204ec-f5c5-4342-b834-fed688a0a56f.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Redouane Achouri&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 4&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i" id="article-link-4305319"&gt;
          13 Things I Learned Building AI Agents for Technical Field Service
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/architecture"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;architecture&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/learning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;learning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>13 Things I Learned Building AI Agents for Technical Field Service</title>
      <dc:creator>Redouane Achouri</dc:creator>
      <pubDate>Tue, 04 Aug 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i</link>
      <guid>https://dev.to/redouane_cc/13-things-i-learned-building-ai-agents-for-technical-field-service-o3i</guid>
      <description>&lt;p&gt;At &lt;a href="https://opero.pro" rel="noopener noreferrer"&gt;Opero&lt;/a&gt; we build agents, the sort of voicebots and chatbots technical staff use in the field or at the office while preparing for a job. They are built on the technical documentation of manufacturers, engineering labs, HVAC companies, and field service teams.&lt;/p&gt;

&lt;p&gt;These agents are not demonstrations. A technician uses them when a machine is broken.&lt;/p&gt;

&lt;p&gt;Many of my first beliefs were incorrect. Some were also expensive. These are the 13 things I learned along the way.&lt;/p&gt;

&lt;p&gt;It would mean the world if this is useful to someone building their own agents.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;This is the software that we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent and API:&lt;/strong&gt; Python, &lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; &lt;a href="https://www.langchain.com/langgraph" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; for the graph, &lt;a href="https://www.langchain.com/" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; for the components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability:&lt;/strong&gt; &lt;a href="https://langfuse.com/" rel="noopener noreferrer"&gt;Langfuse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval:&lt;/strong&gt; OpenAI embeddings, &lt;a href="https://milvus.io/" rel="noopener noreferrer"&gt;Milvus&lt;/a&gt; (hybrid dense and sparse search), &lt;a href="https://www.zeroentropy.dev/" rel="noopener noreferrer"&gt;zerank-2&lt;/a&gt; for reranking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured data:&lt;/strong&gt; Postgres&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; Different providers with automatic failover. If one provider gives an API error during operation, we send the request to a different provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communications:&lt;/strong&gt; agentic phone calls, SMS, and email with &lt;a href="https://hail.so" rel="noopener noreferrer"&gt;Hail&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; servers on &lt;a href="https://www.hetzner.com/" rel="noopener noreferrer"&gt;Hetzner&lt;/a&gt;, storage and some pipeline components on AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14ijw1k62hkjz3kj3oy5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14ijw1k62hkjz3kj3oy5.jpg" alt="System overview — the whole graph before the thirteen parts" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1. Ingestion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lesson 1: Build Your Own Document ETL Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F09fbfdnx51ddjla4a2xe.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F09fbfdnx51ddjla4a2xe.jpg" alt="Own the ETL pipeline — the foundation cannot be a black box" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We started with a commercial document processing platform called "Unstructured".&lt;/p&gt;

&lt;p&gt;The platform was easy to start. We had a system in operation in less than one day. Commercial platforms are good for this.&lt;/p&gt;

&lt;p&gt;Then we found problems.&lt;/p&gt;

&lt;p&gt;The extraction quality was low. The platform gave structured output, but the structure was correct for Unstructured, not for us. We changed our system to agree with their format. This is the incorrect sequence. It's an anti-pattern. We had to twist our pipeline to make it work.&lt;/p&gt;

&lt;p&gt;Then we started working with Zeppelin (a major &lt;a href="https://www.caterpillar.com/" rel="noopener noreferrer"&gt;Caterpillar&lt;/a&gt; dealer), and the first document they shared was 8,000 pages of dense technical documentation. Drawings, schematics, backlinks that cross-reference other pages, complex identifiers, etc.&lt;/p&gt;

&lt;p&gt;The pipeline on Unstructured failed 20 times. We paid for each failure.&lt;/p&gt;

&lt;p&gt;At that time, we made a decision. The cost was one problem, but the larger problem was different: our document processing logic was in a system that we could not examine, repair, or improve.&lt;/p&gt;

&lt;p&gt;We benchmarked the available platforms and libraries, and settled on an open source library called Docling. We built a prototype in one weekend. The prototype gave better results than the commercial platform.&lt;/p&gt;

&lt;p&gt;We continue to improve the pipeline. It is now fully automatic and the results are good.&lt;/p&gt;

&lt;p&gt;The lesson is not "do not use commercial tools". Start with a commercial tool. Release your product MVP. Learn your true requirements. But move key architecture components like document processing to your own system as soon as possible.&lt;/p&gt;

&lt;p&gt;Data processing is the base of all other functions. You need flexibility, cost control, and the ability to repair your own failures.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lessons 2 to 11 are possible only because we control the ETL pipeline.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you want a full article about the ETL pipeline, the architecture, the models, and the orchestration? &lt;a href="https://www.opero.pro/contact" rel="noopener noreferrer"&gt;Tell us&lt;/a&gt; and I will write it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Lesson 2: Owning the Pipeline Cut Our Costs 15 to 20x
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbx8vj003914xs4t5dxi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbx8vj003914xs4t5dxi.jpg" alt="Cost — per-page pricing is arbitrary, your real cost is not" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unstructured charged 20 USD for 1,000 pages at the start. The price then increased to 30 USD.&lt;/p&gt;

&lt;p&gt;Per-page pricing does not reflect real cost. The true processing cost changes with the document. One page of simple text and one page of rotated engineering drawings are not equivalent. But the provider charges the same price for the two pages.&lt;/p&gt;

&lt;p&gt;We operated our own pipeline for some weeks. We measured the cost with real customer documents. Our cost was 15 to 20 times less.&lt;/p&gt;

&lt;p&gt;We did not use low quality models to get this result. We used good models. We operated the models on our own GPU infrastructure with our own routing.&lt;/p&gt;

&lt;p&gt;For a small company, this is not just an improvement — it is months of extra runway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 3: Extraction Quality Sets the Maximum Performance of the System
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkq5xgdkowyb256o3q9e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkq5xgdkowyb256o3q9e.jpg" alt="Extraction is the ceiling — no layer above can exceed the layer below" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You cannot correct bad extraction with a better retriever, a more intelligent agent, or a larger model.&lt;/p&gt;

&lt;p&gt;A spec sheet has a key-value layout. If the extraction makes this layout into unstructured text, the data is lost. If the extraction ignores a rotated page, the data is lost. If a table loses its column alignment, the data is lost. No subsequent process can recover this data.&lt;/p&gt;

&lt;p&gt;Technical documentation has these conditions frequently: rotated pages, dense tables, spec sheets with key-value layouts, scanned manuals that are 30 years old, and diagrams with important text in the image.&lt;/p&gt;

&lt;p&gt;All our improvements in accuracy start with correct extraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 4: Chunking is a Strategy, Not a Default Setting
&lt;/h3&gt;

&lt;p&gt;We use hybrid chunking. This method divides the document by its structure and its hierarchy. It then merges the parts by token count.&lt;/p&gt;

&lt;p&gt;Many developers use a recursive character splitter with a 512 token window. They do not change this setting again. Then they ask why the retrieval quality is low.&lt;/p&gt;

&lt;p&gt;Your chunk boundaries control the possible results of the retriever. If a procedure is divided between two chunks, no retriever can give the full procedure to a technician.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 5: Extract Taxonomy and Tags in the ETL Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8iqupmbtpb2ctjjfu02.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8iqupmbtpb2ctjjfu02.jpg" alt="Taxonomy belongs in the ETL — tag at ingestion, filter at query time" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We define a taxonomy during ingestion: manufacturer, model, and custom tags for each organization. The pipeline extracts the tags during preprocessing. We keep the tags as scalar filters with the vectors.&lt;/p&gt;

&lt;p&gt;This looks like a small administrative task. It becomes a product function in Lesson 13. It is also a good example of a function that a closed commercial platform does not permit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2. Retrieval
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lesson 6: A Simple Top-K Semantic Search is Not Sufficient
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft61c3ofpe3p3bj0emogv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft61c3ofpe3p3bj0emogv.jpg" alt="Naive top-K is not enough — similarity is not relevance" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first version of the agent at Opero did a simple top-K semantic retrieval. It found 10 to 20 documents by semantic similarity. It put the documents in the context. Then it generated an answer.&lt;/p&gt;

&lt;p&gt;There was no reranking. There was no relevance filter. There was no procedure to find if the documents were best available for the user question.&lt;/p&gt;

&lt;p&gt;We hoped that cosine similarity would find useful data. Then we gave the result to the user as an answer.&lt;/p&gt;

&lt;p&gt;This method worked well enough for a demonstration. It failed often enough to be dangerous. This is the most dangerous failure condition in this field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 7: Add a Reranking Step
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft14h7e086qbwr47r7izz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft14h7e086qbwr47r7izz.jpg" alt="Add reranking — retrieve wide, then judge what you retrieved" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We added a reranking step. The system finds many candidate documents. A dedicated model then gives a relevance score to each candidate for that query.&lt;/p&gt;

&lt;p&gt;The improvement was immediate and large. If you apply only one lesson from this list, apply this one.&lt;/p&gt;

&lt;p&gt;We also use the relevance score in the user interface. If the best result has a low score, we tell the user. We do not give a confident answer from low quality context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 8: Rerank Scores Have No Absolute Scale — Pick a Model That Calibrates Them
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2meehsuhh9gbjldns5lf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2meehsuhh9gbjldns5lf.jpg" alt="Standardise the score — a relevance number you cannot compare is not a number" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We started with Cohere Rerank. It operates correctly, but its relevance scores do not use an absolute scale.&lt;/p&gt;

&lt;p&gt;So we had to tune the thresholds for each organization and each industry type. We had to decide which score was high, medium, or low. These thresholds were estimates. They also changed when the document collection changed.&lt;/p&gt;

&lt;p&gt;We changed to zerank-2 from ZeroEntropy. This model gives standardized relevance scores.&lt;/p&gt;

&lt;p&gt;This looks like a small change. It is not a small change. You can define a fixed relevance scale one time. You can then build product logic on this scale. The scale stays correct when your data changes. You do not have to keep a calibration procedure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 9: Use Hybrid Search If Your Users Type Serial Numbers
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fho6hlucm1yxy80q02mc5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fho6hlucm1yxy80q02mc5.jpg" alt="Hybrid retrieval — two channels, one query, the technician types both" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We added BM25 keyword matching with the semantic search.&lt;/p&gt;

&lt;p&gt;Engineers and technicians do not write complete questions. They type "E-047". They type a part number from a label. They type a serial number.&lt;/p&gt;

&lt;p&gt;Vector search has low accuracy with these exact terms. This is not a defect. Embeddings find meaning, and an error code has no meaning.&lt;/p&gt;

&lt;p&gt;Semantic search finds the answer for "why does the compressor short cycle".&lt;/p&gt;

&lt;p&gt;BM25 finds the answer for "SCR-4471-B".&lt;/p&gt;

&lt;p&gt;Users need the two methods, frequently in the same question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 10: Multiple Languages Are Usual
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjmmjslkp6wp6i9wwpsn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwjmmjslkp6wp6i9wwpsn.jpg" alt="Multiple languages are normal — not an edge case, the default case" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of our documentation is in English. But we also have documents in German, Danish, Swedish, and Chinese. European technical customers have documents in these languages.&lt;/p&gt;

&lt;p&gt;Plan for multiple languages from the first day. It is difficult to add this function later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3. Orchestration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lesson 11: Start with a Workflow. Change to an Agent Later.
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkppjdvhgoqresdd4vf6j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkppjdvhgoqresdd4vf6j.jpg" alt="Start as a workflow — agency over an unreliable retriever is an expensive random walk" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our first version was not an agent. It was a DAG. We built it with LangGraph, and we still use LangGraph for all orchestration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node 1:&lt;/strong&gt; classify the input. Is it a question or a greeting? The system answers greetings directly and at low cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 2:&lt;/strong&gt; query the RAG system, format the result, and give it to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the full system. This was the time of GPT-4o and Claude 3.5 Sonnet.&lt;/p&gt;

&lt;p&gt;You can debug a deterministic workflow. You know which step failed. Agentic loops are more difficult to analyze. If you give autonomy to a loop that uses an unreliable retriever, the loop fails in unusual ways instead of predictable ways.&lt;/p&gt;

&lt;p&gt;Repair retrieval first. Add agency second. Use this sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 12: Give the Agent Permission to Try Again
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqf9k46yiekkuco1yeze.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqf9k46yiekkuco1yeze.jpg" alt="Earn the retry — give the agent a score it can read and a line it can judge" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We changed to a ReAct loop. The agent decides if the documents are sufficient. If they are not sufficient, the agent rephrases the query and does a new search.&lt;/p&gt;

&lt;p&gt;This function operates only because of Lessons 7 and 8. The agent reads a standard relevance score. It finds that the results are not sufficient. It writes a new query. Then it does the search again.&lt;/p&gt;

&lt;p&gt;Reranking gives the agent a signal. Standard scores give the agent a threshold. Agency without these two pieces is only an expensive random search.&lt;/p&gt;

&lt;p&gt;We also added a pre-retrieval layer. This layer finds context quickly and sends it to the primary node. So the usual case is fast. The ReAct agent keeps its tools for a more complete search when necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 13: When the Agent Cannot Find an Answer, Give Control to the User
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fds69fvihxsfk8rcgu19g.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fds69fvihxsfk8rcgu19g.jpg" alt="Give control back — graceful degradation beats confident nonsense" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my preferred part of the system. It is the result of Lesson 5.&lt;/p&gt;

&lt;p&gt;If the first query gives results with a high relevance score, the agent responds immediately. If it does not, the agent requeries with a different angle.&lt;/p&gt;

&lt;p&gt;But if the agent gets stuck in a loop and cannot find sufficient results, we do two things. We do not generate an incorrect answer. We also do not show only the message "no results found".&lt;/p&gt;

&lt;p&gt;We show a filter interface. We build this interface from the taxonomy tags. The user selects a manufacturer, a model, or other data that the user knows. We then do the query again automatically with these filters.&lt;/p&gt;

&lt;p&gt;The user knows data that the retriever does not know. Let the user nudge the agent.&lt;/p&gt;

&lt;p&gt;Degrading gracefully is better than a confident incorrect answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4. Communications
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bonus. MCP Makes Integration Simple
&lt;/h3&gt;

&lt;p&gt;The primary loop is now reliable. So integration is configuration work instead of architecture work.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We added MCP servers for customer system integrations. The agent can now get live data, not only documentation, using &lt;a href="https://nango.dev/" rel="noopener noreferrer"&gt;Nango&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We added web search. The user can now include internet sources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We also added voice, SMS, and email in one API/MCP layer using &lt;a href="https://hail.so" rel="noopener noreferrer"&gt;Hail&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This last item changes the function of the agent. The agent is no longer a chatbot that answers questions. It can call a technician. It can send a part number by SMS. It can send a report by email. The interface is no longer a text box. The interface is the system that the user has.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Primary Lesson
&lt;/h2&gt;

&lt;p&gt;There is one pattern in all 13 items.&lt;/p&gt;

&lt;p&gt;Every important improvement comes when we take control of a layer that we initially rented.&lt;/p&gt;

&lt;p&gt;The ETL pipeline. The chunking. The taxonomy. The retrieval strategy. The orchestration logic. Each layer was initially an abstraction from a different company. Each layer becomes better when we can examine it and change it.&lt;/p&gt;

&lt;p&gt;Start with the commercial platform. Release a product. Learn what is important. Do not build infrastructure for a product that has no users.&lt;/p&gt;

&lt;p&gt;But you will get to the limits of that platform. Know which layer you will take control of first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://opero.pro" rel="noopener noreferrer"&gt;Opero&lt;/a&gt;, which supplies AI agents for technical field service, engineering teams, OEMs, HVAC, and manufacturing. I write about the problems we hit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which lesson do you want more information about? Readers ask about the ETL pipeline most frequently. &lt;a href="https://www.opero.pro/contact" rel="noopener noreferrer"&gt;Tell us&lt;/a&gt; and I will make it the next article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>learning</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
