<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: firefrog</title>
    <description>The latest articles on DEV Community by firefrog (@minh-leduc).</description>
    <link>https://dev.to/minh-leduc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1249715%2F8f51264e-2307-4580-ac63-683a5dacc10c.png</url>
      <title>DEV Community: firefrog</title>
      <link>https://dev.to/minh-leduc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/minh-leduc"/>
    <language>en</language>
    <item>
      <title>A Simple Look at TokPress: A Compressor That Uses an LLM's Tokenizer</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:30:01 +0000</pubDate>
      <link>https://dev.to/minh-leduc/a-simple-look-at-tokpress-a-compressor-that-uses-an-llms-tokenizer-3pjh</link>
      <guid>https://dev.to/minh-leduc/a-simple-look-at-tokpress-a-compressor-that-uses-an-llms-tokenizer-3pjh</guid>
      <description>&lt;p&gt;If you've ever compressed a bunch of small JSON log lines, you've probably noticed something annoying: gzip often makes them &lt;em&gt;bigger&lt;/em&gt;, not smaller. A 200-byte log line has so little content that the compressor spends more bytes on its own bookkeeping than it saves.&lt;/p&gt;

&lt;p&gt;TokPress is a tiny weekend project that tries a different trick: instead of compressing raw bytes, it first runs the text through the same tokenizer that OpenAI's models use (&lt;code&gt;o200k_base&lt;/code&gt;), then compresses the &lt;em&gt;tokens&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's the whole idea. An LLM tokenizer has already learned which pieces of text are common — words, punctuation, code patterns. So the compressor gets a much better alphabet to work with for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Three simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tokenize&lt;/strong&gt; — turn the input into token ids using &lt;code&gt;o200k_base&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compress the tokens&lt;/strong&gt; — run a simple LZ77 pass over the token ids.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Entropy-code&lt;/strong&gt; — finish with rANS, an efficient entropy coder.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a &lt;code&gt;.tokz&lt;/code&gt; file that decompresses back to the exact original bytes — even binary data, since the tokenizer works on bytes, not just text.&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;tokpress&lt;/span&gt;

&lt;span class="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# byte-exact
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The trick that makes it useful: a shared dictionary
&lt;/h2&gt;

&lt;p&gt;The real win comes when you have &lt;em&gt;many records that look the same&lt;/em&gt; — log lines, API responses, telemetry. TokPress can train a small dictionary on a sample of your records, then every future record compresses against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokpress train-dict mydict.tokdict samples.jsonl
tokpress compress new_record.json &lt;span class="nt"&gt;--dict&lt;/span&gt; mydict.tokdict &lt;span class="nt"&gt;-o&lt;/span&gt; new_record.tokz

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a trained dictionary, structured-log records went from a ratio of &lt;strong&gt;0.800 to 0.2565&lt;/strong&gt; — about 3× smaller. And that's on records the dictionary had never seen.&lt;/p&gt;

&lt;p&gt;There's an even simpler mode that needs no training at all: compress many records together as one stream, and the model learns as it goes:&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;packed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 150 schema-similar JSON records, per-record compression summed to ratio &lt;strong&gt;1.19&lt;/strong&gt; — the data literally grew. As one stream, it hit &lt;strong&gt;0.0875&lt;/strong&gt;. Same bytes, one header instead of 150.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands (honestly)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On prose&lt;/strong&gt; it beats gzip and ties/beats zstd on some files (a 152KB prose file: 0.298 vs zstd's 0.324).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It still loses to zstd's trained dictionary&lt;/strong&gt; by about 1.3×. zstd has had years of polish on dictionary training; this is a weekend project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's pure Python&lt;/strong&gt;, so it's slow to compress. Decompression is fast (thousands of records/sec); compression is not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tokenization isn't magic.&lt;/strong&gt; The tokenizer just reshapes the data; the real savings come from the entropy coding and the trained dictionary.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/LakoreAI/tokpress
&lt;span class="nb"&gt;cd &lt;/span&gt;tokpress &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import tokpress; print(len(tokpress.compress(b'{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 1}')))"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's ~2900 lines, has 93 tests, and comes with an honest benchmark script. If you have a folder of repetitive logs or JSON, that's exactly the case it was built for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/a-simple-look-at-tokpress-a-compressor-that-uses-an-llm-s-tokenizer-u4pda?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Agent harness or agent framework?</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:30:02 +0000</pubDate>
      <link>https://dev.to/minh-leduc/agent-harness-or-agent-framework-5ae8</link>
      <guid>https://dev.to/minh-leduc/agent-harness-or-agent-framework-5ae8</guid>
      <description>&lt;p&gt;In July I wrote &lt;a href="https://medium.com/@minhle_0210/prompt-context-harness-loop-an-agents-anatomy-642db41429fb" rel="noopener noreferrer"&gt;Prompt, Context, Harness, Loop&lt;/a&gt;, which split an agent into four parts and argued that the harness — the thing that owns the loop and decides what the model sees — was the part nobody talked about.&lt;/p&gt;

&lt;p&gt;I checked this week. Two of the largest projects in the space now use the word in their own one-line description. LangChain's &lt;code&gt;deepagents&lt;/code&gt; calls itself &lt;em&gt;"the batteries-included agent harness."&lt;/em&gt; Y Combinator's &lt;code&gt;qm&lt;/code&gt; calls itself &lt;em&gt;"multiplayer agent harness for work."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the word arrived. What hasn't arrived is the distinction, and every comparison article I can find still files harnesses and frameworks under one heading called "AI agent tools." Those are different products that fail in different ways, and picking the wrong one costs you a rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent framework is a library you build an agent with — you own the loop. An agent harness is a runtime that owns the loop and calls your model. Frameworks give you control and hand you the hard problems; harnesses solve the hard problems and take the control. Most teams reach for a framework when they wanted a harness.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually separates a harness from a framework?
&lt;/h2&gt;

&lt;p&gt;Take the four parts from the original post and ask, for each one, &lt;strong&gt;who owns it — you or the tool?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Framework (LangChain, CrewAI, Mastra)&lt;/th&gt;
&lt;th&gt;Harness (opencode, Codex, goose, qm)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt&lt;/td&gt;
&lt;td&gt;Yours entirely&lt;/td&gt;
&lt;td&gt;Mostly the tool's; you get a config file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;Yours to assemble&lt;/td&gt;
&lt;td&gt;The tool's, with a compaction strategy baked in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;You write it&lt;/td&gt;
&lt;td&gt;This is the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop&lt;/td&gt;
&lt;td&gt;You call it&lt;/td&gt;
&lt;td&gt;The tool runs it and hands you a result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The row that decides everything is &lt;strong&gt;Loop&lt;/strong&gt;. In a framework you call the library from inside your own loop. In a harness, the harness is the program and your code is a plugin.&lt;/p&gt;

&lt;p&gt;That inversion is why "we'll start with a framework and swap it later" so often fails. You're not swapping a dependency; you're inverting control flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landscape, with real numbers
&lt;/h2&gt;

&lt;p&gt;Pulled from the GitHub API on 2026-08-12. Every one of these was pushed to the same day, so activity isn't a differentiator — they're all alive.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Self-described as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;opencode&lt;/td&gt;
&lt;td&gt;196,469&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;"The open source coding agent"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangChain&lt;/td&gt;
&lt;td&gt;144,078&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;"The agent engineering platform"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;105,518&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;"Lightweight coding agent that runs in your terminal"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI&lt;/td&gt;
&lt;td&gt;56,986&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;"Framework for orchestrating role-playing, autonomous AI agents"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;goose&lt;/td&gt;
&lt;td&gt;52,718&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;"Open source, extensible AI agent"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepagents&lt;/td&gt;
&lt;td&gt;27,688&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;"The batteries-included agent harness"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastra&lt;/td&gt;
&lt;td&gt;27,135&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;"The modern TypeScript framework for AI-powered applications"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qm&lt;/td&gt;
&lt;td&gt;13,206&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;"Multiplayer agent harness for work"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the right-hand column as a taxonomy and it sorts itself. &lt;strong&gt;"Framework" and "platform" mean you own the loop. "Agent" and "harness" mean they do.&lt;/strong&gt; The projects are telling you which one they are; the comparison articles just aren't reading it.&lt;/p&gt;

&lt;p&gt;Note also that three of the eight are written in Rust or shipped as terminal binaries. That's not a language preference, it's a symptom: if your product is the loop, you are shipping a program, and programs care about startup time and single-binary distribution in a way libraries never do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one do you actually want?
&lt;/h2&gt;

&lt;p&gt;The honest test is a single question, and it isn't about features.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does an agent run inside your application, or does your application run inside an agent?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your product is a web service that occasionally needs to reason, you want a framework. The agent is a subroutine. You own the request lifecycle, your observability already exists, and you cannot hand control of the process to something else.&lt;/p&gt;

&lt;p&gt;If your product &lt;em&gt;is&lt;/em&gt; the agent — a coding assistant, a research tool, an autonomous worker — you want a harness. The loop, context management, tool dispatch and compaction are the hard parts, and they are also solved parts. Rebuilding them on top of a framework is the most common expensive mistake in this space, and it looks like progress for about six weeks.&lt;/p&gt;

&lt;p&gt;The trap is that frameworks demo better. A framework demo is fifteen lines and a tidy diagram. A harness demo is a terminal, which looks like less work but is the finished product.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does an agent harness actually solve for you?
&lt;/h2&gt;

&lt;p&gt;Having built a small one, these are the parts that ate the time — and they're all invisible in a demo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context compaction.&lt;/strong&gt; Not the summarisation call itself; deciding &lt;em&gt;when&lt;/em&gt;, choosing what survives, and keeping the result stable enough not to destroy your prompt cache. I wrote about what a compaction does to your cache bill — it resets it entirely, which is correct behaviour and still expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool result truncation.&lt;/strong&gt; A tool returns 400KB of JSON. You cannot put that in the context and you cannot drop it. Every harness has a strategy here and they differ a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interruption and resumption.&lt;/strong&gt; The user hits Ctrl-C mid-tool-call. What's the state? Frameworks mostly don't answer this because they assume you own the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission boundaries.&lt;/strong&gt; Which tools can run without asking. &lt;code&gt;qm&lt;/code&gt;'s pitch is isolated workspaces per person, which is this problem at team scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery from malformed output.&lt;/strong&gt; The model emits a tool call with a missing required field. Retry, repair, or surface it? This one decides whether small models are usable at all — which is the subject of the benchmark I'm running next.&lt;/p&gt;

&lt;p&gt;Each is a week of work and none of them is interesting. That's precisely the argument for not writing them yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does the distinction break down?
&lt;/h2&gt;

&lt;p&gt;Two honest complications, because a taxonomy that admits no exceptions is usually wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deepagents&lt;/code&gt; &lt;strong&gt;is a harness shipped as a library.&lt;/strong&gt; It calls itself a harness, and it is one, but it's a Python package you import rather than a binary you run. So it inverts control inside your process. That's a genuine third position and probably where the category is heading — harness semantics, library packaging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangChain calls itself a platform now, not a framework.&lt;/strong&gt; With 144,078 stars and a decade of scope creep it contains both: primitives you build with, and higher-level runners that own the loop. The taxonomy applies to &lt;em&gt;which part you use&lt;/em&gt;, not to the repo.&lt;/p&gt;

&lt;p&gt;So the test isn't "which project is this?" It's "for the piece I'm about to depend on, who runs the loop?" Ask it per component and the answer stays useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with a harness and escape downward if you must.&lt;/strong&gt; The reverse — start with a framework, grow into a harness — means rebuilding the five things above while shipping features, and everyone who does it says the same thing afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the one-line description literally.&lt;/strong&gt; These projects are precise about what they are. "Platform," "framework," "harness," "agent" are load-bearing words chosen by people who know the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check who owns compaction before you commit.&lt;/strong&gt; It's the single highest-leverage behaviour and the hardest to replace. If a tool won't tell you its compaction strategy, that's your answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignore star counts for this decision.&lt;/strong&gt; opencode has 14× qm's stars and they're not solving the same problem. Popularity is a proxy for maturity, not fit — and every project here is actively maintained anyway.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The distinction is control flow, not features.&lt;/strong&gt; In a framework you call the loop; in a harness the loop calls you. Swapping between them isn't a dependency change, it's an inversion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The projects self-identify accurately.&lt;/strong&gt; "Framework" and "platform" mean you own the loop (LangChain, CrewAI, Mastra); "agent" and "harness" mean they do (opencode, Codex, goose, qm, deepagents).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The test is one question:&lt;/strong&gt; does an agent run inside your application, or your application inside an agent?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Harnesses solve five boring, expensive problems&lt;/strong&gt; — compaction, tool-result truncation, interruption, permissions, malformed-output recovery. Each is a week you won't enjoy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;deepagents&lt;/code&gt; &lt;strong&gt;is the interesting edge case&lt;/strong&gt;: harness semantics in library packaging, which may be where the whole category lands.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;GitHub&lt;/span&gt; &lt;span class="n"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;08&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Star&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;languages&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;All&lt;/span&gt; &lt;span class="n"&gt;eight&lt;/span&gt; &lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="n"&gt;had&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="n"&gt;pushed&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Star counts move; the taxonomy doesn't. If you're reading this much later, the numbers are stale and the four-part test still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/@minhle_0210/prompt-context-harness-loop-an-agents-anatomy-642db41429fb" rel="noopener noreferrer"&gt;Prompt, Context, Harness, Loop: An Agent's Anatomy&lt;/a&gt; — the four-part split this post applies. Worth reading first if the terms above felt slippery.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;yc-software/qm&lt;/code&gt; — the clearest example of harness-as-product, and the one that takes multi-user permission boundaries seriously.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;langchain-ai/deepagents&lt;/code&gt; — a harness distributed as a library, from the project best known for the framework half of this distinction.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next: I'm benchmarking whether small models can survive the malformed-output problem above — the one that decides if local agents are viable at all.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 Follow me: &lt;a href="https://www.linkedin.com/in/minhle007/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt; | &lt;a href="https://github.com/MinLee0210" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/agent-harness-or-agent-framework-u2ye1" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Five ways to invalidate your prompt cache</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Wed, 02 Sep 2026 12:30:03 +0000</pubDate>
      <link>https://dev.to/minh-leduc/five-ways-to-invalidate-your-prompt-cache-1g03</link>
      <guid>https://dev.to/minh-leduc/five-ways-to-invalidate-your-prompt-cache-1g03</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of 2 on prompt caching.&lt;/em&gt; &lt;a href="https://dev.towhy-your-coding-agent-s-bill-grows-faster-than-the-chat"&gt;&lt;em&gt;Part 1&lt;/em&gt;&lt;/a&gt; &lt;em&gt;covered the economics.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.towhy-your-coding-agent-s-bill-grows-faster-than-the-chat"&gt;Part 1&lt;/a&gt; established the prize: caching cut an 80-turn agent session from $54.08 to $6.91 in my cost model, an 87.2% saving on input tokens.&lt;/p&gt;

