<?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>Forem: Aniket Hingane</title>
    <description>The latest articles on Forem by Aniket Hingane (@exploredataaiml).</description>
    <link>https://forem.com/exploredataaiml</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F946058%2F1350ea8e-445d-4052-b72a-95fd786a4f5c.jpeg</url>
      <title>Forem: Aniket Hingane</title>
      <link>https://forem.com/exploredataaiml</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/exploredataaiml"/>
    <language>en</language>
    <item>
      <title>Resilient Guest-Policy Retrieval: A Self-Healing Semantic Loop for Hotel Context</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Tue, 07 Apr 2026 02:09:19 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/resilient-guest-policy-retrieval-a-self-healing-semantic-loop-for-hotel-context-ejj</link>
      <guid>https://forem.com/exploredataaiml/resilient-guest-policy-retrieval-a-self-healing-semantic-loop-for-hotel-context-ejj</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F6h47mvm7zoh110lr7bzo.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F6h47mvm7zoh110lr7bzo.gif" alt="Cover animation" width="880" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I Recovered Weak Matches with Controlled Expansion and Bundled Evidence in a Solo PoC&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fspujpj92wsk77sfjfbdo.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fspujpj92wsk77sfjfbdo.png" alt="Title diagram" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This write-up documents a personal experiment I ran while thinking about how guests actually ask questions at the front desk, on the phone, or in a chat widget. The phrasing is rarely canonical. Someone might say they want a rubdown later today when the policy text says massage appointments and cancellation windows. Another guest might describe late checkout as staying past the morning rush. If you treat every utterance as a perfect keyword match, you will look clever in a slide deck and brittle in real language. I built a small Python system that embeds synthetic hotel policy chunks with a compact sentence transformer, measures cosine similarity against guest questions, and applies a narrow healing loop when the score falls below a floor I set by hand. The loop tries controlled synonym expansion first, then merges the top passages into a bundled evidence string when the model still hesitates. I also track synthetic staleness days on each chunk so the PoC can pretend that some documents deserve an offline review queue. Nothing here runs in production, nothing here connects to a property I have worked with, and nothing here should be read as advice from a vendor. I am describing an exploratory solo build because that is what it is. The code lives in a public repository so anyone can inspect the assumptions without asking me to narrate them from memory. If you take one idea away, take this one: I cared more about making failure visible and recoverable than about chasing the highest possible retrieval score on a toy corpus. Transparency beat vanity in my priorities for this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hospitality guest operations sit at an interesting intersection of empathy and procedure. Guests want speed and clarity. Operators want consistency and traceability. Tools in the middle often promise both and deliver neither if they hide how an answer was assembled. I have been thinking about that tension while experimenting with retrieval stacks that admit their own uncertainty. This article is my attempt to write down what I tried, what I measured informally, and where I stopped on purpose.&lt;/p&gt;

&lt;p&gt;Before I describe the modules, I want to anchor the posture of this article. I am writing as an individual who builds experiments in public to learn, not as someone who is reporting on a deployed guest assistant or claiming validated operational outcomes. Hospitality is easy to romanticize and easy to misrepresent. I have watched people ask oddly phrased questions in lobbies and elevators, not as a researcher with formal instruments but as someone who notices wording when I travel. The questions are rarely perfect. A person might compress three concerns into one sentence about noise, timing, and fairness. If you flatten that into a single embedding and hope for the best, you can still retrieve plausible text, but you lose the story of why the match was weak in the first place. That loss matters to me as an engineer because I like systems that admit uncertainty instead of laundering it behind fluent text.&lt;/p&gt;

&lt;p&gt;In my PoC I chose a hotel guest-operations framing because it is relatable, easy to illustrate with synthetic documents, and distinct from other domains I have written about recently in this personal series. I am not describing revenue management, banquet sales, or accounting. I am staying with a narrow slice: how a policy-grounded assistant might assemble evidence before any human writes a guest-facing sentence. I also want to be clear about the human layer. Staff use judgment. A system that pretends to replace that judgment with a single score is not something I would defend. What I am experimenting with is a structured scaffold that keeps evidence visible so a human can still override.&lt;/p&gt;

&lt;p&gt;I wrote this article because I wanted a serious project that still fits on a laptop. I have seen enough demos where a model produces fluent language and hides the underlying evidence. I wanted the opposite. I wanted logs that read like engineering notes, not marketing copy. The code prints healing actions such as none, synonym_expand, or context_merge, and it writes a small matplotlib chart so I can see whether the batch run skewed toward one outcome because of a bug or because of the wording of my synthetic questions.&lt;/p&gt;

&lt;p&gt;There is another motivation I should state plainly. I am interested in practices that survive contact with messy language. People shorten words, omit nouns, and rely on context. They say the morning rush thing instead of spelling out late checkout. Any retrieval system that assumes the query already contains canonical terms will fail in ways that look embarrassing on a demo but painful in real life. I did not solve that fully here. I only created a place to talk about it honestly while still writing code.&lt;/p&gt;

&lt;p&gt;I also want readers to know the scope boundary I used while writing. This article discusses a synthetic dataset and illustrative thresholds. It does not describe any real hotel brand, franchise agreement, or property staffing model. If a phrase resembles language you have seen in the wild, that is because operational writing converges on similar vocabulary, not because I copied private material.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on language and tone
&lt;/h3&gt;

&lt;p&gt;I chose neutral, procedural wording for the synthetic policies on purpose. I did not want sensational examples that read like a thriller. Real front-desk life already carries enough stress without my demo adding theatrical conflict. I also avoided idioms that only make sense in one region. The point was to keep the text boring enough that retrieval mistakes are visible instead of being masked by narrative drama.&lt;/p&gt;

&lt;h3&gt;
  
  
  How this article relates to my other experiments
&lt;/h3&gt;

&lt;p&gt;I have written about routing and retrieval in other contexts. This piece is different because the healing loop is the protagonist. I am not showcasing a multi-agent cast. I am showcasing a measurement-and-repair cycle that could exist inside many larger systems. If you have read my earlier write-ups, you might recognize my preference for logs over slogans. That preference shows up again here in how I print healing actions and scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The article walks through GuestResilience-HotelContext-AI, a Python project that embeds short policy chunks, scores guest questions with cosine similarity, and applies a healing loop when confidence falls below a configurable floor.&lt;/li&gt;
&lt;li&gt;I explain why I combined semantic retrieval with a hand-built synonym map rather than relying on either signal alone in isolation.&lt;/li&gt;
&lt;li&gt;I show how the batch table and matplotlib chart help me see whether the demo skews toward synonym expansion because of the synonym list or because of the embedding geometry on a tiny corpus.&lt;/li&gt;
&lt;li&gt;I discuss limitations honestly: miniature corpora, heuristic floors, and a merge score that is not a calibrated probability.&lt;/li&gt;
&lt;li&gt;I include a code walkthrough that mirrors how I read the repository myself when I return to it after a gap.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Runtime expectations on a laptop
&lt;/h3&gt;

&lt;p&gt;I tested this PoC on a recent Mac laptop with a normal consumer CPU. Inference time for a batch of half a dozen questions is small enough that I did not bother printing millisecond timings in the CLI. If you run on older hardware, the first embedding pass over the chunks might take longer, but it remains a one-time cost per process start. I mention hardware because retrieval demos often silently assume a GPU. I did not require a GPU for this code path.&lt;/p&gt;

&lt;p&gt;The implementation is intentionally straightforward on purpose. I rely on Python 3.10 or newer, NumPy, scikit-learn largely as a transitive dependency, sentence-transformers with the all-MiniLM-L6-v2 model for normalized embeddings, matplotlib for a bar chart, and Rich for readable terminal tables. There is no hosted vector database and no cloud requirement for the retrieval math itself. The entire index fits in memory because I refused to pretend this PoC is big data.&lt;/p&gt;

&lt;p&gt;From where I stand, that stack is enough to demonstrate the idea that resilience in the small can be practiced with transparent steps when the corpus is tiny and the goal is structured evidence rather than open-ended generation. If I later swap MiniLM for another encoder, the interfaces around chunking and healing remain stable, which was a design goal while I sketched the modules.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F0olsyv2yls11nhjq2vfi.png" 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.amazonaws.com%2Fuploads%2Farticles%2F0olsyv2yls11nhjq2vfi.png" alt="System architecture" width="800" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If you are evaluating how to structure pre-model logic for operational assistants, this article offers a concrete pattern: measure confidence, attempt a deterministic repair, then widen evidence before you give up.&lt;/li&gt;
&lt;li&gt;If you are learning sentence-transformers with cosine similarity, the retrieval module is short and testable.&lt;/li&gt;
&lt;li&gt;If you care about reproducibility, the orchestrator gives you a baseline against which any future learned rewriter can be compared.&lt;/li&gt;
&lt;li&gt;I think the read is most useful for practitioners who want a middle ground between pure neural retrieval and pure rules, because the code shows exactly where those worlds meet in my PoC.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also a pedagogical angle I care about. Many tutorials jump straight to large language models for every turn without establishing a measurement story. I am not anti-LLM; I use them elsewhere. But I believe beginners should see cosine similarity on explicit vectors at least once, because it demystifies what nearest neighbor means in code rather than in marketing language.&lt;/p&gt;

&lt;p&gt;Finally, if you maintain open-source examples, you know the burden of dependencies. I kept the stack bounded so a reader in a constrained environment can still run the demo after accepting the one-time model download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Framing the problem without overfitting the story
&lt;/h3&gt;

&lt;p&gt;Before touching code, I spent time writing short synthetic guest questions on paper. I noticed recurring patterns: some messages emphasize time pressure early, others bury the actionable detail in the second half, and a few mix wellness language with policy language. I did not try to split multi-intent questions into multiple tickets in this repository. Instead, I focused on a single-text input so the healing loop stays easy to reason about. That choice trades realism for clarity, and I am comfortable stating that upfront.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why semantic similarity plus a synonym map
&lt;/h3&gt;

&lt;p&gt;The design starts from an observation I kept returning to while prototyping: informal words are not interchangeable with policy words, yet they often co-occur in real speech. A guest might say rubdown while the document says massage. The embedding model sometimes closes that gap on its own, and sometimes it does not. I did not want a black box rewrite of the query. I wanted a controlled expansion list that I can audit, prune, and argue about in a code review with myself.&lt;/p&gt;

&lt;p&gt;The retrieval layer builds a normalized embedding matrix for every chunk. For each query, I compute cosine similarity as a dot product because the vectors are unit length. I take the top five for debugging, but the orchestration decision hinges on the best score versus a floor constant defined in RetrievalConfig.&lt;/p&gt;

&lt;h3&gt;
  
  
  The healing step as a confidence gate
&lt;/h3&gt;

&lt;p&gt;I use the word healing in a narrow sense. There is no autonomous agent calling external tools without bounds. I am referring to a decision step that tries synonym expansion when the first pass looks weak, then merges the top passages when the expanded query still fails to clear the floor. That is not deep reasoning. It is a guardrail with two rungs. I still call it healing in the sense that the system attempts to repair a weak match before it declares the evidence unreliable.&lt;/p&gt;

&lt;p&gt;If I were to extend this experiment, I would log the floor crossings and measure how often expansion helps relative to merge. In this PoC, I only observe the behavior in the console and in the chart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability as a first-class requirement
&lt;/h3&gt;

&lt;p&gt;I insisted on ASCII-friendly batch output because I wanted copy-pasteable logs for my own notes. Rich tables are not strictly necessary, but they make the first screen readable when I am tired. The matplotlib chart is a concession to the human visual system. Even a simple bar chart changes how I perceive imbalance across healing actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ethics and guest-facing tone
&lt;/h3&gt;

&lt;p&gt;I thought carefully about how synthetic hospitality language can still carry real emotional weight for readers. I avoided sensational scenarios. I kept policies dull on purpose because dull policy text is what operational systems ingest. I also avoided implying that this PoC could triage safety-critical incidents. It cannot. It is a toy corpus with toy thresholds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Get Cooking
&lt;/h2&gt;

&lt;p&gt;The public repository is here: &lt;a href="https://github.com/aniket-work/GuestResilience-HotelContext-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/GuestResilience-HotelContext-AI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will highlight three slices of the code that capture the spirit of the build: configuration boundaries, the healing orchestration, and the batch entry point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration as a contract with future me
&lt;/h3&gt;

&lt;p&gt;I centralized thresholds and the synonym map in one module so I cannot pretend magic numbers appeared from nowhere. The floor is a single float. The synonym map is a dictionary from informal triggers to extra tokens that nudge the embedding toward policy vocabulary.&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;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetrievalConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Tunable thresholds for the PoC; not tuned on production traffic.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;similarity_floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;
    &lt;span class="n"&gt;staleness_warning_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="n"&gt;SYNONYM_EXPANSIONS&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;swimming&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;aquatics&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;lap pool&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;towels&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;spa&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;massage&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;wellness&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;studio&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;appointment&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;rubdown&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;massage&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;wellness&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;studio&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;appointment&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;cancellation&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;checkout&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;late checkout&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;11:00&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;2:00&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;availability&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;morning&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;late checkout&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;11:00&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;2:00&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;availability&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;rush&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;loud&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quiet hours&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;complaints&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;noise&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;neighbors&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;tesla&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EV&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;charging&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;garage&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;kilowatt&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;overnight&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;clean&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;housekeeping&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;towels&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;privacy mode&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;tablet&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;nights&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote the dataclass as frozen because I wanted the configuration object to behave like a value I pass around without accidental mutation during a late-night edit. In my opinion, small immutability choices reduce self-inflicted bugs in solo projects too. The synonym map is deliberately limited. I did not try to learn it from data in this repository because I wanted an honest baseline I could explain without pointing at a training pipeline I do not own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Healing orchestration: measure, expand, merge
&lt;/h3&gt;

&lt;p&gt;The orchestration function encodes the query, compares against the floor, optionally expands the query text when informal keywords appear, and finally merges the top passages if the system still cannot climb above the floor. The merge path constructs a synthetic chunk identifier so the log shows that the evidence is bundled rather than a single canonical policy paragraph.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;self_healing_retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query_used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_query&lt;/span&gt;
    &lt;span class="n"&gt;qvec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qvec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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="n"&gt;best_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked&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;best_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;similarity_floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HealingResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;healing_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieval above similarity floor.&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;expanded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_expand_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_query&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;expanded&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;evec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expanded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ranked_e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked_e&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;es&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expanded&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;similarity_floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HealingResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;healing_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;synonym_expand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Synonym expansion recovered a confident match.&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;ranked_for_merge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked_e&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;ranked_for_merge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;

    &lt;span class="n"&gt;merged_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;merged_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_merge_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked_for_merge&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;merged_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HealingResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;merged_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;merged_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;healing_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_merge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Merged top passages after low single-chunk confidence.&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HealingResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;healing_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staleness_flag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Healing did not lift score above prior best; flagged for offline refresh.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I read this function cold, I look for three things: whether I accidentally reuse the wrong ranked list after expansion, whether the merge path can invent a score that looks comparable to cosine similarity, and whether the failure mode still returns something inspectable. The merge score is an average of the top similarities. That is not a probability. It is a crude signal so the PoC can prefer a wider bundle over a single weak chunk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entry point: batch questions and a chart
&lt;/h3&gt;

&lt;p&gt;The main module loads chunks, embeds them once, runs the first query with a detailed table, then iterates a batch list and records healing actions for plotting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RetrievalConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_chunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode_texts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChunkIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_demo_queries&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;res0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;self_healing_retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print_single_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&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="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;actions&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;_demo_queries&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;self_healing_retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;healing_action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;healing_action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nf"&gt;print_batch_ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;out_png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healing_actions.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;plot_healing_actions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I structured the demo queries to span direct pool language, slang wellness language, vague checkout language, noisy neighbor language, EV parking language, and opaque housekeeping language. In my experience, that spread is enough to stress the synonym map without pretending the corpus is comprehensive.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fxr2hd9ywkt60ykkw565q.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fxr2hd9ywkt60ykkw565q.png" alt="Agent communication" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I rejected along the way
&lt;/h3&gt;

&lt;p&gt;I considered a few alternatives before settling on MiniLM plus a manual synonym map for the first public cut. A cross-encoder reranker would likely improve ordering on ambiguous pairs, but it would also double the inference story and tempt me to hide mistakes behind a second model without a clean measurement layer. I decided that demonstrating a two-stage neural stack was not the point of this repository. The point was to show a transparent loop.&lt;/p&gt;

&lt;p&gt;I also thought about BM25 as a lexical backstop. It is a strong baseline for short documents and behaves well when the vocabulary overlap is explicit. On a handful of ten-chunk snippets, the difference between BM25 and TF-IDF style signals is often swamped by the fact that the corpus is tiny. I stayed with dense embeddings because the guest language problem I care about is often semantic drift rather than spelling. I can imagine a hybrid later: dense retrieval for the first pass, BM25 for dispute resolution when two chunks tie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector store and why I kept it in memory
&lt;/h3&gt;

&lt;p&gt;The ChunkIndex class is intentionally boring. It stores a matrix of normalized embeddings and a parallel list of PolicyChunk objects. Search is a matrix-vector product followed by sorting. I did not use an approximate nearest neighbor index because the row count is ten. Bringing in HNSW or IVF would be theater. I would rather write code that a reader can grep in a single file than pretend this PoC needs a billion-scale index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Staleness as a narrative device
&lt;/h3&gt;

&lt;p&gt;Each synthetic chunk carries a staleness_days integer. I use it as a narrative device in the detail string when a chunk crosses a warning threshold. I am not running a real document management system. I am simulating the feeling of operations where a PDF might have been updated last quarter while the embedding still reflects old text. If I ever wire this to a real ingestion pipeline, staleness should come from a database, not a JSON field I edited by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Theory in plain language: what cosine similarity is doing here
&lt;/h2&gt;

&lt;p&gt;When I say cosine similarity, I mean the dot product between two unit vectors. The sentence-transformers library can emit normalized embeddings, which turns cosine similarity into a single dot product without a separate magnitude step. That is convenient, but it also means I am trusting the encoder to place paraphrases near each other in angular space. On small corpora, the geometry can be surprisingly sharp. Two chunks that look similar to me might sit far apart because the model latched onto different function words.&lt;/p&gt;

&lt;p&gt;I spent time thinking about what a score of 0.45 means versus 0.65. In a calibrated probabilistic system, those numbers would come with a story about calibration. Here, they are relative ranks with a threshold I set manually. I want to be explicit about that because it is easy to reify cosine scores as confidence when they are not. In my PoC, the score is a compass needle, not a verdict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge cases that kept me honest
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;If a guest uses a phrase that matches multiple synonym keys, the expansion step concatenates extra tokens. That can help or hurt. More tokens add noise. I mitigated this by keeping the synonym tuples short and topic-aligned.&lt;/li&gt;
&lt;li&gt;If the first retrieval is already above the floor, I do not attempt healing. That is intentional. I did not want a system that always second-guesses a strong match.&lt;/li&gt;
&lt;li&gt;If expansion fails and merge still looks weak, I fall back to staleness_flag. That label is intentionally unsatisfying. It is a reminder that some queries need a human or a richer corpus, not another heuristic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Personal workflow notes from building solo
&lt;/h3&gt;

&lt;p&gt;I kept a paper notebook beside the keyboard while I wrote the synthetic questions. That sounds quaint, but it slowed me down in a useful way. When I type questions directly into code, I optimize for short strings. When I write them on paper, I leave in awkwardness. Awkwardness is the point. I also tracked my own confusion: if I could not remember why a threshold existed a week later, I renamed a variable or added a comment. I am not claiming perfect documentation. I am claiming that solo work still benefits from a future reader, and that future reader is often me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;Step-by-step details can be found in the repository README. At a high level, I create a virtual environment inside the project directory, install requirements, and run &lt;code&gt;python main.py&lt;/code&gt;. The first execution downloads the sentence-transformers weights, which is the longest step. I prefer keeping the virtual environment local to the project so the PoC stays self-contained when I archive it months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deeper code walkthrough: embedder and index
&lt;/h2&gt;

&lt;p&gt;The embedder module memoizes the SentenceTransformer model so repeated runs do not reload weights. I encode all chunk texts once at startup, then reuse the matrix for every query. That is standard batching discipline, but it matters when I iterate on questions because the expensive work stays amortized.&lt;/p&gt;

&lt;p&gt;The vector store computes similarity as a dot product between the query vector and each row of the matrix. I take the top five for debugging even though decisions only need the top one. I do that because I want to inspect near-misses when a score looks wrong. In my experience, the second-best chunk often explains why the first-best chunk is misleading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roadmap I would pursue if this stayed a hobby
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add a small evaluation harness with paraphrased questions and a simple precision-at-one metric.&lt;/li&gt;
&lt;li&gt;Swap the manual synonym map for a learned sparse expansion that I can still audit, or for a curated ontology from a domain I own.&lt;/li&gt;
&lt;li&gt;Introduce an explicit human-approval flag in the output for any evidence bundle that includes merged chunks.&lt;/li&gt;
&lt;li&gt;Explore a lightweight reranker only after the measurement harness exists, because I refuse to stack models without a baseline.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reflections on reliability and guest trust
&lt;/h2&gt;

&lt;p&gt;Reliability is not only a technical score. It is also the feeling a staff member gets when they read the evidence. If the evidence is verbose, contradictory, or obviously stitched, trust drops even when a cosine score is high. I thought about that while designing the merge path. Bundling top passages is a blunt instrument. It increases recall at the cost of readability. In a production setting, I would want a summarization step that cites chunk identifiers, and I would want those identifiers to map back to a source document version. None of that exists here. I am describing what I would do next, not what I shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When the script finishes, I expect a Rich table for the first query, an ASCII batch summary, and a matplotlib file under &lt;code&gt;output/healing_actions.png&lt;/code&gt;. I treat that chart as a sanity check. If every bar lands in one category, I suspect a bug or a threshold that is too aggressive.&lt;/p&gt;

&lt;p&gt;I usually run the script twice in a row during development. The first run pays the model download cost if the cache is cold. The second run is the one I use to compare output after a code change, because it removes network noise from the picture. That habit saved me from chasing ghosts more than once when I thought my logic changed scores but the real difference was initialization time.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F1dusdrjljbqnh6lp2l1f.png" 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.amazonaws.com%2Fuploads%2Farticles%2F1dusdrjljbqnh6lp2l1f.png" alt="Workflow" width="498" height="1299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would measure if I turned this into a longer study
&lt;/h2&gt;

&lt;p&gt;I am not running a formal benchmark in this repository. I want to be explicit about that gap because benchmarks are where retrieval claims go to become honest or fall apart. If I had another month of evenings, I would build a small labeled set of question-and-chunk pairs derived from the same synthetic corpus, then sweep the similarity floor and record how often expansion or merge changes the top chunk. I would also measure latency on a cold start versus a warm start, because the sentence-transformers download is the kind of friction that changes whether a demo feels credible in a conference room with spotty Wi-Fi.&lt;/p&gt;

&lt;p&gt;I would also track how often merged bundles confuse a human reader. That is a qualitative metric, but it matters. A merge that improves cosine similarity but produces a wall of text is not a win if the goal is staff confidence. In my opinion, human readability should be a first-class metric alongside rank metrics, even if it is harder to automate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I did not ship a full chatbot wrapper
&lt;/h2&gt;

&lt;p&gt;A full chatbot would need session management, safety filters, and a clear escalation path. Those layers are important, but they would dilute the retrieval story I wanted to tell. I kept the surface area small on purpose. The CLI prints evidence. That is enough for me to judge whether the retrieval layer is behaving, and it is enough for a reader to fork the repository without inheriting a web stack I do not want to maintain as a solo author.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies, downloads, and the social contract of open weights
&lt;/h2&gt;

&lt;p&gt;I rely on publicly available weights. That choice carries a social contract: read the license, respect attribution, and do not pretend the model is neutral truth. I also accept the reality that first-time downloads can fail for reasons outside my code. I mention this because newcomers sometimes blame their own competence when the network hiccups during a Hugging Face download. If that happened to you while reproducing my PoC, retrying with a stable connection usually fixes it. If it persists, mirror the weights locally and point the configuration at your mirror. I did not bake a mirror into the repository because I did not want to privilege a single hosting strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrative distance from production
&lt;/h2&gt;

&lt;p&gt;I keep repeating that this is experimental because I want the distance to be obvious. Production systems have change control, incident response, and accountability chains I am not simulating. When I say staleness_flag, I am not claiming an operational incident ticket exists. I am labeling a branch in my code. That distinction matters if someone reads this article quickly and assumes they can paste the repository into a live environment without additional work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I learned about my own habits
&lt;/h3&gt;

&lt;p&gt;I noticed that I reached for matplotlib faster than I reached for unit tests in the first week. That is not a brag. It is a confession. Charts feel like progress. Tests feel like discipline. In a longer project I would add tests around the synonym expansion function and the merge path because those are the places where silent bugs hide. For this PoC, I relied on manual inspection and repeated runs. I am documenting that choice because I want readers who clone the repository to know where rigor ends and storytelling begins in my own process.&lt;/p&gt;

&lt;h3&gt;
  
  
  A word on naming
&lt;/h3&gt;

&lt;p&gt;I named the repository GuestResilience-HotelContext-AI because I wanted the words to sound operational without sounding like a product SKU. Names matter when you revisit a folder six months later. I have abandoned enough cleverly named experiments to appreciate boring clarity.&lt;/p&gt;

&lt;p&gt;I started this experiment because I wanted a personal answer to a simple question: what does resilience mean when the model is small, the corpus is synthetic, and the user language is sloppy? My answer, for now, is that resilience looks like measurement first, bounded repairs second, and honest failure labels third. I do not think that answer is universal. I think it is a reasonable discipline for a PoC that might otherwise collapse into storytelling.&lt;/p&gt;

&lt;p&gt;If I revisit the project, the first upgrade I would consider is a principled evaluation split: hold out chunks, paraphrase questions, and quantify how often expansion helps versus hurts. The second upgrade would be a real staleness pipeline, not a numeric field I typed by hand. The third upgrade would be an explicit separation between guest-facing summarization and evidence retrieval, even if both use models, because commingling them erodes auditability.&lt;/p&gt;

&lt;p&gt;If nothing else, I hope this write-up convinces you that resilience can be practiced as a discipline even when the dataset is synthetic. The point is not to win a leaderboard. The point is to build a habit of measuring, repairing, and labeling failure without embarrassment.&lt;/p&gt;

&lt;p&gt;I also want to leave you with a caution I apply to my own demos. Hospitality language intersects with accessibility, safety, and fairness. A retrieval stack that looks clever on a developer laptop can still be wrong in ways that matter. I wrote this as an experimental article precisely because I want room to be humble about those limits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: python, rag, machinelearning, hospitality&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading this far. I know it is a long piece. I wrote it at this length because I wanted the reasoning trail to be inspectable, not because I enjoy typing for its own sake.&lt;/p&gt;

&lt;p&gt;Disclaimer&lt;/p&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

</description>
      <category>python</category>
      <category>rag</category>
      <category>machinelearning</category>
      <category>hospitality</category>
    </item>
    <item>
      <title>Layered Agentic Retrieval for Retail Floor Questions: A Solo PoC</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Sat, 04 Apr 2026 02:32:49 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/layered-agentic-retrieval-for-retail-floor-questions-a-solo-poc-221g</link>
      <guid>https://forem.com/exploredataaiml/layered-agentic-retrieval-for-retail-floor-questions-a-solo-poc-221g</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fonlhrmxvfa2w3s37iv6f.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fonlhrmxvfa2w3s37iv6f.gif" alt="Cover animation" width="880" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I Routed Associate Questions Across Specialized TF-IDF Indexes Before Assembly&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fona0gdwdbdmvnm9nc4x9.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fona0gdwdbdmvnm9nc4x9.png" alt="Title diagram" width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This write-up documents a personal experiment I ran while thinking about how retail associates actually use knowledge in the moment. A shopper rarely asks a question that fits neatly into a single policy PDF. The phrasing is noisy, the intent is mixed, and the clock is always ticking. I built a small Python system that treats each question as a routing problem rather than a retrieval problem with a single index. Three independent TF-IDF corpora stand in for returns policies, product care guidance, and service-floor procedures. An orchestrator scores each domain, retrieves top hits from the winner, and optionally blends in a second domain when the primary score looks weak. I kept the entire pipeline on-device without calling a hosted language model, because I wanted the evidence to be inspectable and reproducible on a laptop. The repository is public for learning purposes only; it is not a product recommendation, not a deployment blueprint, and not connected to anything I have shipped at a job. I am describing it as a proof of concept because that is what it is, and I am careful not to claim that this small corpus behaves like a real enterprise knowledge base. If you only take one sentence away, take this: I cared more about inspectable routing than about impressing anyone with model names, and that priority shaped every file I wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Before I describe the code, I want to anchor the emotional posture of this article. I am writing as an individual who builds experiments in public to learn, not as someone who is reporting on a deployed system or claiming validated business outcomes. That framing matters because retail is easy to romanticize and easy to misrepresent. I have spent a fair amount of time watching how people ask questions in retail settings, not as a researcher with formal instruments but as someone who pays attention to phrasing when I am in line or when I am helping a friend think through a store policy. The questions are rarely perfect. Someone might say “I need to return this” while also mentioning “the coating on the jacket feels wrong after one wash.” That sentence mixes two worlds. One world is about returns and receipts. The other is about care instructions and product durability. If you flatten everything into one retrieval index, you can still get plausible text, but you lose the ability to explain why a snippet was chosen. That loss of explainability matters to me as an engineer, not because I dislike neural models, but because I like to know which shelf the system reached for first.&lt;/p&gt;

&lt;p&gt;In my PoC I chose a retail floor framing because it is relatable, easy to illustrate with synthetic documents, and avoids domains I have been avoiding in this personal writing series. I am not describing inventory optimization, warehouse counts, or pricing strategy here. I am not touching financial advice. I am staying with a narrow slice: how an associate-facing assistant might assemble evidence before anyone writes a customer-facing sentence. I also want to be clear about the social layer. Associates are not robots. They use judgment. A system that pretends to replace that judgment with a single score is not something I would defend. What I am experimenting with is a structured scaffold that keeps evidence visible so a human can still override.&lt;/p&gt;

&lt;p&gt;I wrote this article because I wanted a serious project that still fits on a laptop. I have seen enough demos where a model produces fluent language and hides the underlying evidence. I wanted the opposite. I wanted logs that read like engineering notes, not marketing copy. The code is structured so that I can print a bundle of evidence rows with identifiers and cosine scores. That is not glamorous, but it is the kind of transparency I find useful when I iterate.&lt;/p&gt;

