<?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: Aftab Bashir</title>
    <description>The latest articles on DEV Community by Aftab Bashir (@aftabkh4n).</description>
    <link>https://dev.to/aftabkh4n</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%2F3863150%2F2da9fdf0-d0a8-4a55-b7ef-415aaeba5982.jpeg</url>
      <title>DEV Community: Aftab Bashir</title>
      <link>https://dev.to/aftabkh4n</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aftabkh4n"/>
    <language>en</language>
    <item>
      <title>Turned my MCP server into a Claude Code plugin. Here's everything that broke.</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:35:00 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/turned-my-mcp-server-into-a-claude-code-plugin-heres-everything-that-broke-1bpi</link>
      <guid>https://dev.to/aftabkh4n/turned-my-mcp-server-into-a-claude-code-plugin-heres-everything-that-broke-1bpi</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/aftabkh4n/i-got-tired-of-re-explaining-my-codebase-to-claude-every-morning-so-i-built-this-5eo5"&gt;my last post&lt;/a&gt; I introduced ContextOS, an MCP server that gives AI coding agents memory across sessions. You open Claude Code in a repo, and it already knows the branch you're on, the decisions you made, and what's still open.&lt;/p&gt;

&lt;p&gt;It worked. But installing it took a shell script, a config file, and a &lt;code&gt;claude mcp add&lt;/code&gt; command. That's where most people stop.&lt;/p&gt;

&lt;p&gt;So I packaged it as a Claude Code plugin. Now it's two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add aftabkh4n/contextos
/plugin install contextos@contextos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fh80nzj995jvbgo02vd00.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%2Fh80nzj995jvbgo02vd00.png" alt=" " width="799" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting there took four patch releases in three days. Every bug was something I'd never have found without installing it on my own machine and using it like a stranger would. If you're building a plugin, I hope this saves you some of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plugin layout
&lt;/h2&gt;

&lt;p&gt;A plugin is a folder with a manifest and some optional parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugins/contextos/
├── .claude-plugin/plugin.json   # name, version, MCP server
├── skills/contextos/SKILL.md    # when to remember and recall
├── hooks/hooks.json             # SessionStart hook
└── scripts/ensure-binary.sh     # downloads the binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a &lt;code&gt;marketplace.json&lt;/code&gt; so people can install it by name.&lt;/p&gt;

&lt;p&gt;Two small things caught me straight away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The validator rejects &lt;code&gt;..&lt;/code&gt; in marketplace paths.&lt;/strong&gt; I wanted the marketplace in its own folder pointing at &lt;code&gt;../contextos&lt;/code&gt;. It won't allow that, so the marketplace manifest moved to the repo root. One repo, one install command. Better anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The install command needs the full name.&lt;/strong&gt; &lt;code&gt;/plugin install contextos&lt;/code&gt; only searches the default marketplace. For your own marketplace it's &lt;code&gt;plugin@marketplace&lt;/code&gt;, so &lt;code&gt;contextos@contextos&lt;/code&gt;. My README had it wrong, and so did I, the first time I tried.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You can't bundle a 118 MB binary
&lt;/h2&gt;

&lt;p&gt;ContextOS is a self-contained .NET binary with a local embedding model inside it. Each platform build is about 118 MB. That doesn't belong in a plugin folder.&lt;/p&gt;

&lt;p&gt;So a SessionStart hook downloads the right build for your OS into &lt;code&gt;${CLAUDE_PLUGIN_DATA}&lt;/code&gt;, a folder that survives plugin updates. The MCP server entry in &lt;code&gt;plugin.json&lt;/code&gt; points there.&lt;/p&gt;

&lt;p&gt;First bug: the script copied only the binary. The embedding model lives in a &lt;code&gt;Models/&lt;/code&gt; folder next to it, so the first version installed a server that failed its own selftest on every machine. Validation passed, of course. Validation checks the manifest, not whether your program runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: My new feature deleted everyone's old memories
&lt;/h2&gt;

&lt;p&gt;This one wasn't in the plugin at all. It was in v0.2.0, released the week before.&lt;/p&gt;

&lt;p&gt;I'd added memory decay: memories not recalled in 90 days get archived. To support it I added a &lt;code&gt;last_recalled_at&lt;/code&gt; column. Existing rows got &lt;code&gt;NULL&lt;/code&gt;. The decay rule said "never recalled, so count from the creation date."&lt;/p&gt;

&lt;p&gt;Every memory older than 90 days was archived on the first start after upgrading.&lt;/p&gt;

&lt;p&gt;I only noticed because my test workspace suddenly had "zero decisions," when I knew it had one.&lt;/p&gt;

&lt;p&gt;The fix was two migrations: backfill &lt;code&gt;last_recalled_at&lt;/code&gt; with the upgrade time so existing memories get a fresh window, and a repair migration that restores rows already archived by the bug. On my machine the log said &lt;code&gt;Restored 1 memories&lt;/code&gt;, and the missing decision came back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when you add a column and a rule that reads it in the same release, the default value for existing rows is a product decision, not a database detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: Claude ignored the instructions
&lt;/h2&gt;

&lt;p&gt;I wanted "What was I working on?" answered from the injected context, with no tool calls. I put that rule in &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude ran &lt;code&gt;git log&lt;/code&gt;, listed files, read my old session history, then answered.&lt;/p&gt;

&lt;p&gt;The reason: a skill's body only loads when Claude decides the skill applies. The rule wasn't in context when it mattered.&lt;/p&gt;

&lt;p&gt;ContextOS already injects workspace state through the MCP &lt;code&gt;initialize&lt;/code&gt; handshake, in &lt;code&gt;serverInfo.instructions&lt;/code&gt;, which is always loaded. I moved the rule there, one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use this context to answer questions about prior work, decisions or todos. Do not run git, list files, or read session history to answer them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next test: zero tool calls, answered in five seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; skills are for "how to do this when it comes up." Anything that must always apply belongs somewhere that's always loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: Updates never actually updated
&lt;/h2&gt;

&lt;p&gt;The download script started like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BINARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks whether a binary exists, not which version. And &lt;code&gt;${CLAUDE_PLUGIN_DATA}&lt;/code&gt; survives plugin updates, which is the whole point of it. So after updating the plugin, everyone kept the old binary forever.&lt;/p&gt;

&lt;p&gt;Fix: write a &lt;code&gt;.version&lt;/code&gt; file after each install and compare it with the version in &lt;code&gt;plugin.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 4: The server and the hook start at the same time
&lt;/h2&gt;

&lt;p&gt;On session start, Claude Code launches the MCP server and runs the SessionStart hook in parallel. On a fresh install, the hook is still downloading 118 MB when Claude Code tries to start a binary that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;The server fails, and Claude Code remembers the failure for 15 minutes. So the user's first impression is a broken plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 5: My fix killed every window's server
&lt;/h2&gt;

&lt;p&gt;To replace the binary during an upgrade on Windows, I stopped the running process first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;taskkill //F //IM contextos.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with Bug 4, the hook killed the server Claude Code had just started. Worse, &lt;code&gt;taskkill&lt;/code&gt; by image name kills every &lt;code&gt;contextos.exe&lt;/code&gt; on the machine. If you had two Claude Code windows open, updating in one broke both.&lt;/p&gt;

&lt;p&gt;My first idea was to swap the binary at session end instead. The docs ended that quickly: SessionEnd hooks get a 1.5 second budget that plugins can't raise, and they also fire on &lt;code&gt;/clear&lt;/code&gt; and &lt;code&gt;/resume&lt;/code&gt;, when the session keeps running.&lt;/p&gt;

&lt;p&gt;The fix that finally worked was to stop replacing binaries at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/0.2.3/contextos.exe
bin/0.2.4/contextos.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each version installs into its own folder, and &lt;code&gt;plugin.json&lt;/code&gt; points at the folder for its own version. Nothing running is ever touched, so there's nothing to kill and no file locks. Old folders get cleaned up later, when nothing is using them.&lt;/p&gt;