&lt;p&gt;Once I had that number I got curious about the opposite question — not how much caching saves, but how easy it is to think you have it and not. That turned out to be the more useful half, because every way of losing it is silent.&lt;/p&gt;

&lt;p&gt;This post is about how to lose it. Not by disabling caching — by writing a harness that looks completely correct and quietly never hits the cache. Every failure below is silent. No error, no warning, no degraded output. The agent works exactly as intended and costs several times more than it should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching is a prefix match, not a similarity match. Any change to the earliest part of a request invalidates every cached token after it. In my cost model, putting a timestamp in the system prompt cost 7.8× on an 80-turn session — not because the timestamp is expensive, but because it forfeits the entire discount on every turn.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What exactly does the cache match on?
&lt;/h2&gt;

&lt;p&gt;Providers cache the &lt;strong&gt;longest common prefix&lt;/strong&gt; between your request and something they recently processed.&lt;/p&gt;

&lt;p&gt;Prefix. Byte-order, from the front. Not "a similar request," not "the same information in a different order." The comparison walks forward from token zero and stops at the first difference — and everything from that point on is fresh, at full price.&lt;/p&gt;

&lt;p&gt;Which yields one design principle for the entire harness:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Stable content first. Volatile content last. Never change what came before.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every mistake below is a violation of that one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A dynamic system prompt
&lt;/h2&gt;

&lt;p&gt;The most expensive one-line mistake available, and it looks like good engineering.&lt;/p&gt;

&lt;p&gt;Your system prompt sits at position zero. It's the longest-lived thing in the request. So you put useful context in it:&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;system&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;You are a coding assistant.
Current time: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Working directory: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcwd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Available tools: &lt;/span&gt;&lt;span class="si"&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="nf"&gt;discover_tools&lt;/span&gt;&lt;span class="p"&gt;())&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those three lines is a cache bomb.&lt;/p&gt;

&lt;p&gt;The timestamp changes on every call. So the system prompt changes on every call. So the prefix diverges at token ~15 of a 10,000-token system prompt — and every token after it, including your entire conversation history, is billed fresh. Forever.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Turns&lt;/th&gt;
&lt;th&gt;Stable prompt&lt;/th&gt;
&lt;th&gt;Dynamic prompt&lt;/th&gt;
&lt;th&gt;Penalty&lt;/th&gt;
&lt;th&gt;Multiple&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;$0.33&lt;/td&gt;
&lt;td&gt;$1.16&lt;/td&gt;
&lt;td&gt;$0.83&lt;/td&gt;
&lt;td&gt;3.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;$0.79&lt;/td&gt;
&lt;td&gt;$3.92&lt;/td&gt;
&lt;td&gt;$3.13&lt;/td&gt;
&lt;td&gt;4.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;$2.19&lt;/td&gt;
&lt;td&gt;$14.24&lt;/td&gt;
&lt;td&gt;$12.05&lt;/td&gt;
&lt;td&gt;6.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;$6.91&lt;/td&gt;
&lt;td&gt;$54.08&lt;/td&gt;
&lt;td&gt;$47.17&lt;/td&gt;
&lt;td&gt;7.8×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A timestamp doesn't cost you a timestamp. It costs you the whole discount, on every turn, for the rest of the session — and the penalty &lt;em&gt;grows&lt;/em&gt; with session length, because you're back on the quadratic curve from part 1.&lt;/p&gt;

&lt;p&gt;The fix is not to drop the information. It's to &lt;strong&gt;move it&lt;/strong&gt;:&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;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STATIC_PROMPT&lt;/span&gt;                     &lt;span class="c1"&gt;# never changes, caches beautifully
&lt;/span&gt;&lt;span class="n"&gt;messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;# append-only
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;context&amp;gt;time: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, cwd: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/context&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&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="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same information, delivered at the end of the request where volatility is free. The model reads it just as well.&lt;/p&gt;

&lt;p&gt;Watch for the sneaky variants: a tool list assembled by iterating a &lt;code&gt;set&lt;/code&gt; (non-deterministic order in some paths), a JSON dump with unsorted keys, a "user preferences" block refreshed from a database each call, anything with a request ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tool-definition trap
&lt;/h3&gt;

&lt;p&gt;Worth its own note, because tool schemas sit immediately after the system prompt — near the very front of the prefix, where instability is most expensive — and they're assembled programmatically, which is exactly where non-determinism creeps in.&lt;/p&gt;

&lt;p&gt;Three ways I've seen it happen:&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;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;schema&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;registry&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="c1"&gt;# dict order: usually fine
&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;schema&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;discovered_plugins&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="c1"&gt;# filesystem order: not fine
&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;schema&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enabled_tools&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;            &lt;span class="c1"&gt;# a set: order not guaranteed
&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                               &lt;span class="c1"&gt;# key order follows insertion
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure mode is nasty because it's &lt;em&gt;intermittent&lt;/em&gt;. A set iterates in a consistent order within one process, so your local test passes. Restart the process, or run on a machine with a different hash seed, and the order shifts — so the cache works fine in development and misses in production, or works for an hour and then stops after a redeploy.&lt;/p&gt;

&lt;p&gt;Two defences, both cheap. Sort your tool list by name before serialising, and pass &lt;code&gt;sort_keys=True&lt;/code&gt; when you dump any JSON that lands in the prompt. Then assert on it: hash the serialised system-prompt-plus-tools block at startup and log it. If that hash changes between two runs that should be identical, you have found your cache leak before it found your invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. History that isn't append-only
&lt;/h2&gt;

&lt;p&gt;The second rule from that design principle, and the one that bites at exactly the wrong moment.&lt;/p&gt;

&lt;p&gt;Your conversation history must only ever grow at the end. Editing, reordering, or removing an earlier message shifts the divergence point back to wherever you touched — invalidating everything after it.&lt;/p&gt;

&lt;p&gt;Things that quietly do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trimming old messages&lt;/strong&gt; to stay under the context limit. Dropping the oldest turn changes the prefix at position one and invalidates the entire session cache. You saved context and torched the discount.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-rendering tool results&lt;/strong&gt; with fresh formatting, a new timestamp, or a re-serialised payload.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sorting or deduplicating&lt;/strong&gt; history.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Injecting a reminder&lt;/strong&gt; into the middle of the transcript rather than appending it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trimming case deserves attention because it's a genuine conflict: you may &lt;em&gt;have&lt;/em&gt; to drop messages. Just know that a sliding window over history means you re-pay for the whole transcript every time the window slides. If you must shed context, do it rarely and in large chunks rather than one message per turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Compaction
&lt;/h2&gt;

&lt;p&gt;Compaction — summarising the transcript into something short and continuing — invalidates the cache completely, by design. You've replaced the prefix with a different, shorter prefix. Nothing after position zero matches.&lt;/p&gt;

&lt;p&gt;This one is &lt;strong&gt;not a bug&lt;/strong&gt;. Compaction is doing exactly what it's supposed to, and the cache reset is the correct price for it.&lt;/p&gt;

&lt;p&gt;But it should be a deliberate decision rather than a surprise, because the moment you compact you pay a full cache write on the new prefix and start the accumulation curve over. Two practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't compact on a timer.&lt;/strong&gt; Compact when the context genuinely needs it. Every compaction is a fresh write of everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compact in bigger steps, less often.&lt;/strong&gt; Two compactions cost two cold starts. Same context saved, twice the rewrite.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Letting the cache go cold (how long is a TTL?)
&lt;/h2&gt;

&lt;p&gt;Caches expire. The TTL varies by provider and by tier — five minutes is a common default, an hour is often available at a higher write cost, and routed or brokered inference may inherit whatever the backend it landed on happens to offer.&lt;/p&gt;

&lt;p&gt;That means &lt;strong&gt;wall-clock time is now a cost variable in your agent&lt;/strong&gt;, which is an unusual thing to have to think about. Step away for coffee and come back, and your next message pays a full rewrite of the entire transcript.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario, 80 turns&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Extra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Uninterrupted&lt;/td&gt;
&lt;td&gt;$6.91&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One break at turn 40&lt;/td&gt;
&lt;td&gt;$7.66&lt;/td&gt;
&lt;td&gt;$0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breaks at 20, 40, 60&lt;/td&gt;
&lt;td&gt;$9.16&lt;/td&gt;
&lt;td&gt;$2.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A break every 10 turns&lt;/td&gt;
&lt;td&gt;$12.16&lt;/td&gt;
&lt;td&gt;$5.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Late breaks cost more than early ones, because a cold start rewrites &lt;em&gt;everything so far&lt;/em&gt; — and "everything so far" is bigger later. A break at turn 70 is far more expensive than the same break at turn 10.&lt;/p&gt;

&lt;p&gt;This is also the argument for the longer TTL, but only conditionally — as part 1 showed, on an uninterrupted session the 1-hour option is a straight 2× markup on writes for no benefit. It pays at roughly three cold starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Assuming your provider does it for you
&lt;/h2&gt;

&lt;p&gt;The silent one, and the reason to check rather than assume.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI&lt;/strong&gt; caches eligible prefixes automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anthropic&lt;/strong&gt; and &lt;strong&gt;Gemini&lt;/strong&gt; require explicit cache markers in the request. Omit them and nothing is cached, forever, with no indication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conversation-state APIs&lt;/strong&gt; typically handle it; raw chat-completions calls typically don't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Routers and inference brokers&lt;/strong&gt; may send consecutive requests to different backends. A warm cache lives on the machine that built it — if you're not pinned, you can miss a cache that exists. Consistent routing is worth real money here.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these produce an error. The only symptom is the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you know if your cache is working?
&lt;/h2&gt;

&lt;p&gt;Everything above is invisible without instrumentation, so this is the part I'd implement first — before any of the fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log cache-hit rate per request.&lt;/strong&gt; Every major provider returns cached token counts in the usage block of the response. Read them and record the ratio. The field names differ — Anthropic splits &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; from &lt;code&gt;cache_read_input_tokens&lt;/code&gt;, OpenAI reports &lt;code&gt;cached_tokens&lt;/code&gt; nested inside its prompt token details — so normalise once at your client boundary rather than scattering provider checks through the codebase:&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;cache_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Normalise the usage block into (fresh, written, read).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; \
              &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens_details&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cached_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;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;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;

&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cache_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_hit_rate=%.2f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;read&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single ratio is the whole diagnostic. Everything else on this list shows up in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put effective price per million on a dashboard.&lt;/strong&gt; Divide total input spend by total input tokens. On a healthy long session it should sit far below list price — my model landed at $0.51/M against a $4.00 list. If yours hovers near list price, your cache is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the shape over a session.&lt;/strong&gt; A healthy session starts cold, climbs as the transcript grows, and plateaus high. A sawtooth means expiries. A flat line near zero means something in your prefix changes every turn — go and look for a timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test it deliberately.&lt;/strong&gt; Send the same two-turn conversation twice and assert that the second turn reports cache hits. It's a cheap test and it catches every failure on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isn't this what semantic caching does?
&lt;/h2&gt;

&lt;p&gt;A question that comes up immediately, and the two get conflated constantly, so it's worth separating them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic caching&lt;/strong&gt; sits in &lt;em&gt;your&lt;/em&gt; infrastructure. It embeds an incoming query, looks for a previously answered question that's close enough in vector space, and returns the stored answer without calling the model at all. It caches outputs, it's approximate, and it can be wrong — two questions can be neighbours in embedding space and still want different answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching&lt;/strong&gt; sits in the &lt;em&gt;provider's&lt;/em&gt; infrastructure. It caches the processed state of an input prefix, it's an exact match, and it cannot change what the model returns — only what you're billed for the input.&lt;/p&gt;