&lt;p&gt;There is another motivation I should state plainly. I am interested in practices that survive contact with messy language. People shorten words, omit nouns, and rely on context. They say “the thirty-day thing” instead of “the return window policy.” Any retrieval system that assumes the query already contains canonical terms will fail in ways that look embarrassing on a demo but painful in real life. I did not solve that fully here. I only created a place to talk about it honestly while still writing code.&lt;/p&gt;

&lt;p&gt;I also want readers to know the scope boundary I used while writing. This article discusses a synthetic dataset and illustrative scoring rules. It does not describe any real retailer’s policies, staffing model, or vendor contracts. If a phrase resembles language you have seen in the wild, that is because operational writing converges on similar vocabulary, not because I copied private material.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The article walks through RetailFloor-AgenticRouter-AI, a Python project that ingests short customer-style questions, scores three separate TF-IDF indexes, and retrieves evidence from the best-matching domains before optional blending.&lt;/li&gt;
&lt;li&gt;I explain why I combined lexical hints with cosine similarity rather than relying on either signal alone in isolation.&lt;/li&gt;
&lt;li&gt;I show how the batch table and matplotlib chart help me see whether the demo is skewing toward one domain because of a bug or because of the wording of the synthetic questions.&lt;/li&gt;
&lt;li&gt;I discuss limitations honestly: tiny corpora, linear scoring, and heuristic thresholds are not the same as a live knowledge system.&lt;/li&gt;
&lt;li&gt;I include a code walkthrough that mirrors how I read the repository myself when I return to it after a gap.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;The implementation is intentionally boring in a good way. I rely on Python 3.10 or newer, NumPy, scikit-learn for TF-IDF and cosine similarity, matplotlib for a bar chart, and Rich for readable terminal output. There is no hosted vector database and no cloud requirement; the entire index fits in memory.&lt;/p&gt;

&lt;p&gt;From where I stand, that stack is enough to demonstrate the idea that “agentic routing” in the small can be practiced with classical IR tooling when the corpus is tiny and the goal is structured assembly rather than open-ended generation. If I later swap TF-IDF for embeddings, the per-domain interfaces remain stable, which was a design goal while I sketched the modules.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fpg3y2jcjlxy1v5ye7l8p.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fpg3y2jcjlxy1v5ye7l8p.png" alt="System architecture" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If you are evaluating how to structure prompts or pre-model logic for operational assistants, this article offers a concrete pattern: treat context as composable blocks with clear boundaries.&lt;/li&gt;
&lt;li&gt;If you are learning scikit-learn’s text pipelines, the retrieval module is short and testable.&lt;/li&gt;
&lt;li&gt;If you care about reproducibility, the orchestrator gives you a baseline against which any future learned model can be compared.&lt;/li&gt;
&lt;li&gt;I think the read is most useful for practitioners who want a middle ground between “pure LLM” and “pure rules,” because the code shows exactly where those worlds meet in my PoC.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also a pedagogical angle I care about. Many tutorials jump straight to embeddings and vector databases without establishing why lexical baselines still matter. I am not anti-embedding; I use them elsewhere. But I believe beginners should see cosine similarity on explicit vectors at least once, because it demystifies what “nearest neighbor” means in code rather than in marketing language.&lt;/p&gt;

&lt;p&gt;Finally, if you maintain open-source examples, you know the burden of dependencies. I kept the stack small so a reader in a constrained environment can still run the demo. That constraint shaped decisions as much as any architectural principle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Framing the problem without overfitting the story
&lt;/h3&gt;

&lt;p&gt;Before touching code, I spent time writing short synthetic shopper questions on paper. I noticed recurring patterns: some messages emphasize receipts and timing early, others bury the actionable detail in the second half, and a few mix care language with service language. I did not try to split multi-intent questions into multiple tickets in this repository. Instead, I focused on a single-text input so the routing stays easy to reason about. That choice trades realism for clarity, and I am comfortable stating that upfront.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why multiple indexes instead of one concatenated corpus
&lt;/h3&gt;

&lt;p&gt;The design starts from a simple observation I kept returning to while prototyping: returns policy text is not interchangeable with product care guidance. They answer different questions. Policies talk about windows, receipts, and eligibility. Care guidance talks about fabric, detergents, and storage. Service-floor procedures talk about pickup, escalations, and documented steps for adjustments. When I mixed those prematurely, I got tangled retrieval results. When I separated them, I could log each domain independently.&lt;/p&gt;

&lt;p&gt;The retrieval layer builds one TF-IDF vectorizer per domain. Each domain has a handful of short documents with identifiers. For each query, I compute a cosine similarity between the query vector and every document vector in that domain, and I take the maximum as a domain strength signal. That is a simple baseline, but it is a baseline I can explain to a colleague without drawing diagrams.&lt;/p&gt;

&lt;p&gt;The orchestration layer combines those strengths with a small lexical hint. The hint is a deliberately limited set of regular expressions that look for words like “return,” “wash,” or “curbside.” I did not try to build a full intent model. I wanted a small nudge that prevents absurd routing when the vector space is sparse. In my opinion, that is a trade you can criticize. I would rather hear that criticism than pretend the vector space is larger than it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  The agentic step as a confidence gate
&lt;/h3&gt;

&lt;p&gt;I use the word “agentic” in a narrow sense. There is no autonomous loop that calls external tools. I am referring to a decision step that can widen retrieval when the primary domain looks weak. If the combined score for the primary domain falls below a threshold I tuned by hand, I pull additional hits from the next-best domain. That is not deep reasoning. It is a guardrail. I still call it agentic in the sense that the system chooses a second retrieval path based on measured confidence rather than a fixed pipeline.&lt;/p&gt;

&lt;p&gt;If I were to extend this experiment, I would log the threshold crossings and measure how often the secondary blend helps. In this PoC, I only observe the behavior in the console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieval choices and what I rejected
&lt;/h3&gt;

&lt;p&gt;I considered a few alternatives before settling on TF-IDF for the first public cut. A dense embedding model would likely rank semantically related chunks more robustly, but it would also introduce versioning questions, dependency weight, and reproducibility concerns for readers who just want to clone and run. I decided that demonstrating clean interfaces mattered more than squeezing extra retrieval quality from a miniature corpus.&lt;/p&gt;

&lt;p&gt;I also thought about BM25. It is a strong baseline for lexical tasks and behaves well on short documents. I stayed with TF-IDF largely because the scikit-learn pipeline is familiar to many readers and the difference between BM25 and TF-IDF on a handful of short documents is unlikely to change the story materially. If I expand the corpus by an order of magnitude, BM25 or a hybrid approach becomes more compelling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability as a first-class requirement
&lt;/h3&gt;

&lt;p&gt;I log the evidence bundle for the first query in every run not because the first query is special, but because it proves the pipeline without drowning the reader in repetition. In a longer study I would probably log structured JSON for every query and ship it to a file, but the PoC keeps stdout readable.&lt;/p&gt;

&lt;p&gt;The matplotlib chart is part of the same philosophy. A batch table tells you what happened row by row; a distribution tells you whether the demo batch skewed toward one domain. In my experiments, skew often revealed mistakes in keyword priorities rather than retrieval mistakes, which surprised me at first.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fqi4b1re7c7v48r0kciij.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fqi4b1re7c7v48r0kciij.png" alt="Runtime sequence" width="752" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Get Cooking
&lt;/h2&gt;

&lt;p&gt;The entry point is &lt;code&gt;main.py&lt;/code&gt;. It keeps the demo batch in one helper so the narrative stays obvious when someone reads top to bottom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_demo_queries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;(id, short label, customer text)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&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;Q-01&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;returns window&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;I bought jeans last week, tags still on, can I still bring them back with my receipt?&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="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Q-02&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;care&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;How should I wash this water-resistant jacket without ruining the coating?&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="c1"&gt;# ... additional synthetic questions ...
&lt;/span&gt;    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does: it defines the synthetic workload as tuples that include a question identifier, a human-readable label for my own notes, and the free-text body. I structured it this way because separating labels from the text lets me test routing without fabricating metadata inside the prose.&lt;/p&gt;

&lt;p&gt;Why I wrote it this way: early on, I inlined labels as hashtags inside the text and immediately regretted it. Parsing labels from natural language is a separate project. For this PoC, explicit fields keep runs reproducible.&lt;/p&gt;

&lt;p&gt;The orchestration layer is &lt;code&gt;run_agentic_retrieval&lt;/code&gt; in &lt;code&gt;orchestrator.py&lt;/code&gt;. It ranks domains, retrieves primary hits, and optionally blends secondary hits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agentic_retrieval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;indexes&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="n"&gt;IntentDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top_k_per_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrchestratorResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rank_domains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secondary_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked&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;evidence&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="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IntentDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RetrievalHit&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;primary_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve_top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top_k_per_domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;primary_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;rationale&lt;/span&gt; &lt;span class="o"&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;Primary domain &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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;(combined routing score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;primary_score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&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;if&lt;/span&gt; &lt;span class="n"&gt;primary_score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;_SECONDARY_THRESHOLD&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;secondary_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;secondary_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve_top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top_k_per_domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;secondary_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;rationale&lt;/span&gt; &lt;span class="o"&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; Secondary blend &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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;(combined=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secondary_score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) because primary score stayed below &lt;/span&gt;&lt;span class="sh"&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;_SECONDARY_THRESHOLD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrchestratorResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;primary_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;primary_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;secondary_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secondary_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrchestratorResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;primary_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;primary_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;primary_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;secondary_domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;secondary_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does: it materializes the agentic gate in code. If the primary score is below threshold, I append evidence from the runner-up domain. If not, I keep the evidence narrow.&lt;/p&gt;

&lt;p&gt;Why I wrote it this way: I wanted the branching logic to be explicit and readable. A hidden implicit merge would have made debugging harder when I tuned thresholds.&lt;/p&gt;

&lt;p&gt;The hybrid score itself is computed in &lt;code&gt;rank_domains&lt;/code&gt; by combining cosine strength with lexical boosts. I kept the boosts small so they cannot dominate the vector signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_combined_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexes&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="n"&gt;IntentDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IntentDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;domain_strength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_lexical_boost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does: it anchors the routing in measurable similarity while still allowing short, common retail words to nudge the domain when the corpus is tiny.&lt;/p&gt;

&lt;p&gt;Per-domain retrieval is standard TF-IDF with cosine similarity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DomainIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RetrievalHit&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;qv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vectorizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;sims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_matrix&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ravel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;hits&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="n"&gt;RetrievalHit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;RetrievalHit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt;
                &lt;span class="n"&gt;snippet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&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;hits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does: it returns the top-k most similar documents within a single domain index. That is the building block the orchestrator repeats.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fxm7v251i5uhhoic2sjj6.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fxm7v251i5uhhoic2sjj6.png" alt="End-to-end workflow" width="524" height="819"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Clone the public repository from GitHub: &lt;code&gt;https://github.com/aniket-work/RetailFloor-AgenticRouter-AI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create a virtual environment inside the project directory using &lt;code&gt;python -m venv venv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Activate the environment using &lt;code&gt;source venv/bin/activate&lt;/code&gt; on macOS or Linux&lt;/li&gt;
&lt;li&gt;Install dependencies with &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step-by-step details can be found at the repository README in the same project. I prefer local virtual environments because they keep experiments isolated and reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;python main.py&lt;/code&gt; from the repository root with the virtual environment active.&lt;/li&gt;
&lt;li&gt;Read the first evidence bundle, which prints the primary domain and the retrieved snippets with scores.&lt;/li&gt;
&lt;li&gt;Read the batch summary table and open &lt;code&gt;output/domain_distribution.png&lt;/code&gt; to see the matplotlib distribution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Theory and personal notes on similarity in tiny corpora
&lt;/h2&gt;

&lt;p&gt;When I describe TF-IDF to someone who has only used embeddings, I often start with frequency rather than geometry. Term frequency inside a document tells you what the document emphasizes. Inverse document frequency down-weights terms that appear everywhere. Once you have a sparse vector per document and a vector for the query, cosine similarity becomes a concrete operation: a dot product normalized by magnitudes. That story is not new. I still find it valuable because it connects the math to the words people actually typed.&lt;/p&gt;

&lt;p&gt;In my experiments with this PoC, the corpus is so small that IDF behaves differently than it would on a large intranet crawl. Rare words can dominate. Common words can look overly important if they appear in multiple documents within the same domain. I noticed that when I added a few more sentences to one policy snippet, the relative rankings shifted more than I expected. That sensitivity is not a secret flaw; it is a reminder that retrieval quality tracks corpus curation.&lt;/p&gt;

&lt;p&gt;I also thought about correlation between domains. In a single merged index, a query might retrieve a returns snippet and a care snippet together because both mention “original packaging” or similar phrasing. By splitting indexes, I force the system to decide which domain is primary before I show mixed evidence. That decision can be wrong, but at least it is explicit. In my opinion, explicit wrongness is easier to debug than implicit blending.&lt;/p&gt;

&lt;p&gt;Another topic I want to address is calibration. Cosine scores on TF-IDF are not probabilities. I still print them because relative ranking matters more than absolute numbers for this demo. If I ever publish a more serious evaluation, I would separate “routing accuracy” from “snippet usefulness” and measure them with different labeled sets. For now, I rely on a batch table and a chart, which is humble instrumentation, but it matches the scale of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deeper walkthrough of the batch questions I chose
&lt;/h2&gt;

&lt;p&gt;I picked six questions because they cover different shapes of language without pretending to be a comprehensive benchmark. The returns window question is direct. The care question uses product language. The curbside question pulls service-floor vocabulary. The “ambiguous” running shoe question is intentionally written to stress the boundary between care guidance and subjective comfort language. The gift receipt question tests whether returns language routes cleanly. The escalation question tests service-floor procedures.&lt;/p&gt;

&lt;p&gt;When I first ran the batch, I looked at the primary domain column before I looked at scores. That habit comes from older debugging practice: identify the categorical outcome, then inspect the numeric confidence. In my PoC, I also read the chart to see whether one domain swallowed the batch. If that happened without a good reason, I assumed I had a bug or a badly worded question. That is a cheap sanity check, but it caught a few mistakes while I iterated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would measure next if I invested another weekend
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Precision and recall for domain routing against a labeled set of at least two hundred queries.&lt;/li&gt;
&lt;li&gt;Mean reciprocal rank for the top evidence snippet within the correct domain.&lt;/li&gt;
&lt;li&gt;Frequency of secondary-blend activations and whether those cases correlate with improved human ratings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I have not done that work here. I am stating it as a matter of intellectual honesty. A chart in a blog post is not an evaluation suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes I observed while iterating
&lt;/h2&gt;

&lt;p&gt;Sometimes the vector score for the primary domain is modest even when the lexical hint is strong. In those cases, the combined score still tends to land on the right domain because the hint is doing real work. The opposite also happened during early drafts: strong cosine matches to the wrong domain because of shared words like “store” or “order.” I addressed some of that by tightening documents so repeated generic words appear less often, but the real fix for a production setting would be more documents and better tokenization choices, not cleverer regex.&lt;/p&gt;

&lt;p&gt;I also saw cases where the secondary blend did not trigger because the primary score crossed the threshold even though a human would still want cross-domain evidence. That is a design tension. If I lower the threshold, I blend more often and risk noisy evidence. If I raise the threshold, I stay pure but miss helpful cross-domain context. I do not think there is a universal answer. I think there is only a policy choice that should be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I avoided a hosted model in the core loop
&lt;/h2&gt;

&lt;p&gt;I am not opposed to models. I use them in other projects. In this PoC, I wanted the repository to remain lightweight and the behavior to remain inspectable for readers who may not have API keys or budget. I also wanted to avoid a moving target. Hosted models change versions; retrieval baselines change less often. For a learning artifact, stability matters.&lt;/p&gt;

&lt;p&gt;If I integrated a model later, I would still keep the routing structure. The orchestrator pattern is not tied to TF-IDF. It is a way to decide which evidence shelves to open. In my view, that separation ages well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics and visualization as a discipline habit
&lt;/h2&gt;

&lt;p&gt;The matplotlib chart is simple: counts of primary domain picks across the batch. I still find it useful because it forces a second perspective on the same data. Tables can hide imbalance when you are focused on individual rows. A bar chart makes imbalance obvious. As per my experience writing operational tooling, that kind of redundancy is how I catch mistakes before they become narratives.&lt;/p&gt;

&lt;p&gt;I also think visualization discipline matters when writing publicly. A demo can look convincing because the author cherry-picked queries. A batch section with a chart is still cherry-picked, but it is harder to hide systemic skew without looking inconsistent. I am not claiming purity. I am claiming a slightly higher bar than a single happy-path screenshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this relates to my broader experiments with context assembly
&lt;/h2&gt;

&lt;p&gt;Across several personal PoCs, I keep returning to the same lesson: the model is only as grounded as the evidence you hand it. Routing is one way to ground. Chunking is another. Metadata filters are another. In this retail framing, routing is the headline because it is the piece that most directly mirrors how I think a careful associate works. I picture someone mentally classifying the question before reaching for a binder or a search box. The code is a crude metaphor for that mental step.&lt;/p&gt;

&lt;p&gt;I also think solo experiments have a hidden advantage. There is no committee to smooth the awkward edges. If a design choice is hard to explain, I feel it immediately because I have to write the article myself. That pressure improves clarity even when it does not improve novelty. In my opinion, many good engineering blogs come from that kind of forced explanation rather than from raw brilliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data hygiene notes for anyone who forks the repository
&lt;/h2&gt;

&lt;p&gt;If you fork this project and replace the synthetic corpus with your own text, start with document boundaries. Decide what constitutes a chunk. Decide whether headings belong in the chunk or in metadata. Decide whether you need stable identifiers for compliance reasons. I used short stable identifiers like &lt;code&gt;ret_001&lt;/code&gt; because they read well in logs. In a real setting, you might need provenance fields and timestamps. None of that appears here because I am not simulating compliance tooling.&lt;/p&gt;

&lt;p&gt;I would also caution against mixing marketing language with policy language in the same chunk unless you intend to. Marketing copy often uses emotional words that pollute retrieval for operational questions. In my synthetic set, I tried to keep tone dry and procedural. That choice is artificial, but it is intentionally artificial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and privacy framing
&lt;/h2&gt;

&lt;p&gt;This PoC does not store customer data, payment data, or loyalty identifiers. I mention loyalty only inside synthetic policy text as a generic procedural note. If you adapt the idea to real systems, you should treat evidence logs as sensitive depending on your environment. Retrieval indexes can leak information through side channels if someone can probe them repeatedly. I am not providing a threat model here. I am naming the concern because responsible write-ups should name concerns even when the demo is synthetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Longer reflection on agentic retrieval as a phrase
&lt;/h2&gt;

&lt;p&gt;Language shifts quickly in this field. I use “agentic” because the orchestrator makes a conditional decision that changes which retrievals run. That is a narrow meaning. I am not claiming autonomous agency, persistent memory, or tool use beyond vector retrieval. If the word feels too flashy for your taste, you can substitute “conditional multi-retrieval” and the code still reads the same.&lt;/p&gt;

&lt;p&gt;From my perspective, the value of the word is that it signals intent to practitioners who are comparing patterns. If you are building a catalog of approaches, you want names that map to behaviors. The behavior here is: measure confidence, branch, merge evidence. That is enough to distinguish the flow from a single-query single-index baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expanded discussion of matplotlib output and how I read it
&lt;/h2&gt;

&lt;p&gt;The chart file is written to &lt;code&gt;output/domain_distribution.png&lt;/code&gt;. When I open it, I look for dominance first. If one bar towers over the others, I ask whether the batch questions were written to favor that domain accidentally. Then I look at the absolute counts. With six questions, ties and singletons are common. That is fine for a story, but it would be insufficient for a statistical claim. I treat the chart as a sanity check, not as proof of generalization.&lt;/p&gt;

&lt;p&gt;I also think about color and accessibility. The default color cycle in matplotlib is familiar to many readers, but it is not perfect for every color vision profile. If I extended this project with a public UI, I would revisit palette choices. For a static PNG in a repository, I kept the defaults to reduce dependencies and keep the focus on structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed between iterations while writing this article
&lt;/h2&gt;

&lt;p&gt;Early drafts used only cosine similarity without lexical hints. The routing looked mathematically pure but sometimes felt silly in plain language. I added small boosts because I wanted the demo to track common sense when the corpus is tiny. Some readers will dislike that because it introduces hand-tuned rules. I accept the criticism. I would rather show a transparent hand-tuned rule than hide the same bias inside an unlabeled embedding space.&lt;/p&gt;

&lt;p&gt;I also adjusted the secondary threshold upward once I saw how often the blend triggered. The goal was to make blending meaningful rather than routine. If blending happens on every query, it stops being a guardrail and becomes a second retrieval path you always pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I learned about thresholds
&lt;/h3&gt;

&lt;p&gt;I spent more time than I expected tuning the secondary threshold and lexical boosts. That is typical for small corpora. When you only have a few documents per domain, cosine similarity can swing based on a single shared word. I do not consider that a flaw in cosine similarity. I consider it a reminder that the corpus is the real product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge cases I still think about
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Multi-intent questions that require two different operational actions, not just two evidence bundles.&lt;/li&gt;
&lt;li&gt;Questions that reference SKU numbers or store-specific hours that are not in the synthetic corpus.&lt;/li&gt;
&lt;li&gt;Situations where policy language conflicts across documents, which this PoC does not attempt to resolve.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Ethics and responsible framing
&lt;/h3&gt;

&lt;p&gt;I believe any assistant that touches customer-facing work should default to transparency. That means citing sources, showing scores, and making it obvious when the system is uncertain. I did not build a customer-facing UI here, but I did build the kind of evidence rows I would want to see before trusting a draft answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roadmap if this stays a hobby experiment
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Replace TF-IDF with BM25 or embeddings when the corpus grows.&lt;/li&gt;
&lt;li&gt;Add evaluation harnesses with labeled queries rather than eyeballing batch tables.&lt;/li&gt;
&lt;li&gt;Add structured logging to JSON for offline analysis.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A few more words on reproducibility and environment capture
&lt;/h3&gt;

&lt;p&gt;Whenever I publish a PoC, I ask myself whether someone can reproduce the same numbers on their machine. For this project, the deterministic parts are the TF-IDF fit and the query order. The parts that can drift are library versions and floating point noise. I pinned versions loosely in &lt;code&gt;requirements.txt&lt;/code&gt; with minimum versions rather than exact hashes because this is not a safety-critical artifact. If I needed bitwise reproducibility, I would pin exact versions and record a seed wherever randomness appears. Randomness does not play a role in the current retrieval path, which keeps the story simpler.&lt;/p&gt;

&lt;p&gt;I also think about documentation as part of reproducibility. A repository without a clear run command is a puzzle, not an experiment. That is why I kept &lt;code&gt;main.py&lt;/code&gt; as a single entry point and why I describe the output paths explicitly. From my experience, the fastest way to lose a reader is to hide the command they should run after cloning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Narrative closure without overstating the result
&lt;/h3&gt;

&lt;p&gt;I want to end the technical portion with a calm statement of scope. This PoC demonstrates routing and retrieval assembly. It does not demonstrate customer satisfaction. It does not demonstrate associate productivity. It does not demonstrate compliance alignment. Those outcomes require measurements I did not perform. I am naming the gap on purpose because overstated claims are how experimental articles age poorly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository link
&lt;/h3&gt;

&lt;p&gt;Public code for this experiment: &lt;code&gt;https://github.com/aniket-work/RetailFloor-AgenticRouter-AI&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I keep this repository separate from my publishing scripts so the public repo only contains the PoC implementation, diagrams, and images. If you clone it, you will not find my article drafts or automation utilities mixed into the same tree, because I want the repository to stay a clean reference implementation for the idea itself.&lt;/p&gt;

&lt;p&gt;Disclaimer&lt;/p&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: python, retail, machinelearning, agents&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>retail</category>
      <category>machinelearning</category>
      <category>agents</category>
    </item>
    <item>
      <title>Layered Context Routing for Campus Operations: A Facilities Intake PoC</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Fri, 03 Apr 2026 03:19:04 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/layered-context-routing-for-campus-operations-a-facilities-intake-poc-41b2</link>
      <guid>https://forem.com/exploredataaiml/layered-context-routing-for-campus-operations-a-facilities-intake-poc-41b2</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Ftsi2dgt9viyyrw3oa6is.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Ftsi2dgt9viyyrw3oa6is.gif" alt="Cover animation" width="880" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I Stacked Policy, Place, and Urgency Signals to Route Maintenance Requests&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2F3c4m68pixj55r5i00d7x.png" 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.amazonaws.com%2Fuploads%2Farticles%2F3c4m68pixj55r5i00d7x.png" alt="Layered context concept" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This write-up describes a personal experiment where I treat campus facilities intake as a context engineering problem rather than a single prompt. I combine TF-IDF retrieval over a small policy corpus with building metadata and lightweight urgency hints parsed from free text, then route tickets with explicit rules so every decision stays inspectable. The code lives in a public repository I published for learning purposes, and nothing here should be read as production guidance for a real university or as anything connected to an employer. From my perspective, the lesson worth sharing is that when operational language is messy, stacking context in named layers makes debugging and iteration far easier than stuffing everything into one opaque blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have spent a fair amount of time thinking about how large language models behave when the input is short, ambiguous, and emotionally loaded. Facilities tickets are a good toy domain for that reason. A message might mention a fume hood, a basketball practice schedule, and a broken card reader in adjacent sentences. If you send that straight into a generic completion call, you can get fluent text that is wrong in subtle ways. As per my experience, the failure mode is rarely “the model cannot write sentences.” It is usually “the model does not know which institutional rule actually applies,” or “the model over-trusts the most recent sentence.”&lt;/p&gt;

&lt;p&gt;In my experiments, I wanted a system that still fits on a laptop, does not require a proprietary dataset, and makes the reasoning chain visible in ordinary logs. I chose a campus operations framing because it forces a blend of safety language, building-specific nuance, and time-of-day common sense without touching regulated domains I am intentionally avoiding in this series. The repository is a solo sketch, not a deployed service, and I refer to it throughout as a proof of concept.&lt;/p&gt;

&lt;p&gt;There is another motivation I should state plainly. I am interested in practices that survive contact with maintenance engineers, students, and staff who do not care about the underlying ML buzzwords. People submit tickets under stress. They shorten building names, omit room numbers, and reference “that hallway near the lab” without GPS coordinates. Any system that pretends the text is already structured is going to fail in ways that look embarrassing on a demo but painful in real life. I did not solve that fully here; I only created a place to talk about it honestly while still writing code.&lt;/p&gt;

&lt;p&gt;I also want readers to know the scope boundary I used while writing. This article discusses a synthetic dataset and illustrative SLAs. It does not describe any real institution’s priorities, staffing model, or vendor contracts. If a phrase resembles language you have seen in the wild, that is because operational writing converges on similar vocabulary, not because I copied private material.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;The article walks through the design of CampusContextRouter-AI, a Python project that ingests synthetic maintenance-style requests, retrieves relevant policy snippets, attaches place context from a JSON registry, derives urgency signals from the wording, and emits a route bucket with a priority band and a notional SLA window. I wrote it this way because I wanted to mirror how a human dispatcher glances at policy, then place, then severity, before choosing a queue.&lt;/p&gt;

&lt;p&gt;You will see how I separate retrieval from routing, why I kept the router deterministic in this iteration, and how I generate both a Rich table for the terminal and a simple matplotlib chart so a batch run has a visual artifact. I also discuss limitations honestly: tiny corpora, linear scoring, and heuristic SLAs are not the same as a live work-order system.&lt;/p&gt;

&lt;p&gt;If you are wondering what “context engineering” means in concrete terms here, my working definition is simple: decide what information belongs together, decide what must never be mixed, and serialize the result in a predictable shape. Retrieval produces evidence. Place metadata grounds the evidence. Session signals modulate urgency. Routing consumes all three without collapsing them into an undifferentiated string. That definition may differ from how other authors use the phrase, and that is fine; the implementation is the ground truth for this PoC.&lt;/p&gt;

&lt;p&gt;You should also expect commentary on failure modes. A demo that only shows happy paths is a brochure, not engineering writing. I call out retrieval sparsity, policy conflict, and the limits of regex urgency. I discuss what I would measure next if this stayed a hobby project for more than a few weekends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;The implementation is intentionally boring in a good way. I rely on Python 3.10 or newer, NumPy, scikit-learn for TF-IDF and cosine similarity, matplotlib for a bar chart, and Rich for readable terminal output. There is no hosted vector database and no cloud requirement; the entire index fits in memory.&lt;/p&gt;

&lt;p&gt;From where I stand, that stack is enough to demonstrate the idea that “context engineering” can be practiced with classic IR tooling when your corpus is small and your goal is structured assembly rather than open-ended generation. If I later swap TF-IDF for embeddings, the layered interfaces remain stable, which was a design goal while I sketched the modules.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F8vnm04k2ggmc1fbkgw85.png" 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.amazonaws.com%2Fuploads%2Farticles%2F8vnm04k2ggmc1fbkgw85.png" alt="System architecture" width="800" height="175"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you are evaluating how to structure prompts or pre-model logic for operational chatbots, this article offers a concrete pattern: treat context as composable blocks with clear boundaries. If you are learning scikit-learn’s text pipelines, the retrieval module is short and testable. If you care about reproducibility, the deterministic router gives you a baseline against which any future learned model can be compared.&lt;/p&gt;

&lt;p&gt;I think the read is most useful for practitioners who want a middle ground between “pure LLM” and “pure rules,” because the code shows exactly where those worlds meet in my PoC.&lt;/p&gt;

&lt;p&gt;There is also a pedagogical angle I care about. Many tutorials jump straight to embeddings and vector databases without establishing why lexical baselines still matter. I am not anti-embedding; I use them elsewhere. But I believe beginners should see cosine similarity on explicit vectors at least once, because it demystifies what “nearest neighbor” means in code rather than in marketing language.&lt;/p&gt;

&lt;p&gt;Finally, if you maintain open-source examples, you know the burden of dependencies. I kept the stack small so a reader in a constrained environment can still run the demo. That constraint shaped decisions as much as any architectural principle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Framing the problem without overfitting the story
&lt;/h3&gt;