&lt;p&gt;The tradeoff: right after an update, the new folder is still downloading, so the server fails once and you choose Reconnect in &lt;code&gt;/mcp&lt;/code&gt;. The hook prints a line into Claude's context explaining that. A known, one-time failure with a clear fix beats a silent one.&lt;/p&gt;

&lt;p&gt;I also added a test that reads &lt;code&gt;plugin.json&lt;/code&gt; and fails the build if the server path doesn't contain the manifest's own version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 6: Linux needed a library minimal systems don't have
&lt;/h2&gt;

&lt;p&gt;While cleaning up the release archives, the Linux binary aborted on start in WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Couldn't find a valid ICU package installed on the system.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimal distros and Docker images often don't ship &lt;code&gt;libicu&lt;/code&gt;, and that's exactly where people run agents. The fix is one line in the project file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;InvariantGlobalization&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/InvariantGlobalization&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't want that to quietly change embeddings for memories people had already stored, so I embedded four strings before and after, including accented text, a Turkish İ, and Chinese. The max difference was 0.0 on all of them. Then it shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing about versions
&lt;/h2&gt;

&lt;p&gt;The marketplace reads &lt;code&gt;plugin.json&lt;/code&gt; from the main branch. So the moment I pushed a version bump, new installs pointed at a GitHub release that didn't exist yet, and got a 404. Now I tag right after CI goes green, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it ended up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two-command install&lt;/li&gt;
&lt;li&gt;Upgrades that never touch a running binary&lt;/li&gt;
&lt;li&gt;Claude answers "What was I working on?" from injected context with zero tool calls&lt;/li&gt;
&lt;li&gt;Runs on Linux without libicu&lt;/li&gt;
&lt;li&gt;Four tools, under 700 tokens of tool definitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number taught me something about my own setup. When I checked, my Claude Code sessions were loading 140 MCP servers and 294 skills from plugins I'd installed once to try. ContextOS was one server out of 140. If your sessions feel heavy, run &lt;code&gt;/context&lt;/code&gt; and look before blaming any single tool.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add aftabkh4n/contextos
/plugin install contextos@contextos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Code. If contextos shows as failed in &lt;code&gt;/mcp&lt;/code&gt; on the first start, choose Reconnect while the binary finishes downloading. Then open any git repo and ask: "What was I working on?"&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/aftabkh4n/contextos" rel="noopener noreferrer"&gt;github.com/aftabkh4n/contextos&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If something breaks for you, open an issue. Apparently that's how this thing gets better.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>dotnet</category>
      <category>ai</category>
    </item>
    <item>
      <title>What spanner for an M12 bolt? An agent that knows DIN and ISO disagree</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Sat, 03 Oct 2026 13:25:30 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/what-spanner-for-an-m12-bolt-an-agent-that-knows-din-and-iso-disagree-32km</link>
      <guid>https://dev.to/aftabkh4n/what-spanner-for-an-m12-bolt-an-agent-that-knows-din-and-iso-disagree-32km</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fastener Spec Agent&lt;/strong&gt; answers metric hex fastener questions correctly, including the ones where the official standards disagree with each other.&lt;/p&gt;

&lt;p&gt;DIN 933 and ISO 4017 describe the same part: a fully threaded hex head set screw. ISO 4017 replaced DIN 933, but the trade still orders by the old name every day. The two standards are identical at almost every size, except four, where the head's width across flats changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;DIN 933&lt;/th&gt;
&lt;th&gt;ISO 4017&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;M10&lt;/td&gt;
&lt;td&gt;17 mm&lt;/td&gt;
&lt;td&gt;16 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M12&lt;/td&gt;
&lt;td&gt;19 mm&lt;/td&gt;
&lt;td&gt;18 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M14&lt;/td&gt;
&lt;td&gt;22 mm&lt;/td&gt;
&lt;td&gt;21 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M22&lt;/td&gt;
&lt;td&gt;32 mm&lt;/td&gt;
&lt;td&gt;34 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So "what spanner do I need for an M12 bolt?" has no single correct answer. A chatbot that confidently says "19 mm" is wrong half the time, and a 19 mm spanner on an 18 mm head rounds the corners.&lt;/p&gt;

&lt;p&gt;I manage the e-commerce platform for a UK fastener supplier, so this is a question real customers ask. The agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;looks up both standards in a Sanity Knowledge Base before answering,&lt;/li&gt;
&lt;li&gt;gives both values when they differ and explains how to tell which one you have,&lt;/li&gt;
&lt;li&gt;notes that DIN 933 is withdrawn but still sold under that name, and&lt;/li&gt;
&lt;li&gt;cites the entries it used and shows every tool call it made.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A keyword search wouldn't get there. "M12 across flats" returns 19 mm or 18 mm depending on which page ranks first. The answer only works because the relationship between the two standards is modelled explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&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%2Fshozc63t48ffzd8kha5j.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%2Fshozc63t48ffzd8kha5j.png" alt=" " width="800" height="843"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The four example questions and what the agent gets right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"What spanner do I need for an M12 hex set screw?"&lt;/strong&gt; 19 mm for DIN 933, 18 mm for ISO 4017, plus how to check which you have (measure across the flats).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Is a DIN 933 M22 interchangeable with ISO 4017?"&lt;/strong&gt; No. M22 is the only size where the ISO head is &lt;em&gt;larger&lt;/em&gt; (34 vs 32 mm).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What's the head height of an M16 set screw?"&lt;/strong&gt; 10 mm, identical in both. It doesn't invent a difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Is DIN 933 still a current standard?"&lt;/strong&gt; Withdrawn and superseded by ISO 4017, still widely ordered by the old name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To run it yourself:&lt;/strong&gt; clone the agent repo, set three values with &lt;code&gt;dotnet user-secrets&lt;/code&gt; (a Sanity organization token with Context Viewer permission, the MCP endpoint URL, and an Anthropic API key), then &lt;code&gt;dotnet run&lt;/code&gt;. The README has the full steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent (.NET 9):&lt;/strong&gt; &lt;a href="https://github.com/aftabkh4n/fastener-spec-agent" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/fastener-spec-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity Studio, schemas and seed data:&lt;/strong&gt; &lt;a href="https://github.com/aftabkh4n/sanity-fastener-kb" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/sanity-fastener-kb&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The content model
&lt;/h3&gt;

&lt;p&gt;Five document types: &lt;code&gt;standard&lt;/code&gt;, &lt;code&gt;fastenerType&lt;/code&gt;, &lt;code&gt;propertyClass&lt;/code&gt;, &lt;code&gt;sizeSpec&lt;/code&gt; (one document per size per standard) and &lt;code&gt;equivalence&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;equivalence&lt;/code&gt; is the important one. It links two standards and holds an array of differences, each with the size, the attribute, both values and a plain-English practical impact ("Different spanner: 19 mm for DIN, 18 mm for ISO"). Instead of hoping a model notices that two tables disagree, the disagreement is stored as data.&lt;/p&gt;

&lt;p&gt;Every dimension came from a published standard sheet, with a source URL and a &lt;code&gt;verified&lt;/code&gt; flag I ticked after checking it by hand. Nothing was filled in from a model's memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I pointed Sanity Context at
&lt;/h3&gt;

&lt;p&gt;One Knowledge Base with three sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My Sanity dataset:&lt;/strong&gt; 38 documents across all five types, with references unfolded so each size spec carries its standard's code and status.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A supplier's DIN 933 standard sheet (PDF).&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reference web page&lt;/strong&gt; comparing DIN 931/933 with ISO 4014/4017.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My first build crawled that website without limits and pulled in 43 pages about flange bolting torque and ASTM stud bolts. Limiting the crawl to the one relevant page kept the Knowledge Base focused and well inside the 150-document budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Knowledge Base caught
&lt;/h3&gt;