&lt;p&gt;They're complementary, not alternatives. Semantic caching can skip a call entirely, which beats any discount. Prompt caching makes the calls you do make dramatically cheaper. But only one of them can silently give a user a subtly wrong answer, so they deserve very different amounts of scrutiny before you ship them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order I'd do this in
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instrument first.&lt;/strong&gt; Without a hit-rate number, everything else is guesswork.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check whether your provider caches by default.&lt;/strong&gt; One doc page; potentially a 7× bill difference.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit your system prompt for anything volatile.&lt;/strong&gt; This is the big one, and it's usually a five-minute fix.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify history is append-only&lt;/strong&gt;, especially wherever context-limit trimming happens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Only then&lt;/strong&gt; think about TTL tiers. It's the smallest lever on the list and the only one that costs money to pull.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The cache is a longest-common-prefix match.&lt;/strong&gt; Stable content first, volatile content last, never modify what came before.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A timestamp in the system prompt cost 7.8× on an 80-turn session&lt;/strong&gt; in my model — $6.91 becomes $54.08. Move volatile context into the final user message instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;History must be append-only.&lt;/strong&gt; Sliding-window trimming re-pays for the entire transcript every time the window moves.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compaction resets the cache by design&lt;/strong&gt; — legitimate, but don't do it on a timer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anthropic and Gemini don't cache unless you ask.&lt;/strong&gt; There is no error when you forget; the only symptom is the invoice.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="mf"&gt;3.13&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="n"&gt;stdlib&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt;
&lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="n"&gt;user&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="mi"&gt;000&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="n"&gt;assistant&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="mi"&gt;000&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mf"&gt;4.00&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="nf"&gt;x &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="nf"&gt;x &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Figures are arithmetic on stated assumptions, not measurements of any provider's billing.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic — Prompt caching&lt;/a&gt; — cache breakpoints, TTL tiers, and the usage fields you need to compute a hit rate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/caching" rel="noopener noreferrer"&gt;Google — Gemini context caching&lt;/a&gt; — a usefully different model: caches are explicit objects you create and reference, which makes the prefix contract impossible to ignore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://arxiv.org/abs/2309.06180" rel="noopener noreferrer"&gt;Kwon et al., &lt;em&gt;Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em&gt;, SOSP 2023&lt;/a&gt; — explains why the match is a &lt;em&gt;prefix&lt;/em&gt; rather than anything cleverer. The KV cache is stored in blocks, and two requests can share a block only while their token sequences are still identical. Everything in this post follows from that.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you build agent harnesses,&lt;/em&gt; &lt;a href="https://medium.com/@minhle_0210/prompt-context-harness-loop-an-agents-anatomy-642db41429fb" rel="noopener noreferrer"&gt;&lt;em&gt;Prompt, Context, Harness, Loop&lt;/em&gt;&lt;/a&gt; &lt;em&gt;covers where the harness sits in the first place — the cache lives in exactly the layer that post calls the harness.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 Follow me: &lt;a href="https://www.linkedin.com/in/minhle007/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt; | &lt;a href="https://github.com/MinLee0210" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/five-ways-to-invalidate-your-prompt-cache-cgm7k" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Fun Project: I Built a Compressor That Thinks in Tokens</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Tue, 01 Sep 2026 06:51:33 +0000</pubDate>
      <link>https://dev.to/minh-leduc/fun-project-i-built-a-compressor-that-thinks-in-tokens-5bnb</link>
      <guid>https://dev.to/minh-leduc/fun-project-i-built-a-compressor-that-thinks-in-tokens-5bnb</guid>
      <description>&lt;p&gt;&lt;em&gt;Field notes from a weekend spent teaching an entropy coder to speak LLM.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;LLMs spent billions of dollars learning the best subword dictionary that has ever existed. Somewhere around 200,000 pieces of language, ranked by how much they compress. And I kept looking at that dictionary and thinking: &lt;em&gt;nobody is allowed to just use that for compression?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TokPress is a pure-Python lossless compressor that tokenizes input with OpenAI's&lt;/strong&gt; &lt;code&gt;o200k_base&lt;/code&gt; &lt;strong&gt;vocabulary, applies token-level LZ77, and entropy-codes the result with rANS. For many small, schema-homogeneous records (logs, JSON, telemetry) a trained dictionary takes the ratio to 0.2565, and a batch mode reaches 0.0875x — while compressing each record alone can inflate it past 1.0. It beats gzip on prose and loses honestly to zstd's trained dictionary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built one. Pure Python, no native code, one weekend, way too many cups of coffee. It's called &lt;a href="https://github.com/LakoreAI/tokpress" rel="noopener noreferrer"&gt;TokPress&lt;/a&gt;, it compresses by tokenizing with &lt;code&gt;o200k_base&lt;/code&gt; (the tokenizer OpenAI's models use), applying token-level LZ77, and entropy-coding the result with rANS. And along the way it produced a number that still makes me grin: &lt;strong&gt;150 JSON log lines, compressed as one stream, down to 0.0875× their size — while compressing each one alone made them &lt;em&gt;bigger&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the honest version of how that happened. Including the parts that broke.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "many small records" is a genuine pain
&lt;/h2&gt;

&lt;p&gt;Normal compressors like gzip and zstd are built for big files. They learn their model from the stream, which is great when you have megabytes to amortize it over. But the world is full of &lt;em&gt;small, independent, schema-similar&lt;/em&gt; records: JSON log lines, telemetry events, API responses, package metadata. Each one is 200 to 500 bytes. Each one is on its own. As an engineer who spends most days building ML systems in production — and who has &lt;a href="https://ai.plainenglish.io/i-spent-my-weekend-benchmarking-vietnamese-bert-models-so-you-dont-have-to-0d19aa280736" rel="noopener noreferrer"&gt;spent weekends benchmarking tokenizers&lt;/a&gt; — this is the shape of half the data I touch.&lt;/p&gt;

&lt;p&gt;Compress a single 200-byte log line with gzip and you'll find the header and the dictionary setup cost more than the content you saved. In my measurements, a lone 70-byte JSON sample came out at &lt;strong&gt;81 bytes compressed&lt;/strong&gt; — a compressor that made things bigger. That's not a bug; it's the cold-start penalty of not having a model.&lt;/p&gt;

&lt;p&gt;Zstandard's answer is dictionary mode: train a shared dictionary offline, then every record compresses against it. MongoDB and Amazon DocumentDB now ship exactly this (up to 5× better ratio on JSON documents). And in 2025 the IETF shipped &lt;a href="https://www.rfc-editor.org/rfc/rfc9842" rel="noopener noreferrer"&gt;RFC 9842&lt;/a&gt;, which put &lt;em&gt;dictionary compression into HTTP itself&lt;/em&gt; — browsers, servers, the works. "Known-structure API responses" is literally named as the sweet spot.&lt;/p&gt;

&lt;p&gt;So: shared dictionary + many small homogeneous records = a real, money-sized problem. I just wanted to add one twist — use a &lt;em&gt;tokenizer&lt;/em&gt; as the dictionary.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpVU9tO20AU_JXpPrXCToBWQooKUhKcggohSlKkEiN0bB_Hq6y91u4GCJfXfkA_sV9S-ZIAT3uOz2XGM7vPItYJi55IlX6IMzIOF9OwAID-IhTRxjEMx9okeAzFLXz_BINFKA46mF_9DMbnN8H0e2S6J1Wnz48UOwwmQQ_6cH9_dReRZWgDZ0gWnOBexxSF4rZBGFTrXkLh9IoLyMSG4gXDRSgO2-3-RXAdXODi5uioBlka5mSDjGyGkoxl4yEnF2dQXEDh5BhfPaSKljhusGDlE-8Ahy2gko4NKYu9dtytS8U1_OkiFF87CMbz6dXkNz6b_nj2BXsYnM9n82nQv6yJGHpAF7bmUAdKOnRBCZVO3vO70N_WJpNLdJHI2Pkx2ZiSN1qnLa2YikQm5BjWGaa8JhQsQpHLAkty3EO0lioBKYVds_WwYi7hMobNSSm2brc6qB0bLULRcXr1hC6qM5Itwq7vxyIUtUmyWMJSXiruYQwbZ5yTn-lcL7lgvbbtbbDby3C2CIVOUyULblyu9akjv3Hg35-_6BgqVvZdqdKhqTi9qpIdk7NWjHp4rchsKhUGH4ulkXnFNFqnKRtUKdvWy0xap5up4cepiFacQJuEjb-PbhsdwFG0db9VA36ncUTnpWFr73IqNpUg7d-DLHTBb4Zv5XzBSHgiZ5OTTETvWbiM8-p9JZzSWjnhNV-uycgatepJdeFGlEu1ET3hU1kq9u3GOs49DJQsVpcUz-p8pAvnIRQzXmrGr_NQeJjqSDvt4YzVPTsZk4e-kaQ8WCqsb9nIVHg1yEw-VVwOvpWP4vXVE9FyqJU2oic-PWTSsXj9DxT5U-c%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpVU9tO20AU_JXpPrXCToBWQooKUhKcggohSlKkEiN0bB_Hq6y91u4GCJfXfkA_sV9S-ZIAT3uOz2XGM7vPItYJi55IlX6IMzIOF9OwAID-IhTRxjEMx9okeAzFLXz_BINFKA46mF_9DMbnN8H0e2S6J1Wnz48UOwwmQQ_6cH9_dReRZWgDZ0gWnOBexxSF4rZBGFTrXkLh9IoLyMSG4gXDRSgO2-3-RXAdXODi5uioBlka5mSDjGyGkoxl4yEnF2dQXEDh5BhfPaSKljhusGDlE-8Ahy2gko4NKYu9dtytS8U1_OkiFF87CMbz6dXkNz6b_nj2BXsYnM9n82nQv6yJGHpAF7bmUAdKOnRBCZVO3vO70N_WJpNLdJHI2Pkx2ZiSN1qnLa2YikQm5BjWGaa8JhQsQpHLAkty3EO0lioBKYVds_WwYi7hMobNSSm2brc6qB0bLULRcXr1hC6qM5Itwq7vxyIUtUmyWMJSXiruYQwbZ5yTn-lcL7lgvbbtbbDby3C2CIVOUyULblyu9akjv3Hg35-_6BgqVvZdqdKhqTi9qpIdk7NWjHp4rchsKhUGH4ulkXnFNFqnKRtUKdvWy0xap5up4cepiFacQJuEjb-PbhsdwFG0db9VA36ncUTnpWFr73IqNpUg7d-DLHTBb4Zv5XzBSHgiZ5OTTETvWbiM8-p9JZzSWjnhNV-uycgatepJdeFGlEu1ET3hU1kq9u3GOs49DJQsVpcUz-p8pAvnIRQzXmrGr_NQeJjqSDvt4YzVPTsZk4e-kaQ8WCqsb9nIVHg1yEw-VVwOvpWP4vXVE9FyqJU2oic-PWTSsXj9DxT5U-c%3Ftype%3Dpng" alt="Mermaid Diagram" width="1904" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beat 1 — the idea: the tokenizer is the free dictionary
&lt;/h2&gt;

&lt;p&gt;Here's the pitch. A BPE tokenizer's whole job is finding the pieces of language that compress well. &lt;code&gt;"action"&lt;/code&gt; is one token. &lt;code&gt;"click"&lt;/code&gt; is one token. The tokenizer has already done the hard vocabulary work — for free, at huge scale.&lt;/p&gt;

&lt;p&gt;So: tokenize the input, run LZ77 over the &lt;em&gt;token ids&lt;/em&gt; instead of bytes, then entropy-code with rANS. The token stream is a much better-shaped alphabet than raw bytes, because the multi-byte structure is already baked in.&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;tokpress&lt;/span&gt;

&lt;span class="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# bytes or str
&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# byte-exact
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one non-negotiable requirement: byte-exactness, even for arbitrary binary. Logs aren't always valid UTF-8. tiktoken's public &lt;code&gt;encode&lt;/code&gt; takes a &lt;code&gt;str&lt;/code&gt;, but its internal &lt;code&gt;_encode_bytes&lt;/code&gt;/&lt;code&gt;decode_bytes&lt;/code&gt; pair operates on raw bytes — including a lone &lt;code&gt;0xFF&lt;/code&gt;. That's what I use. It round-trips anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beat 2 — the first wrong turn (and it was a good one)
&lt;/h2&gt;

&lt;p&gt;My first instinct was a domain vocabulary. Train on JSON logs, mine the frequent pieces, get a &lt;em&gt;smaller&lt;/em&gt; tokenizer tailored to my data. Sounds great. It silently failed.&lt;/p&gt;

&lt;p&gt;The problem: a restricted vocabulary is only a &lt;em&gt;valid&lt;/em&gt; tokenizer if it forms a complete hierarchical BPE merge chain — every token has to be the concatenation of two lower-ranked tokens. Mining pieces by longest-match gives you a bag of strings, not a chain. The encoder can't reproduce the merges, so the "tokens" don't round-trip the way you think. I threw the whole domain-profile system out and learned the hard way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A vocabulary you can't merge is a vocabulary you can't trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Beat 3 — three real bugs, all found by being paranoid
&lt;/h2&gt;

&lt;p&gt;Building the entropy coder was where the weekend's real work was. rANS at table precision 2^16 with a 64-bit state, and a selector that builds up to &lt;em&gt;eight&lt;/em&gt; candidate encodings per record and keeps the smallest. That last bit is a nice design: &lt;code&gt;min(candidates, key=len)&lt;/code&gt; — nothing is ever forced, the size decides.&lt;/p&gt;

&lt;p&gt;And it turned out the size decided wrong in three delightful ways. All three were caught only because I looped fuzz inputs over and over instead of testing one fixed example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The single-symbol truncation.&lt;/strong&gt; A table with exactly one active symbol gets frequency exactly 65536 — 100% probability. That needs 17 bits. I was writing it in 16, silently truncating 65536 to 0 on the wire. Fix: transmit &lt;code&gt;freq - 1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The reverse-order escape.&lt;/strong&gt; rANS encodes in reverse logical order. A two-event cascade (context table → fall through to order-0) needs its encode calls issued in the &lt;em&gt;opposite&lt;/em&gt; micro-order from how the decoder consumes them. Get it backwards and it corrupts output only on inputs that exercise the escape path — invisible until it isn't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The double-reverse.&lt;/strong&gt; In adaptive-split mode, the escape list is built in a forward pass before the reverse encode loop — unlike every other mode. So I reversed it a second time, by analogy. Wrong. Only when a record had more than one escape did the values come out scrambled.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three bugs, three regression tests, zero drama. This is the part of compression that doesn't make blog headers but is 90% of the actual work: the wire format must be a perfect mirror between encode and decode, and the only way to trust it is to fuzz it to death.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beat 4 — the dictionary, and the number that made it click
&lt;/h2&gt;