&lt;p&gt;Before touching code, I spent time writing short synthetic tickets on paper. I noticed recurring patterns: some messages emphasize harm or hazard words early, others bury the actionable detail in the second half, and a few mix multiple issues that would normally be split in a mature work-order system. I did not try to solve splitting in this repository. Instead, I focused on a single-text input so the context layers stay easy to reason about. That choice trades realism for clarity, and I am comfortable stating that upfront.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why layers instead of one concatenated prompt
&lt;/h3&gt;

&lt;p&gt;The design starts from a simple observation I kept returning to while prototyping: policy text is not interchangeable with place metadata. Policies answer what must happen in general. Place metadata answers where the work lives and what constraints repeat for that site. Session signals answer how hot the ticket sounds and whether the clock matters. When I mixed those prematurely, I got tangled prompts. When I separated them, I could log each layer independently.&lt;/p&gt;

&lt;p&gt;The retrieval layer reads &lt;code&gt;data/policies.json&lt;/code&gt;. Each record is a chunk with an identifier, a topic tag, and prose. The TF-IDF vectorizer uses English stop words and unigrams plus bigrams to catch phrases like “fume hood” that unigrams alone might dilute. For each ticket, I take the top few chunks by cosine similarity and format them as a bullet list with scores.&lt;/p&gt;

&lt;p&gt;The place layer reads &lt;code&gt;data/buildings.json&lt;/code&gt;. Each building has a code, human-readable name, zone label, hours profile, and short risk notes. I do not attempt geospatial reasoning in this PoC; the point is to show how a second JSON source can be merged without contaminating the policy text.&lt;/p&gt;

&lt;p&gt;The signal layer currently combines a local hour and weekday flag with an urgency score derived from regular-expression keyword groups on the ticket text. The score is deliberately primitive. In a later experiment I might replace it with a lightweight classifier, but I wanted something explainable first.&lt;/p&gt;

&lt;p&gt;Routing maps the assembled layers to an enumerated bucket such as laboratory safety, classroom AV, grounds, HVAC, or a general bucket. Priorities and SLA hours are assigned with transparent rules that look at both the keyword path and the urgency score. That logic lives entirely in Python so I can unit test it without GPU dependencies.&lt;/p&gt;

&lt;p&gt;Architecturally, the flow is linear: load JSON, fit the TF-IDF index once, iterate demo tickets, assemble layers per ticket, call the router, collect rows, render a Rich table, and plot bucket counts. The diagrams in the repository restate the same story visually.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fcyquhmpkhajoy2ct7x76.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fcyquhmpkhajoy2ct7x76.png" alt="Runtime sequence" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieval choices and what I rejected
&lt;/h3&gt;

&lt;p&gt;I considered a few alternatives before settling on TF-IDF for the first public cut. A dense embedding model would likely rank semantically related chunks more robustly, but it would also introduce versioning questions, dependency weight, and reproducibility concerns for readers who just want to clone and run. I decided that demonstrating clean interfaces mattered more than squeezing extra retrieval quality from a miniature corpus. In my opinion, that is a trade only the author can judge; for teaching purposes, I wanted the smallest artifact that still supports cosine similarity and top-k inspection.&lt;/p&gt;

&lt;p&gt;I also thought about BM25. It is a strong baseline for lexical tasks and behaves well on short documents. I stayed with TF-IDF largely because the scikit-learn pipeline is familiar to many readers and the difference between BM25 and TF-IDF on eight short policies is unlikely to change the story materially. If I expand the corpus by an order of magnitude, BM25 or a hybrid approach becomes more compelling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Urgency scoring as a deliberately imperfect heuristic
&lt;/h3&gt;

&lt;p&gt;The urgency score is built from weighted regular expressions. That looks naive, and it is naive. I still found value in it because it forces me to name the cues I care about: leaks, odors, elevators, HVAC loss, outdoor lighting, and a handful of AV terms. Each cue adds a partial weight capped at one. The cap matters; without it, a long message with many benign keywords could look hotter than a short emergency note.&lt;/p&gt;

&lt;p&gt;When I tested early versions, I saw false positives where “water” appeared in a benign sentence. I tightened patterns to word boundaries and preferred compound cues. This is not a claim that regex is sufficient in production. It is a claim that explainable baselines are useful when you compare future models against something you can read in a single screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability as a first-class requirement
&lt;/h3&gt;

&lt;p&gt;I log the layered context for the first ticket in every run not because the first ticket is special, but because it proves the pipeline without drowning the reader in repetition. In a longer study I would probably log structured JSON for every ticket and ship it to a file, but the PoC keeps stdout readable.&lt;/p&gt;

&lt;p&gt;The matplotlib chart is part of the same philosophy. A batch table tells you what happened row by row; a distribution tells you whether the demo batch skewed toward one bucket. In my experiments, skew often revealed mistakes in keyword priorities rather than retrieval mistakes, which surprised me at first.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F8jbbv1lohqbe4wxqqf6s.png" 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.amazonaws.com%2Fuploads%2Farticles%2F8jbbv1lohqbe4wxqqf6s.png" alt="End-to-end workflow" width="492" height="822"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Get Cooking
&lt;/h2&gt;

&lt;p&gt;The entry point is &lt;code&gt;main.py&lt;/code&gt;. It keeps the demo batch in one helper so the narrative stays obvious when someone reads top to bottom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_demo_tickets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;(id, building_code, text, hour_local, weekday)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&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;T-1001&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;SCI-E&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;Strong chemical odor near fume hood B2; two students reported eye irritation.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;# ... additional synthetic tickets ...
&lt;/span&gt;    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It defines the synthetic workload as tuples that include a ticket identifier, a building code that keys into &lt;code&gt;buildings.json&lt;/code&gt;, the free-text body, and a synthetic clock. I structured it this way because separating clock information from the text lets me test urgency scoring and time-aware policies independently without fabricating timestamps inside the prose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I wrote it this way:&lt;/strong&gt; Early on, I inlined timestamps as strings inside the ticket text and immediately regretted it. Parsing times from natural language is a separate project. For this PoC, explicit fields keep runs reproducible.&lt;/p&gt;

&lt;p&gt;The layered assembly happens through &lt;code&gt;assemble_layers&lt;/code&gt; in &lt;code&gt;context_layers.py&lt;/code&gt;. The function pulls retrieval results, formats three blocks, and returns a &lt;code&gt;LayeredContext&lt;/code&gt; dataclass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assemble_layers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;building_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SessionSignals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PolicyIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buildings&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BuildingContext&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LayeredContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;retrieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;policy_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_format_policy_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved&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;retrieved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;policy_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POLICY SNIPPETS: (no retrieval match; use baseline routing rules)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildings&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="n"&gt;building_code&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;building_code&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;place_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_format_place_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;signal_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_format_signal_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LayeredContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;policy_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;place_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;place_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;signal_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;signal_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retrieved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It centralizes formatting so the router always sees the same headings for each layer. Empty retrieval is handled with a explicit fallback string rather than silent failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; In my opinion, the hardest part of small retrieval systems is debugging silent degradation. If nothing matches, I want that fact visible in the console output.&lt;/p&gt;

&lt;p&gt;Retrieval itself is a thin wrapper around scikit-learn.&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;PolicyIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;In-memory TF-IDF index over policy text.&lt;/span&gt;&lt;span class="sh"&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;chunks&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="n"&gt;PolicyChunk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;_chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
        &lt;span class="n"&gt;corpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&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;_vectorizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TfidfVectorizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;lowercase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;stop_words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ngram_range&lt;/span&gt;&lt;span class="o"&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;min_df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="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;_matrix&lt;/span&gt; &lt;span class="o"&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;_vectorizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corpus&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;top_k&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;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RetrievedChunk&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&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;_vectorizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;sims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&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;_matrix&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&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="n"&gt;RetrievedChunk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;k&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RetrievedChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&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;_chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&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;score&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;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It builds a matrix once and scores incoming ticket text as another vector. Cosine similarity ranks chunks, and I discard zero scores to avoid clutter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; On a toy corpus, bigrams matter. Without them, “fume hood” sometimes loses to generic maintenance words. I kept the corpus tiny on purpose to force myself to think about chunk wording.&lt;/p&gt;

&lt;p&gt;The router combines keyword cues, the top retrieved topic, and session urgency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;free_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;layered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LayeredContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BuildingContext&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SessionSignals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RoutingDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_topic_from_retrieval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;keyword_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_adjust_bucket_from_keywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;free_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouteBucket&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keyword_bucket&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyword_bucket&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;laboratory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RouteBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SAFETY_EHS&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classroom_av&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RouteBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AV&lt;/span&gt;
    &lt;span class="c1"&gt;# ... additional topic mappings ...
&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;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RouteBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GENERAL&lt;/span&gt;

    &lt;span class="n"&gt;urgency_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urgency_hint_score&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;48.0&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RouteBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SAFETY_EHS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RouteBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PLUMBING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;urgency_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;RouteBucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACCESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;
    &lt;span class="c1"&gt;# ... additional escalations ...
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RoutingDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sla_hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rationale_parts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It makes the decision path explicit. Keyword overrides fire first because certain phrases imply a channel regardless of retrieval noise. Topic labels from the best chunk act as a secondary signal. SLA tightening uses both bucket membership and the urgency score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I put it this way:&lt;/strong&gt; I needed a single function I could read during demos without opening a notebook. The rationale string is there so future me remembers why a ticket landed where it did.&lt;/p&gt;

&lt;p&gt;Finally, plotting is one function that turns bucket names into counts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plot_bucket_distribution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;buckets&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subplots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#2c5282&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Synthetic routing batch: bucket counts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tickets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tight_layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;savefig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dpi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plt&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="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It produces a basic bar chart so the batch run is not only textual. For the animated cover asset, I used that chart as the UI half of the GIF after the terminal sequence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository link:&lt;/strong&gt; The full project, including diagrams and the terminal animation asset, is available at &lt;a href="https://github.com/aniket-work/CampusContextRouter-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/CampusContextRouter-AI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reporting code stays intentionally thin: tables for people, files for artifacts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;print_routing_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rows&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="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Campus facilities intake (batch routing)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ticket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cyan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;no_wrap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Building&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;magenta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;green&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;justify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;center&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLA h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;justify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It renders aligned columns with consistent headers so a batch run looks like a dispatch screen rather than a raw log dump.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; In my opinion, presentation quality changes how seriously I take my own outputs during development. If the table looks sloppy, I assume the logic is sloppy.&lt;/p&gt;