&lt;p&gt;The build raised three issues, and each taught me something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It found an error in my own data.&lt;/strong&gt; I had seeded DIN 933 with the status &lt;code&gt;current&lt;/code&gt;. The PDF said withdrawn and superseded by ISO 4017, and the PDF was right. I fixed the status in the Studio, linked it to ISO 4017 with &lt;code&gt;supersededBy&lt;/code&gt;, and rebuilt.&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%2Fwkent6zacybjvnialnor.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%2Fwkent6zacybjvnialnor.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It caught a real conflict between published sources.&lt;/strong&gt; The supplier's PDF lists M14 at 21 mm under a DIN 933 heading. That's the ISO value; the original DIN 933 figure is 22 mm. The Knowledge Base showed both claims side by side, each traced to its sources, and I resolved it as 22 mm. That decision persists across builds.&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%2Fwuj9kyp7np4wtz5gh8t4.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%2Fwuj9kyp7np4wtz5gh8t4.png" alt=" " width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It flagged a conflict that wasn't one, which proved the point of the schema.&lt;/strong&gt; The build reported M22 as 32 mm in one entry and 34 mm in another. Both are correct: one is DIN, one is ISO. Picking either card would have created a standing instruction teaching the Knowledge Base something false. Instead, I added an instruction stating that DIN and ISO values for the same size belong to separate standards and aren't conflicting facts, and put the same principle in the Knowledge Base's purpose. The false conflict was gone on the next build.&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%2Fke7tvoym9mopza3balg9.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%2Fke7tvoym9mopza3balg9.png" alt=" " width="799" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That was the clearest argument for structured content I ran into: without the explicit DIN↔ISO modelling, even the build confused the two standards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Context tools the agent uses
&lt;/h3&gt;

&lt;p&gt;The agent connects to a Context MCP endpoint in Knowledge Base mode, which serves three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;initial_context&lt;/code&gt;:&lt;/strong&gt; fetches the Knowledge Base outline at the start of a conversation, so the agent knows which entries exist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;knowledge_base_read&lt;/code&gt;:&lt;/strong&gt; reads specific entries by path, e.g. &lt;code&gt;dimensions/fully_threaded/din_933&lt;/code&gt;, &lt;code&gt;dimensions/fully_threaded/iso_4017&lt;/code&gt; and &lt;code&gt;standards_comparison&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;knowledge_base_search&lt;/code&gt;:&lt;/strong&gt; available for questions that don't map neatly onto the outline&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What the agent does with the content
&lt;/h3&gt;

&lt;p&gt;The agent is a .NET 9 minimal API using the official Model Context Protocol C# SDK, with Claude running a tool loop over the Context tools. For the M12 question, it reads the outline, pulls the DIN 933, ISO 4017 and standards-comparison entries, then compares them and answers with both values and their sources. Every tool call is shown under the answer, so you can see exactly what it read.&lt;/p&gt;

&lt;p&gt;The system prompt rules that mattered most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always consult the Knowledge Base; never answer dimensions from memory.&lt;/li&gt;
&lt;li&gt;When DIN and ISO differ, give both values and say which is which.&lt;/li&gt;
&lt;li&gt;Never guess which standard the user's parts are made to. Tell them how to check instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last rule came from testing. An early answer added "if you bought off-the-shelf stock, it's most likely DIN 933". It sounds helpful, but nothing in the Knowledge Base supported it, so I removed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;tko9cbz7&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema:&lt;/strong&gt; &lt;code&gt;standard&lt;/code&gt;, &lt;code&gt;fastenerType&lt;/code&gt;, &lt;code&gt;propertyClass&lt;/code&gt;, &lt;code&gt;sizeSpec&lt;/code&gt;, &lt;code&gt;equivalence&lt;/code&gt; (see the Studio repo)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;I built both repos with Claude Code. The moment worth showing: I asked it to extract DIN and ISO values from a standard sheet that only tabulated DIN 933. My instructions said to mirror DIN values into ISO if ISO wasn't listed. Following that literally would have produced an equivalence with zero differences, so instead it stopped and explained the collision. I found a second source for the ISO values and confirmed them against a manufacturer's product listings.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I got tired of re-explaining my codebase to Claude every morning, so I built this</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Thu, 24 Sep 2026 09:46:20 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/i-got-tired-of-re-explaining-my-codebase-to-claude-every-morning-so-i-built-this-5eo5</link>
      <guid>https://dev.to/aftabkh4n/i-got-tired-of-re-explaining-my-codebase-to-claude-every-morning-so-i-built-this-5eo5</guid>
      <description>&lt;p&gt;Every morning I open Claude Code and have the same conversation.&lt;/p&gt;

&lt;p&gt;"We use the outbox pattern for Kafka writes." "The auth service is separate from the API." "We decided last week not to use MediatR for this project." "Yes, I know the test coverage is low in the storage layer, we're fixing it."&lt;/p&gt;

&lt;p&gt;Same context. Every session. From scratch.&lt;/p&gt;

&lt;p&gt;I got tired of it. So I built ContextOS.&lt;/p&gt;

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

&lt;p&gt;ContextOS is an MCP server that gives AI coding agents persistent memory across sessions. You store decisions, todos, and notes during a session. The next time you open Claude Code in that workspace, ContextOS injects the relevant context automatically before you type a single word.&lt;/p&gt;

&lt;p&gt;The agent already knows where you left off.&lt;/p&gt;

&lt;p&gt;Here is what that looks like in practice. I open Claude Code in a workspace I have not touched in three weeks and ask "what was I working on?" Without calling any tool, Claude responds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Project: contextos-test on branch master. Recent decision: using the outbox pattern for Kafka writes to ensure exactly-once delivery. Three commits in the last session. One uncommitted file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No tool call. No "let me check your memory." It just knows. That is the feature I built everything around.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Three pieces worth explaining: the MCP tools, the retrieval pipeline, and auto-hydration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP tools
&lt;/h3&gt;

&lt;p&gt;Four tools exposed over the MCP protocol:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;remember&lt;/code&gt; stores a memory with a type (decision, note, todo, gotcha, skill), tags, and an importance score from 0 to 1&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remember_skill&lt;/code&gt; stores a reusable procedure with name, steps, and outcome. Skills never decay.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recall&lt;/code&gt; runs hybrid search over stored memories. Supports &lt;code&gt;scope="global"&lt;/code&gt; to search across all your workspaces.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt; assembles a markdown summary of the current workspace state&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Auto-hydration
&lt;/h3&gt;

&lt;p&gt;When an MCP client connects, the MCP protocol sends an &lt;code&gt;initialize&lt;/code&gt; request. ContextOS responds with the standard handshake and also sets &lt;code&gt;serverInfo.instructions&lt;/code&gt; to a pre-assembled context blob.&lt;/p&gt;

&lt;p&gt;The MCP spec says clients should treat &lt;code&gt;serverInfo.instructions&lt;/code&gt; as background knowledge. Claude Code does exactly that. It reads the injected context and uses it naturally, without the user asking, without any tool call, before the first message.&lt;/p&gt;

&lt;p&gt;The blob includes your current branch, recent commits, active todos, recent decisions, and top skills. The whole thing is assembled in under 20ms and kept under 2KB.&lt;/p&gt;

&lt;h3&gt;
  
  
  The retrieval pipeline
&lt;/h3&gt;

&lt;p&gt;When you call &lt;code&gt;recall&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your query gets embedded using a local ONNX model (all-MiniLM-L6-v2, bundled in the binary, no API keys needed)&lt;/li&gt;
&lt;li&gt;Vector search runs a cosine similarity scan over all stored memory embeddings&lt;/li&gt;
&lt;li&gt;Keyword search runs FTS5 BM25 ranking over memory content and tags&lt;/li&gt;
&lt;li&gt;Both result lists get merged using Reciprocal Rank Fusion (RRF, k=60)&lt;/li&gt;
&lt;li&gt;Results get reranked by &lt;code&gt;exp(-age_days/30) * (0.5 + importance)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Top-k results are returned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RRF is the interesting choice here. It combines ranked lists without needing to normalize scores across different retrieval methods. A memory that ranks 3rd in vector search and 5th in BM25 scores better than one that ranks 1st in only one of them.&lt;/p&gt;

&lt;p&gt;The embeddings run entirely locally. No API call, no latency spike, no cost per query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory decay
&lt;/h3&gt;

&lt;p&gt;Memories auto-archive after 90 days of inactivity. If you recall a memory, the clock resets. Skills represent durable procedural knowledge so they never decay. Setting &lt;code&gt;decay_days=0&lt;/code&gt; on any memory pins it permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built it on
&lt;/h2&gt;