&lt;p&gt;The real win came from a &lt;code&gt;TokDict&lt;/code&gt;: train once on a sample of schema-homogeneous records, then every future record gets three free gifts — an LZ priming buffer (match against &lt;em&gt;other&lt;/em&gt; records' history), a baked order-0 rANS table (no per-record table on the wire), and order-1 context tables for the most common previous-token contexts, with an escape cascade so a record can always contain something the dictionary never saw.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokpress train-dict mydict.tokdict sample1.json sample2.json ...
tokpress compress new_record.json &lt;span class="nt"&gt;--dict&lt;/span&gt; mydict.tokdict &lt;span class="nt"&gt;-o&lt;/span&gt; new_record.tokz
tokpress decompress new_record.tokz &lt;span class="nt"&gt;--dict&lt;/span&gt; mydict.tokdict &lt;span class="nt"&gt;-o&lt;/span&gt; restored.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 230 held-out structured-log records (trained on 184, tested on 46 it never saw):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;per-record, no dictionary&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ TokDict order-0 table&lt;/td&gt;
&lt;td&gt;0.284&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ order-1 context tables&lt;/td&gt;
&lt;td&gt;0.2565&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gzip -9&lt;/td&gt;
&lt;td&gt;0.728&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zstd -19, no dictionary&lt;/td&gt;
&lt;td&gt;0.737&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zstd -19 + matched trained dict&lt;/td&gt;
&lt;td&gt;0.195&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's below gzip and dictionary-free zstd, and within 1.3× of zstd given the &lt;em&gt;same&lt;/em&gt; training data — down from a 5–7× gap with no dictionary at all. Honest footnote: zstd's COVER/FastCover dictionary training is more mature than my concatenation-based priming, and it still wins on the same data. I'm not claiming otherwise.&lt;/p&gt;

&lt;p&gt;And then the batch mode, which is the number I actually open the repo for. Instead of compressing each record on its own, concatenate them all and compress as &lt;strong&gt;one stream&lt;/strong&gt;, with the record lengths stored in the header so decoding is still per-record exact:&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;packed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokpress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# byte-exact, per-record
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 150 schema-homogeneous JSON records (~11.6 KB): compressing each separately summed to ratio &lt;strong&gt;1.19&lt;/strong&gt; — the records literally grew. As one adaptive stream: &lt;strong&gt;0.0875&lt;/strong&gt;. Same bytes, one header instead of 150, one adaptive model spanning the batch instead of 150 cold starts. That's the whole thesis of the project in a single number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beat 5 — the turn: tokenization is not the magic
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable part, and it's a theorem: &lt;strong&gt;tokenization cannot reduce the information content of a message.&lt;/strong&gt; It's injective, so it preserves entropy. Every compression gain has to come from &lt;em&gt;modeling&lt;/em&gt;, never from the transform itself.&lt;/p&gt;

&lt;p&gt;I verified this the hard way. In bulk mode, without a trained dictionary, TokPress is just an LZ + entropy pipeline — and on long prose it now beats gzip and, on the short-prose corpus, even zstd outright (0.298 vs 0.324, and brotli 0.306) — but that's the &lt;em&gt;entropy coding&lt;/em&gt; doing the work, not the tokenizer. Take the tokenizer away and the wins shrink. The tokenizer's job is to reshape the alphabet so a low-order model sees structure that would need a high-order byte model. It's a lever, not a free lunch.&lt;/p&gt;

&lt;p&gt;This also tells you where TokPress sits next to the closest thing it has to a cousin. The &lt;a href="https://github.com/shallowbyte/parmar" rel="noopener noreferrer"&gt;parmar&lt;/a&gt; project measured "tokenize before you compress" across 452 configurations and found tiktoken-then-xz beats plain xz by 7–9.6% — but it &lt;em&gt;pipes the token IDs into a byte-level compressor&lt;/em&gt;, and it explicitly left code, JSON, and logs untested. TokPress goes the other way: it entropy-codes the token IDs &lt;em&gt;directly&lt;/em&gt; with rANS, and it's built for exactly those many-small-records, schema-homogeneous corpora parmar skipped. Same starting bet, opposite half of the design space — and the honest result is that on whole-file prose the direct route wins over piping tokens through xz, while neither is a substitute for a trained dictionary.&lt;/p&gt;

&lt;p&gt;The other honest finding: &lt;strong&gt;a trained dictionary is schema-specific.&lt;/strong&gt; Train on JSON logs, apply to Python code, and the dictionary buys you &lt;em&gt;nothing&lt;/em&gt; over no dictionary at all (0.489 vs 0.485). It's a genuinely domain-specific artifact. Use it on data that resembles its training data, and only then.&lt;/p&gt;

&lt;p&gt;Along the way I also learned the machine's own dirty secret: tiktoken's &lt;code&gt;_encode_bytes&lt;/code&gt; routes valid UTF-8 through a regex (&lt;code&gt;pat_str&lt;/code&gt;) and BPEs &lt;em&gt;per piece&lt;/em&gt; — so a vocabulary trained with naive whole-input BPE fragments on piece boundaries. The fix was training the same way tiktoken encodes: pre-tokenize with the same regex, forbid merges across pieces. Once I did that, a JSON-trained vocabulary beat &lt;code&gt;o200k_base&lt;/code&gt; on held-out JSON (0.162 vs 0.178). The project now ships &lt;code&gt;tokpress train-vocab&lt;/code&gt; and &lt;code&gt;tokpress fit&lt;/code&gt; (vocab + dictionary in one shot).&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolbox, one week later
&lt;/h2&gt;

&lt;p&gt;The weekend project grew a full CLI while I wasn't looking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokpress compress record.json &lt;span class="nt"&gt;-o&lt;/span&gt; record.tokz
tokpress decompress record.tokz &lt;span class="nt"&gt;-o&lt;/span&gt; restored.json
tokpress pack batch.tokz record1.json record2.json ...   &lt;span class="c"&gt;# one adaptive stream&lt;/span&gt;
tokpress unpack batch.tokz out_dir/
tokpress &lt;span class="nb"&gt;read &lt;/span&gt;batch.tokz 7                                &lt;span class="c"&gt;# O(1) random access&lt;/span&gt;
tokpress train-dict mydict.tokdict samples.jsonl
tokpress train-vocab myvocab.ranks corpus.txt
tokpress fit out corpus.txt                               &lt;span class="c"&gt;# both at once&lt;/span&gt;
tokpress tokenize-stats file.txt                          &lt;span class="c"&gt;# tokens/KB, entropy, MI&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is a little love letter to the LLM crowd: it reports the order-0/order-1 token entropy and adjacent-token mutual information of any corpus — compression as a tokenizer-quality signal, which &lt;a href="https://arxiv.org/abs/2403.06265" rel="noopener noreferrer"&gt;Goldman et al. showed correlates with actual model performance&lt;/a&gt;. Tokenize your eval set, get a number that says something about how well the tokenizer fits it.&lt;/p&gt;

&lt;p&gt;The whole thing is deliberately un-optimized, pure Python, ~2900 lines, &lt;strong&gt;93 tests&lt;/strong&gt;. Decompression is fast (thousands of records/sec); compression is slower (the encoder builds every candidate mode and keeps the smallest — thorough, not fast). I made the &lt;code&gt;SymbolStats&lt;/code&gt; alphabet pass active-symbol-only one afternoon and got a 2.8× speedup with byte-identical output, which was the most satisfying 40 lines of the weekend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Share your thoughts in the comments — I’d love to hear how this technology is impacting your industry.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 Be sure to press the like button and follow me. It would be a great motivation for me.&lt;/p&gt;

&lt;p&gt;👉 Follow me: &lt;a href="https://www.linkedin.com/in/minhle007/" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;LinkedIn&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; &lt;em&gt;|&lt;/em&gt; &lt;a href="https://github.com/MinLee0210" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;GitHub&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A tokenizer is a compressor's dictionary that someone else already trained.&lt;/strong&gt; Reusing &lt;code&gt;o200k_base&lt;/code&gt; as the alphabet is free leverage — just don't expect the tokenizer alone to be the win.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Many-small-homogeneous-records is its own regime.&lt;/strong&gt; One stream over 150 records: 0.0875×. One record at a time: 1.19×. The container is the feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modeling is everything; the transform is a lever.&lt;/strong&gt; Tokenization preserves entropy — every gain is the entropy coder and the trained dictionary doing real prediction work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Honest numbers beat impressive ones.&lt;/strong&gt; It beats gzip on prose, ties/beats zstd on some corpora, and still loses to zstd's trained dictionary by 1.3×. I measured all of it, and the losing parts are the parts that made me learn the most.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to poke at it, it's all open source: &lt;a href="https://github.com/LakoreAI/tokpress" rel="noopener noreferrer"&gt;github.com/LakoreAI/tokpress&lt;/a&gt;. The bench harness (&lt;code&gt;scripts/bench.py&lt;/code&gt;) is the source of truth for every number in this post — it round-trip-checks every ratio it prints. I'd love to see what it does on &lt;em&gt;your&lt;/em&gt; logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;The papers I kept going back to while building this — the entropy coder, the tokenizer, and the "compression is prediction" framing that got me started.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;J. Duda, &lt;em&gt;Asymmetric Numeral Systems&lt;/em&gt;, arXiv:0902.0271. The rANS coder — the entropy stage is a direct implementation of the range variant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;J. Ziv and A. Lempel, &lt;em&gt;Compression of Individual Sequences via Variable-Rate Coding&lt;/em&gt;, IEEE Trans. Inf. Theory, 24(5), 1978. Token-level LZ77, plus the finite-length redundancy that makes small records the interesting regime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;P. Gage, &lt;em&gt;A New Algorithm for Data Compression&lt;/em&gt;, The C Users Journal, 12(2), 1994. Byte-pair encoding — the ancestor of every subword tokenizer here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A. Radford, J. Wu, et al., &lt;em&gt;Language Models are Unsupervised Multitask Learners&lt;/em&gt;, OpenAI, 2019. The byte-level BPE tokenization that became &lt;code&gt;o200k_base&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;T. Kudo and J. Richardson, &lt;em&gt;SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing&lt;/em&gt;, EMNLP (demo), arXiv:1808.06226, 2018. The subword-modeling framing behind the whole tokenizer-as-dictionary idea.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;G. Delétang, A. Ruoss, et al., &lt;em&gt;Language Modeling Is Compression&lt;/em&gt;, arXiv:2309.10668, 2023. The prediction-compression equivalence — and the honest "no free lunch" reminder that the transform never carries the win.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;C. S. K. Valmeekam, K. Narayanan, D. Kalathil, J.-F. Chamberland, S. Shakkottai, &lt;em&gt;LLMZip: Lossless Text Compression using Large Language Models&lt;/em&gt;, arXiv:2306.04050, 2023. The LLM-as-predictor end of the spectrum; TokPress is the cheap, static-dictionary end of the same idea.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;O. Goldman, A. Caciularu, M. Eyal, K. Cao, I. Szpektor, R. Tsarfaty, &lt;em&gt;Unpacking Tokenization: Evaluating Text Compression and its Correlation with Model Performance&lt;/em&gt;, arXiv:2403.06265 (EMNLP Findings), 2024. The result behind &lt;code&gt;tokpress tokenize-stats&lt;/code&gt; — tokenizer compression as a quality signal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Y. Collet and M. Kucherawy, &lt;em&gt;Zstandard Compression and the application/zstd Media Type&lt;/em&gt;, RFC 8478, 2018. The main comparison point, including its dictionary mode I keep losing to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;J. G. Cleary and I. H. Witten, &lt;em&gt;Data Compression Using Adaptive Coding and Partial String Matching&lt;/em&gt;, IEEE Trans. Commun., 32(4), 1984. PPM — the escape-to-lower-order idea the per-record order-1 mode borrows.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/fun-project-i-built-a-compressor-that-thinks-in-tokens-n6w41" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How do you deduplicate a stream you don't control?</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Sat, 15 Aug 2026 11:33:33 +0000</pubDate>
      <link>https://dev.to/minh-leduc/how-do-you-deduplicate-a-stream-you-dont-control-7ng</link>
      <guid>https://dev.to/minh-leduc/how-do-you-deduplicate-a-stream-you-dont-control-7ng</guid>
      <description>&lt;p&gt;Field notes, part 1 of 3. Data plumbing from an AI engineer's desk.*&lt;/p&gt;

&lt;p&gt;My job title says AI. A meaningful share of my week is data.&lt;/p&gt;

&lt;p&gt;Not the glamorous part — no model architecture, no eval harness. Just the plumbing that decides whether the model ever sees the right thing at the right time. And lately I've been stuck on a shape of problem I now see everywhere: &lt;strong&gt;something upstream is chatty, and something downstream is expensive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The expensive thing varies: re-embedding a document, an LLM enrichment call, a search index that must be re-committed. The arithmetic doesn't. Upstream emits fifty events about one entity in twenty seconds, and downstream bills you fifty times for an answer that changed once.&lt;/p&gt;

&lt;p&gt;Looking for how this is solved at scale, I landed on a PyCon DE talk by Mirano Tuk and Filip Bacic — &lt;a href="https://www.youtube.com/watch?v=t0ZWNh-UXDs" rel="noopener noreferrer"&gt;&lt;em&gt;How to Search Through 800 Billion Records in Real Time&lt;/em&gt;&lt;/a&gt;. Their scale is far past mine, but the pattern is small and ported straight onto my problems. These notes are me working it through on my own data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deduplicating a stream means collapsing repeated events about one key into a single unit of downstream work. A per-batch set mostly fails, because a batch is a time window whose width you don't control. A TTL buffer decouples that window from throughput — it cut my simulated stream from 904,298 messages to 200,398, a 4.51× reduction.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't a per-batch set work?
&lt;/h2&gt;

&lt;p&gt;First instinct: dedupe within each batch. You're already polling in batches, so throw the keys in a &lt;code&gt;set&lt;/code&gt; and you're done.&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;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;consumer&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;key&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I simulated a stream shaped like the real thing — 200,000 distinct keys, each emitting a burst of updates over a few seconds, 904,298 messages across an hour:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Effective window&lt;/th&gt;
&lt;th&gt;Downstream work&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No dedup&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;904,298&lt;/td&gt;
&lt;td&gt;1.00×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-batch set, 1,000&lt;/td&gt;
&lt;td&gt;4.0s&lt;/td&gt;
&lt;td&gt;548,524&lt;/td&gt;
&lt;td&gt;1.65×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-batch set, 10,000&lt;/td&gt;
&lt;td&gt;39.8s&lt;/td&gt;
&lt;td&gt;260,396&lt;/td&gt;
&lt;td&gt;3.47×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-batch set, 50,000&lt;/td&gt;
&lt;td&gt;199.0s&lt;/td&gt;
&lt;td&gt;212,122&lt;/td&gt;
&lt;td&gt;4.26×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL buffer, 30s&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;215,364&lt;/td&gt;
&lt;td&gt;4.20×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL buffer, 60s&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;td&gt;200,398&lt;/td&gt;
&lt;td&gt;4.51×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL buffer, 300s&lt;/td&gt;
&lt;td&gt;300s&lt;/td&gt;
&lt;td&gt;200,000&lt;/td&gt;
&lt;td&gt;4.52×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the &lt;strong&gt;window&lt;/strong&gt; column before the reduction column. That's the thing I got wrong when I first ran this.&lt;/p&gt;

&lt;p&gt;A batch of 1,000 messages at 251 msg/s is a four-second window. A batch of 50,000 is a 199-second window. Per-batch dedup isn't a different technique from TTL dedup — &lt;strong&gt;it's the same technique with a window you didn't choose&lt;/strong&gt;. Its width is &lt;code&gt;batch_size / throughput&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means it moves in exactly the wrong direction. Traffic doubles, your window halves. The moment duplicates are most abundant is the moment your dedup window is narrowest. That's not a knob, it's a trapdoor.&lt;/p&gt;

&lt;p&gt;The TTL buffer's entire contribution is decoupling the window from throughput. That's it. That's the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can a plain dict work as the buffer?
&lt;/h2&gt;

&lt;p&gt;Here's the part that delighted me, because I expected to need a real data structure.&lt;/p&gt;

&lt;p&gt;Since Python 3.7, regular dicts preserve insertion order — that's a language guarantee, not a CPython implementation detail. So a plain dict &lt;em&gt;is&lt;/em&gt; a FIFO queue. The oldest entry is the first one you iterate. Which means expiry checking is O(1) amortised: look at the front, and if it hasn't expired, nothing behind it has either.&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;Deduplicator&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;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_evict&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;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ttl&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;on_evict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;on_evict&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;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;                      &lt;span class="c1"&gt;# key -&amp;gt; expiry, in insertion order
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&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;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&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;key&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;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;                          &lt;span class="c1"&gt;# already scheduled, drop it
&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expire&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;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;while&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;buf&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="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&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;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;      &lt;span class="c1"&gt;# oldest entry
&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;                       &lt;span class="c1"&gt;# nothing behind it can be older
&lt;/span&gt;            &lt;span class="k"&gt;del&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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;on_evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. No heap, no external store, no Redis.&lt;/p&gt;

&lt;p&gt;I did wonder about Redis, and the answer is a nice piece of reasoning: if your key is also your partition key, every event for a given key lands on the same partition, and therefore the same consumer replica. The buffer is &lt;em&gt;correctly&lt;/em&gt; local. Reaching for a shared store would add a network hop to enforce an invariant the partitioner already gives you for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I'd have shipped
&lt;/h2&gt;

&lt;p&gt;Look again at &lt;code&gt;expire()&lt;/code&gt;. The processing happens on &lt;strong&gt;eviction&lt;/strong&gt; — when the key leaves the buffer — not when it arrives.&lt;/p&gt;

&lt;p&gt;My first version did the obvious thing and processed on arrival, treating the buffer purely as a "have I seen this?" filter. It looks equivalent. It isn't, and the gap is enormous:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Work&lt;/th&gt;
&lt;th&gt;Keys stale at the end&lt;/th&gt;
&lt;th&gt;% stale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ttl=5s, process on insert&lt;/td&gt;
&lt;td&gt;425,508&lt;/td&gt;
&lt;td&gt;58,239&lt;/td&gt;
&lt;td&gt;29.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttl=5s, process on evict&lt;/td&gt;
&lt;td&gt;425,508&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttl=30s, process on insert&lt;/td&gt;
&lt;td&gt;215,364&lt;/td&gt;
&lt;td&gt;141,879&lt;/td&gt;
&lt;td&gt;70.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttl=30s, process on evict&lt;/td&gt;
&lt;td&gt;215,364&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttl=60s, process on insert&lt;/td&gt;
&lt;td&gt;200,398&lt;/td&gt;
&lt;td&gt;155,650&lt;/td&gt;
&lt;td&gt;77.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttl=60s, process on evict&lt;/td&gt;
&lt;td&gt;200,398&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same amount of work. Wildly different correctness.&lt;/p&gt;

&lt;p&gt;Processing on insert commits the &lt;strong&gt;first&lt;/strong&gt; version of the record and then discards every update that arrives inside the window. If the last update for a key lands during its own TTL — which, given that updates arrive in bursts, is the common case, not the edge case — you have permanently stale data and no error anywhere to tell you.&lt;/p&gt;

&lt;p&gt;At a 60-second TTL, &lt;strong&gt;77.8% of keys&lt;/strong&gt; ended the run holding a version that was not the latest one.&lt;/p&gt;

&lt;p&gt;Flipping to process-on-eviction fixes it completely. You can't know which message is the last one for a key, but if you wait out the TTL and &lt;em&gt;then&lt;/em&gt; read the current state, you get the state after the last message in that window. It's an approximation of "process the final update" that costs nothing extra.&lt;/p&gt;

&lt;p&gt;The tell is that the work column is identical. This isn't a trade-off. The insert version is just wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long should the TTL be?
&lt;/h2&gt;

&lt;p&gt;The TTL is latency you are choosing to add. A 60-second buffer means data becomes visible up to a minute after it arrives.&lt;/p&gt;

&lt;p&gt;The returns die fast. Going 30s → 60s buys 4.20× → 4.51×. Going 60s → 300s buys 4.51× → 4.52×, for five times the latency. Almost all of the available win is in the first half-minute, because that's the width of the bursts. &lt;strong&gt;Set the TTL to roughly the width of your upstream's burst, and stop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a floor you can't cross: 200,000 keys means at least 200,000 units of work. At a 300s TTL the buffer is doing literally nothing but adding delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm taking to my own work
&lt;/h2&gt;

&lt;p&gt;The reason I chased this down: the same shape sits underneath a lot of AI infrastructure, and I'd been solving it badly with cron jobs.&lt;/p&gt;

&lt;p&gt;A document store where every edit triggers re-embedding. A user-activity stream where every event triggers a profile refresh through an LLM. Both are chatty-upstream, expensive-downstream, and both are places I'd previously reached for "just batch it hourly" — which is a TTL buffer with the worst possible TTL and no correctness story at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Process on eviction, never on insert.&lt;/strong&gt; Same cost, and it's the difference between fresh and silently stale.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A batch is a window you didn't choose.&lt;/strong&gt; If dedup matters, choose it explicitly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set the TTL from your burst width&lt;/strong&gt;, not from a latency budget someone made up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A per-batch&lt;/strong&gt; &lt;code&gt;set&lt;/code&gt; &lt;strong&gt;is a TTL buffer with a window of&lt;/strong&gt; &lt;code&gt;batch_size / throughput&lt;/code&gt; — it narrows exactly when traffic spikes and duplicates matter most.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A TTL buffer cut 904,298 messages to 200,398 units of work (4.51×)&lt;/strong&gt; in my simulation, against a hard floor of 200,000.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Process on eviction, not insertion.&lt;/strong&gt; At a 60s TTL, processing on insert left 77.8% of keys holding stale data for identical cost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Since Python 3.7 a plain dict is a FIFO queue&lt;/strong&gt;, so the buffer needs no heap and no Redis — the partitioner already guarantees a key's events reach one replica.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce this
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python 3.13 · stdlib only · seed 20260810
200,000 keys · mean 4 updates each · 8s burst spread · 1 hour horizon

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next: &lt;strong&gt;Why committing Kafka offsets out of order loses data&lt;/strong&gt; — the version of this buffer I'd have shipped drops 4,456 messages on the floor the first time it restarts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Credit where it's due: the pattern in this note comes from&lt;/em&gt; &lt;a href="https://www.youtube.com/watch?v=t0ZWNh-UXDs" rel="noopener noreferrer"&gt;&lt;em&gt;Mirano Tuk and Filip Bacic's PyCon DE 2026 talk&lt;/em&gt;&lt;/a&gt;&lt;em&gt;. The simulation, the numbers, and any mistakes are mine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 Follow me: &lt;a href="https://www.linkedin.com/in/minhle007/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt; | &lt;a href="https://github.com/MinLee0210" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/how-do-you-deduplicate-a-stream-you-don-t-control-1rd0w" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>data</category>
      <category>software</category>
    </item>
    <item>
      <title>A Hands-On Guide to kalbee: Your First Kalman Filter (and Beyond)</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Sun, 19 Jul 2026 00:09:00 +0000</pubDate>
      <link>https://dev.to/minh-leduc/a-hands-on-guide-to-kalbee-your-first-kalman-filter-and-beyond-1k9m</link>
      <guid>https://dev.to/minh-leduc/a-hands-on-guide-to-kalbee-your-first-kalman-filter-and-beyond-1k9m</guid>
      <description>&lt;p&gt;&lt;em&gt;Everything you need to go from &lt;code&gt;pip install&lt;/code&gt; to a working multi-object tracker, one runnable snippet at a time.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;kalbee is a Python library for state estimation — the art of recovering a clean signal (position, velocity, temperature, whatever you're measuring) from noisy sensor data. This guide walks through it from the ground up. Every code block runs as-is; copy them into a file and follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;kalbee
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only runtime dependencies are NumPy and SciPy. Optional extras add object-detection (&lt;code&gt;pip install "kalbee[yolo]"&lt;/code&gt;) and plotting (&lt;code&gt;pip install "kalbee[viz]"&lt;/code&gt;) support.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea you need: predict and update
&lt;/h2&gt;

&lt;p&gt;Every filter in kalbee works the same way. You alternate between two steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;predict()&lt;/code&gt;&lt;/strong&gt; — advance the state forward in time using a motion model ("where do I &lt;em&gt;think&lt;/em&gt; the object is now?").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;update(z)&lt;/code&gt;&lt;/strong&gt; — correct that prediction with a new measurement &lt;code&gt;z&lt;/code&gt; ("what does the sensor actually say?").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The filter tracks two things: the &lt;strong&gt;state&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt; (your best estimate) and the &lt;strong&gt;covariance&lt;/strong&gt; &lt;code&gt;P&lt;/code&gt; (how uncertain that estimate is). You read them back via &lt;code&gt;kf.x&lt;/code&gt; and &lt;code&gt;kf.P&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first filter
&lt;/h2&gt;

&lt;p&gt;Let's track an object moving at roughly constant velocity, measuring only its (noisy) position. Instead of hand-building matrices, we use kalbee's ready-made &lt;strong&gt;models&lt;/strong&gt;:&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;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;kalbee&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KalmanFilter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rmse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kalbee.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_measurement_model&lt;/span&gt;

&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="c1"&gt;# Motion model: state is [position, velocity]
&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;process_var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dims&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="c1"&gt;# Measurement model: we observe position only, with noise variance 4.0
&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;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;position_measurement_model&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dims&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;measurement_var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Simulate a noisy trajectory
&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt; &lt;span class="o"&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="mf"&gt;1.0&lt;/span&gt;
&lt;span class="n"&gt;truths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;measurements&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="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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="n"&gt;truths&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;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;measurements&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;pos&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;standard_normal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# std 2.0 -&amp;gt; var 4.0
&lt;/span&gt;
&lt;span class="c1"&gt;# Create the filter: start at zero with high uncertainty (P = 100 * I)
&lt;/span&gt;&lt;span class="n"&gt;kf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KalmanFilter&lt;/span&gt;&lt;span class="p"&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;2&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eye&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Q&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;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;estimates&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;z&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;kf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;]]))&lt;/span&gt;
    &lt;span class="n"&gt;estimates&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;kf&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="mi"&gt;0&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="c1"&gt;# estimated position
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Raw measurement RMSE:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rmse&lt;/span&gt;&lt;span class="p"&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;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&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;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;truths&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Filtered RMSE:       &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rmse&lt;/span&gt;&lt;span class="p"&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;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimates&lt;/span&gt;&lt;span class="p"&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;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;truths&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it and you'll see the filtered error is meaningfully lower than the raw measurement error — the filter is smoothing out the noise. That's the whole game.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the parameters mean
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;process_var&lt;/code&gt;&lt;/strong&gt; — how much you trust the motion model. Higher = the filter reacts faster but is jumpier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;measurement_var&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;R&lt;/code&gt;) — how noisy your sensor is. Higher = the filter leans more on its predictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial &lt;code&gt;P&lt;/code&gt;&lt;/strong&gt; — your starting uncertainty. When in doubt, start large (like &lt;code&gt;100 * I&lt;/code&gt;); the filter converges quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tuning &lt;code&gt;process_var&lt;/code&gt; vs &lt;code&gt;measurement_var&lt;/code&gt; is the main knob you'll turn. (There's an automatic way to set them — see the EM section below.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Going 2-D (and beyond)
&lt;/h2&gt;