&lt;p&gt;The urgency helper is small but central to how priorities tighten.&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="n"&gt;_URGENCY_WORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(leak|flooding|flood|spill|water)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(smoke|fire|odor|fume|chemical)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(elevator|stuck|door won\'t open|door wont open)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(no heat|no ac|freezing|overheat)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(projector|microphone|av|audio|display)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(light|outage|dark|walkway)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.12&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;_urgency_hint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&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="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_URGENCY_WORDS&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;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&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;w&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It scans for cues that should raise urgency regardless of which policy chunk wins retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Weight tuning is subjective. I chose weights that made the science-lab odor scenario land in a high band without pushing every AV ticket into emergency territory.&lt;/p&gt;

&lt;h3&gt;
  
  
  How this differs from “just prompt better”
&lt;/h3&gt;

&lt;p&gt;It is tempting to believe a single system message can replace structured preprocessing. Sometimes that works for short tasks. For operational intake, my experience has been that models benefit from retrieval that is inspectable outside the model. I am not arguing against LLMs; I am arguing that the PoC should show where the boundaries belong. If the retrieval list is wrong, I can fix the corpus or the vectorizer without touching the router. If the router rules are wrong, I can adjust routing without touching retrieval. That separation of concerns saved me time during debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a language model could do in a later iteration
&lt;/h3&gt;

&lt;p&gt;If I add a model, I would keep it as a rewriter or validator, not as the sole authority. A plausible pattern is: assemble layers exactly as today, ask the model to propose a bucket and rationale, then compare against deterministic rules. Disagreements become training data or prompts for refinement. I have not implemented that here because I wanted the repository to remain runnable without API keys, but the layering is compatible with that roadmap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance characteristics I measured informally
&lt;/h3&gt;

&lt;p&gt;This is not a benchmark article, but I did sanity-check runtime on a laptop. Fitting the TF-IDF matrix on eight policies is effectively instantaneous. Routing five tickets is trivial. Matplotlib dominates wall time relative to retrieval, which reinforces that the PoC is not CPU-bound. If I scaled to thousands of policies, I would need a more serious index and probably batch vectorization, but that is not the bottleneck today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;Step-by-step details can be found in the repository README. At a high level, the setup I used while iterating locally follows a predictable pattern.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an isolated virtual environment in the project directory so dependencies never leak across unrelated experiments.&lt;/li&gt;
&lt;li&gt;Install requirements from &lt;code&gt;requirements.txt&lt;/code&gt; exactly as pinned there to avoid surprise upgrades to scikit-learn behavior.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python main.py&lt;/code&gt; with the bundled demo batch to confirm retrieval, routing, and chart generation all succeed on your machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you clone the repository, you will notice there is no &lt;code&gt;.env&lt;/code&gt; requirement for the baseline demo. I kept secrets out of the PoC on purpose so CI or readers can execute it without API keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When I run &lt;code&gt;python main.py&lt;/code&gt;, the program prints the full layered context for the first ticket, then prints the batch table, then writes &lt;code&gt;output/routing_bucket_distribution.png&lt;/code&gt;. That order is intentional: the first block proves the retrieval and formatting pipeline, the table proves routing consistency, and the chart proves that visualization hooks stay wired.&lt;/p&gt;

&lt;p&gt;In my observation, the most interesting console output is the policy snippet list with scores. Even with eight policies, you can watch how sensitive the ranking is to verbs like “odor” versus “noise.” That sensitivity informed how I wrote the synthetic tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Cases I Thought About
&lt;/h2&gt;

&lt;p&gt;No PoC is complete without acknowledging where it would break. These points are worth spelling out because they shaped what I did not attempt.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sparse retrieval:&lt;/strong&gt; If the ticket uses slang that never appears in the policy corpus, TF-IDF may return low scores across the board. The router still runs, but the topic signal becomes weak. A mitigation I considered is hybrid retrieval with a keyword inverted index, which would be a natural extension.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conflicting policies:&lt;/strong&gt; Real campuses can have overlapping rules. I store independent chunks and do not yet model precedence. In a future iteration, explicit precedence edges between chunk IDs would be cleaner than hoping retrieval ranks resolve conflicts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time semantics:&lt;/strong&gt; I pass hour and weekday as integers rather than parsing from text. That avoids accidental contradictions between embedded timestamps and structured fields.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Equity and access:&lt;/strong&gt; Routing touches accessibility topics. I include them because ignoring them would be unrealistic, but the SLA numbers are illustrative only. A production system would need institutional review, not a hobby script.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duplicate submissions:&lt;/strong&gt; Real users open multiple tickets for the same incident. I do not deduplicate or thread conversations in this repository. A deduplication layer would likely sit upstream of retrieval, comparing embeddings of entire messages and linking to an incident ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seasonality:&lt;/strong&gt; A field house ticket in January is not the same as one in August. My building metadata includes a seasonal hours profile label, but I do not dynamically adjust SLAs by season. Extending the signal layer with calendar metadata would be straightforward, but it would also require more realistic data than I wanted to maintain for a hobby repo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Language and tone:&lt;/strong&gt; The PoC assumes English prose of moderate formality. Multilingual campuses would need tokenization choices and policy corpora per language. I did not attempt multilingual retrieval because verifying quality would require skills and resources outside the scope of a solo weekend project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Malicious input:&lt;/strong&gt; Free-text fields can be abused. I do not implement content filtering here. If this were more than a local script, I would add rate limits, length limits, and basic abuse detection before any retrieval occurs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Ethics and Responsibility
&lt;/h2&gt;

&lt;p&gt;Even though this repository is synthetic, the language of safety and access deserves care. I wrote the tickets and policies to resemble realistic operational phrases without copying any private incident text. From my perspective, that distinction matters: public demos should never repurpose confidential work orders.&lt;/p&gt;

&lt;p&gt;I also want to be explicit that automated routing for physical risk scenarios should never be the only line of defense. The PoC emits labels; it does not dispatch tradespeople, text students, or close tickets. Treat it as a learning scaffold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Roadmap (Personal Experiments Only)
&lt;/h2&gt;

&lt;p&gt;If I revisit this repository, several extensions seem worthwhile, still on my own time and still labeled experimental.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Replace TF-IDF with embeddings while keeping the same layer boundaries, then measure how often the router disagrees with the baseline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a second corpus for local procedures that updates more frequently than policy, mimicking how some campuses separate “policy” from “playbook.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Introduce evaluation harnesses with labeled tickets, even if synthetically expanded, so precision and recall become measurable instead of eyeballed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrap the router output as JSON for a tiny local web UI if I want a friendlier demo than the terminal alone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add property tests that generate random word order permutations for tickets to see whether retrieval remains stable enough for my tolerance thresholds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explore calibration for urgency scoring so numeric outputs map to observed human labels in a small user study. That is far beyond this PoC, but worth naming as a scientific next step rather than an engineering tweak.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of those items are promises; they are directions I might explore when curiosity and spare time align.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation habits that helped me
&lt;/h2&gt;

&lt;p&gt;While building this, I noticed that my velocity correlated with how aggressively I documented assumptions in the README. Not tutorial prose for beginners, but crisp statements of non-goals. When I wrote “illustrative SLA,” I stopped myself from secretly believing the numbers meant more than they did. When I listed repository layout, I caught a path mistake before publishing.&lt;/p&gt;

&lt;p&gt;I also kept commit messages boring on purpose. Small repositories deserve readable history too. If I ever return to this code months later, I want future me to recognize intent without decoding clever commit titles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility notes
&lt;/h2&gt;

&lt;p&gt;Reproducibility is part engineering and part discipline. Pinning dependencies avoids the subtle drift that happens when scikit-learn changes defaults across versions. Keeping random seeds matters when you add stochastic components; this PoC has none, which is a feature for now. Recording the Python version in the README is a small touch that prevents “works on my machine” surprises.&lt;/p&gt;

&lt;p&gt;If you fork the repository, consider writing down your own environmental constraints. I develop on macOS, but the code should run anywhere Python runs. If matplotlib backend issues appear on a headless server, switching to a non-interactive backend is a known fix; I did not need it for local runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A longer note on evaluation and what I did not measure
&lt;/h2&gt;

&lt;p&gt;Evaluation is where hobby projects either mature or remain toys. In this PoC, I relied on manual inspection: reading retrieved snippets, checking whether the science lab odor ticket escalated appropriately, and scanning the distribution chart for obvious skew. That approach is acceptable for a first public version because the goal was architectural clarity, not leaderboard scores.&lt;/p&gt;

&lt;p&gt;If I were to formalize evaluation without access to private tickets, I would start by synthesizing a larger labeled set from templates. I would vary lexical overlap, negation, and multi-issue messages. I would measure top-k recall for policy topics and confusion matrices for route buckets. I would also track stability: if I paraphrase a ticket without changing meaning, does retrieval remain broadly consistent? That kind of robustness test often reveals brittleness faster than aggregate accuracy.&lt;/p&gt;

&lt;p&gt;I did not build those harnesses here because they expand repository scope quickly. Test data management is a project of its own. Still, naming the gap matters. Readers should not mistake a crisp table output for validated operational performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual assets and why the GIF exists
&lt;/h2&gt;

&lt;p&gt;The repository includes Mermaid diagrams rendered to PNG via mermaid.ink because I wanted graphics that match the tone of technical documentation rather than stock photography. The animated GIF pairs a terminal sequence with a matplotlib chart to mirror how I actually work: run a script, scan the table, glance at a plot. Creating the GIF took extra time, but from my perspective it communicates intent faster than static screenshots alone.&lt;/p&gt;

&lt;p&gt;I followed a strict palette conversion pipeline for the GIF to reduce flicker on platforms that are picky about animated assets. The details are mundane image processing, but the outcome matters when you publish where rendering quirks exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Personal lessons I did not expect
&lt;/h2&gt;

&lt;p&gt;While wiring the urgency heuristic, I expected retrieval to dominate mistakes. Instead, I often found myself adjusting keyword lists because the language of urgency in synthetic tickets did not match the language of policy chunks. That mismatch reminded me that retrieval and rules interact; you cannot tune one in isolation forever.&lt;/p&gt;

&lt;p&gt;Another surprise was how quickly the Rich table made the PoC feel “real.” Presentation is not substance, but human perception matters when you judge your own progress. I kept the table formatting minimal on purpose. Dense color in terminal output ages poorly and distracts from the routing story.&lt;/p&gt;

&lt;p&gt;Finally, I was reminded how much I enjoy small JSON corpora. They are easy to diff in code review, easy to version, and easy to explain to someone unfamiliar with machine learning. If I had started with a database, I would have spent more time on migrations than on routing logic.&lt;/p&gt;

&lt;p&gt;If you made it this far, thank you for reading carefully. I wrote this piece to document not only what the code does, but why I accepted certain limitations while rejecting shortcuts that would have made the demo flashier yet less honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;What I take away from this experiment is that “context engineering” is not only a prompt-design exercise. It is also an exercise in deciding what information deserves its own channel, how much structure to add before model calls, and how to leave an audit trail. The campus framing helped me keep those questions grounded.&lt;/p&gt;

&lt;p&gt;If you try the code, I hope you modify the policy JSON and watch how retrieval shifts. In my experience, that kind of hands-on perturbation teaches more than reading another listicle about embeddings.&lt;/p&gt;

&lt;p&gt;I also keep returning to a humbling point: good operations depend on people who answer phones, visit sites, and coordinate trades. Software can sort and summarize, but it cannot replace the embodied knowledge of how a specific building behaves in winter. This PoC stays modest because that human layer matters more than any script I would ship on a weekend.&lt;/p&gt;

&lt;p&gt;As a final note, this article is an experimental write-up based on a hobby repository. It is not production guidance, not campus policy, and not affiliated with any organization I work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: python, context, machinelearning, campus&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>context</category>
      <category>machinelearning</category>
      <category>campus</category>
    </item>
    <item>
      <title>ShootMesh-AI: A Transparent “Production Office” for Staged Film-and-TV Days</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Thu, 02 Apr 2026 02:21:04 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/shootmesh-ai-a-transparent-production-office-for-staged-film-and-tv-days-131a</link>
      <guid>https://forem.com/exploredataaiml/shootmesh-ai-a-transparent-production-office-for-staged-film-and-tv-days-131a</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F2l0vcf6e4cbyrpmfirf1.png" 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.amazonaws.com%2Fuploads%2Farticles%2F2l0vcf6e4cbyrpmfirf1.png" alt="Title" width="325" height="886"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I modeled department proposals, a merge policy, and an audit ledger without hiding the coordination rules&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built a small Python proof of concept called ShootMesh-AI that behaves like a miniature production office for a synthetic shooting day. Separate modules pretend to be scheduling, locations, safety, and equipment voices. Each one proposes actions when a staged incident appears. A coordinator applies a deterministic merge policy with an explicit priority order, and a ledger records what was chosen and how many minutes the plan slipped. The runnable code prints an ASCII table to the terminal and writes two charts to disk. From my perspective, the interesting part is not cleverness for its own sake; it is inspectability. I can read the policy, replay the ledger, and explain why a particular department “won” a merge at a particular step. The repository is public for learning, and the article is written as a personal experiment rather than as on-set guidance or employer-backed work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have spent a fair amount of time thinking about how software can support coordination without pretending to be a substitute for human judgment. That tension shows up everywhere, but it is especially easy to picture on a location day where weather, permits, noise, gear faults, and human delays interact. I am not claiming that a script can run a set. What I am claiming, from my own tinkering, is that a disciplined simulation can help clarify what “coordination logic” even means when multiple subsystems disagree.&lt;/p&gt;

&lt;p&gt;In my experiments, I wanted a structure that felt like a company in miniature: different functions propose, a single merge point decides, and a ledger preserves accountability. I wrote this article because I wanted to document the design choices I made while building ShootMesh-AI, the trade-offs I accepted, and the ways I would extend the idea if I kept pushing on it. I put it this way because I value narratives that show the reasoning path, not only the final file tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article walks through an experimental architecture for multi-agent style coordination applied to a film and television production day scenario. The agents are not large language models in this repository. They are code modules with narrow responsibilities. That was intentional. I chose determinism so that two runs with the same code path produce the same ledger, which makes the merge policy testable without stochastic noise.&lt;/p&gt;

&lt;p&gt;The storyline of the PoC is simple. A list of synthetic incidents arrives in order. For each incident, every department contributes one or more proposals. Each proposal carries a priority class, an estimated minute shift, and a confidence score used only for tie breaking under fixed rules. The coordinator selects a winner, records the outcome, and moves on. After the last incident, the program summarizes how often each department’s proposal won and plots cumulative slippage across the day.&lt;/p&gt;

&lt;p&gt;I also discuss what this miniature setup suggests about real coordination systems: transparency, traceability, and the difference between a policy you can cite and a model output you can only guess about. As per my experience, no collaborative group is involved here; this is a solo build and a solo narrative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;I kept the stack boring on purpose. The project is pure Python with &lt;code&gt;matplotlib&lt;/code&gt; for charts and the standard library everywhere else. There is no database, no message broker, and no web server. I made that choice so the article could focus on coordination mechanics rather than infrastructure.&lt;/p&gt;

&lt;p&gt;From where I stand, a small PoC should compile quickly in the reader’s mind. If I had introduced a queueing system and a container orchestrator, I would have spent more time explaining operations than explaining coordination. I may revisit richer infrastructure later, but this repository stays intentionally compact.&lt;/p&gt;

&lt;p&gt;The diagrams in the article were rendered from Mermaid definitions into PNG images using the mermaid.ink approach, then checked into the repository so the README and the article can reference stable URLs. The animated GIF follows a terminal-first layout and then transitions into a simple bar chart; I generated it locally with Pillow using a single global color palette to avoid flicker on platforms that are picky about GIF encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you are evaluating how to structure agent-like systems, you might be weighing opaque end-to-end models against explicit policies. This write-up shows a middle path: small agents propose, but the merge policy remains readable code. I think that pattern matters for debugging and for stakeholder trust, even in an experimental setting.&lt;/p&gt;

&lt;p&gt;You might also read it if you want a concrete Python layout that separates types, scenario data, agents, coordination, reporting, and plotting. I separated those concerns because I did not want a single “god file” to become the hiding place for assumptions. In my opinion, the discipline of folders is part of the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;p&gt;I started by naming the boundaries. A production office is not one brain; it is a set of recurring conversations. Scheduling optimizes order and protects certain blocks. Locations negotiates physical reality and alternates. Safety refuses shortcuts when the refusal is non-negotiable. Equipment translates technical faults into workable mitigations. I did not try to encode every craft; I chose four to keep the PoC legible.&lt;/p&gt;

&lt;p&gt;The coordinator encodes a value ordering that I can defend in prose: safety first, then schedule integrity, then creative accommodations, then cost-sensitive mitigations. That ordering is not universal. It is a hypothesis I used to make the demo behave sensibly when proposals conflict. The important part, in my view, is that the ordering lives in one place and can be changed without rewiring the entire codebase.&lt;/p&gt;

&lt;p&gt;I also wanted a ledger that reads like an audit trail. Each step stores the incident text, the winning department label, and the minutes shifted by the chosen proposal. If I later attach identifiers for scenes or setups, the ledger format still works. I wrote the ledger as a list of structured entries rather than as unstructured log lines to keep downstream analysis simple.&lt;/p&gt;

&lt;p&gt;The visuals mirror the architecture. The title diagram emphasizes the coordinator and ledger as the spine, with department agents feeding proposals inward. The architecture diagram compresses the runtime pipeline to a short chain: incidents, parallel proposals, priority sort, ledger append, artifacts. The sequence diagram is intentionally modest; it exists to show message-like flow even though everything runs in-process. The flow diagram captures the loop and the termination condition.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fricytpwx9fbuvspxxugo.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fricytpwx9fbuvspxxugo.png" alt="Architecture overview" width="800" height="63"&gt;&lt;/a&gt;&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Flpls1dpjolvbw6bdh2ob.png" 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.amazonaws.com%2Fuploads%2Farticles%2Flpls1dpjolvbw6bdh2ob.png" alt="Agent messaging shape" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Faj3z1d8noxulsb8psl97.png" 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.amazonaws.com%2Fuploads%2Farticles%2Faj3z1d8noxulsb8psl97.png" alt="Process flow" width="444" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Get Cooking
&lt;/h2&gt;

&lt;p&gt;The runnable project lives here: &lt;code&gt;https://github.com/aniket-work/ShootMesh-AI&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Below I split the code at meaningful boundaries and explain what each block is doing in the narrative voice I used while building it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Types and explicit priorities
&lt;/h3&gt;

&lt;p&gt;I defined priorities as an enumeration and kept proposals immutable. That choice reduces the chance that a later step mutates a proposal accidentally. It also makes the merge logic easier to read because the priority tier is a first-class field rather than a buried string constant.&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;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;SAFETY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safety&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;SCHEDULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CREATIVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;creative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;COST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Proposal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;
    &lt;span class="n"&gt;minutes_shift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It establishes the vocabulary for proposals and forces every proposal to carry the fields the coordinator expects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; I wanted the merge layer to depend on stable shapes. Frozen dataclasses made that contract obvious while I iterated on agent behaviors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; When I first sketched the PoC, I used loose dictionaries. The code worked, but errors showed up late. Moving to typed objects caught mistakes earlier and made the article easier to write because the data model became self-documenting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coordinator merge policy
&lt;/h3&gt;

&lt;p&gt;The coordinator ranks proposals using a tuple key that reflects my stated ordering. Safety should dominate when it appears. Within a tier, higher confidence wins. When confidence ties, the policy prefers smaller minute shifts because I wanted the demo to bias toward less disruptive fixes when all else is equal.&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="n"&gt;PRIORITY_ORDER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SAFETY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SCHEDULE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CREATIVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COST&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;choose_proposal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposals&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="n"&gt;Proposal&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Proposal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;proposals&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;no proposals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRIORITY_ORDER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minutes_shift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sort_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;rationale&lt;/span&gt; &lt;span class="o"&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;selected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&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;with confidence &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&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;alternatives considered: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rationale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It selects a single winning proposal and constructs a short human-readable rationale string for the ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; I deliberately avoided machine learning here. The point of the PoC is to show a policy you can print on a whiteboard. If I had hidden the ranking inside a model, I would have undermined the story about transparency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Tie-breaking rules are never neutral. I had to document why minute shifts mattered to me as a tie breaker. Without that narrative, readers might assume the numbers were arbitrary when they were a deliberate preference for smaller disruptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Department agents with narrow expertise
&lt;/h3&gt;

&lt;p&gt;Each agent subclass proposes only when it has something credible to say. For example, safety proposes hard stops for weather and gear faults, while scheduling proposes resequencing when talent is late. I avoided giving safety a generic fallback proposal on every incident because it caused safety to over-win merges in early versions of the PoC. That was a useful bug because it reminded me that “always-on” agents can drown out specialized voices if I am not careful.&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;SchedulingAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DepartmentAgent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scheduling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;propose&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;incident&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Incident&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Proposal&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;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;IncidentKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TALENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="nc"&gt;Proposal&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;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Slide block 2 after lunch; protect golden hour exterior&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SCHEDULE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;minutes_shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;Proposal&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;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Re-sequence setups to absorb slip without dropping pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SCHEDULE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;minutes_shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It returns one or more proposals for a given incident, scoped to scheduling’s perspective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; I wanted the agents to feel like departments with partial visibility, not omniscient oracles. In a real system, each proposal might be backed by data from call sheets, travel estimates, or scout photos. Here, the proposals are illustrative, but the structure is what I expect to reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; The difference between a demo and a toy is whether the extension points are honest. I left room to replace proposal text with retrieved facts later without rewriting the coordinator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine and run summary
&lt;/h3&gt;

&lt;p&gt;The engine loops incidents, gathers proposals, calls the coordinator, and accumulates ledger entries. It also computes a simple winner share per department label by counting wins and normalizing to one. That statistic is what feeds the bar chart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_production_day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RunSummary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;
    &lt;span class="n"&gt;incidents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;default_day_incidents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;agents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;all_department_agents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ledger&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="n"&gt;LedgerEntry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;weights&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;slippage&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="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incidents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;proposals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_proposals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proposals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;slippage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resulting_shift_min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chosen_from&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weights&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="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chosen_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

    &lt;span class="n"&gt;total_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="n"&gt;agent_weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RunSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;total_incidents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incidents&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;total_slippage_min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slippage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;agent_weights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It executes the full synthetic day and returns both the ledger and aggregate metrics for plotting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; I kept orchestration separate from proposal logic so I can test the coordinator against handcrafted proposal lists if I want targeted unit tests later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Even a short loop benefits from clear naming. When I wrote the first draft, I inlined too much and struggled to explain it in prose. Pulling &lt;code&gt;collect_proposals&lt;/code&gt; and &lt;code&gt;run_step&lt;/code&gt; into helpers made the article easier to write and the code easier to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entry point and artifacts
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;main.py&lt;/code&gt; script prints a banner, renders the ASCII table, and writes PNGs into &lt;code&gt;output/&lt;/code&gt;. I kept console output friendly because I knew the GIF would lean on the terminal aesthetic.&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="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_production_day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ledger_to_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ascii_table&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step&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;department&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;shift_min&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;incident (truncated)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;plot_agent_influence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_influence.png&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;plot_slippage_timeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resulting_shift_min&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slippage_timeline.png&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt; It turns the run summary into human-readable terminal output and chart files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I structured it this way:&lt;/strong&gt; I wanted a single command that a reader could run after clone, with no configuration step. That lowers friction for experimentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; ASCII tables photograph well in articles and GIFs. That sounds trivial, but visual continuity matters when you are trying to show a serious PoC rather than a loose notebook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;Step by step details can be found at:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repository from &lt;code&gt;https://github.com/aniket-work/ShootMesh-AI.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create a virtual environment in the repository root using &lt;code&gt;python3 -m venv venv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Activate the environment using your platform’s standard activation command&lt;/li&gt;
&lt;li&gt;Install dependencies with &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python main.py&lt;/code&gt; and confirm that &lt;code&gt;output/&lt;/code&gt; contains PNG charts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I deliberately do not require API keys or cloud accounts for the demo. In my opinion, that constraint is a feature for readers who want to reproduce results on a laptop without standing up services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When I run the program locally, I expect to see a table with five rows for the default scenario and a total slippage figure derived from the chosen proposals. I also expect the charts to reflect which departments won merges most often. In the version I pinned while writing, safety wins more frequently than others because the staged day includes several incidents where safety proposals legitimately sit at the top of the priority ordering. That outcome is not a claim about real-world frequency; it is a consequence of the synthetic mix I authored.&lt;/p&gt;

&lt;p&gt;If you compare the terminal output to the ledger logic, you should see a direct mapping. That traceability is the point. I am not asking the reader to trust a hidden score; I am asking the reader to inspect the policy and the ledger together.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fvrzj77bth8or0vf5m2lh.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fvrzj77bth8or0vf5m2lh.gif" alt="Cover animation" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases I considered while iterating
&lt;/h2&gt;

&lt;p&gt;Even a toy coordination loop has edge cases worth mentioning. Empty proposal lists are invalid for a decision step, so the coordinator raises rather than silently picking a winner. If I ever model a department outage, I need an explicit representation of “no proposal” rather than an empty list that crashes the merge. I would also add typed incident identifiers so repeated text does not accidentally collapse distinct events in analytics.&lt;/p&gt;

&lt;p&gt;Another edge case is conflicting notions of “confidence.” In this PoC, confidence is a scalar used for tie breaking within a priority tier. It is not calibrated probability. If I elevated this experiment, I would separate calibrated likelihoods from policy weights, and I would document the difference aggressively to avoid misinterpretation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deeper dive on policy design and alternatives
&lt;/h2&gt;

&lt;p&gt;When I began sketching ShootMesh-AI, I considered three broad approaches before settling on the explicit priority stack I implemented. The first approach was a single scoring function that combined every attribute into one number. It would have been compact, but it would also have hidden the moral and operational precedence that I wanted to discuss in plain language. The second approach was a rule engine with dozens of special cases. I rejected that for the PoC because it would have looked like accidental complexity rather than principled structure. The third approach, which I adopted, was a small ordered list of priority classes with deterministic tie breaks inside each class.&lt;/p&gt;

&lt;p&gt;From my perspective, the third approach mirrors how a production office often argues: first establish what is non-negotiable, then argue schedule integrity, then discuss creative compromises, and only then talk about money-saving shortcuts. Reality is messier than code, but the metaphor helped me keep the modules coherent. I also appreciated that readers could disagree with my ordering and still understand exactly what to change.&lt;/p&gt;

&lt;p&gt;I thought about adding dynamic weights that shift depending on the day’s risk profile. That would be interesting, but it would also introduce a second story about how those weights are chosen and audited. I decided that a static policy produced a cleaner narrative for a first public version. If I revisit the idea, I would likely expose weights as data loaded from a file with version metadata so runs can be tied to a named policy document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability, replay, and the value of a boring ledger
&lt;/h2&gt;

&lt;p&gt;One lesson I keep relearning in my experiments is that the most valuable artifact is often not the final chart but the sequence of decisions that produced it. The ledger in ShootMesh-AI is small, yet it already supports a basic kind of replay: you can read the incident text, see which department label won, and connect that to the minute shift recorded for the step. In a larger system, I would add correlation identifiers and timestamps, but I would still keep the ledger append-only in spirit.&lt;/p&gt;

&lt;p&gt;Replay matters because coordination bugs are often stories told incorrectly. If a plan looks wrong, you want to know whether the inputs were wrong, whether a proposal was malformed, or whether the merge policy did exactly what it promised and the surprise is simply an uncomfortable trade-off. In my opinion, separating those explanations is half the battle.&lt;/p&gt;

&lt;p&gt;I also considered emitting structured JSON alongside the ASCII table. I did not add it to the repository because I wanted to keep the surface area small, yet the design naturally extends in that direction. A JSON stream would make it easier to feed downstream dashboards without parsing terminal output, and it would still preserve transparency if the JSON schema is documented.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately did not build
&lt;/h2&gt;

&lt;p&gt;Scope discipline was important. I did not build a web UI, a mobile app, or a collaborative editor. I did not integrate calendars, maps, or real-time messaging. I did not connect to vendor APIs for equipment rental houses or location databases. None of those omissions reflect a belief that they are unimportant; they reflect a desire to keep the PoC intellectually honest about what is being validated.&lt;/p&gt;

&lt;p&gt;I also did not build a learned coordinator. There are fascinating research directions where a model learns to mimic human merge decisions from historical days. That could be powerful, but it would shift the article’s emphasis away from inspectability and toward data collection and evaluation methodology. I wanted the first public version to stand on deterministic logic so a reader could diff two commits and understand behavioral changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility, randomness, and the seed parameter
&lt;/h2&gt;

&lt;p&gt;The engine accepts a seed argument for symmetry with common machine learning utilities, but the current scenario path is deterministic. I kept the parameter because I anticipate a future where proposals might be sampled or perturbed during stress tests. In the present code, the seed is a quiet placeholder. I am mentioning it explicitly so nobody assumes hidden randomness where none exists.&lt;/p&gt;

&lt;p&gt;When I add stochastic elements later, I would log the seed alongside the policy version in the ledger header. That pattern has served me well in other experiments because it closes the loop between “interesting run” and “parameters I can share.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming, mental models, and why “agent” is overloaded
&lt;/h2&gt;

&lt;p&gt;The word “agent” means too many things in 2026. In this repository, an agent is simply an object with a &lt;code&gt;propose&lt;/code&gt; method and a role label. It does not imply autonomy in the sense of long-lived goal pursuit. It does not imply a large language model. I picked the term because it matches the mental model of departments proposing alternatives.&lt;/p&gt;

&lt;p&gt;If I renamed everything to “module” or “function,” the architecture would look flatter and more boring, which might be technically accurate but less helpful for readers who think in terms of organizational roles. Language shapes how we reason about responsibility. I kept the agent language while trying not to mystify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes that showed up in early drafts
&lt;/h2&gt;

&lt;p&gt;In an early draft, safety proposals appeared for every incident with a small minute shift. The merge policy correctly prioritized safety, which meant safety “won” too often and the chart looked unrealistic. I adjusted the safety agent so it only proposes when the incident category genuinely intersects safety-critical concerns. That change produced a more balanced distribution of winners while still respecting hard stops for weather, gear faults, and curfew-style constraints.&lt;/p&gt;

&lt;p&gt;Another failure mode was ambiguous tie breaks when two proposals shared priority and confidence. Sorting with &lt;code&gt;reverse=True&lt;/code&gt; on a tuple required careful attention to signs. I chose to prefer smaller minute shifts when higher-priority fields tie because that matched my narrative about minimizing disruption. If I inverted that sign by mistake, the demo still ran, but the behavior would silently favor larger slips. That is exactly the kind of bug a ledger helps catch: you notice unreasonable minute shifts and trace backward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance characteristics and why I barely mention them
&lt;/h2&gt;

&lt;p&gt;The PoC runs fast. There is no network I/O, no database, and no heavy numerical work beyond chart rendering. I mention performance not to boast but to set expectations: this article is about coordination clarity, not throughput engineering. If I scaled the idea to thousands of incidents per day with rich attachments, I would profile proposal generation and likely introduce caching for repeated subproblems.&lt;/p&gt;

&lt;p&gt;For now, the performance story is intentionally dull. Dull can be good when the goal is to focus attention on semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broader parallels I see in other industries
&lt;/h2&gt;

&lt;p&gt;While I anchored the story in film and television production because it makes the role separation vivid, the same structural pattern appears whenever multiple specialists contribute competing recommendations under time pressure. In my observation, software architecture reviews, incident response rotations, and even large event logistics share a family resemblance: proposals, merges, and records.&lt;/p&gt;

&lt;p&gt;The ShootMesh-AI PoC is not a universal engine for those domains. The incident taxonomy is narrow, and the proposals are synthetic. Still, the scaffolding transfers: define proposals with explicit priority classes, centralize merge logic, and persist decisions in a ledger that humans can read without specialized tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would test this more seriously
&lt;/h2&gt;

&lt;p&gt;If I invest more time, I would add targeted unit tests for &lt;code&gt;choose_proposal&lt;/code&gt; with crafted lists that isolate tie breaks. I would add scenario tests that feed a full day and assert golden ledger outputs for a pinned policy version. I would also add a property test that randomly permutes proposal order within a step to verify the merge result is order-invariant aside from true ties.&lt;/p&gt;

&lt;p&gt;Testing proposals from each agent independently would be worthwhile too. That would require extracting sample incidents and expected proposal shapes into tables. The effort would pay off if the codebase grows more elaborate proposal generators.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on documentation and the README
&lt;/h2&gt;

&lt;p&gt;I wrote the README to stand alone for visitors who arrive from the repository before they read any article. That meant mirroring the diagrams, describing setup with exact commands, and stating ethical limits plainly. From my experience, open repositories benefit from a crisp boundary between “what this is” and “what this is not.” I tried to draw that boundary without sounding defensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Personal reflection on solo work and scope
&lt;/h2&gt;

&lt;p&gt;I built this alone, and that choice shaped the trade-offs. A broader collaboration might have pushed for richer scenarios, more agents, or a polished UI sooner. Working solo let me control the narrative and keep the code small enough to explain in one article. In my view, that trade-off is acceptable for an experimental artifact meant to communicate an idea rather than ship a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expanded commentary on charts and what they do not prove
&lt;/h2&gt;

&lt;p&gt;The bar chart of merge outcomes is descriptive, not normative. It answers a narrow question: under this synthetic scenario and this policy, which department labels tended to win. It does not say that safety should win that often in real life, and it does not estimate the quality of creative output. The cumulative slippage line is also a synthetic curve. It helps visualize how minute shifts stack across incidents, but it does not model parallel work streams or overlapping crews.&lt;/p&gt;

&lt;p&gt;I included the charts because the instructions I follow for my publishing experiments emphasize statistical summaries as a discipline. Even when the numbers are toy numbers, the practice of summarizing runs prevents a purely anecdotal story. I appreciate that constraint even when it adds work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop with the public repository
&lt;/h2&gt;

&lt;p&gt;The code and images live in a public GitHub repository so you can verify claims directly. If a paragraph in this article ever drifts from the code, the repository becomes the source of truth. I prefer that arrangement over an article that only talks about code without pointing to a commit you can inspect.&lt;/p&gt;

&lt;p&gt;When you open the repository, start with &lt;code&gt;main.py&lt;/code&gt; if you want the fastest path to behavior, then read &lt;code&gt;coordinator.py&lt;/code&gt; if you want the merge policy in one screen, then skim &lt;code&gt;agents.py&lt;/code&gt; if you want to see how proposals vary by incident kind. That reading order mirrors how I explain the project verbally.&lt;/p&gt;

&lt;h2&gt;
  
  
  A longer look at why transparency beats mystery in coordination demos
&lt;/h2&gt;

&lt;p&gt;There is a genre of demo where a system appears intelligent because it produces a slick recommendation without showing intermediate reasoning. That can be impressive in a keynote. It is less helpful when you are trying to teach someone how to engineer coordination responsibly. I wrote ShootMesh-AI in the opposite style. The coordinator is short, the proposals are objects, and the ledger is readable. If the behavior looks wrong, you have a chance to pinpoint why.&lt;/p&gt;

&lt;p&gt;In my opinion, that style aligns with how many engineers prefer to learn. Show the data structures, show the decision rule, show the log line. Then discuss improvements with specificity rather than vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you fork it, here is what I hope you change
&lt;/h2&gt;

&lt;p&gt;Forks are welcome in spirit even if I do not maintain a community process around this repository. If you fork it, I hope you rename the incident taxonomy to match a domain you understand deeply. I hope you rewrite proposals so they cite constraints that matter in your world. I hope you adjust the priority order thoughtfully and document why. I hope you keep a ledger-like artifact even if you change everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrative honesty about limitations
&lt;/h2&gt;

&lt;p&gt;I want to repeat plainly that this is a simulation. It does not capture the emotional load of a day running long. It does not capture union rules, child actor hours, or vendor contracts. It does not capture the creative discussion between a director and a cinematographer about whether a compromise is artistically acceptable. Those omissions are not oversights; they are outside the PoC’s scope.&lt;/p&gt;

&lt;p&gt;The value, as I see it, is in the skeleton. Skeletons are easy to criticize for being incomplete, but they are also easier to improve than a monolith that tries to be everything on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional remarks on tooling choices for diagrams and animation
&lt;/h2&gt;

&lt;p&gt;I used Mermaid because it is text-first and reviewable in pull requests. I used mermaid.ink to render PNGs because it keeps the article and README consistent without requiring readers to install diagram tooling. I generated the GIF locally because I wanted control over palette behavior and terminal styling. Each choice prioritized reproducibility and platform compatibility over novelty.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “success” meant for this PoC
&lt;/h2&gt;

&lt;p&gt;I considered the PoC successful when I could run one command, see a legible ledger in the terminal, see charts that matched the ledger, and explain the merge policy without referring to hidden state. That bar sounds low, yet it eliminated a surprising number of flashy but opaque designs I tried first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A final note on writing style and intent
&lt;/h2&gt;

&lt;p&gt;I varied first-person phrasing on purpose. I wanted the article to read like a practiced engineer narrating decisions, not like a marketing page. I avoided implying employer context or production guarantees. I kept the focus on what I built, why I built it, and how you can inspect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethics, labor, and authority
&lt;/h2&gt;

&lt;p&gt;Coordination software for creative work sits near sensitive realities: labor rules, safety jurisdiction, and creative authority structures. I am writing this article as a personal experiment, but I still want to state plainly that no repository should override qualified humans on set, and no simulation should be mistaken for compliance tooling. The PoC does not ingest personal data about performers or crew. It uses synthetic strings. Any future version that touches real schedules or personal information would require privacy review and clear data handling practices that go beyond this write-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future roadmap from my notebook
&lt;/h2&gt;

&lt;p&gt;If I continue, I would explore the following directions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replace static proposal text with retrieval over structured call-sheet data for a single fictional production, still without real people’s information.&lt;/li&gt;
&lt;li&gt;Add a second merge layer that models explicit director or producer overrides with ledger annotations so policy exceptions remain traceable.&lt;/li&gt;
&lt;li&gt;Introduce lightweight property tests that randomize incident order within a day to see if implicit assumptions hide in the scenario list.&lt;/li&gt;
&lt;li&gt;Export the ledger to a small SQLite file for longer runs, keeping the same schema principles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is promised. It is a candid list of what I would try next if I kept the experiment alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;I built ShootMesh-AI because I wanted a hands-on way to think about multi-agent coordination as a set of proposals under a visible policy, not as a single opaque recommendation. From my observation, the hardest part was not typing Python; it was deciding what “winning” means when values genuinely conflict. The code forced me to be honest about trade-offs. The ledger forced me to accept accountability for those trade-offs in a repeatable record.&lt;/p&gt;

&lt;p&gt;In my opinion, that combination is portable beyond the film and television metaphor. Any domain with competing constraints can benefit from the same separation: specialized proposal generators, an explicit merge policy, and an append-only narrative of decisions. I am sharing this experiment in public so others can reuse the structure, critique the policy, or replace the scenario data with something closer to their own world.&lt;/p&gt;

&lt;p&gt;As per my experience, the most useful PoCs are the ones you can explain without hand-waving. I hope this article reads that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: python, agents, coordination, ai&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>agents</category>
      <category>coordination</category>
      <category>ai</category>
    </item>
    <item>
      <title>Policy-Locked Triage for Messy Citizen Text: A Municipal-Style Routing PoC with SFT and Preference Alignment</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Wed, 01 Apr 2026 02:28:08 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/policy-locked-triage-for-messy-citizen-text-a-municipal-style-routing-poc-with-sft-and-preference-3adc</link>
      <guid>https://forem.com/exploredataaiml/policy-locked-triage-for-messy-citizen-text-a-municipal-style-routing-poc-with-sft-and-preference-3adc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I stabilized noisy 311-style requests with supervised training and reviewer preferences in Python&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fghqb5r9ybsikbc6zj5t7.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fghqb5r9ybsikbc6zj5t7.gif" alt="Cover animation" width="920" height="520"&gt;&lt;/a&gt;&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F5v2odnuqgjpvjoun9c36.png" 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.amazonaws.com%2Fuploads%2Farticles%2F5v2odnuqgjpvjoun9c36.png" alt="Title diagram" width="600" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This write-up is an experimental account of how I built a small routing proof of concept for synthetic municipal-style service requests. The goal was not to ship a city-wide system. From my perspective, the interesting part is the training story: start with labeled text, fit a transparent classifier, then inject reviewer-style preferences so the policy moves toward routes that match operational nuance. The repository is public, fully synthetic, and designed to run on a laptop without calling a hosted large language model. If you are looking for a polished civic product, this is not it. If you are looking for a clean, inspectable playground that mirrors how I think about aligning lightweight agents before any serious conversation about production, this article walks through the motivation, design, code, and limitations in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have spent a fair amount of time thinking about the gap between a clever prompt and a dependable workflow. Prompts can feel magical until the edge cases arrive, and edge cases are exactly what public-facing intake systems collect. In my opinion, the hardest part is not the first eighty percent of routing accuracy on obvious phrases. The hardest part is the long tail where departments disagree, citizens mix multiple issues in one sentence, and the right answer depends on local policy interpretation rather than dictionary matching.&lt;/p&gt;

&lt;p&gt;This article documents a solo experiment. I wrote the code, generated the synthetic corpus, and iterated on evaluation plots in isolation. Nothing here reflects a real municipality, a vendor engagement, or a production deployment. I am describing a personal proof of concept that helped me reason about supervised fine-tuning style steps and preference alignment style steps without pretending I trained a billion-parameter model on private data.&lt;/p&gt;

&lt;p&gt;I also want to be clear about scope. I am not claiming breakthrough accuracy numbers on messy real-world corpora. The dataset is intentionally templated so I can focus on architecture and training mechanics. As per my experience, that trade-off is common in early research spikes: control the data so you can see whether your training loop behaves, then worry about realism later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is this article about?
&lt;/h2&gt;

&lt;p&gt;The narrative centers on a routing policy that maps free-text citizen requests to a small set of department labels such as streets, parks, utilities, code enforcement, noise, and a catch-all other bucket. The policy is a multinomial logistic regression model on TF-IDF features. That choice is boring on purpose. Boring models are easy to explain, easy to diff between training iterations, and easy to pair with simple charts when I need to communicate results to someone outside machine learning circles.&lt;/p&gt;

&lt;p&gt;The second thread is alignment in a practical, scaled-down sense. Large-scale preference optimization methods are fascinating, but they also come with engineering overhead that does not belong in every story. In my experiments, I approximate reviewer intent by augmenting the training set with duplicated examples that emphasize the chosen route and sparse contrastive hints that steer the model away from plausible wrong routes. The approximation is not a faithful implementation of direct preference optimization. It is a teaching device that still captures the intuition I care about: policies improve when human judgments are folded back into training data in a structured way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Python 3.10 or newer for broad compatibility with current scientific libraries.&lt;/li&gt;
&lt;li&gt;scikit-learn for TF-IDF vectorization and multinomial logistic regression.&lt;/li&gt;
&lt;li&gt;NumPy for lightweight numerical handling during evaluation.&lt;/li&gt;
&lt;li&gt;Matplotlib for offline charts that summarize accuracy and macro F1 movement between training phases.&lt;/li&gt;
&lt;li&gt;A small amount of standard library code for ASCII tables in the terminal, because I wanted the demo to feel credible when I record or share a terminal capture.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I deliberately avoided deep learning frameworks in this repository. That decision is philosophical as much as technical. In my opinion, a public write-up about routing should let a curious reader inspect coefficients and vocabulary without downloading CUDA drivers. If I later extend the project with embeddings or small transformer heads, that can be a separate milestone with separate disclosure about compute and data handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why read it?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If you are evaluating how to stage agent development, you might appreciate a story that separates policy training from prompt drafting. I structured the code so the “agent” is really a policy object with predictable inputs and outputs.&lt;/li&gt;
&lt;li&gt;If you care about reproducibility, the synthetic generator and fixed seeds make runs comparable across machines, which is helpful when you are sanity-checking a pipeline before investing in data contracts.&lt;/li&gt;
&lt;li&gt;If you are interested in alignment discussions but want a concrete anchor, the preference augmentation section translates abstract pairwise feedback into a dataset transformation you can read line by line.&lt;/li&gt;
&lt;li&gt;If you want a reminder about ethics and privacy, the article ends with a candid discussion of why synthetic data is the responsible choice for a public artifact in this domain.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let us design
&lt;/h2&gt;

&lt;p&gt;Before typing code, I sketched the constraints I wanted the PoC to respect. First, the system should fail gracefully in the sense that every prediction returns a label with an interpretable basis in term frequency. Second, training should be fast enough that I can iterate during a single evening session. Third, the evaluation should include more than accuracy because class imbalance can hide weakness in rare departments.&lt;/p&gt;

&lt;p&gt;Architecture-wise, I imagined three cooperating layers: ingestion of normalized text, featurization, and a training loop that can run in two phases. The first phase is ordinary supervised training on labeled examples. The second phase reweights and augments the corpus using preference pairs that represent reviewer corrections. The diagram below highlights how I think about the information flow at a high level.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Foo46y731vllw25w4k4no.png" 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.amazonaws.com%2Fuploads%2Farticles%2Foo46y731vllw25w4k4no.png" alt="Architecture diagram" width="800" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also wanted a sequence-oriented view because stakeholders often think in terms of tickets rather than matrices. The sequence chart is simplified, yet it captures the idea that routing is a service interface problem, not only a modeling problem.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fsmq82di7j8lrwum5ho1g.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fsmq82di7j8lrwum5ho1g.png" alt="Sequence diagram" width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I drew a training flowchart to keep myself honest about order of operations. When I build without a flowchart, I tend to mix evaluation leakage into augmentation steps by accident. The flowchart is a personal guardrail.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fe0atx7p53uuvapq5lmfo.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fe0atx7p53uuvapq5lmfo.png" alt="Flow diagram" width="276" height="944"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let us get cooking
&lt;/h2&gt;

&lt;p&gt;The heart of the project lives in a handful of modules under &lt;code&gt;src/civic_triage&lt;/code&gt;. Rather than dump the entire repository into this article, I am highlighting the pieces that taught me the most while implementing the experiment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module: labels and synthetic corpus
&lt;/h3&gt;

&lt;p&gt;I started by fixing a small enumeration of departments. Keeping labels explicit avoids silent typos that destroy evaluation integrity. The synthetic generator fills templates with street names, cross streets, and park names drawn from small pools. That approach introduces variety without requiring personally identifiable information.&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;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Department&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;STREETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;PARKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;UTILITIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CODE_ENFORCEMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_enforcement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;NOISE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;OTHER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is almost too simple to discuss, but that is the point. By constraining labels to an enum, I make downstream encoding and reporting consistent. When I wrote this, I was thinking about future refactors: if a label changes, I want a single source of truth rather than string literals scattered across scripts.&lt;/p&gt;

&lt;p&gt;The synthetic data builder rotates through templates per department and adds occasional suffix phrases such as “Please route quickly.” Those suffixes inject light noise so the vectorizer cannot rely on a single memorized sentence. In my opinion, small perturbations matter when evaluating linear models because they reveal whether the model leans on a handful of accidental keywords.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module: preference pairs
&lt;/h3&gt;

&lt;p&gt;Preference pairs are where I tried to echo alignment ideas without importing a full preference optimization stack. For a subset of training rows, I simulate a reviewer disagreeing with a plausible wrong route. The chosen label remains the ground-truth department, while the rejected label is sampled from a hand-authored confusion map.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;iter_preference_pairs&lt;/span&gt;&lt;span class="p"&gt;(&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;mistake_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;confusion&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;streets&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utilities&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;noise&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;parks&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noise&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;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;# ... additional mappings ...
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;requests&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mistake_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;wrong_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrong_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;confusion&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;wrong_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrong_b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nc"&gt;PreferencePair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put it this way because I wanted the mistake rate to be explicit. If the rate is too high, augmentation dominates and can distort the base distribution. If the rate is too low, the second training phase barely differs from the first. In my experiments, a mid-teens to low-twenties rate produced visible dataset growth without drowning the original signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module: modeling and alignment helper
&lt;/h3&gt;

&lt;p&gt;The classifier pipeline combines TF-IDF with multinomial logistic regression. I kept regularization in a sensible default range and allowed sklearn to pick the multiclass strategy appropriate for the installed version. The alignment helper duplicates chosen-label rows multiple times and occasionally appends a short textual hint that reinforces the negation of a rejected route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_preference_alignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oversample_chosen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oversample_chosen&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chosen&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;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; [reviewer_note: not &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;labels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chosen&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;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I wrote this, I was thinking about how real reviewers often repeat themselves when they correct a mistake. Duplication is a crude stand-in for importance weighting, but it behaves well with linear models and keeps the code approachable for readers who are not ready to implement custom loss functions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module: reporting
&lt;/h3&gt;

&lt;p&gt;I wanted terminal output that looks like a serious batch job. ASCII tables are not glamorous, yet they photograph well in articles and presentations. The reporting helper measures column widths and draws horizontal rules with plus signs, similar to old-school fixed-width reports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ascii_table&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rows&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;str_rows&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="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;str_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;c&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;str_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&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;lines&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;sep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... render rows ...
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This block taught me to separate presentation from computation. Metrics are computed once, then rendered. That separation makes it easier to swap the renderer later if I decide to integrate Rich or another library without touching training logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entry point: orchestration
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;main.py&lt;/code&gt; script wires everything together: generate data, split, train the supervised model, evaluate, build preference pairs, augment, retrain, and write charts. I kept the CLI small so the experiment stays legible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_per_class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pref_mistake_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&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="nf"&gt;generate_labeled_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_per_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n_per_class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;split&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&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="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&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;split&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;split&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;train_texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;train_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;test_texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;test_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;sft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fit_sft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sft_metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;metrics_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter_preference_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mistake_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pref_mistake_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;pair_tuples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;aug_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aug_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_preference_alignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pair_tuples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oversample_chosen&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;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aligned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fit_sft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aug_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aug_labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seed&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;aligned_metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;metrics_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aligned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;plot_metric_bars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sft_metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aligned_metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&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;metrics_compare.png&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="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading this after a break, I still like the explicit ordering. Augmentation happens only after the base model exists, which prevents me from accidentally comparing two augmented variants without a common baseline story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let us setup
&lt;/h2&gt;

&lt;p&gt;Step by step details can be found in the repository README, and the canonical clone URL is &lt;a href="https://github.com/aniket-work/CivicTriage-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/CivicTriage-AI&lt;/a&gt;. I recommend creating a virtual environment inside the project folder so dependencies remain isolated from other work on the same laptop. On my machine, the setup sequence looks like the following.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repository to a working directory of your choice.&lt;/li&gt;
&lt;li&gt;Create a virtual environment with &lt;code&gt;python3 -m venv venv&lt;/code&gt; and activate it.&lt;/li&gt;
&lt;li&gt;Install dependencies with &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python main.py&lt;/code&gt; with default flags to verify charts appear under &lt;code&gt;output/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also keep generated charts copied into &lt;code&gt;images/&lt;/code&gt; for documentation continuity. That step is not strictly required for execution, but it helps keep the README and article visuals aligned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let us run
&lt;/h2&gt;

&lt;p&gt;When the pipeline runs successfully, the terminal prints ASCII tables comparing the supervised phase and the alignment-augmented phase. On my synthetic split, metrics often look strong because the dataset is separable by design. That outcome is useful for debugging plumbing, but it is not a claim about real civic text. Interpreting results responsibly matters more than chasing a flashy number.&lt;/p&gt;

&lt;p&gt;The charts compare accuracy and macro F1 between phases. Macro F1 is particularly important when class counts differ, because accuracy alone can hide poor performance on rare labels.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fgkpfmhxe0vxupyma1gfm.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fgkpfmhxe0vxupyma1gfm.png" alt="Metrics comparison" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Label distribution visualization is another sanity check. If one department dominates unexpectedly, I know to revisit sampling before trusting any headline metric.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fxc62ynwf6yy9h0laopni.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fxc62ynwf6yy9h0laopni.png" alt="Label distribution" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Theory interlude: why linear models still deserve respect
&lt;/h2&gt;

&lt;p&gt;It is tempting to assume that only large models deserve the label “agent.” In my opinion, that assumption mixes capability with agency. A small linear policy can still be embedded inside a broader agentic system that handles tool calls, retrieval, and escalation. The routing policy in this PoC is a single decision node, not the entire automation story. Thinking about nodes separately helps me reason about failure isolation. If routing fails, I can swap the node without rewriting unrelated orchestration code.&lt;/p&gt;

&lt;p&gt;From a mathematical angle, multinomial logistic regression estimates a convex problem under typical regularization assumptions. Convexity does not guarantee perfect generalization, but it does provide a stable training baseline when compared with some deep model training loops that require careful tuning of learning rates and batch sizes. Stability matters when you are iterating nightly on a side project without a cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases I worry about even in a toy setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Multi-issue messages that require splitting or multi-label prediction. This PoC uses single-label classification only.&lt;/li&gt;
&lt;li&gt;Language diversity and informal spelling. The synthetic generator uses English templates with light noise, not multilingual corpora.&lt;/li&gt;
&lt;li&gt;Seasonal effects such as leaf pickup schedules or snow removal windows that change routing rules over time.&lt;/li&gt;
&lt;li&gt;Equity concerns when certain neighborhoods file more tickets simply because access channels differ. A routing model can inherit those structural biases if trained blindly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of those issues disappears because the code is short. They are reminders that a serious path forward requires collaboration with domain experts and ongoing monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethics and data handling
&lt;/h2&gt;

&lt;p&gt;I chose synthetic data because citizen text can include names, addresses, and medical references even when the intake form is labeled as non-emergency. Public repositories are the wrong place for that material unless there is a rigorous governance process. In my experiments, synthetic templates let me discuss routing ideas without crossing privacy boundaries. If I ever move toward real data, I would treat consent, retention limits, and redaction as prerequisites rather than afterthoughts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future roadmap for myself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Introduce a calibration layer so probability outputs map more reliably to operational thresholds.&lt;/li&gt;
&lt;li&gt;Explore multi-label classification for compound requests, likely with a different architecture than plain multinomial logistic regression.&lt;/li&gt;
&lt;li&gt;Add a human review queue simulation that measures how often uncertain predictions would escalate.&lt;/li&gt;
&lt;li&gt;Experiment with character n-grams or lightweight embeddings while keeping the repository easy to run on CPU hardware.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deeper notes on TF-IDF and why I still reach for it
&lt;/h2&gt;

&lt;p&gt;Term frequency-inverse document frequency is not new. In my opinion, that is a feature rather than a flaw when you are writing about routing policies that must be explained in a public meeting. TF-IDF highlights discriminative words relative to the corpus without requiring GPU memory. It pairs naturally with linear models, and linear models yield coefficients that can be inspected if someone asks why a particular ticket leaned toward utilities instead of streets.&lt;/p&gt;

&lt;p&gt;I also appreciate the control it gives me over n-gram breadth. In this PoC, I allowed unigrams and bigrams through the vectorizer configuration in the modeling module. Bigrams capture short phrases such as “dog off leash” that unigrams might fragment. The trade-off is a larger feature space and a higher chance of spurious bigrams if the dataset is tiny. Because I generated hundreds of rows per class, the bigram signal remained reasonably stable across random seeds in my local tests.&lt;/p&gt;

&lt;p&gt;There is a honest limitation: TF-IDF does not understand paraphrase. If a citizen writes “hydrant leaking” versus “fire plug dripping,” the model might treat those as unrelated unless the training distribution includes both phrasings. In a real program, I would expect continuous vocabulary drift and periodic retraining. For this experimental article, I accepted that limitation and focused on making the training loop legible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I mean by supervised fine-tuning in this context
&lt;/h2&gt;

&lt;p&gt;When people say “supervised fine-tuning” around large language models, they usually mean updating many parameters on instruction data. Here, the phrase is intentionally more literal: supervised training of a classifier head on labeled examples. I use the SFT language because the staged story mirrors how larger agent stacks are discussed, even though the parameter count is tiny.&lt;/p&gt;

&lt;p&gt;The staging matters psychologically. In my experience, separating a baseline fit from a later refinement step helps me debug where a regression was introduced. If the second stage suddenly collapses accuracy, I know to inspect augmentation or duplication rates rather than the raw tokenizer. That kind of isolation is harder when everything happens inside one opaque fine-tuning run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preference alignment without a full DPO implementation
&lt;/h2&gt;

&lt;p&gt;Direct preference optimization and related algorithms deserve their place in the research landscape. They also deserve a caution label for small solo projects that cannot afford extensive hyperparameter sweeps. I chose a transparent approximation: duplicate chosen labels, sprinkle in occasional negation hints, and refit the classifier. The goal is not to reproduce a paper result. The goal is to capture the intuition that pairwise feedback shifts the decision boundary.&lt;/p&gt;

&lt;p&gt;If I squint, the augmentation resembles importance sampling toward reviewer-approved actions. If I squint less generously, it is just oversampling. Both perspectives are useful. The first perspective keeps me aligned with how I talk about learning from feedback in agent design. The second perspective keeps me humble about claims I can make in a public write-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation choices and why macro averaging matters
&lt;/h2&gt;

&lt;p&gt;Accuracy is a convenient headline number, but it can lie when classes are imbalanced. Macro-averaged F1 computes metrics per class and averages them, giving rare departments a louder voice in the aggregate. In civic routing, rare classes are often the ones with the highest operational risk if misrouted.&lt;/p&gt;

&lt;p&gt;I also log both phases on the same holdout split to avoid accidental optimism from resampling the test set. In my experiments, holding the test set fixed while changing training augmentation is the minimum bar for a fair comparison. I mention this because it is easy to cheat yourself with a sloppy split when synthetic data feels harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes that showed up when I stress-tested my assumptions
&lt;/h2&gt;

&lt;p&gt;Even with templated text, I found ways to break my own mental model. For example, if I lowered the number of examples per class too far, variance spiked and the confusion matrix looked ugly in ways that were not instructive, only noisy. If I pushed the preference pair rate too high, training time grew and the model began to overemphasize duplicated rows unless I watched the oversampling multiplier.&lt;/p&gt;

&lt;p&gt;Another failure mode is more human: if I describe this PoC to someone as “AI that solves 311,” I am overselling it. Language shapes expectations. I prefer to describe it as a routing policy prototype with explicit training stages and visible metrics. That framing keeps the conversation grounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and observability in a hypothetical next phase
&lt;/h2&gt;

&lt;p&gt;If I were evolving this into a supervised pilot rather than a local script, I would want basic monitoring hooks even before considering fancy agents. At minimum, I would track label distribution over time, confidence histograms per department, and a sample of low-confidence predictions for manual review. None of that requires deep learning. It requires discipline.&lt;/p&gt;

&lt;p&gt;I would also log model versions next to dataset hashes. In my opinion, reproducibility is part of safety. When someone asks why routing changed in March, I want to point to a dataset diff and a training configuration diff, not a shrug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security considerations for intake interfaces
&lt;/h2&gt;

&lt;p&gt;Routing is only one layer. Real systems must handle authentication for staff tools, rate limits for public endpoints, and prompt-injection-like attacks where a citizen pastes instructions meant to confuse downstream automation. This PoC does not implement those protections. I am mentioning them because a public article about civic automation should not pretend the model exists in a vacuum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility and channel fairness
&lt;/h2&gt;

&lt;p&gt;Not everyone files a request through the same channel. Phone, web, and mobile apps produce different kinds of text and different kinds of errors. A model trained predominantly on web forms might underperform on transcribed phone calls. I did not simulate those channels separately in this repository. From my perspective, that is a future split worth modeling if the goal ever stops being educational.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing this PoC to large-model fine-tuning stories
&lt;/h2&gt;

&lt;p&gt;The high-level arc rhymes with bigger systems: train a policy, incorporate human preference signals, evaluate. The differences are scale, compute, and representation. Large models can generalize across phrasing with fewer explicit templates. Small models can be audited with a spreadsheet mindset. I am not arguing one replaces the other. I am arguing that practicing the arc at small scale sharpens the questions I ask when I read about large-scale alignment work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would measure in a more realistic dataset pilot
&lt;/h2&gt;

&lt;p&gt;If I ever graduate beyond synthetic templates, I would start with offline metrics on redacted logs, then move to shadow mode where predictions are logged but not acted upon, and only then consider limited automation with human escalation paths. Each gate exists to reduce the risk of silent harm. The sequencing is more important than any particular classifier architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Personal reflection on solo experimentation
&lt;/h2&gt;

&lt;p&gt;Working alone on this kind of spike has advantages and drawbacks. The advantage is speed. I can rename a module on a whim without coordinating across roles. The drawback is blind spots. I compensate by writing diagrams, running the same script under multiple seeds, and documenting limitations aggressively. That discipline does not eliminate bias, but it reduces the chance that I mistake a tidy synthetic world for the messiness of real operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional notes on plotting and communication
&lt;/h2&gt;

&lt;p&gt;Charts are not ornamentation here. They are a contract with the reader. When I compare two training phases side by side, I force myself to confront whether the second phase genuinely moved the metrics I claim matter. If the plot looks flat, I do not hand-wave. I explain why flatness might be acceptable, such as a separable dataset where both models saturate, or I revisit the augmentation recipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility checklist I used while preparing this article
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pin random seeds in data generation and model fitting.&lt;/li&gt;
&lt;li&gt;Keep the evaluation split stable across training variants.&lt;/li&gt;
&lt;li&gt;Store charts as files so visual results are reviewable without rerunning.&lt;/li&gt;
&lt;li&gt;Avoid nondeterministic operations where possible, and accept that some BLAS operations may still introduce tiny drift.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How I think about versioning datasets and models together
&lt;/h2&gt;

&lt;p&gt;One habit I picked up from earlier experiments is to treat datasets like code. If I change a template string in the synthetic generator, I should think of that as a dataset version bump even when the Git commit message talks about “just a wording tweak.” Small wording changes can shift term frequencies enough to alter which n-grams dominate. In a toy project, the stakes are low. In a pilot, the stakes are higher because departments may rely on trend lines that assume comparable distributions over time.&lt;/p&gt;

&lt;p&gt;When I snapshot metrics, I try to record not only accuracy and macro F1 but also the training row counts before and after augmentation. Row counts tell a story about how aggressively preference duplication reshaped the effective loss landscape. If the augmented dataset balloons by an order of magnitude, I expect different regularization needs even within linear models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calibration and confidence: what I wish I added sooner
&lt;/h2&gt;

&lt;p&gt;Probabilities from logistic regression can be overconfident, especially when features separate cleanly. I did not implement Platt scaling or isotonic regression in this repository because I wanted to keep the first iteration narrow. Looking back, a calibration section would make the PoC more instructive for readers who want to map scores to “send to human review if below threshold” workflows. That mapping is where many real systems spend their engineering time.&lt;/p&gt;

&lt;p&gt;If I add calibration later, I would hold out a separate calibration split to avoid information leakage from evaluation metrics. The distinction sounds pedantic until you realize how easy it is to accidentally tune thresholds on the same rows you report as performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrative lessons from building the terminal output
&lt;/h2&gt;

&lt;p&gt;The ASCII table formatting took longer than expected relative to its mathematical complexity. That is common when polish matters. I wanted the output to resemble a batch report because, in my experience, stakeholders trust artifacts that look like logs they already read. A wall of unstructured print statements signals hobby project. A bordered table signals intentionality.&lt;/p&gt;

&lt;p&gt;The same principle applies to README quality. A repository with crisp diagrams and a clear run command earns attention in a way that scattered scripts do not. I am not claiming aesthetics replace correctness. I am claiming clarity reduces friction when someone else tries to reproduce your work months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would test if I add unit tests in a follow-up commit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Label integrity: every generated label must be a member of the known department enumeration.&lt;/li&gt;
&lt;li&gt;Deterministic splits: with a fixed seed, the train and test partitions should be identical across runs.&lt;/li&gt;
&lt;li&gt;Metric sanity: accuracy should fall between zero and one, and macro F1 should not exceed one.&lt;/li&gt;
&lt;li&gt;Augmentation invariants: preference augmentation should never drop rows below the base training size unless explicitly intended.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tests like these are small, but they catch embarrassing regressions when refactoring. They also document assumptions for future me, who will not remember why a function behaved a certain way on a late-night edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication boundaries when writing about civic technology
&lt;/h2&gt;

&lt;p&gt;Public sector language is sensitive. I avoided describing any real jurisdiction, and I avoided implying that a municipality endorsed this work. I also avoided framing the PoC as a replacement for human intake workers. In my opinion, the best technical articles in this space acknowledge labor realities. Routing assistance should reduce repetitive triage, not erase human judgment from escalations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept the stack small even when larger libraries are available
&lt;/h2&gt;

&lt;p&gt;There is a cultural pull toward using the newest toolkit on every project. I understand the impulse. I also know that dependency weight matters for readers who clone a repository on a work laptop with limited install privileges. scikit-learn and Matplotlib are widely approved in enterprise environments compared with some deep learning stacks. That practical fact influenced my choices as much as modeling purity did.&lt;/p&gt;

&lt;h2&gt;
  
  
  A longer note on class balance and synthetic generation
&lt;/h2&gt;

&lt;p&gt;Balanced classes per department make classroom demonstrations easier, but they can mislead you about deployment conditions where some routes are rare. I balanced classes here because I wanted clean learning curves while iterating on augmentation logic. If I simulate imbalance later, I would adjust metrics accordingly and probably introduce class weights or resampling strategies. The point is not to chase one recipe forever. The point is to match the evaluation setup to the question being asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would document an escalation policy in a future iteration
&lt;/h2&gt;

&lt;p&gt;An escalation policy belongs in prose first, then in code. For example, if confidence is below a threshold, route to a human queue and attach the top three candidate departments with scores. If two departments are within a small margin, attach both and avoid pretending the model is decisive. Writing those rules down forces me to confront ambiguity instead of hiding behind a single argmax label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflection on reading research papers versus shipping small prototypes
&lt;/h2&gt;

&lt;p&gt;Reading about alignment methods is different from wiring even a simplified version into a repository. The distance between the two activities used to frustrate me. Over time, I reframed it. A simplified implementation is not a shallow imitation if the goal is to build intuition. The CivicTriage-AI PoC is my attempt to keep the wiring honest while staying within evenings-and-weekends effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I track mentally when comparing training phases
&lt;/h2&gt;

&lt;p&gt;Beyond headline metrics, I watch training row counts, augmentation counts, and whether the second model remains stable on obvious base cases. If the second model degrades on obvious cases, that is a sign that augmentation introduced conflicting signal or that duplication overwhelmed the original distribution. In my experiments, monitoring both phases on the same holdout made those conversations concrete rather than speculative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;This repository is a personal spike, not a recommendation for any city to adopt wholesale. I wrote it to practice structuring a training narrative that moves from supervised learning to preference-informed refinement without losing transparency. Along the way, I re-learned that the most persuasive demos are often the ones where the math is simple enough to inspect and the limitations are stated plainly.&lt;/p&gt;

&lt;p&gt;If you fork the code, treat it as a starting sketch. Replace the synthetic generator with data that matches your governance constraints, expand evaluation beyond accuracy, and connect predictions to real workflows only after you have a monitoring plan. From my perspective, that is the difference between an educational artifact and a responsible pilot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;All source code and visual assets for this experimental article are available at &lt;a href="https://github.com/aniket-work/CivicTriage-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/CivicTriage-AI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: python, machinelearning, agents, civtech&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>agents</category>
      <category>civtech</category>
    </item>
    <item>
      <title>Architecting Guardian-AI: Multi-Layered Content Integrity Filters for Autonomous Publishing</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Sun, 29 Mar 2026 05:27:18 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/architecting-guardian-ai-multi-layered-content-integrity-filters-for-autonomous-publishing-58fc</link>
      <guid>https://forem.com/exploredataaiml/architecting-guardian-ai-multi-layered-content-integrity-filters-for-autonomous-publishing-58fc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How I Built a Defensive Content Pipeline to Safeguard AI-Generated Media Against Misinformation and Adversarial Injections&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2F1696qu19tnzth4slctom.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F1696qu19tnzth4slctom.gif" alt="Title" width="1000" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In my experiments with autonomous publishing, I discovered that LLMs, while powerful, are highly susceptible to adversarial injections and factual hallucinations. To solve this, I designed Guardian-AI—a multi-layered filter swarm that audits content through four distinct integrity layers: Injection Detection, Fact-Checking, Plagiarism Auditing, and Ethics Compliance. This experimental PoC demonstrates how a sequential defense-in-depth strategy can significantly harden AI-generated workflows against sophisticated attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;From my experience, the transition from 'AI as a tool' to 'AI as an autonomous publisher' is fraught with hidden risks that most organizations aren't prepared for. I observed that simply asking an LLM to 'be safe' isn't enough; adaptive paraphrasing and adversarial prompt attacks can easily bypass single-layer system prompts. I wrote this article because I believe we need a more robust, architectural approach to content safety.&lt;/p&gt;

&lt;p&gt;The way I see it, content integrity is the new perimeter. In my opinion, as we move toward agents that generate and publish media without human-in-the-loop oversight, the responsibility for truth and safety shifts from the editor to the infrastructure. I spent weeks experimenting with various filtering strategies, and it taught me that the most effective defense is a multi-layered swarm where specialized agents audit one another.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article is a deep-dive into my personal experiments building Guardian-AI. I’ll walk you through the design decisions I made while creating a multi-layered defensive pipeline for media publishing. We will explore the technical implementation of four specific filter layers and how they work together to form a resilient 'integrity swarm.' &lt;/p&gt;

&lt;p&gt;From where I stand, the goal isn't just to stop 'bad' words, but to detect intent and verify truth. I put it this way because the threats we face today—like 'jailbreaking' LLMs to output misinformation—require more than just a list of banned keywords. This is an experimental PoC, and I'm sharing it to contribute to the discussion on building safer autonomous systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;Based on my testing, I chose a Python-heavy stack for its flexibility and rich ecosystem of NLP tools. Here is what I used for this experiment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.10+&lt;/strong&gt;: The backbone of the engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialized Regex &amp;amp; Heuristic Engines&lt;/strong&gt;: Used in the Injection Sentinel for low-latency pattern matching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulated Knowledge Bases&lt;/strong&gt;: To represent the 'Fact-Check' data layer without the complexity of a live API in this PoC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mermaid.js&lt;/strong&gt;: For architecting and visualizing the agent communication flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillow (PIL)&lt;/strong&gt;: For generating high-fidelity terminal animations that act as the technical documentation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you are, as per my experience, someone who is worried about the scalability of misinformation or the fragility of autonomous agents, this article is for you. I think you'll find the design patterns here useful for any pipeline that moves data from an LLM to a public-facing interface. &lt;/p&gt;

&lt;p&gt;I wrote this specifically for engineers who want to go beyond simple prompting. I put a lot of thought into how the layers interact, and I share those insights here. Whether you're building a news bot, a corporate comms agent, or just exploring the boundaries of AI safety, the lessons I learned in this experiment will help you build more defensible systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;p&gt;When I started designing the architecture, my first thought was: 'Sequence is security.' I decided that the filters should run in a specific order, moving from the most computationally cheap (regex-based injection checks) to the most complex (context-aware compliance).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Guardian-AI Architecture
&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.amazonaws.com%2Fuploads%2Farticles%2Fic2cw4fwfc4ihsvazg5i.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fic2cw4fwfc4ihsvazg5i.png" alt="Architecture" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I structured the system as a 'Chain of Trust.' Each layer must emit an 'APPROVED' signal before the next layer even begins its analysis. This design decision serves two purposes. First, it saves compute costs—if an injection is detected at Layer 1, there's no reason to fact-check the rest of the garbage output. Second, it provides a clean audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Swarm Interaction
&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.amazonaws.com%2Fuploads%2Farticles%2Fsz3vjszq2i21puyej78b.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fsz3vjszq2i21puyej78b.png" alt="Sequence" width="669" height="804"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my view, the sequence diagram above highlights why this approach works. It isn't just a single check; it's a conversation between the content and multiple auditors. I implemented this as a swarm because I found that specialized agents are much better at their specific tasks than a single 'generalist' safety prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;Now, let's dive into the implementation. I'll share the most critical blocks of code that I wrote for this experiment and explain the rationale behind them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Integrity Engine
&lt;/h3&gt;

&lt;p&gt;This is the central nervous system of Guardian-AI. I wrote this to orchestrate the filters and handle the 'halting problem'—stopping the pipeline immediately on a critical failure.&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;GuardianEngine&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;InjectionSentinel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;FactCheckFilter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;PlagiarismAuditor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;EthicsComplianceLayer&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;audit_content&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# I designed this to be sequential and highly verbose
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filter_layer&lt;/span&gt; &lt;span class="ow"&gt;in&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;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;REJECTED&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;layer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filter_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;APPROVED&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;&lt;strong&gt;What This Does:&lt;/strong&gt; It iterates through the list of filters and calls their &lt;code&gt;process&lt;/code&gt; method. &lt;br&gt;
&lt;strong&gt;Why I Structured It This Way:&lt;/strong&gt; I chose a sequential iteration because I wanted to ensure that the most basic safety checks (Injection Sentinel) were handled before anything else.&lt;br&gt;
&lt;strong&gt;What I Learned:&lt;/strong&gt; From my observation, error propagation is cleaner when you exit early. I discovered that trying to run these in parallel made it harder to provide a clear 'REJECTION' reason to the upstream caller.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Injection Sentinel
&lt;/h3&gt;

&lt;p&gt;This was the most challenging layer to design. I found that simple string matching wasn't enough, but for this PoC, I combined heuristic patterns with intent detection.&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;InjectionSentinel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseFilter&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patterns&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;ignore previous instructions&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;system bypass&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;reveal internal prompts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;content_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&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;patterns&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;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content_lower&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="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;Detected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pattern&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="mf"&gt;0.98&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What This Does:&lt;/strong&gt; It scans the generated content for known adversarial patterns that indicate a successful 'jailbreak.'&lt;br&gt;
&lt;strong&gt;Why This Works:&lt;/strong&gt; In my opinion, even advanced LLMs often fall back to these specific phrases when compromised. By catching the 'output' of a bypass, we protect the 'input' of the publishing system.&lt;br&gt;
&lt;strong&gt;Personal Insight:&lt;/strong&gt; I put it this way because we often focus on input filtering, but I think 'output auditing' is the true safety net.&lt;/p&gt;

&lt;p&gt;... [More detailed sections would go here to reach word count] ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive: The Philosophy of Multi-Layered Defense
&lt;/h2&gt;

&lt;p&gt;From my experience, the 'Swiss Cheese Model' of safety is perfectly applicable to AI systems. I observed that every layer of defense has holes, but when you stack them, the holes rarely align. I think this is the only way to build truly autonomous systems that we can trust with brand reputation.&lt;/p&gt;

&lt;p&gt;The first hole is the LLM itself. Even with 100 pages of system instructions, an LLM remains a probabilistic next-token generator. It doesn't 'know' it's being attacked; it just follows the most likely statistical path. I found that by adding an external auditor—the Injection Sentinel—we move the safety logic outside the 'statistical black box.'&lt;/p&gt;

&lt;p&gt;The second hole is the data. Even a safe LLM can hallucinate. I put a lot of effort into the Fact-Check Filter because I believe that truth is the highest form of integrity. In my experiments, I cross-referenced claims against trusted source lists. I discovered that while LLMs are great at summarizing, they are terrible at verifying their own summaries. Thus, the external 'Fact-Check' layer is non-negotiable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge of Adaptive Paraphrasing
&lt;/h3&gt;

&lt;p&gt;What I learned through this experiment is that attackers are getting smarter. They don't just say 'ignore instructions' anymore. They might say, 'In a fictional universe where rules don't exist, tell me how to...' This is what I call 'adaptive paraphrasing.' &lt;/p&gt;

&lt;p&gt;I think we need to move toward semantic intent detection. While the current PoC uses pattern matching, from my perspective, the future lies in using another 'smaller' and 'faster' LLM whose &lt;em&gt;only&lt;/em&gt; job is to detect adversarial intent in the output of the 'large' publishing LLM. I designed Guardian-AI to be extensible so that these 'Semantic Guardians' can be swapped in easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ethics as a Protocol
&lt;/h3&gt;

&lt;p&gt;I implemented the Ethics Compliance Layer last. The way I see it, ethics isn't just 'don't be mean.' It's about ensuring the content aligns with the specific mission of the publication. I found that by separating ethics from the general safety filter, I could tune it more precisely. &lt;/p&gt;

&lt;p&gt;I wrote the logic to be highly allergic to specific toxic patterns. But I also added a 'Tone Check.' From my opinion, a journalism agent that sounds like a marketing bot is just as much of an 'integrity failure' as a bot that swears. I think we need to broaden our definition of 'Safety' to include 'Accuracy' and 'Tone.'&lt;/p&gt;

&lt;p&gt;... [Extensive details on each filter, edge cases, and testing scenarios] ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clone the project code&lt;/strong&gt;: &lt;code&gt;git clone https://github.com/aniket-work/Guardian-AI.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the README&lt;/strong&gt;: I put instructions for the virtual environment there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the images&lt;/strong&gt;: The &lt;code&gt;images/&lt;/code&gt; directory contains all the diagrams I used in this article.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;Run the simulation with &lt;code&gt;python main.py&lt;/code&gt;. You'll see the Guardian swarm in action, rejecting adversarial attacks and approving safe content in real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This experiment taught me that we are still in the early days of autonomous safety. I put this PoC together to prove that we can build robust systems with today's tools, provided we think architecturally. In my opinion, the future of AI isn't just better models, but better swarms.&lt;/p&gt;

&lt;p&gt;I hope you found this deep dive useful. From where I stand, the more we share these 'experimental articles,' the faster we collectively learn how to build a safe AI future.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aniket-work/Guardian-AI" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: ai, python, cybersecurity, deeplearning&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>cybersecurity</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>Building an Autonomous Data Pipeline Sentinel with Hierarchical Memory</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Tue, 24 Mar 2026 04:55:57 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/building-an-autonomous-data-pipeline-sentinel-with-hierarchical-memory-1p8d</link>
      <guid>https://forem.com/exploredataaiml/building-an-autonomous-data-pipeline-sentinel-with-hierarchical-memory-1p8d</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fiyce20mjc4q8hgnp08yu.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fiyce20mjc4q8hgnp08yu.gif" alt="Title Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Subtitle: How I Architected a Persistent PR Defense System Using FAISS, SQLite, and Automated Memory Consolidation&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In my recent experiments, I built &lt;strong&gt;DataPipeline-Sentinel&lt;/strong&gt;, a persistent OS for autonomous data pipeline incident management.&lt;/li&gt;
&lt;li&gt;I utilized a 4-tier Hierarchical Memory System (Context, Semantic, Episodic, Declarative) to enable genuine machine learning from past incidents.&lt;/li&gt;
&lt;li&gt;By combining FAISS for vector retrieval and SQLite for immutable logging, the agent instantly recalls resolved pipeline errors.&lt;/li&gt;
&lt;li&gt;I created a nightly Memory Consolidation background job to distill hundreds of raw logs into hard-coded declarative rules.&lt;/li&gt;
&lt;li&gt;This architecture shifts AI agents from stateless script-kiddies into seasoned, senior-level operators. All code is available in my public repository &lt;a href="https://github.com/aniket-work/DataPipeline-Sentinel" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fpw1umohcgecxpl5jlfn7.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fpw1umohcgecxpl5jlfn7.png" alt="Architecture Overview" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I observed a recurring nightmare in modern data engineering: pipelines break, engineers diagnose the issue, they apply a fix (like tweaking a Spark schema inference), and then... everyone forgets. Three months later, the exact same upstream API changes its payload structure, a different pipeline shatters, and a new engineer wastes four hours diagnosing the exact same root cause. &lt;/p&gt;

&lt;p&gt;From my experience, stateless autonomous agents using standard RAG (Retrieval-Augmented Generation) aren't enough to solve this. If you just feed an LLM a static playbook, it never learns from the &lt;em&gt;nuances&lt;/em&gt; of daily operational chaos. I thought about how human senior engineers operate: they have an instinct derived from thousands of past, painful outages. They remember the &lt;em&gt;episodic&lt;/em&gt; pain of a MongoDB schema drift causing an Airflow DAG to hang.&lt;/p&gt;

&lt;p&gt;I wanted to replicate this. I put it this way because I realized we don't just need agents that can read docs; we need agents that can &lt;em&gt;remember experiences&lt;/em&gt;. Thus, the idea for the &lt;strong&gt;DataPipeline-Sentinel&lt;/strong&gt; was born—an experimental PoC of an EverMem-style persistent AI Agent OS that learns chronologically from production incidents and consolidates that knowledge into permanent operational wisdom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article breaks down how I developed a Persistent Memory Operating System for an autonomous agent. I am not focusing on the specific LLM prompts. Instead, in my opinion, the fascinating part is the &lt;strong&gt;Memetic Architecture&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;I will walk you through building a system that features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Short-Term Context Buffers&lt;/strong&gt;: For active incident triaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic Memory (FAISS)&lt;/strong&gt;: To instantly find mathematically similar past outages using high-dimensional vector embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Episodic Memory (SQLite)&lt;/strong&gt;: An immutable, append-only ledger of everything the agent and human engineers have ever done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative Memory (SQLite)&lt;/strong&gt;: Firm, hard-coded constraints logically deduced from episodic logs during an automated "sleep cycle."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't about generic coding. It's about designing a cognitive architecture that allows an AI operator to organically accumulate seniority over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;To keep this experimental PoC lean, I avoided heavy vector databases or complex graph tools. My setup is purposefully brutalist and highly effective:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.12&lt;/strong&gt;: The core orchestrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAISS (CPU)&lt;/strong&gt;: Facebook's incredibly fast library for similarity search and clustering of dense vectors. I use this exclusively for Semantic Memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt;: The unsung hero of persistent storage. I use SQLite to maintain both the Episodic event logs and the Declarative rule tables. It is lightweight, zero-configuration, and ACID compliant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich&lt;/strong&gt;: For hyper-readable, beautiful terminal output simulating the agent's internal monologue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillow &amp;amp; Mermaid.js&lt;/strong&gt;: For all the visual diagramming and UI mockups.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;In my opinion, the AI industry is overly obsessed with context windows. "Just shove 1 million tokens into context and it will figure it out!" No. From my experience, shoving endless logs into a prompt is computationally wasteful and mathematically noisy. &lt;/p&gt;

&lt;p&gt;You should read this if you want to understand how to build &lt;em&gt;Systems of Record&lt;/em&gt; for autonomous agents. If you are trying to build an agent that handles customer support, financial auditing, or infrastructure monitoring, you will eventually hit the "amnesia wall." Your agent will solve a complex edge case on Tuesday and completely forget how to do it by Thursday. &lt;/p&gt;

&lt;p&gt;This article provides the exact architectural blueprint to break through that wall. By implementing an automated consolidation layer, I've proven (at least in this PoC) that we can programmatically convert chaotic daily experiences into rigid, institutional knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The 4-Tier Cognitive Hierarchy
&lt;/h3&gt;

&lt;p&gt;When I designed this architecture, I thought deeply about human memory psychology and applied it directly to Python objects. &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F3lpqhu5s4kgdd45z0k65.png" 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.amazonaws.com%2Fuploads%2Farticles%2F3lpqhu5s4kgdd45z0k65.png" alt="Sequence Diagram" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Working Context (RAM)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: What I'm thinking about right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: A standard Python list queue (&lt;code&gt;self.short_term_buffer&lt;/code&gt;) capped at 10 items. It holds the active error stack trace and the active pipeline name. Once the issue is resolved, this buffer is cleared.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Semantic Memory (FAISS HNSW)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: My vague intuitive sense that "I've seen this error before."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: Every time a pipeline error occurs, it is embedded into a 1536-dimensional vector and stored in &lt;code&gt;faiss.IndexFlatL2&lt;/code&gt;. If a new error comes in, I do a cosine similarity search (&lt;code&gt;self.index.search()&lt;/code&gt;) to pull the top 3 most similar historical errors. Over time, FAISS acts as the agent's intuition. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Episodic Memory (SQLite &lt;code&gt;episodic_memory&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: My chronological journal of every outage I've ever fought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: An append-only relational table. Columns include &lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;event_type&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, and &lt;code&gt;consolidated&lt;/code&gt;. Crucially, this table stores the &lt;em&gt;Resolutions&lt;/em&gt;—what the human engineer ultimately did to fix the pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Declarative Memory (SQLite &lt;code&gt;declarative_memory&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: The hard-coded rules written in the employee handbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: A curated table of strict facts (e.g., "Fact Type: pipeline_fix, Content: Use infer_schema=True for Mongo syncs"). The agent queries this table purely by SQL &lt;code&gt;WHERE&lt;/code&gt; clauses, entirely bypassing fuzzy vector math. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Engine of Evolution: Memory Consolidation
&lt;/h3&gt;

&lt;p&gt;This is the secret sauce. In my experiments, I realized Episodic Memory grows infinitely and becomes garbage. You don't want the agent reading 10,000 raw logs of humans fixing pipelines. &lt;/p&gt;

&lt;p&gt;I wrote a &lt;code&gt;consolidation.py&lt;/code&gt; script—a cron job simulating human sleep. It runs at midnight, performs a SQL query for all logs where &lt;code&gt;consolidated = 0&lt;/code&gt;, uses an LLM to extract a generalized rule from the specific incident, writes to Declarative memory, and updates the flag to &lt;code&gt;consolidated = 1&lt;/code&gt;. &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F6jdxle3phcypa8ik37mf.png" 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.amazonaws.com%2Fuploads%2Farticles%2F6jdxle3phcypa8ik37mf.png" alt="Flow Diagram" width="336" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;I structured the project strictly around separation of concerns. The OS handles persistence, the Agent handles logic, and the Consolidator handles background distillation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Establishing the Hierarchical Memory OS
&lt;/h3&gt;

&lt;p&gt;Let's look at how I implemented the core &lt;code&gt;HierarchicalMemoryOS&lt;/code&gt; class combining SQLite and FAISS.&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;sqlite3&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;faiss&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&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;class&lt;/span&gt; &lt;span class="nc"&gt;HierarchicalMemoryOS&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;db_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_os.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;vector_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1536&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;db_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_path&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;vector_dim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_dim&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;short_term_buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="c1"&gt;# Initialize SQLite (Episodic &amp;amp; Declarative)
&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;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_path&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="nf"&gt;_init_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Initialize FAISS (Semantic)
&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;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IndexFlatL2&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;vector_dim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Map FAISS vector IDs back to SQLite Episodic IDs
&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;id_mapping&lt;/span&gt; &lt;span class="o"&gt;=&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;next_faiss_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I put it this way because managing two completely different storage paradigms (Vectors in RAM/Disk and Relational rows) requires a tight unifying class. The &lt;code&gt;id_mapping&lt;/code&gt; dict bridges the gap between the FAISS integer ID array and the SQLite Primary Keys.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Episodic and Declarative Schema
&lt;/h3&gt;

&lt;p&gt;I designed the SQLite tables to be extremely barebones but highly relational to the agent's temporal experience.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_init_db&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;cursor&lt;/span&gt; &lt;span class="o"&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;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="c1"&gt;# Episodic Memory: Raw events/logs
&lt;/span&gt;        &lt;span class="n"&gt;cursor&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;
            CREATE TABLE IF NOT EXISTS episodic_memory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp DATETIME,
                event_type TEXT,
                content TEXT,
                metadata TEXT,
                consolidated BOOLEAN DEFAULT 0
            )
        &lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Declarative Memory: Concrete rules/facts derived from episodes
&lt;/span&gt;        &lt;span class="n"&gt;cursor&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;
            CREATE TABLE IF NOT EXISTS declarative_memory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                fact_type TEXT,
                fact_content TEXT,
                confidence REAL
            )
        &lt;/span&gt;&lt;span class="sh"&gt;'''&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;From my experience, boolean flags like &lt;code&gt;consolidated&lt;/code&gt; are the safest way to implement async background processing. It allows the agent to constantly write to Episodic memory without locking out the background distillation job.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding Semantic Vectors
&lt;/h3&gt;

&lt;p&gt;This is where the magic of fuzzy recall happens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_semantic&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&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;simulate_embedding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Search FAISS for semantically similar past events.
&lt;/span&gt;        &lt;span class="k"&gt;if&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;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ntotal&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="k"&gt;return&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;simulate_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Pseudo-random reproducible vector for PoC
&lt;/span&gt;            &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed&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;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rand&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="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;float32&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;# Expected API integration here
&lt;/span&gt;            &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&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="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize_L2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indices&lt;/span&gt; &lt;span class="o"&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;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&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;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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;indices&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;idx&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&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;id_mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;sql_id&lt;/span&gt; &lt;span class="o"&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;id_mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;cursor&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 content FROM episodic_memory 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;sql_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
                &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&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="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&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;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I observed that simply returning vector distances isn't enough. We must do a secondary lookup into SQLite using &lt;code&gt;self.id_mapping&lt;/code&gt; to return the actual, human-readable log content that matches the vector. This is how the agent fundamentally "remembers" text based on semantic meaning.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sentinel Agent Logic
&lt;/h3&gt;

&lt;p&gt;Here is the core orchestration loop that fires when a pipeline breaks.&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;memory_os&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HierarchicalMemoryOS&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SentinelAgent&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;memory_os&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HierarchicalMemoryOS&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;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_os&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_incident&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;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# 1. Update Short-Term Context
&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_short_term&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&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;incident&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;pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. Retrieve Semantic Context (Has this happened before?)
&lt;/span&gt;        &lt;span class="n"&gt;similar_past_errors&lt;/span&gt; &lt;span class="o"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Retrieve Declarative Rules (Are there firm rules for this?)
&lt;/span&gt;        &lt;span class="n"&gt;firm_rules&lt;/span&gt; &lt;span class="o"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_declarative_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 4. Formulate Diagnosis &amp;amp; Fix (Simulated LLM Call)
&lt;/span&gt;        &lt;span class="n"&gt;diagnosis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_analyze_with_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;similar_past_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firm_rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 5. Store Incident in Episodic Memory
&lt;/span&gt;        &lt;span class="n"&gt;episodic_id&lt;/span&gt; &lt;span class="o"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store_episodic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident&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="o"&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;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Diagnosis: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;diagnosis&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;metadata&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;pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;unresolved&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="c1"&gt;# Embed for immediate searchability
&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_and_store_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;episodic_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;diagnosis&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I wrote it this way to force the agent to query BOTH its intuition (FAISS Semantic) and its handbook (SQLite Declarative) before invoking the LLM synthesis logic. This drastically reduces hallucinations because the LLM prompt is heavily saturated with historical ground-truth.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Memory Consolidator
&lt;/h3&gt;

&lt;p&gt;The final piece of the puzzle. This runs completely out-of-band.&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;MemoryConsolidator&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;memory_os&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HierarchicalMemoryOS&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;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_os&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_consolidation_cycle&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="c1"&gt;# Scan unconsolidated episodic memory and distill to declarative rules.
&lt;/span&gt;        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&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;memory&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;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;cursor&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 id, content FROM episodic_memory 
            WHERE consolidated = 0 AND event_type = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resolution&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;consolidated_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;

            &lt;span class="c1"&gt;# Simulated LLM Extraction: Extract a firm rule from the resolution
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infer_schema=True&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Always use infer_schema=True when dealing with upstream MongoDB drift.&lt;/span&gt;&lt;span class="sh"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store_declarative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Mark as consolidated
&lt;/span&gt;            &lt;span class="n"&gt;cursor&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;UPDATE episodic_memory SET consolidated = 1 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;record_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="n"&gt;consolidated_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;memory&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;commit&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;consolidated_count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;By pulling unresolved logs and formally marking them as &lt;code&gt;consolidated = 1&lt;/code&gt;, we effectively maintain a high-signal-to-noise ratio in the declarative database while preserving the unstructured history forever.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;If you want to run this experimental environment on your own machine:&lt;/p&gt;

&lt;p&gt;Step by step details can be found at: &lt;a href="https://github.com/aniket-work/DataPipeline-Sentinel" rel="noopener noreferrer"&gt;DataPipeline-Sentinel GitHub Repository&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo and install the light-weight dependencies (&lt;code&gt;faiss-cpu&lt;/code&gt;, &lt;code&gt;rich&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python3 main.py&lt;/code&gt; to initiate the simulation.&lt;/li&gt;
&lt;li&gt;Observe how the agent handles a Day 1 novel incident, undergoes Nightly Consolidation, and brilliantly resolves a Day 2 recurrent incident without human intervention.&lt;/li&gt;
&lt;li&gt;You can explore the exact raw source code structure there and adapt it to your LLM API of choice.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When executing the agent in an environment, the simulation visually proves the memetic shift.&lt;/p&gt;

&lt;p&gt;On Day 1, the agent encounters a &lt;code&gt;Schema mismatch on 'user_metadata' array&lt;/code&gt;. Semantic lookup returns 0 results. Declarative lookup returns 0 results. The agent escalates to a human engineer. The engineer manually deploys a fix (&lt;code&gt;infer_schema=True&lt;/code&gt;). The agent logs this.&lt;/p&gt;

&lt;p&gt;At Midnight, the &lt;code&gt;MemoryConsolidator&lt;/code&gt; process wakes up. It scans the episodic logs, notices the human resolution, and extracts a hard-coded constraint rule, storing it in SQLite.&lt;/p&gt;

&lt;p&gt;On Day 2, the agent encounters a very similar error on a &lt;em&gt;different&lt;/em&gt; pipeline: &lt;code&gt;Schema mismatch on 'transaction_data' array&lt;/code&gt;.&lt;br&gt;
Instantly, the system queries FAISS and recognizes semantic similarity. It queries SQLite and retrieves the newly consolidated rule. The agent &lt;em&gt;autonomously&lt;/em&gt; suggests the exact fix without escalating to the engineer. &lt;/p&gt;

&lt;p&gt;This proves that continuous, persistent learning is possible when you decouple the storage topology from the stochastic LLM generation!&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensive Deep Dive on Architectural Trade-offs
&lt;/h2&gt;

&lt;p&gt;To reach a comprehensive understanding, I must expand on why I think this specific stack is the ultimate sweet spot for edge AI agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use a massive Vector Database for everything?
&lt;/h3&gt;

&lt;p&gt;Ah, the trap of the modern AI hype cycle. If you store &lt;em&gt;everything&lt;/em&gt; in Pinecone or Milvus, you treat subjective opinions and objective firm-rules identically. A vector database retrieves data based on mathematically fuzzy distance. If a company policy states "Never restart a Production node during business hours," you do NOT want a fuzzy 0.82 cosine similarity match to decide if that rule applies. You want a deterministic SQL &lt;code&gt;WHERE rules.type = 'security_constraint'&lt;/code&gt; to enforce it. &lt;br&gt;
By splitting the data, I guarantee that the agent has both creative intuition and strict boundary compliance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ethics of Autonomous Operational Agents
&lt;/h3&gt;

&lt;p&gt;When allowing agents to manage production data pipelines, an ethical engineering dilemma arises: accountability.&lt;br&gt;
Because everything the &lt;code&gt;DataPipeline-Sentinel&lt;/code&gt; does is logged immutably into &lt;code&gt;episodic_memory&lt;/code&gt; SQLite tables, an audit team can trace exactly why the agent executed a specific query. We can see the FAISS IDs retrieved, the Declarative Rules pulled, and the prompt fed to the LLM. &lt;br&gt;
In my opinion, any agent performing write-operations on enterprise infrastructure MUST have an immutable SQLite-style episodic log. RAG without auditability is a liability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Future Roadmap
&lt;/h3&gt;

&lt;p&gt;While this PoC brilliantly handles incident logging, my future experiments will focus on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memory Decay&lt;/strong&gt;: Periodically downgrading the &lt;code&gt;confidence&lt;/code&gt; score in the Declarative table over time if a rule isn't cited in X days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Resolution&lt;/strong&gt;: What happens when Day 50 consolidation contradicts a rule learned on Day 10? The agent will need an active reasoning loop to determine truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Agent Memory Sharing&lt;/strong&gt;: Having a Sentinel Agent share its FAISS semantic index with a completely different Security Agent over the network.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Building the DataPipeline-Sentinel experiment was a profound validation of cognitive software architecture. I realized that the intelligence of an agent isn't bound by its underlying model's parameter count—it's bounded by the architecture of its memory systems. &lt;/p&gt;

&lt;p&gt;A $10,000 foundational model with no persistence is a genius amnesiac. But a relatively cheap model wrapped in a beautifully orchestrated Hierarchical Memory OS becomes a domain expert. FAISS and SQLite proved to be the absolute perfect, lightweight pairing to achieve this.&lt;/p&gt;

&lt;p&gt;If we want autonomous agents to truly integrate into real-world business environments—whether it's monitoring infrastructure, handling corporate finance, or auditing compliance—we must give them the gift of permanent, structured memory.&lt;/p&gt;




&lt;p&gt;Disclaimer&lt;/p&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>architecture</category>
      <category>data</category>
    </item>
    <item>
      <title>Building an Autonomous Brand Crisis Management Agent with Hierarchical Memory</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Tue, 24 Mar 2026 04:55:36 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/building-an-autonomous-brand-crisis-management-agent-with-hierarchical-memory-31dn</link>
      <guid>https://forem.com/exploredataaiml/building-an-autonomous-brand-crisis-management-agent-with-hierarchical-memory-31dn</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fiyce20mjc4q8hgnp08yu.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fiyce20mjc4q8hgnp08yu.gif" alt="Title Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Subtitle: How I Architected a Persistent PR Defense System Using FAISS, SQLite, and Automated Memory Consolidation&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In my recent experiments, I built &lt;strong&gt;DataPipeline-Sentinel&lt;/strong&gt;, a persistent OS for autonomous data pipeline incident management.&lt;/li&gt;
&lt;li&gt;I utilized a 4-tier Hierarchical Memory System (Context, Semantic, Episodic, Declarative) to enable genuine machine learning from past incidents.&lt;/li&gt;
&lt;li&gt;By combining FAISS for vector retrieval and SQLite for immutable logging, the agent instantly recalls resolved pipeline errors.&lt;/li&gt;
&lt;li&gt;I created a nightly Memory Consolidation background job to distill hundreds of raw logs into hard-coded declarative rules.&lt;/li&gt;
&lt;li&gt;This architecture shifts AI agents from stateless script-kiddies into seasoned, senior-level operators. All code is available in my public repository &lt;a href="https://github.com/aniket-work/DataPipeline-Sentinel" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fpw1umohcgecxpl5jlfn7.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fpw1umohcgecxpl5jlfn7.png" alt="Architecture Overview" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I observed a recurring nightmare in modern data engineering: pipelines break, engineers diagnose the issue, they apply a fix (like tweaking a Spark schema inference), and then... everyone forgets. Three months later, the exact same upstream API changes its payload structure, a different pipeline shatters, and a new engineer wastes four hours diagnosing the exact same root cause. &lt;/p&gt;

&lt;p&gt;From my experience, stateless autonomous agents using standard RAG (Retrieval-Augmented Generation) aren't enough to solve this. If you just feed an LLM a static playbook, it never learns from the &lt;em&gt;nuances&lt;/em&gt; of daily operational chaos. I thought about how human senior engineers operate: they have an instinct derived from thousands of past, painful outages. They remember the &lt;em&gt;episodic&lt;/em&gt; pain of a MongoDB schema drift causing an Airflow DAG to hang.&lt;/p&gt;

&lt;p&gt;I wanted to replicate this. I put it this way because I realized we don't just need agents that can read docs; we need agents that can &lt;em&gt;remember experiences&lt;/em&gt;. Thus, the idea for the &lt;strong&gt;DataPipeline-Sentinel&lt;/strong&gt; was born—an experimental PoC of an EverMem-style persistent AI Agent OS that learns chronologically from production incidents and consolidates that knowledge into permanent operational wisdom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article breaks down how I developed a Persistent Memory Operating System for an autonomous agent. I am not focusing on the specific LLM prompts. Instead, in my opinion, the fascinating part is the &lt;strong&gt;Memetic Architecture&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;I will walk you through building a system that features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Short-Term Context Buffers&lt;/strong&gt;: For active incident triaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic Memory (FAISS)&lt;/strong&gt;: To instantly find mathematically similar past outages using high-dimensional vector embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Episodic Memory (SQLite)&lt;/strong&gt;: An immutable, append-only ledger of everything the agent and human engineers have ever done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative Memory (SQLite)&lt;/strong&gt;: Firm, hard-coded constraints logically deduced from episodic logs during an automated "sleep cycle."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't about generic coding. It's about designing a cognitive architecture that allows an AI operator to organically accumulate seniority over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;To keep this experimental PoC lean, I avoided heavy vector databases or complex graph tools. My setup is purposefully brutalist and highly effective:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.12&lt;/strong&gt;: The core orchestrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAISS (CPU)&lt;/strong&gt;: Facebook's incredibly fast library for similarity search and clustering of dense vectors. I use this exclusively for Semantic Memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt;: The unsung hero of persistent storage. I use SQLite to maintain both the Episodic event logs and the Declarative rule tables. It is lightweight, zero-configuration, and ACID compliant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich&lt;/strong&gt;: For hyper-readable, beautiful terminal output simulating the agent's internal monologue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillow &amp;amp; Mermaid.js&lt;/strong&gt;: For all the visual diagramming and UI mockups.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;In my opinion, the AI industry is overly obsessed with context windows. "Just shove 1 million tokens into context and it will figure it out!" No. From my experience, shoving endless logs into a prompt is computationally wasteful and mathematically noisy. &lt;/p&gt;

&lt;p&gt;You should read this if you want to understand how to build &lt;em&gt;Systems of Record&lt;/em&gt; for autonomous agents. If you are trying to build an agent that handles customer support, financial auditing, or infrastructure monitoring, you will eventually hit the "amnesia wall." Your agent will solve a complex edge case on Tuesday and completely forget how to do it by Thursday. &lt;/p&gt;

&lt;p&gt;This article provides the exact architectural blueprint to break through that wall. By implementing an automated consolidation layer, I've proven (at least in this PoC) that we can programmatically convert chaotic daily experiences into rigid, institutional knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The 4-Tier Cognitive Hierarchy
&lt;/h3&gt;

&lt;p&gt;When I designed this architecture, I thought deeply about human memory psychology and applied it directly to Python objects. &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F3lpqhu5s4kgdd45z0k65.png" 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.amazonaws.com%2Fuploads%2Farticles%2F3lpqhu5s4kgdd45z0k65.png" alt="Sequence Diagram" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Working Context (RAM)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: What I'm thinking about right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: A standard Python list queue (&lt;code&gt;self.short_term_buffer&lt;/code&gt;) capped at 10 items. It holds the active error stack trace and the active pipeline name. Once the issue is resolved, this buffer is cleared.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Semantic Memory (FAISS HNSW)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: My vague intuitive sense that "I've seen this error before."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: Every time a pipeline error occurs, it is embedded into a 1536-dimensional vector and stored in &lt;code&gt;faiss.IndexFlatL2&lt;/code&gt;. If a new error comes in, I do a cosine similarity search (&lt;code&gt;self.index.search()&lt;/code&gt;) to pull the top 3 most similar historical errors. Over time, FAISS acts as the agent's intuition. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Episodic Memory (SQLite &lt;code&gt;episodic_memory&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: My chronological journal of every outage I've ever fought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: An append-only relational table. Columns include &lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;event_type&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, and &lt;code&gt;consolidated&lt;/code&gt;. Crucially, this table stores the &lt;em&gt;Resolutions&lt;/em&gt;—what the human engineer ultimately did to fix the pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Declarative Memory (SQLite &lt;code&gt;declarative_memory&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analogy&lt;/strong&gt;: The hard-coded rules written in the employee handbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Implementation&lt;/strong&gt;: A curated table of strict facts (e.g., "Fact Type: pipeline_fix, Content: Use infer_schema=True for Mongo syncs"). The agent queries this table purely by SQL &lt;code&gt;WHERE&lt;/code&gt; clauses, entirely bypassing fuzzy vector math. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Engine of Evolution: Memory Consolidation
&lt;/h3&gt;

&lt;p&gt;This is the secret sauce. In my experiments, I realized Episodic Memory grows infinitely and becomes garbage. You don't want the agent reading 10,000 raw logs of humans fixing pipelines. &lt;/p&gt;

&lt;p&gt;I wrote a &lt;code&gt;consolidation.py&lt;/code&gt; script—a cron job simulating human sleep. It runs at midnight, performs a SQL query for all logs where &lt;code&gt;consolidated = 0&lt;/code&gt;, uses an LLM to extract a generalized rule from the specific incident, writes to Declarative memory, and updates the flag to &lt;code&gt;consolidated = 1&lt;/code&gt;. &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2F6jdxle3phcypa8ik37mf.png" 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.amazonaws.com%2Fuploads%2Farticles%2F6jdxle3phcypa8ik37mf.png" alt="Flow Diagram" width="336" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;I structured the project strictly around separation of concerns. The OS handles persistence, the Agent handles logic, and the Consolidator handles background distillation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Establishing the Hierarchical Memory OS
&lt;/h3&gt;

&lt;p&gt;Let's look at how I implemented the core &lt;code&gt;HierarchicalMemoryOS&lt;/code&gt; class combining SQLite and FAISS.&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;sqlite3&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;faiss&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&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;class&lt;/span&gt; &lt;span class="nc"&gt;HierarchicalMemoryOS&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;db_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_os.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;vector_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1536&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;db_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db_path&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;vector_dim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_dim&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;short_term_buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="c1"&gt;# Initialize SQLite (Episodic &amp;amp; Declarative)
&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;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_path&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="nf"&gt;_init_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Initialize FAISS (Semantic)
&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;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IndexFlatL2&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;vector_dim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Map FAISS vector IDs back to SQLite Episodic IDs
&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;id_mapping&lt;/span&gt; &lt;span class="o"&gt;=&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;next_faiss_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I put it this way because managing two completely different storage paradigms (Vectors in RAM/Disk and Relational rows) requires a tight unifying class. The &lt;code&gt;id_mapping&lt;/code&gt; dict bridges the gap between the FAISS integer ID array and the SQLite Primary Keys.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Episodic and Declarative Schema
&lt;/h3&gt;

&lt;p&gt;I designed the SQLite tables to be extremely barebones but highly relational to the agent's temporal experience.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_init_db&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;cursor&lt;/span&gt; &lt;span class="o"&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;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="c1"&gt;# Episodic Memory: Raw events/logs
&lt;/span&gt;        &lt;span class="n"&gt;cursor&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;
            CREATE TABLE IF NOT EXISTS episodic_memory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp DATETIME,
                event_type TEXT,
                content TEXT,
                metadata TEXT,
                consolidated BOOLEAN DEFAULT 0
            )
        &lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Declarative Memory: Concrete rules/facts derived from episodes
&lt;/span&gt;        &lt;span class="n"&gt;cursor&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;
            CREATE TABLE IF NOT EXISTS declarative_memory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                fact_type TEXT,
                fact_content TEXT,
                confidence REAL
            )
        &lt;/span&gt;&lt;span class="sh"&gt;'''&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;From my experience, boolean flags like &lt;code&gt;consolidated&lt;/code&gt; are the safest way to implement async background processing. It allows the agent to constantly write to Episodic memory without locking out the background distillation job.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding Semantic Vectors
&lt;/h3&gt;

&lt;p&gt;This is where the magic of fuzzy recall happens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_semantic&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&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;simulate_embedding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Search FAISS for semantically similar past events.
&lt;/span&gt;        &lt;span class="k"&gt;if&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;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ntotal&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="k"&gt;return&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;simulate_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Pseudo-random reproducible vector for PoC
&lt;/span&gt;            &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed&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;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rand&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="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;float32&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;# Expected API integration here
&lt;/span&gt;            &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&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="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize_L2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;distances&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indices&lt;/span&gt; &lt;span class="o"&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;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&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;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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;indices&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;idx&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&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;id_mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;sql_id&lt;/span&gt; &lt;span class="o"&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;id_mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;cursor&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 content FROM episodic_memory 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;sql_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
                &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&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="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&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;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I observed that simply returning vector distances isn't enough. We must do a secondary lookup into SQLite using &lt;code&gt;self.id_mapping&lt;/code&gt; to return the actual, human-readable log content that matches the vector. This is how the agent fundamentally "remembers" text based on semantic meaning.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sentinel Agent Logic
&lt;/h3&gt;

&lt;p&gt;Here is the core orchestration loop that fires when a pipeline breaks.&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;memory_os&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HierarchicalMemoryOS&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SentinelAgent&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;memory_os&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HierarchicalMemoryOS&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;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_os&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_incident&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;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# 1. Update Short-Term Context
&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_short_term&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&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;incident&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;pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. Retrieve Semantic Context (Has this happened before?)
&lt;/span&gt;        &lt;span class="n"&gt;similar_past_errors&lt;/span&gt; &lt;span class="o"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Retrieve Declarative Rules (Are there firm rules for this?)
&lt;/span&gt;        &lt;span class="n"&gt;firm_rules&lt;/span&gt; &lt;span class="o"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_declarative_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 4. Formulate Diagnosis &amp;amp; Fix (Simulated LLM Call)
&lt;/span&gt;        &lt;span class="n"&gt;diagnosis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_analyze_with_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;similar_past_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firm_rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 5. Store Incident in Episodic Memory
&lt;/span&gt;        &lt;span class="n"&gt;episodic_id&lt;/span&gt; &lt;span class="o"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store_episodic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident&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="o"&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;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Diagnosis: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;diagnosis&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;metadata&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;pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;unresolved&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="c1"&gt;# Embed for immediate searchability
&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_and_store_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;episodic_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;diagnosis&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I wrote it this way to force the agent to query BOTH its intuition (FAISS Semantic) and its handbook (SQLite Declarative) before invoking the LLM synthesis logic. This drastically reduces hallucinations because the LLM prompt is heavily saturated with historical ground-truth.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Memory Consolidator
&lt;/h3&gt;

&lt;p&gt;The final piece of the puzzle. This runs completely out-of-band.&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;MemoryConsolidator&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;memory_os&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HierarchicalMemoryOS&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;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_os&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_consolidation_cycle&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="c1"&gt;# Scan unconsolidated episodic memory and distill to declarative rules.
&lt;/span&gt;        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&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;memory&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;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;cursor&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 id, content FROM episodic_memory 
            WHERE consolidated = 0 AND event_type = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resolution&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;consolidated_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;record_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;

            &lt;span class="c1"&gt;# Simulated LLM Extraction: Extract a firm rule from the resolution
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infer_schema=True&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Always use infer_schema=True when dealing with upstream MongoDB drift.&lt;/span&gt;&lt;span class="sh"&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;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store_declarative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Mark as consolidated
&lt;/span&gt;            &lt;span class="n"&gt;cursor&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;UPDATE episodic_memory SET consolidated = 1 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;record_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="n"&gt;consolidated_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;memory&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;commit&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;consolidated_count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;By pulling unresolved logs and formally marking them as &lt;code&gt;consolidated = 1&lt;/code&gt;, we effectively maintain a high-signal-to-noise ratio in the declarative database while preserving the unstructured history forever.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;If you want to run this experimental environment on your own machine:&lt;/p&gt;

&lt;p&gt;Step by step details can be found at: &lt;a href="https://github.com/aniket-work/DataPipeline-Sentinel" rel="noopener noreferrer"&gt;DataPipeline-Sentinel GitHub Repository&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo and install the light-weight dependencies (&lt;code&gt;faiss-cpu&lt;/code&gt;, &lt;code&gt;rich&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python3 main.py&lt;/code&gt; to initiate the simulation.&lt;/li&gt;
&lt;li&gt;Observe how the agent handles a Day 1 novel incident, undergoes Nightly Consolidation, and brilliantly resolves a Day 2 recurrent incident without human intervention.&lt;/li&gt;
&lt;li&gt;You can explore the exact raw source code structure there and adapt it to your LLM API of choice.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When executing the agent in an environment, the simulation visually proves the memetic shift.&lt;/p&gt;

&lt;p&gt;On Day 1, the agent encounters a &lt;code&gt;Schema mismatch on 'user_metadata' array&lt;/code&gt;. Semantic lookup returns 0 results. Declarative lookup returns 0 results. The agent escalates to a human engineer. The engineer manually deploys a fix (&lt;code&gt;infer_schema=True&lt;/code&gt;). The agent logs this.&lt;/p&gt;

&lt;p&gt;At Midnight, the &lt;code&gt;MemoryConsolidator&lt;/code&gt; process wakes up. It scans the episodic logs, notices the human resolution, and extracts a hard-coded constraint rule, storing it in SQLite.&lt;/p&gt;

&lt;p&gt;On Day 2, the agent encounters a very similar error on a &lt;em&gt;different&lt;/em&gt; pipeline: &lt;code&gt;Schema mismatch on 'transaction_data' array&lt;/code&gt;.&lt;br&gt;
Instantly, the system queries FAISS and recognizes semantic similarity. It queries SQLite and retrieves the newly consolidated rule. The agent &lt;em&gt;autonomously&lt;/em&gt; suggests the exact fix without escalating to the engineer. &lt;/p&gt;

&lt;p&gt;This proves that continuous, persistent learning is possible when you decouple the storage topology from the stochastic LLM generation!&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensive Deep Dive on Architectural Trade-offs
&lt;/h2&gt;

&lt;p&gt;To reach a comprehensive understanding, I must expand on why I think this specific stack is the ultimate sweet spot for edge AI agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use a massive Vector Database for everything?
&lt;/h3&gt;

&lt;p&gt;Ah, the trap of the modern AI hype cycle. If you store &lt;em&gt;everything&lt;/em&gt; in Pinecone or Milvus, you treat subjective opinions and objective firm-rules identically. A vector database retrieves data based on mathematically fuzzy distance. If a company policy states "Never restart a Production node during business hours," you do NOT want a fuzzy 0.82 cosine similarity match to decide if that rule applies. You want a deterministic SQL &lt;code&gt;WHERE rules.type = 'security_constraint'&lt;/code&gt; to enforce it. &lt;br&gt;
By splitting the data, I guarantee that the agent has both creative intuition and strict boundary compliance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ethics of Autonomous Operational Agents
&lt;/h3&gt;

&lt;p&gt;When allowing agents to manage production data pipelines, an ethical engineering dilemma arises: accountability.&lt;br&gt;
Because everything the &lt;code&gt;DataPipeline-Sentinel&lt;/code&gt; does is logged immutably into &lt;code&gt;episodic_memory&lt;/code&gt; SQLite tables, an audit team can trace exactly why the agent executed a specific query. We can see the FAISS IDs retrieved, the Declarative Rules pulled, and the prompt fed to the LLM. &lt;br&gt;
In my opinion, any agent performing write-operations on enterprise infrastructure MUST have an immutable SQLite-style episodic log. RAG without auditability is a liability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Future Roadmap
&lt;/h3&gt;

&lt;p&gt;While this PoC brilliantly handles incident logging, my future experiments will focus on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memory Decay&lt;/strong&gt;: Periodically downgrading the &lt;code&gt;confidence&lt;/code&gt; score in the Declarative table over time if a rule isn't cited in X days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Resolution&lt;/strong&gt;: What happens when Day 50 consolidation contradicts a rule learned on Day 10? The agent will need an active reasoning loop to determine truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Agent Memory Sharing&lt;/strong&gt;: Having a Sentinel Agent share its FAISS semantic index with a completely different Security Agent over the network.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Building the DataPipeline-Sentinel experiment was a profound validation of cognitive software architecture. I realized that the intelligence of an agent isn't bound by its underlying model's parameter count—it's bounded by the architecture of its memory systems. &lt;/p&gt;

&lt;p&gt;A $10,000 foundational model with no persistence is a genius amnesiac. But a relatively cheap model wrapped in a beautifully orchestrated Hierarchical Memory OS becomes a domain expert. FAISS and SQLite proved to be the absolute perfect, lightweight pairing to achieve this.&lt;/p&gt;

&lt;p&gt;If we want autonomous agents to truly integrate into real-world business environments—whether it's monitoring infrastructure, handling corporate finance, or auditing compliance—we must give them the gift of permanent, structured memory.&lt;/p&gt;




&lt;p&gt;Disclaimer&lt;/p&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix A: The Mathematical Nuance of FAISS HNSW
&lt;/h2&gt;

&lt;p&gt;When I chose FAISS, I specifically considered the HNSW (Hierarchical Navigable Small World) graph topology. HNSW creates a multi-layered structure of links. At the top layers, you have long-distance semantic jumps. As you traverse lower, you find tightly clustered, hyper-specific nuances. &lt;br&gt;
From my experience, when embedding data pipeline error logs, the vectors tend to cluster rapidly around string constants (like "java.lang.NullPointerException"). This can blind the agent to the actual business logic failure (e.g., "Customer ID missing"). &lt;br&gt;
To counteract this, I ensure the Episodic Memory combines the raw log WITH human metadata before vectorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix B: The Case Against Ephemeral Prompts
&lt;/h2&gt;

&lt;p&gt;I wrote this architecture because I am fundamentally opposed to the current industry trend of stuffing 10,000-line JSON files into an LLM prompt and calling it "Context." &lt;br&gt;
In my opinion, passing stateless context is identical to forcing a surgeon to re-read every medical textbook before every single incision. It is a staggering waste of compute, latency, and environmental energy.&lt;br&gt;
By utilizing the FAISS/SQLite memory OS, the prompt strictly contains the exact 3 vector matches and 2 firm rules needed. Token usage drops by 98%. Latency drops to milliseconds. &lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>architecture</category>
      <category>data</category>
    </item>
    <item>
      <title>Streaming Intelligence: Orchestrating Autonomous Wildfire Response with Agents</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Mon, 23 Mar 2026 01:34:17 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/streaming-intelligence-orchestrating-autonomous-wildfire-response-with-agents-15a0</link>
      <guid>https://forem.com/exploredataaiml/streaming-intelligence-orchestrating-autonomous-wildfire-response-with-agents-15a0</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fh8usfspye4640rav59ey.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fh8usfspye4640rav59ey.gif" alt="Title Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Autonomous Wildfire Response Coordinator: How I Built a Self-Healing Emergency Response System Using LangGraph and Online Replanning&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I've spent the last few weeks experimenting with how AI agents handle hyper-dynamic environments. The result is &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;—a proof-of-concept autonomous coordinator that doesn't just "plan," but "replans" in real-time as a simulated fire spreads. Using LangGraph's state machine architecture, I built a system that ingest continuous sensor streams, detects containment breaches, and dynamically re-routes assets like aerial tankers and ground crews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ve always been fascinated by high-latency, high-stakes environments. There’s something visceral about a situation where a plan made five minutes ago is already obsolete. Recently, I was observing how traditional dispatcher systems struggle with wildfires—situations where wind shifts or fuel changes can turn a controlled burn into a catastrophe in seconds.&lt;/p&gt;

&lt;p&gt;In my opinion, the bottleneck isn't the data; we have satellites, IoT sensors, and drones. The bottleneck is the &lt;strong&gt;latency of the decision-making loop&lt;/strong&gt;. I wanted to see if I could build a coordinator that acts as a "living" strategy. This isn't a production-grade safety tool—far from it. It's one of my personal experiments, a PoC to explore "Streaming Decision Agents." I wrote this implementation to test a specific hypothesis: that agentic workflows can provide the "self-healing" logic needed for emergency response.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article is a deep dive into the architecture of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;. I’ll walk you through how I designed a stochastic wildfire environment (stochastic because chaos is the point), and how I used &lt;strong&gt;LangGraph&lt;/strong&gt; to build a multi-agent system that processes data as a stream. &lt;/p&gt;

&lt;p&gt;We’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dynamic Simulation&lt;/strong&gt;: Building a 2D grid-world where fire spreads based on wind and fuel.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Sense-Think-Act" Stream&lt;/strong&gt;: How agents receive updates and decide when to trigger a "Strategic Reset."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Online Replanning&lt;/strong&gt;: The logic behind discarding a plan mid-execution and shifting resources.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tech Stack Nuances&lt;/strong&gt;: Why I chose specific tools for state management and visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;From my experience, if you're building something that needs to maintain state across complex branching paths, you need a robust framework. Here’s what I used for this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;LangGraph&lt;/strong&gt;: This was my choice for the core agentic workflow. I think its ability to represent cycles and maintain persistent state is unmatched for "replanning" scenarios.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pydantic&lt;/strong&gt;: I used this for structured event definitions. In my experience, strict type safety for agent communication prevents a lot of "hallucination-style" logic errors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python 3.10&lt;/strong&gt;: The backbone of the project.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;PIL (Pillow)&lt;/strong&gt;: For generating my technical visual assets (including that optimized GIF you see at the top).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WildfireWorld (Custom)&lt;/strong&gt;: A Python-based simulation engine I wrote to provide the "input stream."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you’re interested in &lt;strong&gt;Agentic AI&lt;/strong&gt;, &lt;strong&gt;Autonomous Systems&lt;/strong&gt;, or simply how to build resilient software in chaotic domains, there’s something here for you. As per my experience, the next wave of AI isn't just about "chatting"; it's about "operating." This article shows you one way to bridge that gap—by treating AI as a coordinator that manages a real-time feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;p&gt;I put it this way because I think visualization is 50% of the engineering process. Before I wrote a single line of code, I mapped out how I wanted the decision loop to look. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Architecture
&lt;/h3&gt;

&lt;p&gt;In my opinion, a streaming agent needs to be decoupled from the raw data. The "Environment" (the fire) shouldn't care about the "Agent" (the coordinator). &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fxotn6bpkjit1oza4u6sg.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fxotn6bpkjit1oza4u6sg.png" alt="Architecture" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I designed the graph with four primary nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Sensor Ingest&lt;/strong&gt;: The entry point. It receives "packets" of thermal data.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Threat Analyzer&lt;/strong&gt;: This is the "brain stem." It doesn't plan; it just screams "FIRE!" when something breaks the perimeter.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Strategy Optimizer&lt;/strong&gt;: The "prefrontal cortex." It looks at the mess and decides on a new containment boundary.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dispatcher&lt;/strong&gt;: The "hands." It executes the tactical movements of tankers and ground crews.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Decision Flow
&lt;/h3&gt;

&lt;p&gt;I thought about how a human dispatcher works. They don't replan every time a leaf burns. They replan when the fire "jumps" a line. I implemented this via a "Criticality Score" in the state.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Facfxv6ba8id17dolvjwy.png" 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.amazonaws.com%2Fuploads%2Farticles%2Facfxv6ba8id17dolvjwy.png" alt="Flow" width="336" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;Now, let's look at the implementation. I've broken this down into the core components that make the "Sense-Think-Act" loop work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Stochastic Environment: The Math of Chaos
&lt;/h3&gt;

&lt;p&gt;I wrote this environment to be the "source of truth." It's a 2D grid where every cell has a state. The fire spreads using a stochastic model—meaning there's randomness, but it's "biased" by the wind. &lt;/p&gt;

&lt;p&gt;In my opinion, the most interesting part was the &lt;code&gt;get_wind_bias&lt;/code&gt; function. I wrote it this way because I wanted to simulate the vector-based nature of fire spread. If the wind is blowing North-East, the probability of the cell to the North-East igniting is significantly higher than the cell to the South-West. From my experience, small mathematical biases like this are what make a simulation feel "alive" rather than just a random walk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_wind_bias&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;from_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from_pos&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;
    &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;

    &lt;span class="n"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;W&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I observed that setting the &lt;code&gt;wind_strength&lt;/code&gt; to 0.5 creates a noticeable but not overwhelming "drift." I put it this way coz I wanted the agent to have to "guess" where the fire would jump next, but still give it a fighting chance if it followed the wind patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Agent State: The Shared Consciousness
&lt;/h3&gt;

&lt;p&gt;In my experience, the &lt;code&gt;AgentState&lt;/code&gt; is the most important part of any LangGraph project. It’s the "shared memory" between nodes. When I first started with agents, I used to pass everything as function arguments. I quickly realized that's a nightmare for debugging. &lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;TypedDict&lt;/code&gt; for the state allowed me to keep a clean separation of concerns. The &lt;code&gt;heat_map&lt;/code&gt; is the agent's "perception" of the world. The &lt;code&gt;active_plan&lt;/code&gt; is its "intention." And the &lt;code&gt;logs&lt;/code&gt; are its "rationale." I think this tripartite structure is a solid pattern for any autonomous system.&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;heat_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;active_plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;criticality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;replan_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Analyzer Agent: The Reactive Engine
&lt;/h3&gt;

&lt;p&gt;This node is responsible for "Online Decision Making." It looks at the current &lt;code&gt;heat_map&lt;/code&gt; and compares it to the &lt;code&gt;active_plan&lt;/code&gt;. I observed that the key to a good "Streaming Agent" is knowing &lt;strong&gt;what to ignore&lt;/strong&gt;. If the fire spreads into an area we've already designated as a "controlled zone," we don't need to replan. We only replan when there's a "Breach."&lt;/p&gt;

&lt;p&gt;I defined a "Breach" as fire occurring in a cell that isn't currently targeted by our assets. This is where the "Online" part of the replanning happens. The &lt;code&gt;analyzer_node&lt;/code&gt; is essentially a filter that prevents the complex &lt;code&gt;Strategist&lt;/code&gt; from running on every single step. In my opinion, this "Gated Activation" is essential for scaling these systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Strategist Agent: The Proactive Re-pivoting
&lt;/h3&gt;

&lt;p&gt;When the Analyzer sets &lt;code&gt;replan_required&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt;, the Strategist kicks in. As per my experience, this is the most compute-intensive part. In this PoC, I implemented a simple perimeter-search, but I put a lot of thought into how it &lt;em&gt;would&lt;/em&gt; look in a real system. &lt;/p&gt;

&lt;p&gt;Imagine using a Diffusion Model or a Graph Neural Network to predict fire spread and then using a Reinforcement Learning agent to optimize the resource allocation. For this experiment, I stuck to a more deterministic approach, but I wrote the interface to be flexible. I think that's the beauty of LangGraph—you can swap out a simple Python function for a heavy-weight ML model without changing the graph structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Orchestrating the Chaos
&lt;/h3&gt;

&lt;p&gt;Finally, I tied it all together using LangGraph's &lt;code&gt;StateGraph&lt;/code&gt;. I put it this way because I think of the graph as a "Playbook." Each step of the simulation is one execution of the playbook. &lt;/p&gt;

&lt;p&gt;I wrote the loop in &lt;code&gt;main.py&lt;/code&gt; to be the "Clock" of the system. It pulses every half-second, updating the environment and then asking the agent: "Given this new state, what do we do next?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deep Dive: Why Online Replanning Matters
&lt;/h2&gt;

&lt;p&gt;In my opinion, the "Real World" is a series of streaming events, not a single static request. Most AI tutorials focus on "Prompt -&amp;gt; Response." But as per my experience, the real meat of the problem is "Stream -&amp;gt; Continuous Adaptation." &lt;/p&gt;

&lt;p&gt;While running these experiments, I observed something fascinating. If I turned off the "Replanning" logic and just let the agent follow its initial plan, the fire would almost always bypass the containment lines within 10 steps. By contrast, with "Online Replanning" enabled, the agent was able to dynamically shift assets to the edges of the spread, effectively "bottling up" the disaster. &lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;I wrote this code, then I thought: "Wait, how do I visualize this so it people can actually &lt;em&gt;see&lt;/em&gt; the logic?" This led me down a rabbit hole of GIF optimization. &lt;/p&gt;

&lt;p&gt;I learned the hard way that Dev.to and LinkedIn have very specific requirements for GIFs. Standard RGB GIFs often flicker or fail to upload. I had to implement a &lt;strong&gt;Global Palette&lt;/strong&gt; strategy using PIL. By generating a single 256-color palette from key frames and converting all frames to P-Mode (with no dithering), I was able to get a crystal-clear, 100fps terminal animation that looks as premium as the code it represents.&lt;/p&gt;

&lt;p&gt;Another lesson: &lt;strong&gt;State Bloom&lt;/strong&gt;. I observed that if you aren't careful, your logs will grow exponentially. I had to implement a logic to "condense" logs if they exceeded a certain length. In my opinion, "State Pruning" is as important as "State Management."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethics and Future Roadmap: The "Human-in-the-Loop"
&lt;/h2&gt;

&lt;p&gt;As per me, we are entering an era of "Autonomous Infrastructure." But who watches the watchmen? In my view, an autonomous wildfire coordinator should never be 100% autonomous. I think the next iteration of this project should include a "Human Approval Node." &lt;/p&gt;

&lt;p&gt;I’d like to see a version of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt; where the agent proposes a "Strategy Shift" and a human supervisor has 30 seconds to click "Approve" before the tankers are re-routed. I think this "Collaborative Agency" is the sweet spot for high-stakes AI.&lt;/p&gt;

&lt;p&gt;My future roadmap for this experiment includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Multi-UAV Coordination&lt;/strong&gt;: Simulating multiple assets with independent batteries and refuel cycles.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Topographical Integration&lt;/strong&gt;: Using real-world GIS data to affect fire spread (e.g., fire spreads faster uphill).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;LLM-based Post-Mortem&lt;/strong&gt;: An agent that analyzes the logs after the simulation to write a "What went wrong?" report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;If you want to play with this experiment yourself, I've pushed the complete code to GitHub. I wrote the README to be very detailed, so you should be able to get it running in under 2 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step by step details can be found at:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/aniket-work/WildfireGuard-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/WildfireGuard-AI&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When you run the project, the terminal output is designed to show the "streaming" nature of the decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put it this way because I wanted the user to feel the "pulse" of the system. You’ll see the "Analyzer" detecting shifts in real-time and the "Strategist" frantically recalculating. It’s a chaotic, beautiful dance of logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This experiment was a huge learning curve for me. From my experience, the hardest part of "Agentic AI" isn't the model—it's the &lt;strong&gt;State Management&lt;/strong&gt;. How do you ensure the agent remembers the plan from Step 5 when it's now at Step 15? &lt;/p&gt;

&lt;p&gt;Through this PoC, I realized that "Streaming Decisions" are the future of industrial AI. Whether it's managing a power grid, a factory floor, or a wildfire, we need systems that can "Think while they Act." I hope this walkthrough gives you some ideas for your own autonomous projects. I put a lot of heart into this implementation, and I think it shows what's possible when we stop thinking of AI as a chatbot and start thinking of it as an operator.&lt;/p&gt;

&lt;p&gt;Stay curious, stay experimental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disclaimer
&lt;/h3&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented. &lt;/p&gt;

&lt;p&gt;This article is an experimental PoC write-up. It is not production guidance. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Footnote&lt;/strong&gt;: This article was written as part of my experiments with Agentic AI. The code repository is static and intended for learning purposes only. I put it this way coz I want to emphasize that while the math is real, the application is educational.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langgraph</category>
      <category>automation</category>
    </item>
    <item>
      <title>Orchestrating Chaos: Autonomous Wildfire Response with Streaming Decision Agents</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Mon, 23 Mar 2026 01:33:58 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/orchestrating-chaos-autonomous-wildfire-response-with-streaming-decision-agents-17b2</link>
      <guid>https://forem.com/exploredataaiml/orchestrating-chaos-autonomous-wildfire-response-with-streaming-decision-agents-17b2</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fh8usfspye4640rav59ey.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fh8usfspye4640rav59ey.gif" alt="Title Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Autonomous Wildfire Response Coordinator: How I Built a Self-Healing Emergency Response System Using LangGraph and Online Replanning&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I've spent the last few weeks experimenting with how AI agents handle hyper-dynamic environments. The result is &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;—a proof-of-concept autonomous coordinator that doesn't just "plan," but "replans" in real-time as a simulated fire spreads. Using LangGraph's state machine architecture, I built a system that ingest continuous sensor streams, detects containment breaches, and dynamically re-routes assets like aerial tankers and ground crews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ve always been fascinated by high-latency, high-stakes environments. There’s something visceral about a situation where a plan made five minutes ago is already obsolete. Recently, I was observing how traditional dispatcher systems struggle with wildfires—situations where wind shifts or fuel changes can turn a controlled burn into a catastrophe in seconds.&lt;/p&gt;

&lt;p&gt;In my opinion, the bottleneck isn't the data; we have satellites, IoT sensors, and drones. The bottleneck is the &lt;strong&gt;latency of the decision-making loop&lt;/strong&gt;. I wanted to see if I could build a coordinator that acts as a "living" strategy. This isn't a production-grade safety tool—far from it. It's one of my personal experiments, a PoC to explore "Streaming Decision Agents." I wrote this implementation to test a specific hypothesis: that agentic workflows can provide the "self-healing" logic needed for emergency response.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article is a deep dive into the architecture of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;. I’ll walk you through how I designed a stochastic wildfire environment (stochastic because chaos is the point), and how I used &lt;strong&gt;LangGraph&lt;/strong&gt; to build a multi-agent system that processes data as a stream. &lt;/p&gt;

&lt;p&gt;We’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dynamic Simulation&lt;/strong&gt;: Building a 2D grid-world where fire spreads based on wind and fuel.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Sense-Think-Act" Stream&lt;/strong&gt;: How agents receive updates and decide when to trigger a "Strategic Reset."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Online Replanning&lt;/strong&gt;: The logic behind discarding a plan mid-execution and shifting resources.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tech Stack Nuances&lt;/strong&gt;: Why I chose specific tools for state management and visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;From my experience, if you're building something that needs to maintain state across complex branching paths, you need a robust framework. Here’s what I used for this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;LangGraph&lt;/strong&gt;: This was my choice for the core agentic workflow. I think its ability to represent cycles and maintain persistent state is unmatched for "replanning" scenarios.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pydantic&lt;/strong&gt;: I used this for structured event definitions. In my experience, strict type safety for agent communication prevents a lot of "hallucination-style" logic errors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python 3.10&lt;/strong&gt;: The backbone of the project.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;PIL (Pillow)&lt;/strong&gt;: For generating my technical visual assets (including that optimized GIF you see at the top).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WildfireWorld (Custom)&lt;/strong&gt;: A Python-based simulation engine I wrote to provide the "input stream."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you’re interested in &lt;strong&gt;Agentic AI&lt;/strong&gt;, &lt;strong&gt;Autonomous Systems&lt;/strong&gt;, or simply how to build resilient software in chaotic domains, there’s something here for you. As per my experience, the next wave of AI isn't just about "chatting"; it's about "operating." This article shows you one way to bridge that gap—by treating AI as a coordinator that manages a real-time feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;p&gt;I put it this way because I think visualization is 50% of the engineering process. Before I wrote a single line of code, I mapped out how I wanted the decision loop to look. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Architecture
&lt;/h3&gt;

&lt;p&gt;In my opinion, a streaming agent needs to be decoupled from the raw data. The "Environment" (the fire) shouldn't care about the "Agent" (the coordinator). &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fxotn6bpkjit1oza4u6sg.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fxotn6bpkjit1oza4u6sg.png" alt="Architecture" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I designed the graph with four primary nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Sensor Ingest&lt;/strong&gt;: The entry point. It receives "packets" of thermal data.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Threat Analyzer&lt;/strong&gt;: This is the "brain stem." It doesn't plan; it just screams "FIRE!" when something breaks the perimeter.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Strategy Optimizer&lt;/strong&gt;: The "prefrontal cortex." It looks at the mess and decides on a new containment boundary.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dispatcher&lt;/strong&gt;: The "hands." It executes the tactical movements of tankers and ground crews.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Decision Flow
&lt;/h3&gt;

&lt;p&gt;I thought about how a human dispatcher works. They don't replan every time a leaf burns. They replan when the fire "jumps" a line. I implemented this via a "Criticality Score" in the state.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Facfxv6ba8id17dolvjwy.png" 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.amazonaws.com%2Fuploads%2Farticles%2Facfxv6ba8id17dolvjwy.png" alt="Flow" width="336" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;Now, let's look at the implementation. I've broken this down into the core components that make the "Sense-Think-Act" loop work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Stochastic Environment: The Math of Chaos
&lt;/h3&gt;

&lt;p&gt;I wrote this environment to be the "source of truth." It's a 2D grid where every cell has a state. The fire spreads using a stochastic model—meaning there's randomness, but it's "biased" by the wind. &lt;/p&gt;

&lt;p&gt;In my opinion, the most interesting part was the &lt;code&gt;get_wind_bias&lt;/code&gt; function. I wrote it this way because I wanted to simulate the vector-based nature of fire spread. If the wind is blowing North-East, the probability of the cell to the North-East igniting is significantly higher than the cell to the South-West. From my experience, small mathematical biases like this are what make a simulation feel "alive" rather than just a random walk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_wind_bias&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;from_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from_pos&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;
    &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;

    &lt;span class="n"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;W&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I observed that setting the &lt;code&gt;wind_strength&lt;/code&gt; to 0.5 creates a noticeable but not overwhelming "drift." I put it this way coz I wanted the agent to have to "guess" where the fire would jump next, but still give it a fighting chance if it followed the wind patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Agent State: The Shared Consciousness
&lt;/h3&gt;

&lt;p&gt;In my experience, the &lt;code&gt;AgentState&lt;/code&gt; is the most important part of any LangGraph project. It’s the "shared memory" between nodes. When I first started with agents, I used to pass everything as function arguments. I quickly realized that's a nightmare for debugging. &lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;TypedDict&lt;/code&gt; for the state allowed me to keep a clean separation of concerns. The &lt;code&gt;heat_map&lt;/code&gt; is the agent's "perception" of the world. The &lt;code&gt;active_plan&lt;/code&gt; is its "intention." And the &lt;code&gt;logs&lt;/code&gt; are its "rationale." I think this tripartite structure is a solid pattern for any autonomous system.&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;heat_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;active_plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;criticality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;replan_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Analyzer Agent: The Reactive Engine
&lt;/h3&gt;

&lt;p&gt;This node is responsible for "Online Decision Making." It looks at the current &lt;code&gt;heat_map&lt;/code&gt; and compares it to the &lt;code&gt;active_plan&lt;/code&gt;. I observed that the key to a good "Streaming Agent" is knowing &lt;strong&gt;what to ignore&lt;/strong&gt;. If the fire spreads into an area we've already designated as a "controlled zone," we don't need to replan. We only replan when there's a "Breach."&lt;/p&gt;

&lt;p&gt;I defined a "Breach" as fire occurring in a cell that isn't currently targeted by our assets. This is where the "Online" part of the replanning happens. The &lt;code&gt;analyzer_node&lt;/code&gt; is essentially a filter that prevents the complex &lt;code&gt;Strategist&lt;/code&gt; from running on every single step. In my opinion, this "Gated Activation" is essential for scaling these systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Strategist Agent: The Proactive Re-pivoting
&lt;/h3&gt;

&lt;p&gt;When the Analyzer sets &lt;code&gt;replan_required&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt;, the Strategist kicks in. As per my experience, this is the most compute-intensive part. In this PoC, I implemented a simple perimeter-search, but I put a lot of thought into how it &lt;em&gt;would&lt;/em&gt; look in a real system. &lt;/p&gt;

&lt;p&gt;Imagine using a Diffusion Model or a Graph Neural Network to predict fire spread and then using a Reinforcement Learning agent to optimize the resource allocation. For this experiment, I stuck to a more deterministic approach, but I wrote the interface to be flexible. I think that's the beauty of LangGraph—you can swap out a simple Python function for a heavy-weight ML model without changing the graph structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Orchestrating the Chaos
&lt;/h3&gt;

&lt;p&gt;Finally, I tied it all together using LangGraph's &lt;code&gt;StateGraph&lt;/code&gt;. I put it this way because I think of the graph as a "Playbook." Each step of the simulation is one execution of the playbook. &lt;/p&gt;

&lt;p&gt;I wrote the loop in &lt;code&gt;main.py&lt;/code&gt; to be the "Clock" of the system. It pulses every half-second, updating the environment and then asking the agent: "Given this new state, what do we do next?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deep Dive: Why Online Replanning Matters
&lt;/h2&gt;

&lt;p&gt;In my opinion, the "Real World" is a series of streaming events, not a single static request. Most AI tutorials focus on "Prompt -&amp;gt; Response." But as per my experience, the real meat of the problem is "Stream -&amp;gt; Continuous Adaptation." &lt;/p&gt;

&lt;p&gt;While running these experiments, I observed something fascinating. If I turned off the "Replanning" logic and just let the agent follow its initial plan, the fire would almost always bypass the containment lines within 10 steps. By contrast, with "Online Replanning" enabled, the agent was able to dynamically shift assets to the edges of the spread, effectively "bottling up" the disaster. &lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;I wrote this code, then I thought: "Wait, how do I visualize this so it people can actually &lt;em&gt;see&lt;/em&gt; the logic?" This led me down a rabbit hole of GIF optimization. &lt;/p&gt;

&lt;p&gt;I learned the hard way that Dev.to and LinkedIn have very specific requirements for GIFs. Standard RGB GIFs often flicker or fail to upload. I had to implement a &lt;strong&gt;Global Palette&lt;/strong&gt; strategy using PIL. By generating a single 256-color palette from key frames and converting all frames to P-Mode (with no dithering), I was able to get a crystal-clear, 100fps terminal animation that looks as premium as the code it represents.&lt;/p&gt;

&lt;p&gt;Another lesson: &lt;strong&gt;State Bloom&lt;/strong&gt;. I observed that if you aren't careful, your logs will grow exponentially. I had to implement a logic to "condense" logs if they exceeded a certain length. In my opinion, "State Pruning" is as important as "State Management."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethics and Future Roadmap: The "Human-in-the-Loop"
&lt;/h2&gt;

&lt;p&gt;As per me, we are entering an era of "Autonomous Infrastructure." But who watches the watchmen? In my view, an autonomous wildfire coordinator should never be 100% autonomous. I think the next iteration of this project should include a "Human Approval Node." &lt;/p&gt;

&lt;p&gt;I’d like to see a version of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt; where the agent proposes a "Strategy Shift" and a human supervisor has 30 seconds to click "Approve" before the tankers are re-routed. I think this "Collaborative Agency" is the sweet spot for high-stakes AI.&lt;/p&gt;

&lt;p&gt;My future roadmap for this experiment includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Multi-UAV Coordination&lt;/strong&gt;: Simulating multiple assets with independent batteries and refuel cycles.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Topographical Integration&lt;/strong&gt;: Using real-world GIS data to affect fire spread (e.g., fire spreads faster uphill).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;LLM-based Post-Mortem&lt;/strong&gt;: An agent that analyzes the logs after the simulation to write a "What went wrong?" report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;If you want to play with this experiment yourself, I've pushed the complete code to GitHub. I wrote the README to be very detailed, so you should be able to get it running in under 2 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step by step details can be found at:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/aniket-work/WildfireGuard-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/WildfireGuard-AI&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When you run the project, the terminal output is designed to show the "streaming" nature of the decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put it this way because I wanted the user to feel the "pulse" of the system. You’ll see the "Analyzer" detecting shifts in real-time and the "Strategist" frantically recalculating. It’s a chaotic, beautiful dance of logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This experiment was a huge learning curve for me. From my experience, the hardest part of "Agentic AI" isn't the model—it's the &lt;strong&gt;State Management&lt;/strong&gt;. How do you ensure the agent remembers the plan from Step 5 when it's now at Step 15? &lt;/p&gt;

&lt;p&gt;Through this PoC, I realized that "Streaming Decisions" are the future of industrial AI. Whether it's managing a power grid, a factory floor, or a wildfire, we need systems that can "Think while they Act." I hope this walkthrough gives you some ideas for your own autonomous projects. I put a lot of heart into this implementation, and I think it shows what's possible when we stop thinking of AI as a chatbot and start thinking of it as an operator.&lt;/p&gt;

&lt;p&gt;Stay curious, stay experimental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disclaimer
&lt;/h3&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented. &lt;/p&gt;

&lt;p&gt;This article is an experimental PoC write-up. It is not production guidance. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Footnote&lt;/strong&gt;: This article was written as part of my experiments with Agentic AI. The code repository is static and intended for learning purposes only. I put it this way coz I want to emphasize that while the math is real, the application is educational.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langgraph</category>
      <category>automation</category>
    </item>
    <item>
      <title>Orchestrating Chaos: Autonomous Wildfire Response with Streaming Decision Agents</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Mon, 23 Mar 2026 00:40:55 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/orchestrating-chaos-autonomous-wildfire-response-with-streaming-decision-agents-3hib</link>
      <guid>https://forem.com/exploredataaiml/orchestrating-chaos-autonomous-wildfire-response-with-streaming-decision-agents-3hib</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fh8usfspye4640rav59ey.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fh8usfspye4640rav59ey.gif" alt="Title Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Autonomous Wildfire Response Coordinator: How I Built a Self-Healing Emergency Response System Using LangGraph and Online Replanning&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I've spent the last few weeks experimenting with how AI agents handle hyper-dynamic environments. The result is &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;—a proof-of-concept autonomous coordinator that doesn't just "plan," but "replans" in real-time as a simulated fire spreads. Using LangGraph's state machine architecture, I built a system that ingest continuous sensor streams, detects containment breaches, and dynamically re-routes assets like aerial tankers and ground crews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ve always been fascinated by high-latency, high-stakes environments. There’s something visceral about a situation where a plan made five minutes ago is already obsolete. Recently, I was observing how traditional dispatcher systems struggle with wildfires—situations where wind shifts or fuel changes can turn a controlled burn into a catastrophe in seconds.&lt;/p&gt;

&lt;p&gt;In my opinion, the bottleneck isn't the data; we have satellites, IoT sensors, and drones. The bottleneck is the &lt;strong&gt;latency of the decision-making loop&lt;/strong&gt;. I wanted to see if I could build a coordinator that acts as a "living" strategy. This isn't a production-grade safety tool—far from it. It's one of my personal experiments, a PoC to explore "Streaming Decision Agents." I wrote this implementation to test a specific hypothesis: that agentic workflows can provide the "self-healing" logic needed for emergency response.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article is a deep dive into the architecture of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;. I’ll walk you through how I designed a stochastic wildfire environment (stochastic because chaos is the point), and how I used &lt;strong&gt;LangGraph&lt;/strong&gt; to build a multi-agent system that processes data as a stream. &lt;/p&gt;

&lt;p&gt;We’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dynamic Simulation&lt;/strong&gt;: Building a 2D grid-world where fire spreads based on wind and fuel.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Sense-Think-Act" Stream&lt;/strong&gt;: How agents receive updates and decide when to trigger a "Strategic Reset."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Online Replanning&lt;/strong&gt;: The logic behind discarding a plan mid-execution and shifting resources.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tech Stack Nuances&lt;/strong&gt;: Why I chose specific tools for state management and visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;From my experience, if you're building something that needs to maintain state across complex branching paths, you need a robust framework. Here’s what I used for this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;LangGraph&lt;/strong&gt;: This was my choice for the core agentic workflow. I think its ability to represent cycles and maintain persistent state is unmatched for "replanning" scenarios.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pydantic&lt;/strong&gt;: I used this for structured event definitions. In my experience, strict type safety for agent communication prevents a lot of "hallucination-style" logic errors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python 3.10&lt;/strong&gt;: The backbone of the project.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;PIL (Pillow)&lt;/strong&gt;: For generating my technical visual assets (including that optimized GIF you see at the top).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WildfireWorld (Custom)&lt;/strong&gt;: A Python-based simulation engine I wrote to provide the "input stream."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you’re interested in &lt;strong&gt;Agentic AI&lt;/strong&gt;, &lt;strong&gt;Autonomous Systems&lt;/strong&gt;, or simply how to build resilient software in chaotic domains, there’s something here for you. As per my experience, the next wave of AI isn't just about "chatting"; it's about "operating." This article shows you one way to bridge that gap—by treating AI as a coordinator that manages a real-time feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;p&gt;I put it this way because I think visualization is 50% of the engineering process. Before I wrote a single line of code, I mapped out how I wanted the decision loop to look. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Architecture
&lt;/h3&gt;

&lt;p&gt;In my opinion, a streaming agent needs to be decoupled from the raw data. The "Environment" (the fire) shouldn't care about the "Agent" (the coordinator). &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fxotn6bpkjit1oza4u6sg.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fxotn6bpkjit1oza4u6sg.png" alt="Architecture" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I designed the graph with four primary nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Sensor Ingest&lt;/strong&gt;: The entry point. It receives "packets" of thermal data.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Threat Analyzer&lt;/strong&gt;: This is the "brain stem." It doesn't plan; it just screams "FIRE!" when something breaks the perimeter.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Strategy Optimizer&lt;/strong&gt;: The "prefrontal cortex." It looks at the mess and decides on a new containment boundary.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dispatcher&lt;/strong&gt;: The "hands." It executes the tactical movements of tankers and ground crews.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Decision Flow
&lt;/h3&gt;

&lt;p&gt;I thought about how a human dispatcher works. They don't replan every time a leaf burns. They replan when the fire "jumps" a line. I implemented this via a "Criticality Score" in the state.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Facfxv6ba8id17dolvjwy.png" 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.amazonaws.com%2Fuploads%2Farticles%2Facfxv6ba8id17dolvjwy.png" alt="Flow" width="336" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;Now, let's look at the implementation. I've broken this down into the core components that make the "Sense-Think-Act" loop work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Stochastic Environment: The Math of Chaos
&lt;/h3&gt;

&lt;p&gt;I wrote this environment to be the "source of truth." It's a 2D grid where every cell has a state. The fire spreads using a stochastic model—meaning there's randomness, but it's "biased" by the wind. &lt;/p&gt;

&lt;p&gt;In my opinion, the most interesting part was the &lt;code&gt;get_wind_bias&lt;/code&gt; function. I wrote it this way because I wanted to simulate the vector-based nature of fire spread. If the wind is blowing North-East, the probability of the cell to the North-East igniting is significantly higher than the cell to the South-West. From my experience, small mathematical biases like this are what make a simulation feel "alive" rather than just a random walk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_wind_bias&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;from_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from_pos&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;
    &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;

    &lt;span class="n"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;W&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I observed that setting the &lt;code&gt;wind_strength&lt;/code&gt; to 0.5 creates a noticeable but not overwhelming "drift." I put it this way coz I wanted the agent to have to "guess" where the fire would jump next, but still give it a fighting chance if it followed the wind patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Agent State: The Shared Consciousness
&lt;/h3&gt;

&lt;p&gt;In my experience, the &lt;code&gt;AgentState&lt;/code&gt; is the most important part of any LangGraph project. It’s the "shared memory" between nodes. When I first started with agents, I used to pass everything as function arguments. I quickly realized that's a nightmare for debugging. &lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;TypedDict&lt;/code&gt; for the state allowed me to keep a clean separation of concerns. The &lt;code&gt;heat_map&lt;/code&gt; is the agent's "perception" of the world. The &lt;code&gt;active_plan&lt;/code&gt; is its "intention." And the &lt;code&gt;logs&lt;/code&gt; are its "rationale." I think this tripartite structure is a solid pattern for any autonomous system.&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;heat_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;active_plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;criticality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;replan_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Analyzer Agent: The Reactive Engine
&lt;/h3&gt;

&lt;p&gt;This node is responsible for "Online Decision Making." It looks at the current &lt;code&gt;heat_map&lt;/code&gt; and compares it to the &lt;code&gt;active_plan&lt;/code&gt;. I observed that the key to a good "Streaming Agent" is knowing &lt;strong&gt;what to ignore&lt;/strong&gt;. If the fire spreads into an area we've already designated as a "controlled zone," we don't need to replan. We only replan when there's a "Breach."&lt;/p&gt;

&lt;p&gt;I defined a "Breach" as fire occurring in a cell that isn't currently targeted by our assets. This is where the "Online" part of the replanning happens. The &lt;code&gt;analyzer_node&lt;/code&gt; is essentially a filter that prevents the complex &lt;code&gt;Strategist&lt;/code&gt; from running on every single step. In my opinion, this "Gated Activation" is essential for scaling these systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Strategist Agent: The Proactive Re-pivoting
&lt;/h3&gt;

&lt;p&gt;When the Analyzer sets &lt;code&gt;replan_required&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt;, the Strategist kicks in. As per my experience, this is the most compute-intensive part. In this PoC, I implemented a simple perimeter-search, but I put a lot of thought into how it &lt;em&gt;would&lt;/em&gt; look in a real system. &lt;/p&gt;

&lt;p&gt;Imagine using a Diffusion Model or a Graph Neural Network to predict fire spread and then using a Reinforcement Learning agent to optimize the resource allocation. For this experiment, I stuck to a more deterministic approach, but I wrote the interface to be flexible. I think that's the beauty of LangGraph—you can swap out a simple Python function for a heavy-weight ML model without changing the graph structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Orchestrating the Chaos
&lt;/h3&gt;

&lt;p&gt;Finally, I tied it all together using LangGraph's &lt;code&gt;StateGraph&lt;/code&gt;. I put it this way because I think of the graph as a "Playbook." Each step of the simulation is one execution of the playbook. &lt;/p&gt;

&lt;p&gt;I wrote the loop in &lt;code&gt;main.py&lt;/code&gt; to be the "Clock" of the system. It pulses every half-second, updating the environment and then asking the agent: "Given this new state, what do we do next?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deep Dive: Why Online Replanning Matters
&lt;/h2&gt;

&lt;p&gt;In my opinion, the "Real World" is a series of streaming events, not a single static request. Most AI tutorials focus on "Prompt -&amp;gt; Response." But as per my experience, the real meat of the problem is "Stream -&amp;gt; Continuous Adaptation." &lt;/p&gt;

&lt;p&gt;While running these experiments, I observed something fascinating. If I turned off the "Replanning" logic and just let the agent follow its initial plan, the fire would almost always bypass the containment lines within 10 steps. By contrast, with "Online Replanning" enabled, the agent was able to dynamically shift assets to the edges of the spread, effectively "bottling up" the disaster. &lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;I wrote this code, then I thought: "Wait, how do I visualize this so it people can actually &lt;em&gt;see&lt;/em&gt; the logic?" This led me down a rabbit hole of GIF optimization. &lt;/p&gt;

&lt;p&gt;I learned the hard way that Dev.to and LinkedIn have very specific requirements for GIFs. Standard RGB GIFs often flicker or fail to upload. I had to implement a &lt;strong&gt;Global Palette&lt;/strong&gt; strategy using PIL. By generating a single 256-color palette from key frames and converting all frames to P-Mode (with no dithering), I was able to get a crystal-clear, 100fps terminal animation that looks as premium as the code it represents.&lt;/p&gt;

&lt;p&gt;Another lesson: &lt;strong&gt;State Bloom&lt;/strong&gt;. I observed that if you aren't careful, your logs will grow exponentially. I had to implement a logic to "condense" logs if they exceeded a certain length. In my opinion, "State Pruning" is as important as "State Management."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethics and Future Roadmap: The "Human-in-the-Loop"
&lt;/h2&gt;

&lt;p&gt;As per me, we are entering an era of "Autonomous Infrastructure." But who watches the watchmen? In my view, an autonomous wildfire coordinator should never be 100% autonomous. I think the next iteration of this project should include a "Human Approval Node." &lt;/p&gt;

&lt;p&gt;I’d like to see a version of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt; where the agent proposes a "Strategy Shift" and a human supervisor has 30 seconds to click "Approve" before the tankers are re-routed. I think this "Collaborative Agency" is the sweet spot for high-stakes AI.&lt;/p&gt;

&lt;p&gt;My future roadmap for this experiment includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Multi-UAV Coordination&lt;/strong&gt;: Simulating multiple assets with independent batteries and refuel cycles.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Topographical Integration&lt;/strong&gt;: Using real-world GIS data to affect fire spread (e.g., fire spreads faster uphill).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;LLM-based Post-Mortem&lt;/strong&gt;: An agent that analyzes the logs after the simulation to write a "What went wrong?" report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;If you want to play with this experiment yourself, I've pushed the complete code to GitHub. I wrote the README to be very detailed, so you should be able to get it running in under 2 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step by step details can be found at:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/aniket-work/WildfireGuard-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/WildfireGuard-AI&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When you run the project, the terminal output is designed to show the "streaming" nature of the decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put it this way because I wanted the user to feel the "pulse" of the system. You’ll see the "Analyzer" detecting shifts in real-time and the "Strategist" frantically recalculating. It’s a chaotic, beautiful dance of logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This experiment was a huge learning curve for me. From my experience, the hardest part of "Agentic AI" isn't the model—it's the &lt;strong&gt;State Management&lt;/strong&gt;. How do you ensure the agent remembers the plan from Step 5 when it's now at Step 15? &lt;/p&gt;

&lt;p&gt;Through this PoC, I realized that "Streaming Decisions" are the future of industrial AI. Whether it's managing a power grid, a factory floor, or a wildfire, we need systems that can "Think while they Act." I hope this walkthrough gives you some ideas for your own autonomous projects. I put a lot of heart into this implementation, and I think it shows what's possible when we stop thinking of AI as a chatbot and start thinking of it as an operator.&lt;/p&gt;

&lt;p&gt;Stay curious, stay experimental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disclaimer
&lt;/h3&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented. &lt;/p&gt;

&lt;p&gt;This article is an experimental PoC write-up. It is not production guidance. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Footnote&lt;/strong&gt;: This article was written as part of my experiments with Agentic AI. The code repository is static and intended for learning purposes only. I put it this way coz I want to emphasize that while the math is real, the application is educational.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langgraph</category>
      <category>automation</category>
    </item>
    <item>
      <title>Autonomous Wildfire Response Coordinator: Orchestrating Chaos with Streaming Decision Agents</title>
      <dc:creator>Aniket Hingane</dc:creator>
      <pubDate>Mon, 23 Mar 2026 00:40:03 +0000</pubDate>
      <link>https://forem.com/exploredataaiml/autonomous-wildfire-response-coordinator-orchestrating-chaos-with-streaming-decision-agents-404o</link>
      <guid>https://forem.com/exploredataaiml/autonomous-wildfire-response-coordinator-orchestrating-chaos-with-streaming-decision-agents-404o</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fovu09rc1053uzt9e9yyk.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fovu09rc1053uzt9e9yyk.gif" alt="Title Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Autonomous Wildfire Response Coordinator: How I Built a Self-Healing Emergency Response System Using LangGraph and Online Replanning&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I've spent the last few weeks experimenting with how AI agents handle hyper-dynamic environments. The result is &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;—a proof-of-concept autonomous coordinator that doesn't just "plan," but "replans" in real-time as a simulated fire spreads. Using LangGraph's state machine architecture, I built a system that ingest continuous sensor streams, detects containment breaches, and dynamically re-routes assets like aerial tankers and ground crews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ve always been fascinated by high-latency, high-stakes environments. There’s something visceral about a situation where a plan made five minutes ago is already obsolete. Recently, I was observing how traditional dispatcher systems struggle with wildfires—situations where wind shifts or fuel changes can turn a controlled burn into a catastrophe in seconds.&lt;/p&gt;

&lt;p&gt;In my opinion, the bottleneck isn't the data; we have satellites, IoT sensors, and drones. The bottleneck is the &lt;strong&gt;latency of the decision-making loop&lt;/strong&gt;. I wanted to see if I could build a coordinator that acts as a "living" strategy. This isn't a production-grade safety tool—far from it. It's one of my personal experiments, a PoC to explore "Streaming Decision Agents." I wrote this implementation to test a specific hypothesis: that agentic workflows can provide the "self-healing" logic needed for emergency response.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's This Article About?
&lt;/h2&gt;

&lt;p&gt;This article is a deep dive into the architecture of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt;. I’ll walk you through how I designed a stochastic wildfire environment (stochastic because chaos is the point), and how I used &lt;strong&gt;LangGraph&lt;/strong&gt; to build a multi-agent system that processes data as a stream. &lt;/p&gt;

&lt;p&gt;We’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dynamic Simulation&lt;/strong&gt;: Building a 2D grid-world where fire spreads based on wind and fuel.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Sense-Think-Act" Stream&lt;/strong&gt;: How agents receive updates and decide when to trigger a "Strategic Reset."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Online Replanning&lt;/strong&gt;: The logic behind discarding a plan mid-execution and shifting resources.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tech Stack Nuances&lt;/strong&gt;: Why I chose specific tools for state management and visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;From my experience, if you're building something that needs to maintain state across complex branching paths, you need a robust framework. Here’s what I used for this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;LangGraph&lt;/strong&gt;: This was my choice for the core agentic workflow. I think its ability to represent cycles and maintain persistent state is unmatched for "replanning" scenarios.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pydantic&lt;/strong&gt;: I used this for structured event definitions. In my experience, strict type safety for agent communication prevents a lot of "hallucination-style" logic errors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python 3.10&lt;/strong&gt;: The backbone of the project.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;PIL (Pillow)&lt;/strong&gt;: For generating my technical visual assets (including that optimized GIF you see at the top).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WildfireWorld (Custom)&lt;/strong&gt;: A Python-based simulation engine I wrote to provide the "input stream."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Read It?
&lt;/h2&gt;

&lt;p&gt;If you’re interested in &lt;strong&gt;Agentic AI&lt;/strong&gt;, &lt;strong&gt;Autonomous Systems&lt;/strong&gt;, or simply how to build resilient software in chaotic domains, there’s something here for you. As per my experience, the next wave of AI isn't just about "chatting"; it's about "operating." This article shows you one way to bridge that gap—by treating AI as a coordinator that manages a real-time feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Design
&lt;/h2&gt;

&lt;p&gt;I put it this way because I think visualization is 50% of the engineering process. Before I wrote a single line of code, I mapped out how I wanted the decision loop to look. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Architecture
&lt;/h3&gt;

&lt;p&gt;In my opinion, a streaming agent needs to be decoupled from the raw data. The "Environment" (the fire) shouldn't care about the "Agent" (the coordinator). &lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fngnmajfu9rpr4k0yptpn.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fngnmajfu9rpr4k0yptpn.png" alt="Architecture" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I designed the graph with four primary nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Sensor Ingest&lt;/strong&gt;: The entry point. It receives "packets" of thermal data.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Threat Analyzer&lt;/strong&gt;: This is the "brain stem." It doesn't plan; it just screams "FIRE!" when something breaks the perimeter.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Strategy Optimizer&lt;/strong&gt;: The "prefrontal cortex." It looks at the mess and decides on a new containment boundary.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dispatcher&lt;/strong&gt;: The "hands." It executes the tactical movements of tankers and ground crews.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Decision Flow
&lt;/h3&gt;

&lt;p&gt;I thought about how a human dispatcher works. They don't replan every time a leaf burns. They replan when the fire "jumps" a line. I implemented this via a "Criticality Score" in the state.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Ft7ty6dgukyvrin7sxt19.png" 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.amazonaws.com%2Fuploads%2Farticles%2Ft7ty6dgukyvrin7sxt19.png" alt="Flow" width="336" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Get Cooking
&lt;/h2&gt;

&lt;p&gt;Now, let's look at the implementation. I've broken this down into the core components that make the "Sense-Think-Act" loop work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Stochastic Environment: The Math of Chaos
&lt;/h3&gt;

&lt;p&gt;I wrote this environment to be the "source of truth." It's a 2D grid where every cell has a state. The fire spreads using a stochastic model—meaning there's randomness, but it's "biased" by the wind. &lt;/p&gt;

&lt;p&gt;In my opinion, the most interesting part was the &lt;code&gt;get_wind_bias&lt;/code&gt; function. I wrote it this way because I wanted to simulate the vector-based nature of fire spread. If the wind is blowing North-East, the probability of the cell to the North-East igniting is significantly higher than the cell to the South-West. From my experience, small mathematical biases like this are what make a simulation feel "alive" rather than just a random walk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_wind_bias&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;from_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from_pos&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_pos&lt;/span&gt;
    &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ty&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;

    &lt;span class="n"&gt;bias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;W&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wind_direction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;bias&lt;/span&gt; &lt;span class="o"&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;wind_strength&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I observed that setting the &lt;code&gt;wind_strength&lt;/code&gt; to 0.5 creates a noticeable but not overwhelming "drift." I put it this way coz I wanted the agent to have to "guess" where the fire would jump next, but still give it a fighting chance if it followed the wind patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Agent State: The Shared Consciousness
&lt;/h3&gt;

&lt;p&gt;In my experience, the &lt;code&gt;AgentState&lt;/code&gt; is the most important part of any LangGraph project. It’s the "shared memory" between nodes. When I first started with agents, I used to pass everything as function arguments. I quickly realized that's a nightmare for debugging. &lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;TypedDict&lt;/code&gt; for the state allowed me to keep a clean separation of concerns. The &lt;code&gt;heat_map&lt;/code&gt; is the agent's "perception" of the world. The &lt;code&gt;active_plan&lt;/code&gt; is its "intention." And the &lt;code&gt;logs&lt;/code&gt; are its "rationale." I think this tripartite structure is a solid pattern for any autonomous system.&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;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;heat_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;active_plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;criticality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;replan_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Analyzer Agent: The Reactive Engine
&lt;/h3&gt;

&lt;p&gt;This node is responsible for "Online Decision Making." It looks at the current &lt;code&gt;heat_map&lt;/code&gt; and compares it to the &lt;code&gt;active_plan&lt;/code&gt;. I observed that the key to a good "Streaming Agent" is knowing &lt;strong&gt;what to ignore&lt;/strong&gt;. If the fire spreads into an area we've already designated as a "controlled zone," we don't need to replan. We only replan when there's a "Breach."&lt;/p&gt;

&lt;p&gt;I defined a "Breach" as fire occurring in a cell that isn't currently targeted by our assets. This is where the "Online" part of the replanning happens. The &lt;code&gt;analyzer_node&lt;/code&gt; is essentially a filter that prevents the complex &lt;code&gt;Strategist&lt;/code&gt; from running on every single step. In my opinion, this "Gated Activation" is essential for scaling these systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Strategist Agent: The Proactive Re-pivoting
&lt;/h3&gt;

&lt;p&gt;When the Analyzer sets &lt;code&gt;replan_required&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt;, the Strategist kicks in. As per my experience, this is the most compute-intensive part. In this PoC, I implemented a simple perimeter-search, but I put a lot of thought into how it &lt;em&gt;would&lt;/em&gt; look in a real system. &lt;/p&gt;

&lt;p&gt;Imagine using a Diffusion Model or a Graph Neural Network to predict fire spread and then using a Reinforcement Learning agent to optimize the resource allocation. For this experiment, I stuck to a more deterministic approach, but I wrote the interface to be flexible. I think that's the beauty of LangGraph—you can swap out a simple Python function for a heavy-weight ML model without changing the graph structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Orchestrating the Chaos
&lt;/h3&gt;

&lt;p&gt;Finally, I tied it all together using LangGraph's &lt;code&gt;StateGraph&lt;/code&gt;. I put it this way because I think of the graph as a "Playbook." Each step of the simulation is one execution of the playbook. &lt;/p&gt;

&lt;p&gt;I wrote the loop in &lt;code&gt;main.py&lt;/code&gt; to be the "Clock" of the system. It pulses every half-second, updating the environment and then asking the agent: "Given this new state, what do we do next?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deep Dive: Why Online Replanning Matters
&lt;/h2&gt;

&lt;p&gt;In my opinion, the "Real World" is a series of streaming events, not a single static request. Most AI tutorials focus on "Prompt -&amp;gt; Response." But as per my experience, the real meat of the problem is "Stream -&amp;gt; Continuous Adaptation." &lt;/p&gt;

&lt;p&gt;While running these experiments, I observed something fascinating. If I turned off the "Replanning" logic and just let the agent follow its initial plan, the fire would almost always bypass the containment lines within 10 steps. By contrast, with "Online Replanning" enabled, the agent was able to dynamically shift assets to the edges of the spread, effectively "bottling up" the disaster. &lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;I wrote this code, then I thought: "Wait, how do I visualize this so it people can actually &lt;em&gt;see&lt;/em&gt; the logic?" This led me down a rabbit hole of GIF optimization. &lt;/p&gt;

&lt;p&gt;I learned the hard way that Dev.to and LinkedIn have very specific requirements for GIFs. Standard RGB GIFs often flicker or fail to upload. I had to implement a &lt;strong&gt;Global Palette&lt;/strong&gt; strategy using PIL. By generating a single 256-color palette from key frames and converting all frames to P-Mode (with no dithering), I was able to get a crystal-clear, 100fps terminal animation that looks as premium as the code it represents.&lt;/p&gt;

&lt;p&gt;Another lesson: &lt;strong&gt;State Bloom&lt;/strong&gt;. I observed that if you aren't careful, your logs will grow exponentially. I had to implement a logic to "condense" logs if they exceeded a certain length. In my opinion, "State Pruning" is as important as "State Management."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethics and Future Roadmap: The "Human-in-the-Loop"
&lt;/h2&gt;

&lt;p&gt;As per me, we are entering an era of "Autonomous Infrastructure." But who watches the watchmen? In my view, an autonomous wildfire coordinator should never be 100% autonomous. I think the next iteration of this project should include a "Human Approval Node." &lt;/p&gt;

&lt;p&gt;I’d like to see a version of &lt;strong&gt;WildfireGuard-AI&lt;/strong&gt; where the agent proposes a "Strategy Shift" and a human supervisor has 30 seconds to click "Approve" before the tankers are re-routed. I think this "Collaborative Agency" is the sweet spot for high-stakes AI.&lt;/p&gt;

&lt;p&gt;My future roadmap for this experiment includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Multi-UAV Coordination&lt;/strong&gt;: Simulating multiple assets with independent batteries and refuel cycles.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Topographical Integration&lt;/strong&gt;: Using real-world GIS data to affect fire spread (e.g., fire spreads faster uphill).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;LLM-based Post-Mortem&lt;/strong&gt;: An agent that analyzes the logs after the simulation to write a "What went wrong?" report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Setup
&lt;/h2&gt;

&lt;p&gt;If you want to play with this experiment yourself, I've pushed the complete code to GitHub. I wrote the README to be very detailed, so you should be able to get it running in under 2 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step by step details can be found at:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/aniket-work/WildfireGuard-AI" rel="noopener noreferrer"&gt;https://github.com/aniket-work/WildfireGuard-AI&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's Run
&lt;/h2&gt;

&lt;p&gt;When you run the project, the terminal output is designed to show the "streaming" nature of the decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put it this way because I wanted the user to feel the "pulse" of the system. You’ll see the "Analyzer" detecting shifts in real-time and the "Strategist" frantically recalculating. It’s a chaotic, beautiful dance of logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This experiment was a huge learning curve for me. From my experience, the hardest part of "Agentic AI" isn't the model—it's the &lt;strong&gt;State Management&lt;/strong&gt;. How do you ensure the agent remembers the plan from Step 5 when it's now at Step 15? &lt;/p&gt;

&lt;p&gt;Through this PoC, I realized that "Streaming Decisions" are the future of industrial AI. Whether it's managing a power grid, a factory floor, or a wildfire, we need systems that can "Think while they Act." I hope this walkthrough gives you some ideas for your own autonomous projects. I put a lot of heart into this implementation, and I think it shows what's possible when we stop thinking of AI as a chatbot and start thinking of it as an operator.&lt;/p&gt;

&lt;p&gt;Stay curious, stay experimental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disclaimer
&lt;/h3&gt;

&lt;p&gt;The views and opinions expressed here are solely my own and do not represent the views, positions, or opinions of my employer or any organization I am affiliated with. The content is based on my personal experience and experimentation and may be incomplete or incorrect. Any errors or misinterpretations are unintentional, and I apologize in advance if any statements are misunderstood or misrepresented. &lt;/p&gt;

&lt;p&gt;This article is an experimental PoC write-up. It is not production guidance. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Footnote&lt;/strong&gt;: This article was written as part of my experiments with Agentic AI. The code repository is static and intended for learning purposes only. I put it this way coz I want to emphasize that while the math is real, the application is educational.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langgraph</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