&lt;p&gt;.NET 10. The self-contained single-file binary story is solid. One file, no dependencies, runs anywhere. The release pipeline produces four platform binaries averaging 118MB each, including the .NET runtime, ONNX Runtime, LibGit2Sharp, and the embedding model.&lt;/p&gt;

&lt;p&gt;126 tests. CI on Linux. Apache 2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cursor support is not manually verified for auto-hydration (tools work, hydration behavior unknown)&lt;/li&gt;
&lt;li&gt;No team or cloud sync, workspace-local SQLite only&lt;/li&gt;
&lt;li&gt;No automatic git ingestion, manual &lt;code&gt;remember&lt;/code&gt; calls only&lt;/li&gt;
&lt;li&gt;Tested primarily with Claude Code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install in one command
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/aftabkh4n/contextos/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://raw.githubusercontent.com/aftabkh4n/contextos/main/install.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Or via .NET tool:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; ContextOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open Claude Code in any git repo and ask: "What was I working on?"&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/aftabkh4n/contextos" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/contextos&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I want to know what breaks. Open an issue or comment here.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Built 50 Developer Tools That Run Entirely in the Browser</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Mon, 07 Sep 2026 05:33:23 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/how-i-built-50-developer-tools-that-run-entirely-in-the-browser-3a6a</link>
      <guid>https://dev.to/aftabkh4n/how-i-built-50-developer-tools-that-run-entirely-in-the-browser-3a6a</guid>
      <description>&lt;p&gt;I kept ending up on the same kind of website. I needed a quick JSON formatter or a YAML validator, I searched for one, and I landed on a page buried in ads that took four seconds to load and asked me to accept cookies twice. The tool itself was fine. Everything around it was miserable.&lt;/p&gt;

&lt;p&gt;So over a few months I built my own. 50 tools, all free, all running fully in the browser. No backend, no accounts, no tracking. This is what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule: everything runs client-side
&lt;/h2&gt;

&lt;p&gt;I gave myself a single constraint at the start. Every tool has to work without a server. You paste your data, the work happens in your browser, nothing gets sent anywhere.&lt;/p&gt;

&lt;p&gt;This was partly a privacy decision. When you use my JSON formatter, your JSON never leaves your machine. There is no server to log it because there is no server at all. For developers pasting in tokens, config, or API responses, that matters.&lt;/p&gt;

&lt;p&gt;But the constraint also made the whole project possible. No backend means no hosting bill, no database to secure, no API keys to rotate, no server to keep alive. The entire site is static files on GitHub Pages. It costs nothing to run and it will keep working whether one person uses it or a thousand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What client-side actually handles
&lt;/h2&gt;

&lt;p&gt;More than I expected. Modern browser APIs are strong enough that most developer tools do not need a server at all.&lt;/p&gt;

&lt;p&gt;Hashing was the surprise. The Web Crypto API does SHA-256 and SHA-512 natively, so my hash generator is a few lines calling crypto.subtle.digest. No library needed.&lt;/p&gt;

&lt;p&gt;UUID generation is one call to crypto.randomUUID. Base64 is built in through btoa and atob. The clipboard, file downloads, image resizing through canvas, all native. My image compressor resizes and recompresses photos using a canvas element and never uploads a thing.&lt;/p&gt;

&lt;p&gt;For the few things the browser cannot do alone I pulled in one small library. YAML parsing uses js-yaml. QR codes use a tiny generator library. That is it for dependencies across all 50 tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that fought back
&lt;/h2&gt;

&lt;p&gt;QR codes took three tries. The first two libraries I picked had broken or moved CDN builds, and I spent an evening watching the browser console throw 404s at me before I found one that actually loaded and had a sane API. Lesson learned: pin the version, do not trust the latest tag.&lt;/p&gt;

&lt;p&gt;Client-side image compression is fiddly. Canvas will happily hand you a PNG that is larger than the original if you are not careful, because PNG is lossless and re-encoding adds overhead. I had to detect that case and tell the user when their file was already optimised rather than pretend I saved them space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no framework
&lt;/h2&gt;

&lt;p&gt;The whole thing is plain HTML, CSS, and vanilla JavaScript. One HTML file per tool. No build step, no npm install, no bundler.&lt;/p&gt;

&lt;p&gt;For a project like this that was the right call. Each tool is self-contained and small. If I want to fix the regex tester I open one file, change it, and push. There is no toolchain between me and the code. In a few years when some framework has moved on, these files will still open and run exactly as they do today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some tools I use myself
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://aftabkh4n.github.io/tools/yaml-validator/" rel="noopener noreferrer"&gt;YAML validator&lt;/a&gt; is the one I open most, usually to check a Kubernetes manifest before applying it. It validates the syntax and shows the parsed JSON so I can confirm the structure.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://aftabkh4n.github.io/tools/sql-formatter/" rel="noopener noreferrer"&gt;SQL formatter&lt;/a&gt; cleans up the one-line queries that come out of application logs.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://aftabkh4n.github.io/tools/diff-checker/" rel="noopener noreferrer"&gt;diff checker&lt;/a&gt; and &lt;a href="https://aftabkh4n.github.io/tools/regex-tester/" rel="noopener noreferrer"&gt;regex tester&lt;/a&gt; get daily use too.&lt;/p&gt;

&lt;p&gt;All 50 are at &lt;a href="https://aftabkh4n.github.io" rel="noopener noreferrer"&gt;aftabkh4n.github.io&lt;/a&gt; and the full source is on &lt;a href="https://github.com/aftabkh4n/aftabkh4n.github.io" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I do it again
&lt;/h2&gt;

&lt;p&gt;Probably yes. The no-server rule sounded restrictive when I started and it ended up being the thing that kept the project alive. Simple code, zero hosting cost, and a privacy story I do not have to lie about.&lt;/p&gt;

&lt;p&gt;If you build small utilities, try the browser-only version first. You can do more without a backend than you would guess. Worst case you hit a wall and add a server later, but I never did.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>developer</category>
      <category>resources</category>
      <category>ai</category>
    </item>
    <item>
      <title>My tests were describing the code, not checking it</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Sun, 30 Aug 2026 07:47:41 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/my-tests-were-describing-the-code-not-checking-it-3m2p</link>
      <guid>https://dev.to/aftabkh4n/my-tests-were-describing-the-code-not-checking-it-3m2p</guid>
      <description>&lt;p&gt;Built a .NET invoicing library with Claude Code over about a week. Zero dependencies, VAT and GST arithmetic, JSON and CSV export, the usual. By the third phase I had 202 tests passing on two target frameworks and green CI on three operating systems.&lt;/p&gt;

&lt;p&gt;It would also have been wrong by 3p on any UK invoice with three items at £3.99.&lt;/p&gt;

&lt;p&gt;Not wrong in some pathological edge case. Wrong on a completely normal invoice, at the most common price point in British retail, in a way that ends up in someone's VAT return.&lt;/p&gt;

&lt;p&gt;I found it because I got suspicious of my own green checkmark.&lt;/p&gt;

&lt;p&gt;Why agent-written tests lie to you&lt;/p&gt;

&lt;p&gt;Ask an agent to implement something and write tests for it. It writes both. They pass.&lt;/p&gt;

&lt;p&gt;You have not tested the code. You have confirmed the code agrees with itself.&lt;/p&gt;

&lt;p&gt;I know how obvious that reads. It was not obvious to me at the time, because the suite looked legitimate. Property tests. Golden scenarios with hand-calculated values across USD, JPY and KWD. A file literally called CsvExportTests with a culture-swap test in it, which is the exact test you write if you know about the .NET decimal formatting trap.&lt;/p&gt;

&lt;p&gt;That test could not catch the bug it existed to catch. I will get to it.&lt;/p&gt;

&lt;p&gt;Breaking my own code on purpose&lt;/p&gt;

&lt;p&gt;Mutation testing, if you have not run into it: introduce a deliberate bug, see if any test fails. Nothing fails, that behaviour is not tested, regardless of what the test names imply.&lt;/p&gt;