&lt;p&gt;Every model takes an &lt;code&gt;n_dims&lt;/code&gt; argument. Want to track an object in a plane? Ask for two dimensions and the state becomes &lt;code&gt;[x, vx, y, vy]&lt;/code&gt;:&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;F&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="nf"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&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;process_var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dims&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="c1"&gt;# 4x4
&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;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;position_measurement_model&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dims&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;measurement_var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need acceleration too? Use &lt;code&gt;constant_acceleration&lt;/code&gt; (state becomes &lt;code&gt;[pos, vel, acc]&lt;/code&gt; per axis). Tracking something that turns? &lt;code&gt;constant_turn&lt;/code&gt; gives you a coordinated-turn model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nonlinear systems: EKF and UKF
&lt;/h2&gt;

&lt;p&gt;When your motion or measurement isn't linear, swap in the Extended or Unscented Kalman Filter. Instead of matrices, you pass &lt;strong&gt;functions&lt;/strong&gt;:&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;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;kalbee&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UnscentedKalmanFilter&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&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;dt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="c1"&gt;# state transition
&lt;/span&gt;    &lt;span class="n"&gt;F&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;array&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;dt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;h&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="c1"&gt;# measurement function
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&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="c1"&gt;# observe position only
&lt;/span&gt;
&lt;span class="n"&gt;ukf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UnscentedKalmanFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;state&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;2&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;covariance&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;eye&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;transition_covariance&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;eye&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="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;measurement_covariance&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;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;]]),&lt;/span&gt;
    &lt;span class="n"&gt;transition_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;measurement_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ukf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&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;ukf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;1.2&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;ukf&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UKF needs no derivatives; the EKF (&lt;code&gt;ExtendedKalmanFilter&lt;/code&gt;) is similar but also takes Jacobian functions. The same &lt;code&gt;predict&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; loop applies to all ten filters kalbee ships — including Particle, Ensemble, Information, Square-Root, and Vectorized filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not sure which filter? Compare them