&lt;p&gt;I tried Stryker.NET. It fell over on the multi-target setup and I burned fifteen minutes before giving up. Doing it by hand ended up better anyway, because I picked the mutations rather than getting a percentage at the end. I do not really want a coverage score. I want to know if the four rounding call sites are pinned.&lt;/p&gt;

&lt;p&gt;The loop, which is as dumb as it sounds:&lt;/p&gt;

&lt;p&gt;Apply one mutation. Run tests. Write down what failed. Revert. Check green. Next one.&lt;/p&gt;

&lt;p&gt;Under three failures on an arithmetic mutation, I called it a hole.&lt;/p&gt;

&lt;p&gt;What fell out&lt;/p&gt;

&lt;p&gt;Nothing pinned the rounding policy. The library rounds half-up, MidpointRounding.AwayFromZero, because that is what tax authorities expect. .NET defaults to banker's rounding, which will quietly hand you off-by-a-penny invoices. So I flipped it to ToEven and ran the suite.&lt;/p&gt;

&lt;p&gt;Five failures. All five were unit tests of the rounding helper itself. None of the twenty-five golden invoice scenarios blinked.&lt;/p&gt;

&lt;p&gt;Not one of them had a midpoint value in it. I had proved the helper rounds correctly on its own. I had not proved the calculator ever calls it. Two different things and I was only covering one.&lt;/p&gt;

&lt;p&gt;Here is the part that got me. The scenario I would have put money on catching this, ¥995 at 10% giving ¥99.5, does not discriminate at all. ToEven rounds 99.5 up to 100, because 100 is the even one. Same answer. My "obvious" midpoint test was not a midpoint test.&lt;/p&gt;

&lt;p&gt;Discounts, same story. Pulled the rounding out of the invoice-level discount calculation. One test failed, and it was a generic property test about all decimals being rounded. Every discount scenario I had written used a clean percentage of a clean subtotal. Nothing landed on a half-penny, so nothing broke.&lt;/p&gt;

&lt;p&gt;The culture test was decorative. This one still annoys me. The CSV test swapped CultureInfo.CurrentCulture to de-DE and tr-TR, exported, asserted. Correct in every way. When I swapped InvariantCulture for CurrentCulture in the actual export code, it passed.&lt;/p&gt;

&lt;p&gt;The fixture invoice totalled 100, 20 and 120. Under de-DE, 100m.ToString() gives you "100". Same as invariant. The test was fine except for the single detail that mattered, which was having a number with a decimal point in it.&lt;/p&gt;

&lt;p&gt;Changed the unit price to 10.50. Suddenly the mutation kills three tests.&lt;/p&gt;

&lt;p&gt;And then the release pipeline just lied. Tagged v0.3.0 and forgot to bump the version in the project file. Workflow packed 0.2.0. NuGet returned a 409 because that version was already up there. --skip-duplicate ate the error. Every step green. Nothing published.&lt;/p&gt;

&lt;p&gt;Green does not mean shipped. I did not know that until it happened to me.&lt;/p&gt;

&lt;p&gt;The one I am glad I did not ship&lt;/p&gt;

&lt;p&gt;Those four are test-quality problems. Annoying, fixable. The fifth was a different animal.&lt;/p&gt;

&lt;p&gt;I was adding a per-line tax mode. HMRC and the ATO both let you round tax per line instead of on the subtotal, and if the accounting system on the other end does it per line, your totals disagree by a penny. Reasonable feature.&lt;/p&gt;

&lt;p&gt;Somewhere around phase three I had picked up the habit of making the agent answer design questions on paper first, with actual numbers, before touching code. So I asked four. One of them was what happens when per-line meets tax-inclusive pricing.&lt;/p&gt;

&lt;p&gt;The numbers came back and they were not good.&lt;/p&gt;

&lt;p&gt;Extracting a tax-exclusive base out of an inclusive price leaves a residual on every line. Always zero or minus one minor unit. Never positive. One line, who cares. But the residual is periodic, and which prices trigger it is completely predictable:&lt;/p&gt;

&lt;p&gt;10% tax: line total in minor units where x mod 11 = 5&lt;br&gt;
20% VAT: x mod 6 = 3&lt;/p&gt;

&lt;p&gt;£3.99 is 399 pence. 399 mod 6 is 3.&lt;/p&gt;

&lt;p&gt;Every £3.99 line loses a penny. Three lines, 3p. Seven lines, 7p. And the reconciliation rule I had written earlier only absorbs one minor unit, so it would have shaved the difference off the tax figure and produced a VAT breakdown implying 19.82% on a 20% invoice. That is not a rounding artefact, that is a wrong number on a filing report.&lt;/p&gt;

&lt;p&gt;So I did not build it. Constructor throws on that combination, the analysis is written down, someone can pick it up later. Probably me.&lt;/p&gt;

&lt;p&gt;If I had written the code first, a property test would have caught the broken invariant eventually. But the implementation would already exist by then, and at that point the tempting move is to loosen the invariant rather than admit the design is wrong. On paper it cost twenty minutes and no ego.&lt;/p&gt;

&lt;p&gt;What I would actually suggest&lt;/p&gt;

&lt;p&gt;Not "adopt mutation testing." Everyone nods at that and nobody does it.&lt;/p&gt;

&lt;p&gt;Smaller: find the three or four places in your code where being wrong is expensive and quiet. Mine were four rounding call sites. Break each one by hand, run the tests, count. Half an hour, no tooling, and you will learn something uncomfortable.&lt;/p&gt;

&lt;p&gt;And when an agent is building something with non-obvious edge cases, make it do the hard design questions in writing before it writes anything. Not a plan document. Worked arithmetic. The £3.99 thing was invisible in prose and screamed the second someone actually did the sums.&lt;/p&gt;

&lt;p&gt;I am not turning this into a methodology. One project, a few questions I happened to get lucky with, and I would bet there is still stuff wrong in there that none of my mutations went near. The takeaway is smaller than a lesson: a passing test suite written by whatever wrote the code tells you those two things are consistent with each other. Not that either is right. Took me a few hundred green tests to properly feel that.&lt;/p&gt;

&lt;p&gt;Library is on NuGet as &lt;a href="https://www.nuget.org/packages/InvoiceCore/" rel="noopener noreferrer"&gt;InvoiceCore &lt;/a&gt;if you want it. Honestly it is more useful as a worked example of the above than as an invoicing package, and I would rather you took the protocol than the dependency.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>techtalks</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>BlazorMemory 1.0 is out. Ten months, 14 packages, and what I got wrong along the way.</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Thu, 20 Aug 2026 08:27:59 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/blazormemory-10-is-out-ten-months-14-packages-and-what-i-got-wrong-along-the-way-1548</link>
      <guid>https://dev.to/aftabkh4n/blazormemory-10-is-out-ten-months-14-packages-and-what-i-got-wrong-along-the-way-1548</guid>
      <description>&lt;p&gt;I started BlazorMemory in January because I wanted an AI chat assistant in Blazor that remembered things. I looked for a .NET library that did this. There wasn't one. Everything in the space was Python, and everything assumed you had a server and a vector database.&lt;/p&gt;

&lt;p&gt;Blazor WASM has neither. So I built it.&lt;/p&gt;

&lt;p&gt;v1.0.0 shipped this week. Here is what is in it, and more usefully, what I would do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 1.0 includes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Four storage backends.&lt;/strong&gt; IndexedDB runs in the browser with no backend at all, which is the case I originally built this for. EF Core covers SQL Server and SQLite. pgvector handles PostgreSQL with native vector search and an HNSW index. InMemory is for tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four AI providers.&lt;/strong&gt; OpenAI, Anthropic, Azure OpenAI, Ollama. Each one works for both embeddings and fact extraction, and they mix freely. You can run Ollama embeddings with a Claude extractor if you want.&lt;/p&gt;

&lt;p&gt;The Ollama combination is the one I like most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorMemory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseIndexedDbStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOllamaExtractor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key. No cost per request. Everything local. For development this removes the last reason not to try the library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A drop-in UI component.&lt;/strong&gt; One line of markup gives you a panel with the stored memories, delete and clear, export and import, thumbs up and down feedback, and a mode toggle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;MemoryPanel UserId="@userId" IsOpen="true" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A memory graph.&lt;/strong&gt; Memories render as nodes, connected by edges where the cosine similarity crosses a threshold. Vanilla JS force simulation, no D3, no external libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;MemoryGraph UserId="@userId" SimilarityThreshold="0.65f" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Semantic Kernel integration.&lt;/strong&gt; BlazorMemory implements SK's &lt;code&gt;IMemoryStore&lt;/code&gt;, so it drops into an existing SK app without changing anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent shared memory.&lt;/strong&gt; Several agents share one memory pool. Each agent writes to its own namespace but reads across all of them, so agents see what other agents learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The storage interface changed three times.&lt;/strong&gt; First when I added namespaces, then again when I added importance scores. Each change meant touching every adapter, every test, every extension method. If I had used an options object from the start instead of positional parameters, those would have been additive changes instead of breaking ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consolidation was harder than everything else combined.&lt;/strong&gt; Storing memories is easy. Not storing forty near-duplicate memories is not. Early versions produced "User works in software," "User is a developer," and "User writes code" as three separate entries.&lt;/p&gt;

&lt;p&gt;The fix was a priority order in the consolidation prompt: NONE first, then UPDATE, then DELETE, then ADD. The model has to justify adding something new rather than defaulting to it. That single reordering did more for memory quality than any amount of prompt polish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I trusted local models to return clean JSON.&lt;/strong&gt; The Ollama extractor worked in testing and failed constantly in practice, because llama3.2 wraps its JSON in markdown fences roughly half the time and sometimes adds a sentence of commentary first. The parse failed silently and extraction returned nothing.&lt;/p&gt;

&lt;p&gt;Now the extractor strips code fences, hunts for the first bracket and its match, and retries once with a stricter prompt if the parse fails. Obvious in hindsight. Cost me a version to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting this
&lt;/h2&gt;

&lt;p&gt;Ship small and often. Fourteen packages sounds like a lot but no single release was large. v0.2 added namespaces. v0.3 added export and import. v0.5 added thumbs up and down. Each one took a day or two and each one gave me something to write about.&lt;/p&gt;

&lt;p&gt;The writing mattered more than I expected. Downloads spiked after every article, not after every release. The releases without a post attached barely moved the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory
dotnet add package BlazorMemory.Components
dotnet add package BlazorMemory.Storage.IndexedDb
dotnet add package BlazorMemory.Embeddings.Ollama
dotnet add package BlazorMemory.Extractor.Ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;14 packages, 132 tests, MIT licensed.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/aftabkh4n/BlazorMemory" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/BlazorMemory&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>BlazorMemory v0.8.0: Semantic Kernel adapter, Ollama embeddings, and memory decay</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Sun, 09 Aug 2026 09:57:37 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/blazormemory-v080-semantic-kernel-adapter-ollama-embeddings-and-memory-decay-oom</link>
      <guid>https://dev.to/aftabkh4n/blazormemory-v080-semantic-kernel-adapter-ollama-embeddings-and-memory-decay-oom</guid>
      <description>&lt;p&gt;Four things shipped in v0.8.0. I will go through each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic Kernel integration
&lt;/h2&gt;

&lt;p&gt;The most requested feature since I published the library. BlazorMemory now implements SK's &lt;code&gt;IMemoryStore&lt;/code&gt; interface directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory.SemanticKernel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorMemory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseInMemoryStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSemanticKernelMemoryStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers &lt;code&gt;BlazorMemoryMemoryStore&lt;/code&gt; as SK's &lt;code&gt;IMemoryStore&lt;/code&gt; in DI. You can now pass it to SK's &lt;code&gt;SemanticTextMemory&lt;/code&gt; or any SK plugin that takes an &lt;code&gt;IMemoryStore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One design decision worth explaining: SK has no concept of userId, but BlazorMemory requires one for every store operation. The adapter takes a &lt;code&gt;userId&lt;/code&gt; parameter (defaults to "sk") and uses it for all operations. If you are building a multi-user app, create one adapter per user or pass the userId at registration time.&lt;/p&gt;

&lt;p&gt;SK collections map to BlazorMemory namespaces. So &lt;code&gt;CreateCollectionAsync("work")&lt;/code&gt; creates a namespace called "work" and all records in that collection are scoped to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama embeddings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory.Embeddings.Ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorMemory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key. No cost per request. Runs against a local Ollama instance at &lt;code&gt;http://localhost:11434&lt;/code&gt;. Default model is &lt;code&gt;nomic-embed-text&lt;/code&gt; which produces 768-dimensional embeddings.&lt;/p&gt;

&lt;p&gt;You can configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mxbai-embed-large"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://my-server:11434"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dimensions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1024&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;The package has zero external dependencies. It uses &lt;code&gt;System.Net.Http.Json&lt;/code&gt; directly. No Ollama SDK to pull in.&lt;/p&gt;

&lt;p&gt;The obvious use case is development. Run Ollama locally, pull a model, and test your AI assistant without spending money on embeddings. Switch to OpenAI embeddings in production by changing one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory decay and summarization
&lt;/h2&gt;

&lt;p&gt;This solves a real problem. A user who chats with your assistant every day for a month can accumulate hundreds of memories. Most of them are redundant or stale. The memory panel becomes noise.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SummarizeOldMemoriesAsync&lt;/code&gt; collapses old memories into a summary entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SummarizeOldMemoriesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;maxMemories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keepRecent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user has fewer than 50 memories, nothing happens. If they have more, the oldest ones (count minus 20) get passed to the LLM extractor for summarization, then deleted. A new memory is stored with the summary, prefixed with &lt;code&gt;[Summary]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The summary prompt is: "Summarize these facts about a user into a single concise paragraph. Start with 'User background:'. Facts: ..."&lt;/p&gt;

&lt;p&gt;You can call this on a schedule or after each conversation. It is a no-op when memory count is below the threshold so calling it frequently is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verbatim mode importance scoring
&lt;/h2&gt;

&lt;p&gt;Thumbs up and down now work in verbatim mode too. Previously they only showed in Smart mode. The &lt;code&gt;MemoryPanel&lt;/code&gt; component dispatches to the right service method based on the current mode automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkVerbatimImportantAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkVerbatimUnimportantAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResetVerbatimImportanceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same scoring logic as Smart mode. Important memories get a 1.5x multiplier on relevance. Unimportant ones drop to 0.3x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;11 packages on NuGet, 108 tests passing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory
dotnet add package BlazorMemory.SemanticKernel
dotnet add package BlazorMemory.Embeddings.Ollama
dotnet add package BlazorMemory.Components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/aftabkh4n/BlazorMemory" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/BlazorMemory&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>semantickernel</category>
      <category>ollama</category>
      <category>ai</category>
    </item>
    <item>
      <title>Turning TravelAI.Core Into a Real Production System</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:42:12 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/turning-travelaicore-into-a-real-production-system-3npm</link>
      <guid>https://dev.to/aftabkh4n/turning-travelaicore-into-a-real-production-system-3npm</guid>
      <description>&lt;p&gt;A few months ago I published TravelAI.Core, a .NET library for searching travel destinations using AI. Version 2.0.0 shipped with support for OpenAI, Anthropic, Azure OpenAI, and Ollama. It works. People are using it.&lt;/p&gt;

&lt;p&gt;But it's still just a library. One package, doing one job, called directly from your code.&lt;/p&gt;

&lt;p&gt;That's not how production systems work. So I'm upgrading it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TravelAI.Core looks like today
&lt;/h2&gt;

&lt;p&gt;You install the package, call a method, and it searches destinations using whichever AI provider you configured. Simple, useful for small projects.&lt;/p&gt;

&lt;p&gt;Plug it into a real product with real traffic and a few problems show up fast:&lt;/p&gt;

&lt;p&gt;One process does everything. Search, AI calls, and business logic all run in the same place. If the AI provider is slow, your whole app waits. You can't scale just the search part without scaling everything else. And if something breaks, you're digging through console output trying to figure out which call caused it.&lt;/p&gt;