&lt;/h2&gt;

&lt;p&gt;kalbee has a built-in experiment runner that races several filters on a synthetic 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kalbee&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run_experiment&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;# or "linear", "step", "maneuver"
&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kf&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;ekf&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;ukf&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;pf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;noise_std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&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="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a ranked table of position/velocity RMSE and a consistency metric (NEES) for each filter — a quick way to pick the right tool before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking many objects
&lt;/h2&gt;

&lt;p&gt;To track multiple objects (people in a video, blips on a radar), wrap a filter in a &lt;code&gt;MultiObjectTracker&lt;/code&gt;. You supply a small factory that builds a fresh filter for each new object:&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;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;kalbee&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KalmanFilter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MultiObjectTracker&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kalbee.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_measurement_model&lt;/span&gt;

&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&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;process_var&lt;/span&gt;&lt;span class="o"&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;n_dims&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;H&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;position_measurement_model&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dims&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;measurement_var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&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;new_track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Seed state [x, vx, y, vy] at the detection, zero initial velocity.
&lt;/span&gt;    &lt;span class="n"&gt;x0&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;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;z&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="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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;z&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="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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;KalmanFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&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;eye&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Q&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;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tracker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultiObjectTracker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_init&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;max_age&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;gate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Each frame, pass a (D, 2) array of detected positions:
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;detections&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;detection_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;confirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detections&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;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  pos=(&lt;/span&gt;&lt;span class="si"&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&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="si"&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;state&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tracker handles the hard parts for you: matching detections to existing tracks (Hungarian algorithm with distance gating) and managing each track's lifecycle. The knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;n_init&lt;/code&gt;&lt;/strong&gt; — how many consecutive detections before a track is "confirmed" (filters out flicker).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_age&lt;/code&gt;&lt;/strong&gt; — how many missed frames before a track is deleted (handles occlusion).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gate&lt;/code&gt;&lt;/strong&gt; — the maximum distance for a valid match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only confirmed tracks come back from &lt;code&gt;update()&lt;/code&gt;. Feed it the box centers from a detector like YOLO and you have a complete tracking pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the data pick your noise values
&lt;/h2&gt;

&lt;p&gt;Struggling to choose &lt;code&gt;Q&lt;/code&gt; and &lt;code&gt;R&lt;/code&gt;? Learn them from a recording with EM (Expectation-Maximization):&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;kalbee&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;em_kalman&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kalbee.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_measurement_model&lt;/span&gt;

&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constant_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&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;n_dims&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;H&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="nf"&gt;position_measurement_model&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_dims&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="c1"&gt;# measurements: array of shape (T, m)
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;em_kalman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Learned Q:&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="n"&gt;result&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Learned R:&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="n"&gt;result&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Converged:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;converged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fit once on representative data, then plug &lt;code&gt;result.Q&lt;/code&gt; and &lt;code&gt;result.R&lt;/code&gt; into a live &lt;code&gt;KalmanFilter&lt;/code&gt;. It's a principled alternative to hand-tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shapes matter.&lt;/strong&gt; States are column vectors &lt;code&gt;(n, 1)&lt;/code&gt;; measurements are &lt;code&gt;(m, 1)&lt;/code&gt;. When in doubt, &lt;code&gt;reshape(-1, 1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start uncertain.&lt;/strong&gt; A large initial &lt;code&gt;P&lt;/code&gt; lets the filter trust early measurements and converge fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch consistency, not just error.&lt;/strong&gt; If the average NEES is far from your state dimension, your &lt;code&gt;Q&lt;/code&gt;/&lt;code&gt;R&lt;/code&gt; are mistuned — the metrics module (&lt;code&gt;nees&lt;/code&gt;, &lt;code&gt;nis&lt;/code&gt;, &lt;code&gt;log_likelihood&lt;/code&gt;) tells you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility is built in.&lt;/strong&gt; Sampling filters (particle, ensemble) and the signal generators accept a &lt;code&gt;seed&lt;/code&gt;/&lt;code&gt;rng&lt;/code&gt; argument, so runs are deterministic when you want them to be.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://minlee0210.github.io/kalbee" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; has a dedicated page per filter with the underlying math and worked examples.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;examples/&lt;/code&gt; folder has runnable scripts, including a full multi-object tracking demo and YOLO integration.&lt;/li&gt;
&lt;li&gt;Everything shares the same &lt;code&gt;predict&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; interface — so once you've done this tutorial, the rest of the library is just variations on what you already know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy estimating.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>computervision</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Meet kalbee: State Estimation and Target Tracking, Minus the Pain</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:54:44 +0000</pubDate>
      <link>https://dev.to/minh-leduc/meet-kalbee-state-estimation-and-target-tracking-minus-the-pain-3pmg</link>
      <guid>https://dev.to/minh-leduc/meet-kalbee-state-estimation-and-target-tracking-minus-the-pain-3pmg</guid>
      <description>&lt;p&gt;If you've ever tracked a car from jittery GPS, smoothed noisy YOLO boxes, or fused a handful of sensors, you know the drill. You either hand-roll filter equations and pray your covariance stays positive-definite, or you wrestle a heavyweight legacy toolkit built for a PhD thesis, not your project.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ibct3yutjwde6e67n0a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ibct3yutjwde6e67n0a.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;kalbee&lt;/strong&gt; is the third option: a clean, modular, numerically stable Python library for modern state estimation and target tracking — one unified API across 10 filters, a smoother, and a full diagnostics suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;10 filters, one interface:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kalman Filter&lt;/strong&gt; — Joseph-form updates for a rock-solid baseline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended KF&lt;/strong&gt; — analytical Jacobians for nonlinear systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unscented KF&lt;/strong&gt; — sigma-point propagation, no Jacobians needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Particle Filter&lt;/strong&gt; — non-Gaussian / Monte Carlo estimation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensemble KF&lt;/strong&gt; — built for high-dimensional states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Information Filter&lt;/strong&gt; — the natural fit for multi-sensor fusion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpha-Beta-Gamma&lt;/strong&gt; — lightweight fixed-gain tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive KF&lt;/strong&gt; — estimates noise Q and R on the fly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Square-Root KF&lt;/strong&gt; — Cholesky-based updates, roundoff-proof&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vectorized KF&lt;/strong&gt; — batched multi-target tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus the &lt;strong&gt;IMM&lt;/strong&gt; estimator (switching motion models), an &lt;strong&gt;RTS smoother&lt;/strong&gt; for backward passes, and diagnostics — &lt;strong&gt;RMSE, NEES, NIS, log-likelihood&lt;/strong&gt; — so you can actually prove your filter is consistent, not just eyeball it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stability you don't have to think about
&lt;/h2&gt;

&lt;p&gt;Textbook Kalman filters die quietly. Roundoff error creeps in, &lt;code&gt;P&lt;/code&gt; loses symmetry or positive-definiteness, and your estimate drifts or crashes. kalbee handles this for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Joseph-form updates&lt;/strong&gt; keep &lt;code&gt;P&lt;/code&gt; positive-semidefinite through aggressive measurement updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Square-Root filtering&lt;/strong&gt; propagates the Cholesky factor directly, sidestepping singularity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Singular-inversion fallbacks&lt;/strong&gt; auto-regularize and fall back to pseudoinverses when things get numerically ugly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Showcase: tracking a car through YOLO boxes
&lt;/h2&gt;

&lt;p&gt;Occlusions and sharp turns are where naive trackers fall apart. On a real video stream, tracking a vehicle against raw YOLO detections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Filter                | Mean deviation (px) | Notes
----------------------|---------------------|---------------------------------
Standard Kalman       |        7.36         | Overshoots on turns
Square-Root Kalman    |        7.36         | Same accuracy, roundoff-robust
Particle Filter       |       57.84         | Drifts when YOLO loses confidence
IMM Blended           |        6.03         | Best — adapts to speed changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why IMM wins:&lt;/strong&gt; it runs Constant-Velocity and Constant-Acceleration models in parallel. Cruising straight? It leans on Constant Velocity. The instant the car brakes or turns, weight shifts to Constant Acceleration, cutting lag. During occlusions, it extrapolates smoothly from velocity history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three lines to your first comparison
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#pip install kalbee
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kalbee&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run_experiment&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sine&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kf&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;ekf&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;ukf&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;pf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;noise_std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call, four filters, a ranked report. Swap signals, add filters, tune noise — iterate in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/MinLee0210/kalbee" rel="noopener noreferrer"&gt;https://github.com/MinLee0210/kalbee&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://minlee0210.github.io/kalbee/" rel="noopener noreferrer"&gt;https://minlee0210.github.io/kalbee/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tutorial:&lt;/strong&gt; the hands-on &lt;code&gt;tracking_tutorial.ipynb&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a robot, processing camera feeds, or researching sensor fusion — kalbee gives you the numerical stability of a serious toolkit with an interface you'll actually enjoy using.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>wavio</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Sat, 11 Jul 2026 03:16:46 +0000</pubDate>
      <link>https://dev.to/minh-leduc/wavio-3d6</link>
      <guid>https://dev.to/minh-leduc/wavio-3d6</guid>
      <description>&lt;p&gt;| Audio Fingerprinting Without the ML Tax&lt;br&gt;
Most audio identification tools today reach for embeddings and neural nets. &lt;code&gt;wavio&lt;/code&gt; doesn't.&lt;/p&gt;

&lt;p&gt;It's a fast, deterministic acoustic fingerprinting library written in Rust — built on the same peak-based approach as Shazam, with none of the ML overhead. No embeddings, no models, no runtime. Just spectral peaks, combinatorial hashing, and raw speed.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;wavio runs a straightforward DSP pipeline:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcw5puas6d3wcy9rgdqm0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcw5puas6d3wcy9rgdqm0.png" alt="pipeline" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each step is deterministic: the same input always produces the same fingerprint. That makes results reproducible and debuggable — no model drift, no version mismatches.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it's fast
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-memory &amp;amp; on-disk indexing&lt;/strong&gt; — query thousands of tracks in microseconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero unsafe code&lt;/strong&gt; (&lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional parallelism&lt;/strong&gt; via rayon&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benchmarks on synthetic 22,050 Hz audio (release build):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fingerprint a 3-min track&lt;/td&gt;
&lt;td&gt;88.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index 1,000 tracks&lt;/td&gt;
&lt;td&gt;1.04 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query (1,000 lookups)&lt;/td&gt;
&lt;td&gt;0.57 µs/query&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Use it from Rust, Python, or the CLI
&lt;/h2&gt;

&lt;p&gt;wavio ships as a Rust crate, a Python package (via PyO3/maturin), and a CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wavio-cli index &lt;span class="nt"&gt;--db&lt;/span&gt; ./wavio.db ./music/
wavio-cli query &lt;span class="nt"&gt;--db&lt;/span&gt; ./wavio.db ./clip.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;If you're building music identification, duplicate detection, or content matching and don't want to carry an ML stack for it, wavio gives you a lean, predictable alternative.&lt;/p&gt;

&lt;p&gt;Check it out: &lt;a href="https://github.com/MinLee0210/wavio" rel="noopener noreferrer"&gt;github.com/MinLee0210/wavio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>learning</category>
      <category>startup</category>
    </item>
    <item>
      <title>Enigmar</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Thu, 02 Jul 2026 14:40:52 +0000</pubDate>
      <link>https://dev.to/minh-leduc/enigmar-35lf</link>
      <guid>https://dev.to/minh-leduc/enigmar-35lf</guid>
      <description>&lt;p&gt;| A High-Performance Enigma Machine Simulator in Rust, with Python Bindings&lt;/p&gt;

&lt;p&gt;Link: &lt;a href="https://lib.rs/crates/enigmar" rel="noopener noreferrer"&gt;https://lib.rs/crates/enigmar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Few machines capture the imagination like the Enigma. Used by the Wehrmacht in World War II and famously broken at Bletchley Park, its cipher logic — rotors, reflectors, plugboards, and a maddening quirk called double-stepping — is a rite of passage for anyone interested in classical cryptography.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enigmar&lt;/strong&gt; is an educational library that faithfully recreates the Enigma M3/M4 machines. It's written in Rust for speed and correctness, with Python bindings via PyO3 so you can experiment without leaving your notebook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It's Interesting
&lt;/h2&gt;

&lt;p&gt;Most Enigma simulators get the easy parts right and fumble the details. Enigmar focuses on historical accuracy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotors I–VIII&lt;/strong&gt; and &lt;strong&gt;Reflectors B, C, B-thin, C-thin&lt;/strong&gt;, matching the real wiring diagrams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct double-stepping&lt;/strong&gt; — the mechanical anomaly where the middle rotor sometimes steps twice in a row, a quirk of the real machine's ratchet mechanism that many simulators simplify away&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reciprocal encryption&lt;/strong&gt; — because of how the reflector works, encrypting and decrypting are literally the same operation with the same settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No letter self-encryption&lt;/strong&gt; — a structural weakness of the real Enigma that cryptanalysts exploited; Enigmar reproduces it faithfully rather than "fixing" it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, the core uses &lt;code&gt;[u8; 26]&lt;/code&gt; lookup tables for zero-allocation, O(1) character mapping, so it's fast enough for large-scale experimentation or teaching demos.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Signal Path Works
&lt;/h2&gt;

&lt;p&gt;Every keystroke travels through the plugboard, three rotors, a reflector, and back out through the rotors and plugboard again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Plugboard → Rotor III → Rotor II → Rotor I → Reflector
                                                          ↓
Output ← Plugboard ← Rotor III ← Rotor II ← Rotor I ←────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before each character is processed, the right rotor always steps. If it's sitting at its notch, the middle rotor steps too. And if the middle rotor is at its notch, both it and the left rotor step together — the double-stepping anomaly that made the real machine's period shorter than naive rotor math would suggest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rust
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;enigmar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;enigmar&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EnigmaBuilder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EnigmaBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.rotor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.rotor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"II"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.rotor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"III"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.reflector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.plugboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AV BS CG DL FU HZ IN KM OW RX"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="nf"&gt;.process_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HELLOWORLD"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Encrypted: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&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;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;maturin
maturin develop &lt;span class="nt"&gt;--features&lt;/span&gt; extension-module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;enigmar&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EnigmaBuilder&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EnigmaBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotor&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;II&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;III&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plugboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AV BS CG DL FU HZ IN KM OW RX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HELLOWORLD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Encrypted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ciphertext&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="c1"&gt;# Save and restore state
&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;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export_key&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;import_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decryption uses the exact same settings — build a fresh machine with identical rotors, reflector, and plugboard, then run the ciphertext back through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  API at a Glance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EnigmaBuilder&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create an empty builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.rotor(type, position, ring)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add a rotor, left to right. Types &lt;code&gt;"I"&lt;/code&gt;–&lt;code&gt;"VIII"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.reflector(type)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set reflector: &lt;code&gt;"B"&lt;/code&gt;, &lt;code&gt;"C"&lt;/code&gt;, &lt;code&gt;"B-thin"&lt;/code&gt;, &lt;code&gt;"C-thin"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.plugboard(pairs)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set up to 13 plug pairs, e.g. &lt;code&gt;"AB CD EF"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.build()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Produce the &lt;code&gt;EnigmaMachine&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;EnigmaMachine&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;process_string(input)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encrypt/decrypt; non-alpha characters are dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export_key()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Serialize state to JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import_key(key)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restore state from JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reset()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reset rotors to their initial positions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who It's For
&lt;/h2&gt;

&lt;p&gt;Enigmar is built for learning: cryptography students exploring rotor ciphers, developers curious about PyO3-based Rust/Python interop, or anyone who wants to reproduce Bletchley-era encryption on modern hardware. The JSON key export also makes it easy to save, share, and reproduce exact machine configurations for teaching or testing.&lt;/p&gt;

&lt;p&gt;Give it a try — encrypt something, export the key, and decrypt it back with a fresh machine instance. It's a small, satisfying way to see 1930s engineering behave exactly as designed.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>performance</category>
      <category>python</category>
      <category>rust</category>
    </item>
    <item>
      <title>How to build an autonomous news Generator with AI using Fluvio</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Mon, 02 Sep 2024 07:47:17 +0000</pubDate>
      <link>https://dev.to/minh-leduc/how-to-build-an-autonomous-news-generator-with-ai-using-fluvio-8g3</link>
      <guid>https://dev.to/minh-leduc/how-to-build-an-autonomous-news-generator-with-ai-using-fluvio-8g3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Building a News Bot with Fluvio&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In my previous article, I introduced the concept of event-driven architecture (EDA) and demonstrated its capabilities using Fluvio. I showcased how an application could leverage EDA to asynchronously send quotes from a publisher to subscribers at regular intervals.&lt;/p&gt;

&lt;p&gt;In this article, I will expand upon my previous work by introducing additional features that enhance the application's functionality. I will delve into how to integrate a search engine to discover relevant quotes and utilize Large Language Models (LLMs) to summarize these quotes effectively. By combining these elements, I aim to create a more robust and informative application that leverages the power of EDA and AI, named Wipe.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: For better experience, I recommend reading my post on &lt;a href="https://minhleduc.substack.com/p/how-to-build-an-autonomous-news-generator" rel="noopener noreferrer"&gt;Substack&lt;/a&gt;. In addition, I am participating in a quest on Quira, please up vote for me. Here is the &lt;a href="https://quira.sh/repo/MinLee0210-Wipe-844324899" rel="noopener noreferrer"&gt;link&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  What is Wipe?
&lt;/h1&gt;

&lt;p&gt;Tired of Falling Behind in the Fast-Paced World of AI? I understand the frustration of trying to keep up with the constant stream of new technologies and trends in the AI landscape. It can be overwhelming to stay informed about the latest developments while also focusing on building your projects.&lt;/p&gt;

&lt;p&gt;Introducing Wipe, your AI-powered solution. By leveraging a powerful combination of search engines and Large Language Models, Wipe automatically curates the most relevant AI news and condenses it into concise summaries. No more sifting through countless articles. With Wipe, you can stay ahead of the curve and ensure your projects are always built with the latest insights and technologies.&lt;/p&gt;

&lt;h1&gt;
  
  
  Features
&lt;/h1&gt;

&lt;p&gt;As an AI enthusiast, I've often found myself asking: How can I stay up-to-date with the rapid advancements in this field? Is there a way to capture the essence of countless articles in a matter of seconds?&lt;/p&gt;

&lt;p&gt;Wipe, your AI-powered solution, offers the following benefits:&lt;/p&gt;

&lt;p&gt;Real-time Updates: Stay informed about the latest AI trends and breakthroughs.&lt;/p&gt;

&lt;p&gt;Instant Summarization: Use Large Language Models to quickly grasp the key points of articles.&lt;/p&gt;

&lt;p&gt;Workflow&lt;/p&gt;

&lt;p&gt;Data Ingestion: The Publisher continuously collects and ingests raw AI trend data from various sources.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature Extraction: The Feature component processes this data, extracting relevant features and insights through techniques like natural language processing and data analysis.&lt;/li&gt;
&lt;li&gt;Content Refinement: The Feature component further refines the extracted content, summarizing key points or providing additional context.&lt;/li&gt;
&lt;li&gt;Notification Distribution: The Notification component sends the processed and refined AI trend updates to interested Consumers. In these settings, fluvio handles this component very pretty.&lt;/li&gt;
&lt;li&gt;Consumer Utilization: Consumers receive these updates and leverage them for their specific AI applications, such as model training, product development, or research.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Prerequisites
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Event-driven Architecture (EDA)
&lt;/h2&gt;

&lt;p&gt;I would love to remind myself a little bit about the EDA. Event-driven architecture is a design pattern where applications respond to events asynchronously. This allows for greater scalability and responsiveness compared to traditional request-response models. Events can be triggered by various sources, such as user actions, system changes, or external data feeds.&lt;/p&gt;

&lt;p&gt;Inside event-driven architectures EDA is widely used in various domains, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time data processing: Processing financial market data, IoT sensor data, and other time-sensitive information.&lt;/li&gt;
&lt;li&gt;Microservices architecture: Decoupling services, facilitating asynchronous communication, and enabling independent scaling.&lt;/li&gt;
&lt;li&gt;Serverless computing: Executing functions in response to events, such as file uploads, database changes, or API calls.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;At the core of Wipe lies a robust technological stack designed to deliver real-time updates and insightful summaries. Let's break down the key components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fluvio: As a high-performance streaming engine, Fluvio efficiently handles the continuous flow of data, ensuring that news articles are processed and delivered promptly. Its Rust-based ## architecture guarantees low latency and security.&lt;/li&gt;
&lt;li&gt;Redis: This in-memory data store acts as a central hub, storing and retrieving data seamlessly between the publisher and consumer components.&lt;/li&gt;
&lt;li&gt;Langchain: By providing a vast array of Large Language Models (LLMs), Langchain empowers Wipe to understand and summarize complex articles with exceptional accuracy.&lt;/li&gt;
&lt;li&gt;Tavily Search: This AI-integrated search engine plays a crucial role in identifying relevant news articles, ensuring that Wipe delivers only the most pertinent information to its users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these components form a powerful synergy that enables Wipe to provide users with timely, accurate, and informative AI news updates.&lt;/p&gt;

&lt;h1&gt;
  
  
  Getting Started
&lt;/h1&gt;

&lt;p&gt;Wipe relies heavily on Docker; therefore, you should utilize Docker to get the best result. First and foremost, let’s clone the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone &amp;lt;https://github.com/MinLee0210/Wipe.git&amp;gt;
cd ./Wipe
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Environment Setup
&lt;/h2&gt;

&lt;p&gt;To setup the environment for the project, you must to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the Fluvio (view my previous article or on Fluvio’s website).&lt;/li&gt;
&lt;li&gt;Install Redis (Wipe used Redis on Docker, I’ll leave the link here).&lt;/li&gt;
&lt;li&gt;Get API keys from Tavily (a must) and a LLM’s provider that you want to use (Gemini, Groq, OpenAI), remember to change the configuration from config.yaml file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure to create a .env file that follows this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TAVILY_API_KEY=""
GEMINI_API_KEY=""
GROQ_API_KEY=""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Experiment Setup
&lt;/h2&gt;

&lt;p&gt;I can not show everything of the code in this blog; however, I will show 3 most important components of the app and the logic of the news_features .&lt;/p&gt;

&lt;p&gt;The WipeProducer.&lt;/p&gt;

&lt;p&gt;The WipeConsumer.&lt;/p&gt;

&lt;p&gt;The WipeDB.&lt;/p&gt;

&lt;p&gt;Before deep dive into those 3 components, I will show the get_latest_trends()function that allows getting the latest trends in the AI field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TREND = "What is the latest trend in AI 2024?"

def get_latest_trend() -&amp;gt; tuple[list[Article], list[Event]]:
    """
    Retrieves the latest trend in AI and returns a list of summarized articles.

    Returns:
        list[Article]: A list of Article objects containing summaries of relevant news.
    """

    # Find relevant URLs
    urls = [result["url"] for result in searcher.run(TREND)["results"]]

    # Filter out unsupported URLs and scrape content
    docs = []
    for url in urls:
        try:
            docs.append(scraper.run(url))
        except ValueError:
            continue  # Skip unsupported URLs


    # Process and summarize articles
    articles, events = [], []
    for idx, doc in enumerate(docs):
        metadata = doc[0].metadata
        content = clean(doc[0].page_content)

        summary_prompt = SUMMARY_ARTICLE.format(article=content)
        summary = llm.invoke(summary_prompt).content  

        # Create Article object with metadata and summary
        article = Article(summary=summary, **metadata)
        articles.append(article)
        # Create Event object with Article's information
        event_title = f"Latest Trend in AI (2024) - {article.title}"  # Modify title creation if needed
        event = Event(title=event_title, 
                    article_id=article.id)
        events.append(event)

    return (articles, events)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TREND is defined to be strict and related to our topic ai-trends. However, it can be extended further into any topics that you want it to be. The flow of the algorithm behaves as follows:&lt;/p&gt;

&lt;p&gt;It gets trends from a bunch of websites that are relevant based on Tavily Search Engine.&lt;/p&gt;

&lt;p&gt;Those websites are then scrapped via LangChain’s WebBaseLoader and summarized via an LLM (I used Gemini; additionally, you can use another library to simplify this stage, such as ScrapeGraphAI).&lt;/p&gt;

&lt;p&gt;The processed documents are then fed into 2 objects: Event and Article. The former is sent to the Consumer to notify them there are new trends that are gathered successfully; the latter is saved in the database, and based on the Consumer’s choice, the Article is then read by getting it from the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  WipeProducer
&lt;/h3&gt;

&lt;p&gt;Here is the code of the Producer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
"""
A simple Fluvio producer that produces records to a topic.
"""
import subprocess

from fluvio import Fluvio

class WipeProducer:
    """
    A class to produce records to a Fluvio topic.

    Attributes:
    ----------
    topic_name : str
        The name of the topic to produce to.
    partition : int
        The partition to produce to.
    producer : Fluvio.topic_producer
        The Fluvio producer object.

    Methods:
    -------
    produce_records(num_records)
        Produces a specified number of records to the topic.
    flush()
        Flushes the producer to ensure all records are sent.
    """
    ROLE = "producer"

    def __init__(self, topic_name: str, partition: int):
        """
        Initializes the FluvioProducer object.

        Parameters:
        ----------
        topic_name : str
            The name of the topic to produce to.
        partition : int
            The partition to produce to.
        """
        self.topic_name = topic_name
        self.partition = partition
        self.producer = Fluvio.connect().topic_producer(topic_name)

    def produce_records(self, event: str) -&amp;gt; None:
        """
        Produces a specified event.

        Parameters:
        ----------
        event : str
            The information of the event
        """
        try:
                self.producer.send_string(event)

        except Exception as e:
            print(f"Error producing records: {e}")

    def flush(self) -&amp;gt; None:
        """
        Flushes the producer to ensure all records are sent.
        """
        try:
            self.producer.flush()
            print("Producer flushed successfully")
        except Exception as e:
            print(f"Error flushing producer: {e}")

    def __create_topic(self, topic_name:str):
        """
        Create a topic. 

        Parameters: 
        ----------
        topic_name: str
            The name of the topic
        """
        try:
            shell_cmd = ['fluvio', 'topic', 'create', topic_name]
            subprocess.run(shell_cmd, check=True)
        except subprocess.CalledProcessError as e:
            print(f'Command {e.cmd} failed with error {e.returncode}')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Producer object has 3 main methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to the chosen topic via the constructor.&lt;/li&gt;
&lt;li&gt;Send records to the Consumer.&lt;/li&gt;
&lt;li&gt;Flushes the producer to ensure all records are sent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also provide a method for Python code to execute shell commands, allowing creating topics via the Producer interface.&lt;/p&gt;

&lt;p&gt;The logic of the Producer is defined as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;producer = WipeProducer(topic_name=config["pubsub"]["topic"],
                            partition=config["pubsub"]["partition"])

# ===== PRODUCER'S METHODS =====
def pub_produce_articles():
    """
    Publishes summarized articles to the defined topic.
    """
    trends = get_latest_trend() #   (articles, events)
    for article, event in zip(trends[0], trends[1]):

        event_str = json_to_str(event.json())
        producer.produce_records(event_str)  # Serialize event to JSON

        article_str = json_to_str(article.json())
        wipe_db.set_article(id=article.id, 
                            role=producer.ROLE, 
                            content=article_str)
    producer.flush()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  WipeConsumer
&lt;/h3&gt;

&lt;p&gt;The implementation of the WipeConsumer is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
"""
A simple Fluvio consumer that consumes records from a topic.
"""

from datetime import datetime
from fluvio import Fluvio, Offset

class WIPEConsumer:
    """
    A class to consume records from a Fluvio topic.

    Attributes:
    ----------
    role : str
        The role of the consumer (in this case, 'customer').
    topic_name : str
        The name of the topic to consume from.
    partition : int
        The partition to consume from.
    consumer : Fluvio.partition_consumer
        The Fluvio consumer object.

    Methods:
    -------
    consume_records(num_records)
        Consumes a specified number of records from the topic.
    """

    ROLE = 'customer'

    def __init__(self, topic_name: str, partition: int):
        """
        Initializes the WIPEConsumer object.

        Parameters:
        ----------
        topic_name : str
            The name of the topic to consume from.
        partition : int
            The partition to consume from.
        """
        self.topic_name = topic_name
        self.partition = partition
        self.consumer = Fluvio.connect().partition_consumer(topic_name, partition)
        self.notification = []

    def consume_records(self, num_records: int) -&amp;gt; None:
        """
        Consumes a specified number of records from the topic.

        Parameters:
        ----------
        num_records : int
            The number of records to consume.
        """
        try:
            for idx, record in enumerate(self.consumer.stream(Offset.from_end(num_records))):
                print(f"Record {idx+1}: {record.value_string()}: timestamp: {datetime.now()}")
                self.notification.append(record.value_string())
                if idx &amp;gt;= num_records - 1:
                    break
        except Exception as e:
            print(f"Error consuming records: {e}")

    def flush(self): 
        """
        Delete the notification
        """
        self.notification = []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Consumer object has 3 main methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a connection to the Producer based on the chosen topic via the constructor.&lt;/li&gt;
&lt;li&gt;Consume the records: Wipe sets the Consumer to consume the 1-latest article from the Producer.&lt;/li&gt;
&lt;li&gt;Delete notification: Everytime the Producer creates an article, it will notify its consumers. The Consumer will then store it in the &lt;code&gt;self.notification&lt;/code&gt;; based on the Consumer’s choice, the article is then read by getting it from the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The logic of the Consumer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ===== CONSUMER'S METHODS =====
def sub_catch_articles():
    """
    Consumes events from the topic and processes them (implementation pending).
    """
    logger.info("[CONSUMER]: Catch events from Producer")
    consumer.consume_records(config["pubsub"]["num_records_consume"])

def sub_read_articles(): 
    """
    Retrieve articles from the database based on the events.
    Note: 
        Assume the Consumer chose the 1-latest event. 
    """

    logger.info("[CONSUMER]: Get event")
    event = consumer.notification[-1]

    while not isinstance(event, dict):
        event = str_to_json(event)

    logger.info("[CONSUMER]: Get article")
    article_id = event['article_id']
    article = wipe_db.get_article(id=article_id, 
                                  role=consumer.ROLE)

    logger.info(f"[CONSUMER]: Reading\n{article}")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  WipeDB
&lt;/h3&gt;

&lt;p&gt;For the sake of simplicity, the implementation of the database is quite simple: get and set articles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
"""
Define database logic for WIPE.
"""

import redis

AUTHORIZED_METHODS = {
    'get': ['customer', 'producer'], 
    'set': 'producer'
}

class WIPEDB(object):
    """
    A class to handle database operations for WIPE.

    Attributes:
    ----------
    server : redis.Redis
        The Redis database connection.

    Methods:
    -------
    get_article(id, role)
        Retrieves an article from the database.
    set_article(id, role)
        Sets an article in the database.
    """

    def __init__(self, db_config: dict):
        """
        Initializes the WIPEDB object.

        Parameters:
        ----------
        db_config : dict
            A dictionary containing the Redis database configuration.
        """
        self.server = self.__set_db_connection(db_config)

    def get_article(self, id: str, role: str) -&amp;gt; str:
        """
        Retrieves an article from the database.

        Parameters:
        ----------
        id : str
            The ID of the article to retrieve.
        role : str
            The role of the user requesting the article.

        Returns:
        -------
        str
            The article content if the user is authorized, otherwise None.
        """
        if role not in AUTHORIZED_METHODS['get']:
            return None
        try:
            return self.server.get(id)
        except redis.exceptions.RedisError as e:
            raise e

    def set_article(self, id: str, role: str, content: str) -&amp;gt; bool:
        """
        Sets an article in the database.

        Parameters:
        ----------
        id : str
            The ID of the article to set.
        role : str
            The role of the user setting the article.
        content : str
            The content of the article.

        Returns:
        -------
        bool
            True if the article was set successfully, otherwise False.
        """
        if role != AUTHORIZED_METHODS['set']:
            return False
        try:
            self.server.set(id, content)
            return True
        except redis.exceptions.RedisError as e:
            raise e

    def __set_db_connection(self, db_config: dict) -&amp;gt; redis.Redis:
        """
        Establishes a connection to the Redis database.

        Parameters:
        ----------
        db_config : dict
            A dictionary containing the Redis database configuration.

        Returns:
        -------
        redis.Redis
            The Redis database connection.
        """
        try:
            server = redis.Redis(**db_config)
            return server
        except redis.ConnectionError as r_ce:
            raise r_ce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the app more fun, Wipe constructs the app to have the authorization for interacting with the database. For instance, the Consumer is restricted to make a set method to the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code in Action
&lt;/h2&gt;

&lt;p&gt;From the producer, the code be like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./producer.py

import time

from controller.pubsub import pub_produce_articles


while True: 
    pub_produce_articles()  # Avg of 35 secs per call.
    time.sleep(10)
For every 10 seconds, the Producer will look up the Internet for the latest AI trends.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Consumer will also get the notification after every 10 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./consumer.py

import time
import random
from controller.pubsub import sub_catch_articles, sub_read_articles

while True: 
    sub_catch_articles()
    time.sleep(10)

    rand_idx = random.randint(a=0, b=10)
    if rand_idx % 2 == 0: 
        sub_read_articles()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a more engaging experience, I incorporated a random element into the notification system. If the random outcome met specific conditions, the Consumer would be shown the article.&lt;/p&gt;

&lt;p&gt;To run the app, simply type&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python producer.py &amp;amp;
python cosumer.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Result
&lt;/h1&gt;

&lt;p&gt;The Producer automatically gets the latest trends from the Internet and uses AI to summarize the website every 10 seconds. After the summarization is done, it makes an event to notify its customers.&lt;/p&gt;

&lt;p&gt;The Customer retrieves a notification from its producer. In this experiment, I set it to randomly choose whether to "read" the news from the notification or not.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I have successfully extended the capabilities of the event-driven architecture (EDA) application introduced in the previous installment.&lt;/p&gt;

&lt;p&gt;By integrating a search engine and utilizing Large Language Models (LLMs), the application, now named Wipe, has become a more comprehensive and informative tool. The ability to discover relevant quotes and generate concise summaries enhances the user experience and provides valuable insights into the vast world of AI.&lt;/p&gt;

&lt;p&gt;The successful implementation of these features demonstrates the versatility and power of EDA in creating robust and scalable applications.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>pubsub</category>
      <category>challenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to build an event-driven architecture with Fluvio</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Wed, 28 Aug 2024 03:29:20 +0000</pubDate>
      <link>https://dev.to/minh-leduc/how-to-build-an-event-driven-architecture-with-fluvio-3enh</link>
      <guid>https://dev.to/minh-leduc/how-to-build-an-event-driven-architecture-with-fluvio-3enh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Get started on a journey into the world of event-driven architecture with Fluvio. This powerful platform offers a streamlined approach to building real-time, scalable, and resilient applications. By leveraging Fluvio's capabilities, you can unlock the full potential of event-driven design and create innovative solutions that meet the demands of today's dynamic environments.&lt;/p&gt;

&lt;p&gt;In this guide, we'll delve into the intricacies of Fluvio, exploring its key features, benefits, and practical implementation strategies. You'll learn how to utilize the power of event-driven architecture to build applications that are responsive, scalable, and efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some information
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Event-driven architecture
&lt;/h3&gt;

&lt;p&gt;Imagine you're hosting a party. You want to notify everyone when the pizza arrives. Instead of shouting to each guest individually, you could simply announce it once, and everyone who's interested in pizza will hear and react accordingly.&lt;/p&gt;

&lt;p&gt;This is essentially the concept of event-driven architecture. It's a design pattern where components of a system communicate by producing and consuming events. Think of it as a way to create a more dynamic and responsive system, similar to how your party guests react to your announcement.&lt;/p&gt;

&lt;p&gt;Now, let's introduce Pub/Sub.&lt;/p&gt;

&lt;p&gt;Imagine you're the party host (the publisher). When the pizza arrives, you publish an event called "Pizza Is Here.". Your guests (the subscribers) can subscribe to this event. When they hear your announcement (the event), they'll take action (e.g., grab a slice).&lt;/p&gt;

&lt;p&gt;In a pub/sub system, the publisher sends out events, and subscribers can choose to listen to specific events. This decouples the components, making the system more scalable, flexible, and resilient.&lt;/p&gt;

&lt;p&gt;Here's a more technical breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publisher: Produces events and sends them to a message broker.&lt;/li&gt;
&lt;li&gt;Message Broker: Stores and distributes events to interested subscribers.&lt;/li&gt;
&lt;li&gt;Subscriber: Consumes events and takes appropriate actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine a social media platform. When a user posts a new message, that's an event. Other users who follow that user can subscribe to their posts and receive notifications whenever a new message is published.&lt;/p&gt;

&lt;p&gt;Key benefits of Pub/Sub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalability: handles large volumes of events efficiently.&lt;/li&gt;
&lt;li&gt;Flexibility: Allows for dynamic subscriptions and decoupled components.&lt;/li&gt;
&lt;li&gt;Resilience: Ensures messages are delivered even if components fail.&lt;/li&gt;
&lt;li&gt;Real-time updates: Enables real-time communication and updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: I found an interesting video that can help you easily understand the concept; here is the link.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fluvio
&lt;/h3&gt;

&lt;p&gt;Fluvio's exceptional performance and efficiency make it a standout choice for real-time data processing. Its low-latency capabilities ensure that data is processed swiftly, enabling applications to respond to events in a timely manner. Furthermore, Fluvio's lightweight design and optimized architecture minimize resource consumption, making it suitable for even the most resource-constrained environments.&lt;/p&gt;

&lt;p&gt;Fluvio's rich API support and customizable stream processing capabilities make it a developer's dream. With client libraries available for popular programming languages, you can easily integrate Fluvio into your existing applications. The platform's programmability allows you to tailor data processing pipelines to meet your specific requirements, ensuring maximum flexibility and control.&lt;/p&gt;

&lt;p&gt;Moreover, Fluvio's WebAssembly integration enables you to securely execute custom stream processing logic, providing a powerful and efficient way to extend the platform's capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code in Action
&lt;/h2&gt;

&lt;p&gt;Please read the article via this &lt;a href="https://minhleduc.substack.com/p/how-to-build-an-event-driven-architecture" rel="noopener noreferrer"&gt;website&lt;/a&gt; for detailed implementation and better visualizations. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we talked about one of the greatest architecture in programming: Pub/Sub, a fundamental component of event-driven architecture. It provides a robust and scalable foundation for event-driven architectures, enabling loosely coupled, asynchronous communication between components. In addition, we used Fluvio to demonstrate the architecture by allowing the publisher to generate quote every 7 seconds to the Consumer. Clearly, this framework provides us an easy approach to event-driven architecture.&lt;/p&gt;

&lt;p&gt;If you guys want me to continue this approach in LLMs applications or develop it further,. You guys can comment to let me know!&lt;/p&gt;




&lt;p&gt;Thank you for reading this article; I hope it added something to your knowledge bank! Just before you leave:&lt;/p&gt;

&lt;p&gt;👉 Be sure to press the like button and follow me. It would be a great motivation for me.&lt;/p&gt;

&lt;p&gt;👉 More details of the code refer to: &lt;a href="https://github.com/8Opt/Fluvio-Quote" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 Follow me: &lt;a href="//www.linkedin.com/in/minhle007"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://github.com/MinLee0210" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>eventdriven</category>
      <category>python</category>
      <category>pubsub</category>
    </item>
    <item>
      <title>Boost Your RAG Performance with Tavily Search API</title>
      <dc:creator>firefrog</dc:creator>
      <pubDate>Wed, 31 Jul 2024 09:54:12 +0000</pubDate>
      <link>https://dev.to/minh-leduc/boost-your-rag-performance-with-tavily-search-api-211b</link>
      <guid>https://dev.to/minh-leduc/boost-your-rag-performance-with-tavily-search-api-211b</guid>
      <description>&lt;p&gt;LLMs and RAG systems have shown to be advantageous over time. They not only provide engaging discussions that deliver helpful information, but they also open up new avenues for tailored and intelligent applications, transforming areas ranging from customer service to scientific research. Despite their unique and powerful skills, there is evidence that they can produce plausible-sounding but inaccurate information, particularly when confronted with unclear questions or a lack of relevant data. Furthermore, they have demonstrated a lack of knowledge updates, causing them to occasionally present "old" information.&lt;/p&gt;

&lt;p&gt;To mitigate those issues, the ability to connect to reliable and up-to-date resources is essential. Using an additional tool to retrieve external knowledge can help RAG and LLMs access up-to-date information, mitigating hallucinations and enhancing factual accuracy.&lt;/p&gt;

&lt;p&gt;The Tavily Search API is suitable for that job. It is a search engine designed specifically for LLMs and RAG, with the goal of providing efficient, rapid, and permanent search results. Tavily specializes in improving search results for AI developers and autonomous AI agents. Furthermore, Tavily uses private financial, coding, news, and other internal data sources to supplement web content. As a result, Tavily empowers developers to build more accurate, insightful, and contextually aware AI applications.&lt;/p&gt;

&lt;p&gt;We will talk about the Tavily Search API, diving into its functionalities and how it leverages AI for enhanced search. The structure of this writing is as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding the Power of Tavily Search API: A quick overview of the Tavily Search API, including why it is important and how it works.&lt;/li&gt;
&lt;li&gt;Code in Action: Start with a basic code example showcasing a simple search query using Tavily.&lt;/li&gt;
&lt;li&gt;Conclusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The link of the writing left below 👇👇👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>api</category>
      <category>python</category>
    </item>
  </channel>
</rss>