&lt;p&gt;These aren't bugs. They're what happens when a library grows past its original scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting it into services
&lt;/h2&gt;

&lt;p&gt;I'm breaking TravelAI.Core into three pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API Service&lt;/strong&gt; - the front door. Takes requests, validates them, passes work along. It doesn't do the heavy lifting itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search Service&lt;/strong&gt; - handles destination search and filtering. Can scale independently if search traffic grows without touching the AI side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Service&lt;/strong&gt; - handles all calls to OpenAI, Anthropic, Azure, or Ollama. Isolated so a slow or unavailable provider doesn't freeze everything else.&lt;/p&gt;

&lt;p&gt;Each service does one job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a message queue
&lt;/h2&gt;

&lt;p&gt;Instead of the services calling each other directly, I'm putting RabbitMQ between them.&lt;/p&gt;

&lt;p&gt;Here's why it matters. If the API service calls the AI service directly and the AI service is slow, the API service waits. With a queue, the API service sends a message and moves on. The AI service picks it up when it's ready. Nothing blocks.&lt;/p&gt;

&lt;p&gt;It also means if the AI service crashes and restarts, no requests get lost. They sit in the queue until the service comes back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Centralized logging
&lt;/h2&gt;

&lt;p&gt;I'm adding Serilog so every service writes to one place.&lt;/p&gt;

&lt;p&gt;This sounds small. It isn't. When something breaks in a multi-service system, the hard part usually isn't fixing the bug. It's working out which service caused it. Scattered logs across three consoles turn a five-minute investigation into an hour of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;A single library shows you can write clean code. A multi-service system with a message queue and centralized logging shows you can design something that holds up under real conditions: slow networks, provider outages, traffic spikes.&lt;/p&gt;

&lt;p&gt;And it costs nothing to build. Everything runs locally through Docker.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Split the codebase into three services&lt;/li&gt;
&lt;li&gt;Add RabbitMQ between them&lt;/li&gt;
&lt;li&gt;Wire up Serilog&lt;/li&gt;
&lt;li&gt;Test under load locally&lt;/li&gt;
&lt;li&gt;Write up the before and after with real numbers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TravelAI.Core started as a simple idea. It's turning into something more useful: a real example of how to structure a system that doesn't fall over.&lt;/p&gt;

&lt;p&gt;More updates as this progresses.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/aftabkh4n/TravelAI.Core" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/TravelAI.Core&lt;/a&gt;&lt;br&gt;
NuGet: &lt;a href="https://www.nuget.org/packages/TravelAI.Core" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/TravelAI.Core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Reducing boilerplate in AI memory with a chat wrapper for .NET</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Thu, 30 Jul 2026 08:12:12 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/reducing-boilerplate-in-ai-memory-with-a-chat-wrapper-for-net-59lc</link>
      <guid>https://dev.to/aftabkh4n/reducing-boilerplate-in-ai-memory-with-a-chat-wrapper-for-net-59lc</guid>
      <description>&lt;p&gt;Every developer who uses BlazorMemory writes the same code after every chat message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CallLlmAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExtractAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"User: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\nAssistant: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reply&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;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query memories, build a prompt, call the LLM, extract new facts. Four steps, every time, in every app. v0.7.0 wraps all of that into one call.&lt;/p&gt;

&lt;h2&gt;
  
  
  MemoryEnabledChat
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;MemoryEnabledChat&lt;/code&gt; service takes a delegate for the LLM call and handles everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register in Program.cs&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorMemory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseIndexedDbStorage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseMemoryEnabledChat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// new&lt;/span&gt;

&lt;span class="c1"&gt;// Use it in your chat service&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MemoryEnabledChat&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemPrompt&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="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CallOpenAiAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemPrompt&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delegate receives a system prompt that already contains the relevant memories. You call your LLM with it and return the reply. &lt;code&gt;MemoryEnabledChat&lt;/code&gt; handles the rest.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Query relevant memories&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;memories&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;QueryOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Build system prompt with memory context&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Call your LLM via the delegate&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;llmCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Extract new facts in background&lt;/span&gt;
&lt;span class="c1"&gt;// Errors are swallowed so extraction failures never crash the chat&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ExtractSafelyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt;&lt;span class="err"&gt;);&lt;/span&gt;

&lt;span class="nn"&gt;return&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Extraction errors are swallowed intentionally. A failed extraction is a degraded experience, not a crash. The user still gets their reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customising behaviour
&lt;/h2&gt;

&lt;p&gt;Two properties control how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;QueryOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.60f&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseSystemPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You are a helpful assistant for a software company."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;QueryOptions&lt;/code&gt; controls how many memories are retrieved and the similarity threshold. &lt;code&gt;BaseSystemPrompt&lt;/code&gt; is prepended before the memory context block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single-service approach
&lt;/h2&gt;

&lt;p&gt;If you do not want to inject &lt;code&gt;MemoryEnabledChat&lt;/code&gt; separately, the same behaviour is available directly on &lt;code&gt;IMemoryService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChatWithMemoryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemPrompt&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="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CallOpenAiAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemPrompt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same delegate pattern, same behaviour, fewer injections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;Before v0.7.0 a typical chat service looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;memories&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;QueryOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.65f&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&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="s"&gt;"\n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"- &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"You are a helpful assistant.\n\nWhat you know:\n&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&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;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CallOpenAiAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExtractAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"User: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\nAssistant: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reply&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;span class="n"&gt;userId&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;reply&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;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CallOpenAiAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic is the same. The boilerplate is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting v0.7.0
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory
dotnet add package BlazorMemory.Components
dotnet add package BlazorMemory.Storage.IndexedDb
dotnet add package BlazorMemory.Embeddings.OpenAi
dotnet add package BlazorMemory.Extractor.OpenAi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;84 tests passing across 9 packages.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/aftabkh4n/BlazorMemory" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/BlazorMemory&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>ai</category>
      <category>openai</category>
    </item>
    <item>
      <title>I made my .NET travel AI library work with OpenAI, Anthropic, Ollama, and Azure. Not just one.</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Mon, 27 Jul 2026 07:50:59 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/i-made-my-net-travel-ai-library-work-with-openai-anthropic-ollama-and-azure-not-just-one-16d0</link>
      <guid>https://dev.to/aftabkh4n/i-made-my-net-travel-ai-library-work-with-openai-anthropic-ollama-and-azure-not-just-one-16d0</guid>
      <description>&lt;p&gt;When I first shipped TravelAI.Core, it only worked with Azure OpenAI and Azure AI Search. You needed an Azure subscription, a deployed GPT-4o model, a configured AI Search index, and the patience to wire it all up before you could generate a single itinerary.&lt;/p&gt;

&lt;p&gt;Downloads were slow. Not surprising in hindsight.&lt;/p&gt;

&lt;p&gt;Most developers don't have Azure credentials sitting around. They want to try something before committing to a cloud provider. I was basically asking people to do significant setup work before they could see if the library was even useful to them.&lt;/p&gt;

&lt;p&gt;So I rebuilt the provider layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;The core interfaces stayed exactly the same. IItineraryGenerationService, IDestinationSearchService, IPriceAnomalyDetector all look identical from the outside. What changed is how you wire up the backend.&lt;/p&gt;

&lt;p&gt;In v1.0.0 you needed a config section with Azure credentials. In v2.0.0 you pick a provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTravelAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseMock&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTravelAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sk-..."&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTravelAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sk-ant-..."&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTravelAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOllama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:11434"&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTravelAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAzureOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mock provider is the one I'm most pleased with. Zero credentials, works offline, returns a realistic 3-day Rome itinerary with activities, costs, and timing. You can build and test the full integration flow on a train with no internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the abstraction works
&lt;/h2&gt;

&lt;p&gt;There's a single ILlmProvider interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ILlmProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GenerateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&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;Each provider implements it. ItineraryGenerationService now takes ILlmProvider instead of AzureOpenAIClient. The Anthropic adapter uses Anthropic.SDK, the Ollama adapter makes raw HTTP calls to /api/chat, the mock just returns a hardcoded JSON string.&lt;/p&gt;

&lt;p&gt;The destination search side has a mock too. MockDestinationSearchService does keyword scoring in memory against five curated destinations. Good enough to build against while you decide whether you want Azure AI Search or something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ollama adapter
&lt;/h2&gt;

&lt;p&gt;Ollama's API is simple but you're dealing with streaming responses and the JSON format varies slightly across model versions. I went with a non-streaming request to keep the adapter stateless, which works fine for itinerary generation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userPrompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing clever. Just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the library does
&lt;/h2&gt;

&lt;p&gt;Four things once registered:&lt;/p&gt;

&lt;p&gt;Itinerary generation takes a traveller profile and destination and returns a structured day-by-day plan with activities and cost estimates. Price anomaly detection analyses flight options against historical baselines and flags anything unusual. Destination search understands natural language queries like "warm with beaches and local food, not too touristy". Booking automation runs the end-to-end flow with retry logic and rollback on failure.&lt;/p&gt;

&lt;p&gt;The whole thing is deployed to Azure Kubernetes Service. The GitHub Actions pipeline builds for linux/arm64, pushes to GHCR, and deploys with a manual approval gate. Took me a while to figure out the arm64 part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package TravelAI.Core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with UseMock(). If it does what you need, switch to a real provider. The rest of your code doesn't change.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/aftabkh4n/TravelAI.Core" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/TravelAI.Core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>azure</category>
      <category>webdev</category>
      <category>claude</category>
    </item>
    <item>
      <title>I added pgvector support to my .NET AI memory library</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:37:50 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/i-added-pgvector-support-to-my-net-ai-memory-library-53i</link>
      <guid>https://dev.to/aftabkh4n/i-added-pgvector-support-to-my-net-ai-memory-library-53i</guid>
      <description>&lt;p&gt;BlazorMemory has had EF Core storage from the start. It works. But there is a problem with how it handles search.&lt;/p&gt;

&lt;p&gt;The current &lt;code&gt;EfCoreMemoryStore&lt;/code&gt; loads every memory for a user into C#, then runs cosine similarity in a loop. For a user with 50 memories that is fine. For a user with 5,000 it is a full table scan on every query.&lt;/p&gt;

&lt;p&gt;v0.6.0 ships a new package that fixes this properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  BlazorMemory.Storage.Pgvector
&lt;/h2&gt;

&lt;p&gt;The pgvector adapter moves vector search into PostgreSQL. Instead of loading everything and computing in C#, it runs a single SQL query using the &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; cosine distance operator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"Memories"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"UserId"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"Embedding"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;queryVector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL handles the math. The .NET code just maps the results.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory.Storage.Pgvector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need PostgreSQL with the pgvector extension. If you are on a fresh database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then wire it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddBlazorMemory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UsePgvectorStorage&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenAiExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;AppDbContext&lt;/code&gt; needs to extend &lt;code&gt;PgvectorMemoryDbContext&lt;/code&gt; or apply the configuration manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppDbContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgvectorMemoryDbContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PgvectorMemoryDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dimensions&lt;/code&gt; parameter must match your embedding model. OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; uses 1536. Adjust for other providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HNSW index
&lt;/h2&gt;

&lt;p&gt;The package creates an HNSW index on the embedding column automatically via EF migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hnsw"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasOperators&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vector_cosine_ops"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasDatabaseName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IX_Memories_Embedding_Hnsw"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HNSW is approximate nearest neighbour search. It trades a small amount of recall for much faster query times at scale. For memory retrieval this is the right tradeoff. You do not need perfect recall, you need fast and good enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which adapter
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;BlazorMemory.Storage.EfCore&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are building a server-side Blazor or ASP.NET Core app&lt;/li&gt;
&lt;li&gt;You have a small number of memories per user (under a few hundred)&lt;/li&gt;
&lt;li&gt;You want the simplest possible setup with SQLite or SQL Server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;BlazorMemory.Storage.Pgvector&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are on PostgreSQL already&lt;/li&gt;
&lt;li&gt;You expect large memory stores per user&lt;/li&gt;
&lt;li&gt;You want search to scale without loading data into memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;BlazorMemory.Storage.IndexedDb&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are building Blazor WASM&lt;/li&gt;
&lt;li&gt;You want zero backend&lt;/li&gt;
&lt;li&gt;Everything stays in the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;9 packages on NuGet, 70 tests passing.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/aftabkh4n/BlazorMemory" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/BlazorMemory&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>postgressql</category>
      <category>vectordatabase</category>
      <category>ai</category>
    </item>
    <item>
      <title>Letting users teach an AI which memories matter</title>
      <dc:creator>Aftab Bashir</dc:creator>
      <pubDate>Tue, 16 Jun 2026 18:16:25 +0000</pubDate>
      <link>https://dev.to/aftabkh4n/letting-users-teach-an-ai-which-memories-matter-3an8</link>
      <guid>https://dev.to/aftabkh4n/letting-users-teach-an-ai-which-memories-matter-3an8</guid>
      <description>&lt;p&gt;I built BlazorMemory to give Blazor AI assistants persistent memory. The library uses an LLM to extract facts from conversations, embeds them as vectors, and retrieves the relevant ones for each new query.&lt;br&gt;
The system worked but it had a quiet failure mode. The LLM decided what was important. The user had no say. So when someone marked their birthday as a memory and the assistant kept surfacing "user enjoys coffee" instead, there was no way to fix it.&lt;br&gt;
v0.5.0 fixes this.&lt;br&gt;
How it works&lt;br&gt;
Each memory now has an&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportanceScore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;field. Default is 1.0. Users can mark a memory as important (1.5) or unimportant (0.3) via thumbs up and thumbs down buttons in the panel.&lt;br&gt;
During search, the score acts as a multiplier on cosine similarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In QueryAsync&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRelevanceScore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RelevanceScore&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="m"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImportanceScore&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RelevanceScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire algorithm. A memory marked important with a 0.7 similarity gets boosted to 1.05 and beats a neutral memory at 0.9. A memory marked unimportant with a 0.9 similarity drops to 0.27 and falls off the list.&lt;/p&gt;

&lt;p&gt;API&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Boost in future searches&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkImportantAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Down-rank but do not delete&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkUnimportantAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Back to neutral&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResetImportanceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Custom score&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetImportanceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1.8f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the UI&lt;br&gt;
If you use the  component, the thumbs buttons appear on every memory card automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;MemoryPanel&lt;/span&gt; &lt;span class="na"&gt;UserId=&lt;/span&gt;&lt;span class="s"&gt;"@userId"&lt;/span&gt; &lt;span class="na"&gt;IsOpen=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can hide them with AllowFeedback="false" if you do not want users to override the LLM.&lt;br&gt;
Important memories get a green left border. Unimportant ones get dimmed and a red left border. Easy to scan at a glance.&lt;br&gt;
Why down-rank instead of delete&lt;br&gt;
The thumbs down button could have just deleted the memory. I decided against it for two reasons.&lt;br&gt;
First, users click the wrong button. A delete that can not be undone is hostile UX.&lt;br&gt;
Second, sometimes you want to suppress a memory without erasing it. Maybe the assistant keeps bringing up an old job title and you want to push it down without losing the context completely. Down-ranking with a score of 0.3 makes the memory available but unlikely to surface.&lt;br&gt;
If users actually want to delete, the ✕ button right next to the thumbs is one click away.&lt;/p&gt;

&lt;p&gt;Getting v0.5.0&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package BlazorMemory
dotnet add package BlazorMemory.Components
dotnet add package BlazorMemory.Storage.IndexedDb
dotnet add package BlazorMemory.Embeddings.OpenAi
dotnet add package BlazorMemory.Extractor.OpenAi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 8 packages on NuGet. Tests passing.&lt;br&gt;
GitHub: &lt;a href="https://github.com/aftabkh4n/BlazorMemory" rel="noopener noreferrer"&gt;https://github.com/aftabkh4n/BlazorMemory&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>ai</category>
      <category>openai</category>
    </item>
  </channel>
</rss>
